##// END OF EJS Templates
pylons: fixed code and test suite after removal of pylons.
marcink -
r2358:d7106a21 default
parent child Browse files
Show More

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

@@ -1,63 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22
22
23 RhodeCode, a web based repository management software
23 RhodeCode, a web based repository management software
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 """
25 """
26
26
27 import os
27 import os
28 import sys
28 import sys
29 import platform
29 import platform
30
30
31 VERSION = tuple(open(os.path.join(
31 VERSION = tuple(open(os.path.join(
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33
33
34 BACKENDS = {
34 BACKENDS = {
35 'hg': 'Mercurial repository',
35 'hg': 'Mercurial repository',
36 'git': 'Git repository',
36 'git': 'Git repository',
37 'svn': 'Subversion repository',
37 'svn': 'Subversion repository',
38 }
38 }
39
39
40 CELERY_ENABLED = False
40 CELERY_ENABLED = False
41 CELERY_EAGER = False
41 CELERY_EAGER = False
42
42
43 # link to config for pylons
43 # link to config for pyramid
44 CONFIG = {}
44 CONFIG = {}
45
45
46 # Populated with the settings dictionary from application init in
46 # Populated with the settings dictionary from application init in
47 # rhodecode.conf.environment.load_pyramid_environment
47 # rhodecode.conf.environment.load_pyramid_environment
48 PYRAMID_SETTINGS = {}
48 PYRAMID_SETTINGS = {}
49
49
50 # Linked module for extensions
50 # Linked module for extensions
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 81 # defines current db version for migrations
54 __dbversion__ = 81 # defines current db version for migrations
55 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
58 __url__ = 'https://code.rhodecode.com'
58 __url__ = 'https://code.rhodecode.com'
59
59
60 is_windows = __platform__ in ['Windows']
60 is_windows = __platform__ in ['Windows']
61 is_unix = not is_windows
61 is_unix = not is_windows
62 is_test = False
62 is_test = False
63 disable_error_handler = False
63 disable_error_handler = False
@@ -1,2066 +1,2067 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib import repo_maintenance
33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 from rhodecode.lib.utils2 import str2bool, time_to_datetime
35 from rhodecode.lib.utils2 import str2bool, time_to_datetime
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 from rhodecode.model.changeset_status import ChangesetStatusModel
38 from rhodecode.model.changeset_status import ChangesetStatusModel
39 from rhodecode.model.comment import CommentsModel
39 from rhodecode.model.comment import CommentsModel
40 from rhodecode.model.db import (
40 from rhodecode.model.db import (
41 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
41 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
42 ChangesetComment)
42 ChangesetComment)
43 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.scm import ScmModel, RepoList
44 from rhodecode.model.scm import ScmModel, RepoList
45 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
45 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
46 from rhodecode.model import validation_schema
46 from rhodecode.model import validation_schema
47 from rhodecode.model.validation_schema.schemas import repo_schema
47 from rhodecode.model.validation_schema.schemas import repo_schema
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 @jsonrpc_method()
52 @jsonrpc_method()
53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
54 """
54 """
55 Gets an existing repository by its name or repository_id.
55 Gets an existing repository by its name or repository_id.
56
56
57 The members section so the output returns users groups or users
57 The members section so the output returns users groups or users
58 associated with that repository.
58 associated with that repository.
59
59
60 This command can only be run using an |authtoken| with admin rights,
60 This command can only be run using an |authtoken| with admin rights,
61 or users with at least read rights to the |repo|.
61 or users with at least read rights to the |repo|.
62
62
63 :param apiuser: This is filled automatically from the |authtoken|.
63 :param apiuser: This is filled automatically from the |authtoken|.
64 :type apiuser: AuthUser
64 :type apiuser: AuthUser
65 :param repoid: The repository name or repository id.
65 :param repoid: The repository name or repository id.
66 :type repoid: str or int
66 :type repoid: str or int
67 :param cache: use the cached value for last changeset
67 :param cache: use the cached value for last changeset
68 :type: cache: Optional(bool)
68 :type: cache: Optional(bool)
69
69
70 Example output:
70 Example output:
71
71
72 .. code-block:: bash
72 .. code-block:: bash
73
73
74 {
74 {
75 "error": null,
75 "error": null,
76 "id": <repo_id>,
76 "id": <repo_id>,
77 "result": {
77 "result": {
78 "clone_uri": null,
78 "clone_uri": null,
79 "created_on": "timestamp",
79 "created_on": "timestamp",
80 "description": "repo description",
80 "description": "repo description",
81 "enable_downloads": false,
81 "enable_downloads": false,
82 "enable_locking": false,
82 "enable_locking": false,
83 "enable_statistics": false,
83 "enable_statistics": false,
84 "followers": [
84 "followers": [
85 {
85 {
86 "active": true,
86 "active": true,
87 "admin": false,
87 "admin": false,
88 "api_key": "****************************************",
88 "api_key": "****************************************",
89 "api_keys": [
89 "api_keys": [
90 "****************************************"
90 "****************************************"
91 ],
91 ],
92 "email": "user@example.com",
92 "email": "user@example.com",
93 "emails": [
93 "emails": [
94 "user@example.com"
94 "user@example.com"
95 ],
95 ],
96 "extern_name": "rhodecode",
96 "extern_name": "rhodecode",
97 "extern_type": "rhodecode",
97 "extern_type": "rhodecode",
98 "firstname": "username",
98 "firstname": "username",
99 "ip_addresses": [],
99 "ip_addresses": [],
100 "language": null,
100 "language": null,
101 "last_login": "2015-09-16T17:16:35.854",
101 "last_login": "2015-09-16T17:16:35.854",
102 "lastname": "surname",
102 "lastname": "surname",
103 "user_id": <user_id>,
103 "user_id": <user_id>,
104 "username": "name"
104 "username": "name"
105 }
105 }
106 ],
106 ],
107 "fork_of": "parent-repo",
107 "fork_of": "parent-repo",
108 "landing_rev": [
108 "landing_rev": [
109 "rev",
109 "rev",
110 "tip"
110 "tip"
111 ],
111 ],
112 "last_changeset": {
112 "last_changeset": {
113 "author": "User <user@example.com>",
113 "author": "User <user@example.com>",
114 "branch": "default",
114 "branch": "default",
115 "date": "timestamp",
115 "date": "timestamp",
116 "message": "last commit message",
116 "message": "last commit message",
117 "parents": [
117 "parents": [
118 {
118 {
119 "raw_id": "commit-id"
119 "raw_id": "commit-id"
120 }
120 }
121 ],
121 ],
122 "raw_id": "commit-id",
122 "raw_id": "commit-id",
123 "revision": <revision number>,
123 "revision": <revision number>,
124 "short_id": "short id"
124 "short_id": "short id"
125 },
125 },
126 "lock_reason": null,
126 "lock_reason": null,
127 "locked_by": null,
127 "locked_by": null,
128 "locked_date": null,
128 "locked_date": null,
129 "members": [
129 "members": [
130 {
130 {
131 "name": "super-admin-name",
131 "name": "super-admin-name",
132 "origin": "super-admin",
132 "origin": "super-admin",
133 "permission": "repository.admin",
133 "permission": "repository.admin",
134 "type": "user"
134 "type": "user"
135 },
135 },
136 {
136 {
137 "name": "owner-name",
137 "name": "owner-name",
138 "origin": "owner",
138 "origin": "owner",
139 "permission": "repository.admin",
139 "permission": "repository.admin",
140 "type": "user"
140 "type": "user"
141 },
141 },
142 {
142 {
143 "name": "user-group-name",
143 "name": "user-group-name",
144 "origin": "permission",
144 "origin": "permission",
145 "permission": "repository.write",
145 "permission": "repository.write",
146 "type": "user_group"
146 "type": "user_group"
147 }
147 }
148 ],
148 ],
149 "owner": "owner-name",
149 "owner": "owner-name",
150 "permissions": [
150 "permissions": [
151 {
151 {
152 "name": "super-admin-name",
152 "name": "super-admin-name",
153 "origin": "super-admin",
153 "origin": "super-admin",
154 "permission": "repository.admin",
154 "permission": "repository.admin",
155 "type": "user"
155 "type": "user"
156 },
156 },
157 {
157 {
158 "name": "owner-name",
158 "name": "owner-name",
159 "origin": "owner",
159 "origin": "owner",
160 "permission": "repository.admin",
160 "permission": "repository.admin",
161 "type": "user"
161 "type": "user"
162 },
162 },
163 {
163 {
164 "name": "user-group-name",
164 "name": "user-group-name",
165 "origin": "permission",
165 "origin": "permission",
166 "permission": "repository.write",
166 "permission": "repository.write",
167 "type": "user_group"
167 "type": "user_group"
168 }
168 }
169 ],
169 ],
170 "private": true,
170 "private": true,
171 "repo_id": 676,
171 "repo_id": 676,
172 "repo_name": "user-group/repo-name",
172 "repo_name": "user-group/repo-name",
173 "repo_type": "hg"
173 "repo_type": "hg"
174 }
174 }
175 }
175 }
176 """
176 """
177
177
178 repo = get_repo_or_error(repoid)
178 repo = get_repo_or_error(repoid)
179 cache = Optional.extract(cache)
179 cache = Optional.extract(cache)
180
180
181 include_secrets = False
181 include_secrets = False
182 if has_superadmin_permission(apiuser):
182 if has_superadmin_permission(apiuser):
183 include_secrets = True
183 include_secrets = True
184 else:
184 else:
185 # check if we have at least read permission for this repo !
185 # check if we have at least read permission for this repo !
186 _perms = (
186 _perms = (
187 'repository.admin', 'repository.write', 'repository.read',)
187 'repository.admin', 'repository.write', 'repository.read',)
188 validate_repo_permissions(apiuser, repoid, repo, _perms)
188 validate_repo_permissions(apiuser, repoid, repo, _perms)
189
189
190 permissions = []
190 permissions = []
191 for _user in repo.permissions():
191 for _user in repo.permissions():
192 user_data = {
192 user_data = {
193 'name': _user.username,
193 'name': _user.username,
194 'permission': _user.permission,
194 'permission': _user.permission,
195 'origin': get_origin(_user),
195 'origin': get_origin(_user),
196 'type': "user",
196 'type': "user",
197 }
197 }
198 permissions.append(user_data)
198 permissions.append(user_data)
199
199
200 for _user_group in repo.permission_user_groups():
200 for _user_group in repo.permission_user_groups():
201 user_group_data = {
201 user_group_data = {
202 'name': _user_group.users_group_name,
202 'name': _user_group.users_group_name,
203 'permission': _user_group.permission,
203 'permission': _user_group.permission,
204 'origin': get_origin(_user_group),
204 'origin': get_origin(_user_group),
205 'type': "user_group",
205 'type': "user_group",
206 }
206 }
207 permissions.append(user_group_data)
207 permissions.append(user_group_data)
208
208
209 following_users = [
209 following_users = [
210 user.user.get_api_data(include_secrets=include_secrets)
210 user.user.get_api_data(include_secrets=include_secrets)
211 for user in repo.followers]
211 for user in repo.followers]
212
212
213 if not cache:
213 if not cache:
214 repo.update_commit_cache()
214 repo.update_commit_cache()
215 data = repo.get_api_data(include_secrets=include_secrets)
215 data = repo.get_api_data(include_secrets=include_secrets)
216 data['members'] = permissions # TODO: this should be deprecated soon
216 data['members'] = permissions # TODO: this should be deprecated soon
217 data['permissions'] = permissions
217 data['permissions'] = permissions
218 data['followers'] = following_users
218 data['followers'] = following_users
219 return data
219 return data
220
220
221
221
222 @jsonrpc_method()
222 @jsonrpc_method()
223 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
223 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
224 """
224 """
225 Lists all existing repositories.
225 Lists all existing repositories.
226
226
227 This command can only be run using an |authtoken| with admin rights,
227 This command can only be run using an |authtoken| with admin rights,
228 or users with at least read rights to |repos|.
228 or users with at least read rights to |repos|.
229
229
230 :param apiuser: This is filled automatically from the |authtoken|.
230 :param apiuser: This is filled automatically from the |authtoken|.
231 :type apiuser: AuthUser
231 :type apiuser: AuthUser
232 :param root: specify root repository group to fetch repositories.
232 :param root: specify root repository group to fetch repositories.
233 filters the returned repositories to be members of given root group.
233 filters the returned repositories to be members of given root group.
234 :type root: Optional(None)
234 :type root: Optional(None)
235 :param traverse: traverse given root into subrepositories. With this flag
235 :param traverse: traverse given root into subrepositories. With this flag
236 set to False, it will only return top-level repositories from `root`.
236 set to False, it will only return top-level repositories from `root`.
237 if root is empty it will return just top-level repositories.
237 if root is empty it will return just top-level repositories.
238 :type traverse: Optional(True)
238 :type traverse: Optional(True)
239
239
240
240
241 Example output:
241 Example output:
242
242
243 .. code-block:: bash
243 .. code-block:: bash
244
244
245 id : <id_given_in_input>
245 id : <id_given_in_input>
246 result: [
246 result: [
247 {
247 {
248 "repo_id" : "<repo_id>",
248 "repo_id" : "<repo_id>",
249 "repo_name" : "<reponame>"
249 "repo_name" : "<reponame>"
250 "repo_type" : "<repo_type>",
250 "repo_type" : "<repo_type>",
251 "clone_uri" : "<clone_uri>",
251 "clone_uri" : "<clone_uri>",
252 "private": : "<bool>",
252 "private": : "<bool>",
253 "created_on" : "<datetimecreated>",
253 "created_on" : "<datetimecreated>",
254 "description" : "<description>",
254 "description" : "<description>",
255 "landing_rev": "<landing_rev>",
255 "landing_rev": "<landing_rev>",
256 "owner": "<repo_owner>",
256 "owner": "<repo_owner>",
257 "fork_of": "<name_of_fork_parent>",
257 "fork_of": "<name_of_fork_parent>",
258 "enable_downloads": "<bool>",
258 "enable_downloads": "<bool>",
259 "enable_locking": "<bool>",
259 "enable_locking": "<bool>",
260 "enable_statistics": "<bool>",
260 "enable_statistics": "<bool>",
261 },
261 },
262 ...
262 ...
263 ]
263 ]
264 error: null
264 error: null
265 """
265 """
266
266
267 include_secrets = has_superadmin_permission(apiuser)
267 include_secrets = has_superadmin_permission(apiuser)
268 _perms = ('repository.read', 'repository.write', 'repository.admin',)
268 _perms = ('repository.read', 'repository.write', 'repository.admin',)
269 extras = {'user': apiuser}
269 extras = {'user': apiuser}
270
270
271 root = Optional.extract(root)
271 root = Optional.extract(root)
272 traverse = Optional.extract(traverse, binary=True)
272 traverse = Optional.extract(traverse, binary=True)
273
273
274 if root:
274 if root:
275 # verify parent existance, if it's empty return an error
275 # verify parent existance, if it's empty return an error
276 parent = RepoGroup.get_by_group_name(root)
276 parent = RepoGroup.get_by_group_name(root)
277 if not parent:
277 if not parent:
278 raise JSONRPCError(
278 raise JSONRPCError(
279 'Root repository group `{}` does not exist'.format(root))
279 'Root repository group `{}` does not exist'.format(root))
280
280
281 if traverse:
281 if traverse:
282 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
282 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
283 else:
283 else:
284 repos = RepoModel().get_repos_for_root(root=parent)
284 repos = RepoModel().get_repos_for_root(root=parent)
285 else:
285 else:
286 if traverse:
286 if traverse:
287 repos = RepoModel().get_all()
287 repos = RepoModel().get_all()
288 else:
288 else:
289 # return just top-level
289 # return just top-level
290 repos = RepoModel().get_repos_for_root(root=None)
290 repos = RepoModel().get_repos_for_root(root=None)
291
291
292 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
292 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
293 return [repo.get_api_data(include_secrets=include_secrets)
293 return [repo.get_api_data(include_secrets=include_secrets)
294 for repo in repo_list]
294 for repo in repo_list]
295
295
296
296
297 @jsonrpc_method()
297 @jsonrpc_method()
298 def get_repo_changeset(request, apiuser, repoid, revision,
298 def get_repo_changeset(request, apiuser, repoid, revision,
299 details=Optional('basic')):
299 details=Optional('basic')):
300 """
300 """
301 Returns information about a changeset.
301 Returns information about a changeset.
302
302
303 Additionally parameters define the amount of details returned by
303 Additionally parameters define the amount of details returned by
304 this function.
304 this function.
305
305
306 This command can only be run using an |authtoken| with admin rights,
306 This command can only be run using an |authtoken| with admin rights,
307 or users with at least read rights to the |repo|.
307 or users with at least read rights to the |repo|.
308
308
309 :param apiuser: This is filled automatically from the |authtoken|.
309 :param apiuser: This is filled automatically from the |authtoken|.
310 :type apiuser: AuthUser
310 :type apiuser: AuthUser
311 :param repoid: The repository name or repository id
311 :param repoid: The repository name or repository id
312 :type repoid: str or int
312 :type repoid: str or int
313 :param revision: revision for which listing should be done
313 :param revision: revision for which listing should be done
314 :type revision: str
314 :type revision: str
315 :param details: details can be 'basic|extended|full' full gives diff
315 :param details: details can be 'basic|extended|full' full gives diff
316 info details like the diff itself, and number of changed files etc.
316 info details like the diff itself, and number of changed files etc.
317 :type details: Optional(str)
317 :type details: Optional(str)
318
318
319 """
319 """
320 repo = get_repo_or_error(repoid)
320 repo = get_repo_or_error(repoid)
321 if not has_superadmin_permission(apiuser):
321 if not has_superadmin_permission(apiuser):
322 _perms = (
322 _perms = (
323 'repository.admin', 'repository.write', 'repository.read',)
323 'repository.admin', 'repository.write', 'repository.read',)
324 validate_repo_permissions(apiuser, repoid, repo, _perms)
324 validate_repo_permissions(apiuser, repoid, repo, _perms)
325
325
326 changes_details = Optional.extract(details)
326 changes_details = Optional.extract(details)
327 _changes_details_types = ['basic', 'extended', 'full']
327 _changes_details_types = ['basic', 'extended', 'full']
328 if changes_details not in _changes_details_types:
328 if changes_details not in _changes_details_types:
329 raise JSONRPCError(
329 raise JSONRPCError(
330 'ret_type must be one of %s' % (
330 'ret_type must be one of %s' % (
331 ','.join(_changes_details_types)))
331 ','.join(_changes_details_types)))
332
332
333 pre_load = ['author', 'branch', 'date', 'message', 'parents',
333 pre_load = ['author', 'branch', 'date', 'message', 'parents',
334 'status', '_commit', '_file_paths']
334 'status', '_commit', '_file_paths']
335
335
336 try:
336 try:
337 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
337 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
338 except TypeError as e:
338 except TypeError as e:
339 raise JSONRPCError(e.message)
339 raise JSONRPCError(e.message)
340 _cs_json = cs.__json__()
340 _cs_json = cs.__json__()
341 _cs_json['diff'] = build_commit_data(cs, changes_details)
341 _cs_json['diff'] = build_commit_data(cs, changes_details)
342 if changes_details == 'full':
342 if changes_details == 'full':
343 _cs_json['refs'] = cs._get_refs()
343 _cs_json['refs'] = cs._get_refs()
344 return _cs_json
344 return _cs_json
345
345
346
346
347 @jsonrpc_method()
347 @jsonrpc_method()
348 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
348 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
349 details=Optional('basic')):
349 details=Optional('basic')):
350 """
350 """
351 Returns a set of commits limited by the number starting
351 Returns a set of commits limited by the number starting
352 from the `start_rev` option.
352 from the `start_rev` option.
353
353
354 Additional parameters define the amount of details returned by this
354 Additional parameters define the amount of details returned by this
355 function.
355 function.
356
356
357 This command can only be run using an |authtoken| with admin rights,
357 This command can only be run using an |authtoken| with admin rights,
358 or users with at least read rights to |repos|.
358 or users with at least read rights to |repos|.
359
359
360 :param apiuser: This is filled automatically from the |authtoken|.
360 :param apiuser: This is filled automatically from the |authtoken|.
361 :type apiuser: AuthUser
361 :type apiuser: AuthUser
362 :param repoid: The repository name or repository ID.
362 :param repoid: The repository name or repository ID.
363 :type repoid: str or int
363 :type repoid: str or int
364 :param start_rev: The starting revision from where to get changesets.
364 :param start_rev: The starting revision from where to get changesets.
365 :type start_rev: str
365 :type start_rev: str
366 :param limit: Limit the number of commits to this amount
366 :param limit: Limit the number of commits to this amount
367 :type limit: str or int
367 :type limit: str or int
368 :param details: Set the level of detail returned. Valid option are:
368 :param details: Set the level of detail returned. Valid option are:
369 ``basic``, ``extended`` and ``full``.
369 ``basic``, ``extended`` and ``full``.
370 :type details: Optional(str)
370 :type details: Optional(str)
371
371
372 .. note::
372 .. note::
373
373
374 Setting the parameter `details` to the value ``full`` is extensive
374 Setting the parameter `details` to the value ``full`` is extensive
375 and returns details like the diff itself, and the number
375 and returns details like the diff itself, and the number
376 of changed files.
376 of changed files.
377
377
378 """
378 """
379 repo = get_repo_or_error(repoid)
379 repo = get_repo_or_error(repoid)
380 if not has_superadmin_permission(apiuser):
380 if not has_superadmin_permission(apiuser):
381 _perms = (
381 _perms = (
382 'repository.admin', 'repository.write', 'repository.read',)
382 'repository.admin', 'repository.write', 'repository.read',)
383 validate_repo_permissions(apiuser, repoid, repo, _perms)
383 validate_repo_permissions(apiuser, repoid, repo, _perms)
384
384
385 changes_details = Optional.extract(details)
385 changes_details = Optional.extract(details)
386 _changes_details_types = ['basic', 'extended', 'full']
386 _changes_details_types = ['basic', 'extended', 'full']
387 if changes_details not in _changes_details_types:
387 if changes_details not in _changes_details_types:
388 raise JSONRPCError(
388 raise JSONRPCError(
389 'ret_type must be one of %s' % (
389 'ret_type must be one of %s' % (
390 ','.join(_changes_details_types)))
390 ','.join(_changes_details_types)))
391
391
392 limit = int(limit)
392 limit = int(limit)
393 pre_load = ['author', 'branch', 'date', 'message', 'parents',
393 pre_load = ['author', 'branch', 'date', 'message', 'parents',
394 'status', '_commit', '_file_paths']
394 'status', '_commit', '_file_paths']
395
395
396 vcs_repo = repo.scm_instance()
396 vcs_repo = repo.scm_instance()
397 # SVN needs a special case to distinguish its index and commit id
397 # SVN needs a special case to distinguish its index and commit id
398 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
398 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
399 start_rev = vcs_repo.commit_ids[0]
399 start_rev = vcs_repo.commit_ids[0]
400
400
401 try:
401 try:
402 commits = vcs_repo.get_commits(
402 commits = vcs_repo.get_commits(
403 start_id=start_rev, pre_load=pre_load)
403 start_id=start_rev, pre_load=pre_load)
404 except TypeError as e:
404 except TypeError as e:
405 raise JSONRPCError(e.message)
405 raise JSONRPCError(e.message)
406 except Exception:
406 except Exception:
407 log.exception('Fetching of commits failed')
407 log.exception('Fetching of commits failed')
408 raise JSONRPCError('Error occurred during commit fetching')
408 raise JSONRPCError('Error occurred during commit fetching')
409
409
410 ret = []
410 ret = []
411 for cnt, commit in enumerate(commits):
411 for cnt, commit in enumerate(commits):
412 if cnt >= limit != -1:
412 if cnt >= limit != -1:
413 break
413 break
414 _cs_json = commit.__json__()
414 _cs_json = commit.__json__()
415 _cs_json['diff'] = build_commit_data(commit, changes_details)
415 _cs_json['diff'] = build_commit_data(commit, changes_details)
416 if changes_details == 'full':
416 if changes_details == 'full':
417 _cs_json['refs'] = {
417 _cs_json['refs'] = {
418 'branches': [commit.branch],
418 'branches': [commit.branch],
419 'bookmarks': getattr(commit, 'bookmarks', []),
419 'bookmarks': getattr(commit, 'bookmarks', []),
420 'tags': commit.tags
420 'tags': commit.tags
421 }
421 }
422 ret.append(_cs_json)
422 ret.append(_cs_json)
423 return ret
423 return ret
424
424
425
425
426 @jsonrpc_method()
426 @jsonrpc_method()
427 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
427 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
428 ret_type=Optional('all'), details=Optional('basic'),
428 ret_type=Optional('all'), details=Optional('basic'),
429 max_file_bytes=Optional(None)):
429 max_file_bytes=Optional(None)):
430 """
430 """
431 Returns a list of nodes and children in a flat list for a given
431 Returns a list of nodes and children in a flat list for a given
432 path at given revision.
432 path at given revision.
433
433
434 It's possible to specify ret_type to show only `files` or `dirs`.
434 It's possible to specify ret_type to show only `files` or `dirs`.
435
435
436 This command can only be run using an |authtoken| with admin rights,
436 This command can only be run using an |authtoken| with admin rights,
437 or users with at least read rights to |repos|.
437 or users with at least read rights to |repos|.
438
438
439 :param apiuser: This is filled automatically from the |authtoken|.
439 :param apiuser: This is filled automatically from the |authtoken|.
440 :type apiuser: AuthUser
440 :type apiuser: AuthUser
441 :param repoid: The repository name or repository ID.
441 :param repoid: The repository name or repository ID.
442 :type repoid: str or int
442 :type repoid: str or int
443 :param revision: The revision for which listing should be done.
443 :param revision: The revision for which listing should be done.
444 :type revision: str
444 :type revision: str
445 :param root_path: The path from which to start displaying.
445 :param root_path: The path from which to start displaying.
446 :type root_path: str
446 :type root_path: str
447 :param ret_type: Set the return type. Valid options are
447 :param ret_type: Set the return type. Valid options are
448 ``all`` (default), ``files`` and ``dirs``.
448 ``all`` (default), ``files`` and ``dirs``.
449 :type ret_type: Optional(str)
449 :type ret_type: Optional(str)
450 :param details: Returns extended information about nodes, such as
450 :param details: Returns extended information about nodes, such as
451 md5, binary, and or content. The valid options are ``basic`` and
451 md5, binary, and or content. The valid options are ``basic`` and
452 ``full``.
452 ``full``.
453 :type details: Optional(str)
453 :type details: Optional(str)
454 :param max_file_bytes: Only return file content under this file size bytes
454 :param max_file_bytes: Only return file content under this file size bytes
455 :type details: Optional(int)
455 :type details: Optional(int)
456
456
457 Example output:
457 Example output:
458
458
459 .. code-block:: bash
459 .. code-block:: bash
460
460
461 id : <id_given_in_input>
461 id : <id_given_in_input>
462 result: [
462 result: [
463 {
463 {
464 "name" : "<name>"
464 "name" : "<name>"
465 "type" : "<type>",
465 "type" : "<type>",
466 "binary": "<true|false>" (only in extended mode)
466 "binary": "<true|false>" (only in extended mode)
467 "md5" : "<md5 of file content>" (only in extended mode)
467 "md5" : "<md5 of file content>" (only in extended mode)
468 },
468 },
469 ...
469 ...
470 ]
470 ]
471 error: null
471 error: null
472 """
472 """
473
473
474 repo = get_repo_or_error(repoid)
474 repo = get_repo_or_error(repoid)
475 if not has_superadmin_permission(apiuser):
475 if not has_superadmin_permission(apiuser):
476 _perms = (
476 _perms = (
477 'repository.admin', 'repository.write', 'repository.read',)
477 'repository.admin', 'repository.write', 'repository.read',)
478 validate_repo_permissions(apiuser, repoid, repo, _perms)
478 validate_repo_permissions(apiuser, repoid, repo, _perms)
479
479
480 ret_type = Optional.extract(ret_type)
480 ret_type = Optional.extract(ret_type)
481 details = Optional.extract(details)
481 details = Optional.extract(details)
482 _extended_types = ['basic', 'full']
482 _extended_types = ['basic', 'full']
483 if details not in _extended_types:
483 if details not in _extended_types:
484 raise JSONRPCError(
484 raise JSONRPCError(
485 'ret_type must be one of %s' % (','.join(_extended_types)))
485 'ret_type must be one of %s' % (','.join(_extended_types)))
486 extended_info = False
486 extended_info = False
487 content = False
487 content = False
488 if details == 'basic':
488 if details == 'basic':
489 extended_info = True
489 extended_info = True
490
490
491 if details == 'full':
491 if details == 'full':
492 extended_info = content = True
492 extended_info = content = True
493
493
494 _map = {}
494 _map = {}
495 try:
495 try:
496 # check if repo is not empty by any chance, skip quicker if it is.
496 # check if repo is not empty by any chance, skip quicker if it is.
497 _scm = repo.scm_instance()
497 _scm = repo.scm_instance()
498 if _scm.is_empty():
498 if _scm.is_empty():
499 return []
499 return []
500
500
501 _d, _f = ScmModel().get_nodes(
501 _d, _f = ScmModel().get_nodes(
502 repo, revision, root_path, flat=False,
502 repo, revision, root_path, flat=False,
503 extended_info=extended_info, content=content,
503 extended_info=extended_info, content=content,
504 max_file_bytes=max_file_bytes)
504 max_file_bytes=max_file_bytes)
505 _map = {
505 _map = {
506 'all': _d + _f,
506 'all': _d + _f,
507 'files': _f,
507 'files': _f,
508 'dirs': _d,
508 'dirs': _d,
509 }
509 }
510 return _map[ret_type]
510 return _map[ret_type]
511 except KeyError:
511 except KeyError:
512 raise JSONRPCError(
512 raise JSONRPCError(
513 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
513 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
514 except Exception:
514 except Exception:
515 log.exception("Exception occurred while trying to get repo nodes")
515 log.exception("Exception occurred while trying to get repo nodes")
516 raise JSONRPCError(
516 raise JSONRPCError(
517 'failed to get repo: `%s` nodes' % repo.repo_name
517 'failed to get repo: `%s` nodes' % repo.repo_name
518 )
518 )
519
519
520
520
521 @jsonrpc_method()
521 @jsonrpc_method()
522 def get_repo_refs(request, apiuser, repoid):
522 def get_repo_refs(request, apiuser, repoid):
523 """
523 """
524 Returns a dictionary of current references. It returns
524 Returns a dictionary of current references. It returns
525 bookmarks, branches, closed_branches, and tags for given repository
525 bookmarks, branches, closed_branches, and tags for given repository
526
526
527 It's possible to specify ret_type to show only `files` or `dirs`.
527 It's possible to specify ret_type to show only `files` or `dirs`.
528
528
529 This command can only be run using an |authtoken| with admin rights,
529 This command can only be run using an |authtoken| with admin rights,
530 or users with at least read rights to |repos|.
530 or users with at least read rights to |repos|.
531
531
532 :param apiuser: This is filled automatically from the |authtoken|.
532 :param apiuser: This is filled automatically from the |authtoken|.
533 :type apiuser: AuthUser
533 :type apiuser: AuthUser
534 :param repoid: The repository name or repository ID.
534 :param repoid: The repository name or repository ID.
535 :type repoid: str or int
535 :type repoid: str or int
536
536
537 Example output:
537 Example output:
538
538
539 .. code-block:: bash
539 .. code-block:: bash
540
540
541 id : <id_given_in_input>
541 id : <id_given_in_input>
542 "result": {
542 "result": {
543 "bookmarks": {
543 "bookmarks": {
544 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
544 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
545 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
545 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
546 },
546 },
547 "branches": {
547 "branches": {
548 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
548 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
549 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
549 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
550 },
550 },
551 "branches_closed": {},
551 "branches_closed": {},
552 "tags": {
552 "tags": {
553 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
553 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
554 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
554 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
555 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
555 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
556 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
556 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
557 }
557 }
558 }
558 }
559 error: null
559 error: null
560 """
560 """
561
561
562 repo = get_repo_or_error(repoid)
562 repo = get_repo_or_error(repoid)
563 if not has_superadmin_permission(apiuser):
563 if not has_superadmin_permission(apiuser):
564 _perms = ('repository.admin', 'repository.write', 'repository.read',)
564 _perms = ('repository.admin', 'repository.write', 'repository.read',)
565 validate_repo_permissions(apiuser, repoid, repo, _perms)
565 validate_repo_permissions(apiuser, repoid, repo, _perms)
566
566
567 try:
567 try:
568 # check if repo is not empty by any chance, skip quicker if it is.
568 # check if repo is not empty by any chance, skip quicker if it is.
569 vcs_instance = repo.scm_instance()
569 vcs_instance = repo.scm_instance()
570 refs = vcs_instance.refs()
570 refs = vcs_instance.refs()
571 return refs
571 return refs
572 except Exception:
572 except Exception:
573 log.exception("Exception occurred while trying to get repo refs")
573 log.exception("Exception occurred while trying to get repo refs")
574 raise JSONRPCError(
574 raise JSONRPCError(
575 'failed to get repo: `%s` references' % repo.repo_name
575 'failed to get repo: `%s` references' % repo.repo_name
576 )
576 )
577
577
578
578
579 @jsonrpc_method()
579 @jsonrpc_method()
580 def create_repo(
580 def create_repo(
581 request, apiuser, repo_name, repo_type,
581 request, apiuser, repo_name, repo_type,
582 owner=Optional(OAttr('apiuser')),
582 owner=Optional(OAttr('apiuser')),
583 description=Optional(''),
583 description=Optional(''),
584 private=Optional(False),
584 private=Optional(False),
585 clone_uri=Optional(None),
585 clone_uri=Optional(None),
586 landing_rev=Optional('rev:tip'),
586 landing_rev=Optional('rev:tip'),
587 enable_statistics=Optional(False),
587 enable_statistics=Optional(False),
588 enable_locking=Optional(False),
588 enable_locking=Optional(False),
589 enable_downloads=Optional(False),
589 enable_downloads=Optional(False),
590 copy_permissions=Optional(False)):
590 copy_permissions=Optional(False)):
591 """
591 """
592 Creates a repository.
592 Creates a repository.
593
593
594 * If the repository name contains "/", repository will be created inside
594 * If the repository name contains "/", repository will be created inside
595 a repository group or nested repository groups
595 a repository group or nested repository groups
596
596
597 For example "foo/bar/repo1" will create |repo| called "repo1" inside
597 For example "foo/bar/repo1" will create |repo| called "repo1" inside
598 group "foo/bar". You have to have permissions to access and write to
598 group "foo/bar". You have to have permissions to access and write to
599 the last repository group ("bar" in this example)
599 the last repository group ("bar" in this example)
600
600
601 This command can only be run using an |authtoken| with at least
601 This command can only be run using an |authtoken| with at least
602 permissions to create repositories, or write permissions to
602 permissions to create repositories, or write permissions to
603 parent repository groups.
603 parent repository groups.
604
604
605 :param apiuser: This is filled automatically from the |authtoken|.
605 :param apiuser: This is filled automatically from the |authtoken|.
606 :type apiuser: AuthUser
606 :type apiuser: AuthUser
607 :param repo_name: Set the repository name.
607 :param repo_name: Set the repository name.
608 :type repo_name: str
608 :type repo_name: str
609 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
609 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
610 :type repo_type: str
610 :type repo_type: str
611 :param owner: user_id or username
611 :param owner: user_id or username
612 :type owner: Optional(str)
612 :type owner: Optional(str)
613 :param description: Set the repository description.
613 :param description: Set the repository description.
614 :type description: Optional(str)
614 :type description: Optional(str)
615 :param private: set repository as private
615 :param private: set repository as private
616 :type private: bool
616 :type private: bool
617 :param clone_uri: set clone_uri
617 :param clone_uri: set clone_uri
618 :type clone_uri: str
618 :type clone_uri: str
619 :param landing_rev: <rev_type>:<rev>
619 :param landing_rev: <rev_type>:<rev>
620 :type landing_rev: str
620 :type landing_rev: str
621 :param enable_locking:
621 :param enable_locking:
622 :type enable_locking: bool
622 :type enable_locking: bool
623 :param enable_downloads:
623 :param enable_downloads:
624 :type enable_downloads: bool
624 :type enable_downloads: bool
625 :param enable_statistics:
625 :param enable_statistics:
626 :type enable_statistics: bool
626 :type enable_statistics: bool
627 :param copy_permissions: Copy permission from group in which the
627 :param copy_permissions: Copy permission from group in which the
628 repository is being created.
628 repository is being created.
629 :type copy_permissions: bool
629 :type copy_permissions: bool
630
630
631
631
632 Example output:
632 Example output:
633
633
634 .. code-block:: bash
634 .. code-block:: bash
635
635
636 id : <id_given_in_input>
636 id : <id_given_in_input>
637 result: {
637 result: {
638 "msg": "Created new repository `<reponame>`",
638 "msg": "Created new repository `<reponame>`",
639 "success": true,
639 "success": true,
640 "task": "<celery task id or None if done sync>"
640 "task": "<celery task id or None if done sync>"
641 }
641 }
642 error: null
642 error: null
643
643
644
644
645 Example error output:
645 Example error output:
646
646
647 .. code-block:: bash
647 .. code-block:: bash
648
648
649 id : <id_given_in_input>
649 id : <id_given_in_input>
650 result : null
650 result : null
651 error : {
651 error : {
652 'failed to create repository `<repo_name>`'
652 'failed to create repository `<repo_name>`'
653 }
653 }
654
654
655 """
655 """
656
656
657 owner = validate_set_owner_permissions(apiuser, owner)
657 owner = validate_set_owner_permissions(apiuser, owner)
658
658
659 description = Optional.extract(description)
659 description = Optional.extract(description)
660 copy_permissions = Optional.extract(copy_permissions)
660 copy_permissions = Optional.extract(copy_permissions)
661 clone_uri = Optional.extract(clone_uri)
661 clone_uri = Optional.extract(clone_uri)
662 landing_commit_ref = Optional.extract(landing_rev)
662 landing_commit_ref = Optional.extract(landing_rev)
663
663
664 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
664 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
665 if isinstance(private, Optional):
665 if isinstance(private, Optional):
666 private = defs.get('repo_private') or Optional.extract(private)
666 private = defs.get('repo_private') or Optional.extract(private)
667 if isinstance(repo_type, Optional):
667 if isinstance(repo_type, Optional):
668 repo_type = defs.get('repo_type')
668 repo_type = defs.get('repo_type')
669 if isinstance(enable_statistics, Optional):
669 if isinstance(enable_statistics, Optional):
670 enable_statistics = defs.get('repo_enable_statistics')
670 enable_statistics = defs.get('repo_enable_statistics')
671 if isinstance(enable_locking, Optional):
671 if isinstance(enable_locking, Optional):
672 enable_locking = defs.get('repo_enable_locking')
672 enable_locking = defs.get('repo_enable_locking')
673 if isinstance(enable_downloads, Optional):
673 if isinstance(enable_downloads, Optional):
674 enable_downloads = defs.get('repo_enable_downloads')
674 enable_downloads = defs.get('repo_enable_downloads')
675
675
676 schema = repo_schema.RepoSchema().bind(
676 schema = repo_schema.RepoSchema().bind(
677 repo_type_options=rhodecode.BACKENDS.keys(),
677 repo_type_options=rhodecode.BACKENDS.keys(),
678 # user caller
678 # user caller
679 user=apiuser)
679 user=apiuser)
680
680
681 try:
681 try:
682 schema_data = schema.deserialize(dict(
682 schema_data = schema.deserialize(dict(
683 repo_name=repo_name,
683 repo_name=repo_name,
684 repo_type=repo_type,
684 repo_type=repo_type,
685 repo_owner=owner.username,
685 repo_owner=owner.username,
686 repo_description=description,
686 repo_description=description,
687 repo_landing_commit_ref=landing_commit_ref,
687 repo_landing_commit_ref=landing_commit_ref,
688 repo_clone_uri=clone_uri,
688 repo_clone_uri=clone_uri,
689 repo_private=private,
689 repo_private=private,
690 repo_copy_permissions=copy_permissions,
690 repo_copy_permissions=copy_permissions,
691 repo_enable_statistics=enable_statistics,
691 repo_enable_statistics=enable_statistics,
692 repo_enable_downloads=enable_downloads,
692 repo_enable_downloads=enable_downloads,
693 repo_enable_locking=enable_locking))
693 repo_enable_locking=enable_locking))
694 except validation_schema.Invalid as err:
694 except validation_schema.Invalid as err:
695 raise JSONRPCValidationError(colander_exc=err)
695 raise JSONRPCValidationError(colander_exc=err)
696
696
697 try:
697 try:
698 data = {
698 data = {
699 'owner': owner,
699 'owner': owner,
700 'repo_name': schema_data['repo_group']['repo_name_without_group'],
700 'repo_name': schema_data['repo_group']['repo_name_without_group'],
701 'repo_name_full': schema_data['repo_name'],
701 'repo_name_full': schema_data['repo_name'],
702 'repo_group': schema_data['repo_group']['repo_group_id'],
702 'repo_group': schema_data['repo_group']['repo_group_id'],
703 'repo_type': schema_data['repo_type'],
703 'repo_type': schema_data['repo_type'],
704 'repo_description': schema_data['repo_description'],
704 'repo_description': schema_data['repo_description'],
705 'repo_private': schema_data['repo_private'],
705 'repo_private': schema_data['repo_private'],
706 'clone_uri': schema_data['repo_clone_uri'],
706 'clone_uri': schema_data['repo_clone_uri'],
707 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
707 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
708 'enable_statistics': schema_data['repo_enable_statistics'],
708 'enable_statistics': schema_data['repo_enable_statistics'],
709 'enable_locking': schema_data['repo_enable_locking'],
709 'enable_locking': schema_data['repo_enable_locking'],
710 'enable_downloads': schema_data['repo_enable_downloads'],
710 'enable_downloads': schema_data['repo_enable_downloads'],
711 'repo_copy_permissions': schema_data['repo_copy_permissions'],
711 'repo_copy_permissions': schema_data['repo_copy_permissions'],
712 }
712 }
713
713
714 task = RepoModel().create(form_data=data, cur_user=owner)
714 task = RepoModel().create(form_data=data, cur_user=owner)
715 from celery.result import BaseAsyncResult
715 from celery.result import BaseAsyncResult
716 task_id = None
716 task_id = None
717 if isinstance(task, BaseAsyncResult):
717 if isinstance(task, BaseAsyncResult):
718 task_id = task.task_id
718 task_id = task.task_id
719 # no commit, it's done in RepoModel, or async via celery
719 # no commit, it's done in RepoModel, or async via celery
720 return {
720 return {
721 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
721 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
722 'success': True, # cannot return the repo data here since fork
722 'success': True, # cannot return the repo data here since fork
723 # can be done async
723 # can be done async
724 'task': task_id
724 'task': task_id
725 }
725 }
726 except Exception:
726 except Exception:
727 log.exception(
727 log.exception(
728 u"Exception while trying to create the repository %s",
728 u"Exception while trying to create the repository %s",
729 schema_data['repo_name'])
729 schema_data['repo_name'])
730 raise JSONRPCError(
730 raise JSONRPCError(
731 'failed to create repository `%s`' % (schema_data['repo_name'],))
731 'failed to create repository `%s`' % (schema_data['repo_name'],))
732
732
733
733
734 @jsonrpc_method()
734 @jsonrpc_method()
735 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
735 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
736 description=Optional('')):
736 description=Optional('')):
737 """
737 """
738 Adds an extra field to a repository.
738 Adds an extra field to a repository.
739
739
740 This command can only be run using an |authtoken| with at least
740 This command can only be run using an |authtoken| with at least
741 write permissions to the |repo|.
741 write permissions to the |repo|.
742
742
743 :param apiuser: This is filled automatically from the |authtoken|.
743 :param apiuser: This is filled automatically from the |authtoken|.
744 :type apiuser: AuthUser
744 :type apiuser: AuthUser
745 :param repoid: Set the repository name or repository id.
745 :param repoid: Set the repository name or repository id.
746 :type repoid: str or int
746 :type repoid: str or int
747 :param key: Create a unique field key for this repository.
747 :param key: Create a unique field key for this repository.
748 :type key: str
748 :type key: str
749 :param label:
749 :param label:
750 :type label: Optional(str)
750 :type label: Optional(str)
751 :param description:
751 :param description:
752 :type description: Optional(str)
752 :type description: Optional(str)
753 """
753 """
754 repo = get_repo_or_error(repoid)
754 repo = get_repo_or_error(repoid)
755 if not has_superadmin_permission(apiuser):
755 if not has_superadmin_permission(apiuser):
756 _perms = ('repository.admin',)
756 _perms = ('repository.admin',)
757 validate_repo_permissions(apiuser, repoid, repo, _perms)
757 validate_repo_permissions(apiuser, repoid, repo, _perms)
758
758
759 label = Optional.extract(label) or key
759 label = Optional.extract(label) or key
760 description = Optional.extract(description)
760 description = Optional.extract(description)
761
761
762 field = RepositoryField.get_by_key_name(key, repo)
762 field = RepositoryField.get_by_key_name(key, repo)
763 if field:
763 if field:
764 raise JSONRPCError('Field with key '
764 raise JSONRPCError('Field with key '
765 '`%s` exists for repo `%s`' % (key, repoid))
765 '`%s` exists for repo `%s`' % (key, repoid))
766
766
767 try:
767 try:
768 RepoModel().add_repo_field(repo, key, field_label=label,
768 RepoModel().add_repo_field(repo, key, field_label=label,
769 field_desc=description)
769 field_desc=description)
770 Session().commit()
770 Session().commit()
771 return {
771 return {
772 'msg': "Added new repository field `%s`" % (key,),
772 'msg': "Added new repository field `%s`" % (key,),
773 'success': True,
773 'success': True,
774 }
774 }
775 except Exception:
775 except Exception:
776 log.exception("Exception occurred while trying to add field to repo")
776 log.exception("Exception occurred while trying to add field to repo")
777 raise JSONRPCError(
777 raise JSONRPCError(
778 'failed to create new field for repository `%s`' % (repoid,))
778 'failed to create new field for repository `%s`' % (repoid,))
779
779
780
780
781 @jsonrpc_method()
781 @jsonrpc_method()
782 def remove_field_from_repo(request, apiuser, repoid, key):
782 def remove_field_from_repo(request, apiuser, repoid, key):
783 """
783 """
784 Removes an extra field from a repository.
784 Removes an extra field from a repository.
785
785
786 This command can only be run using an |authtoken| with at least
786 This command can only be run using an |authtoken| with at least
787 write permissions to the |repo|.
787 write permissions to the |repo|.
788
788
789 :param apiuser: This is filled automatically from the |authtoken|.
789 :param apiuser: This is filled automatically from the |authtoken|.
790 :type apiuser: AuthUser
790 :type apiuser: AuthUser
791 :param repoid: Set the repository name or repository ID.
791 :param repoid: Set the repository name or repository ID.
792 :type repoid: str or int
792 :type repoid: str or int
793 :param key: Set the unique field key for this repository.
793 :param key: Set the unique field key for this repository.
794 :type key: str
794 :type key: str
795 """
795 """
796
796
797 repo = get_repo_or_error(repoid)
797 repo = get_repo_or_error(repoid)
798 if not has_superadmin_permission(apiuser):
798 if not has_superadmin_permission(apiuser):
799 _perms = ('repository.admin',)
799 _perms = ('repository.admin',)
800 validate_repo_permissions(apiuser, repoid, repo, _perms)
800 validate_repo_permissions(apiuser, repoid, repo, _perms)
801
801
802 field = RepositoryField.get_by_key_name(key, repo)
802 field = RepositoryField.get_by_key_name(key, repo)
803 if not field:
803 if not field:
804 raise JSONRPCError('Field with key `%s` does not '
804 raise JSONRPCError('Field with key `%s` does not '
805 'exists for repo `%s`' % (key, repoid))
805 'exists for repo `%s`' % (key, repoid))
806
806
807 try:
807 try:
808 RepoModel().delete_repo_field(repo, field_key=key)
808 RepoModel().delete_repo_field(repo, field_key=key)
809 Session().commit()
809 Session().commit()
810 return {
810 return {
811 'msg': "Deleted repository field `%s`" % (key,),
811 'msg': "Deleted repository field `%s`" % (key,),
812 'success': True,
812 'success': True,
813 }
813 }
814 except Exception:
814 except Exception:
815 log.exception(
815 log.exception(
816 "Exception occurred while trying to delete field from repo")
816 "Exception occurred while trying to delete field from repo")
817 raise JSONRPCError(
817 raise JSONRPCError(
818 'failed to delete field for repository `%s`' % (repoid,))
818 'failed to delete field for repository `%s`' % (repoid,))
819
819
820
820
821 @jsonrpc_method()
821 @jsonrpc_method()
822 def update_repo(
822 def update_repo(
823 request, apiuser, repoid, repo_name=Optional(None),
823 request, apiuser, repoid, repo_name=Optional(None),
824 owner=Optional(OAttr('apiuser')), description=Optional(''),
824 owner=Optional(OAttr('apiuser')), description=Optional(''),
825 private=Optional(False), clone_uri=Optional(None),
825 private=Optional(False), clone_uri=Optional(None),
826 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
826 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
827 enable_statistics=Optional(False),
827 enable_statistics=Optional(False),
828 enable_locking=Optional(False),
828 enable_locking=Optional(False),
829 enable_downloads=Optional(False), fields=Optional('')):
829 enable_downloads=Optional(False), fields=Optional('')):
830 """
830 """
831 Updates a repository with the given information.
831 Updates a repository with the given information.
832
832
833 This command can only be run using an |authtoken| with at least
833 This command can only be run using an |authtoken| with at least
834 admin permissions to the |repo|.
834 admin permissions to the |repo|.
835
835
836 * If the repository name contains "/", repository will be updated
836 * If the repository name contains "/", repository will be updated
837 accordingly with a repository group or nested repository groups
837 accordingly with a repository group or nested repository groups
838
838
839 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
839 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
840 called "repo-test" and place it inside group "foo/bar".
840 called "repo-test" and place it inside group "foo/bar".
841 You have to have permissions to access and write to the last repository
841 You have to have permissions to access and write to the last repository
842 group ("bar" in this example)
842 group ("bar" in this example)
843
843
844 :param apiuser: This is filled automatically from the |authtoken|.
844 :param apiuser: This is filled automatically from the |authtoken|.
845 :type apiuser: AuthUser
845 :type apiuser: AuthUser
846 :param repoid: repository name or repository ID.
846 :param repoid: repository name or repository ID.
847 :type repoid: str or int
847 :type repoid: str or int
848 :param repo_name: Update the |repo| name, including the
848 :param repo_name: Update the |repo| name, including the
849 repository group it's in.
849 repository group it's in.
850 :type repo_name: str
850 :type repo_name: str
851 :param owner: Set the |repo| owner.
851 :param owner: Set the |repo| owner.
852 :type owner: str
852 :type owner: str
853 :param fork_of: Set the |repo| as fork of another |repo|.
853 :param fork_of: Set the |repo| as fork of another |repo|.
854 :type fork_of: str
854 :type fork_of: str
855 :param description: Update the |repo| description.
855 :param description: Update the |repo| description.
856 :type description: str
856 :type description: str
857 :param private: Set the |repo| as private. (True | False)
857 :param private: Set the |repo| as private. (True | False)
858 :type private: bool
858 :type private: bool
859 :param clone_uri: Update the |repo| clone URI.
859 :param clone_uri: Update the |repo| clone URI.
860 :type clone_uri: str
860 :type clone_uri: str
861 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
861 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
862 :type landing_rev: str
862 :type landing_rev: str
863 :param enable_statistics: Enable statistics on the |repo|, (True | False).
863 :param enable_statistics: Enable statistics on the |repo|, (True | False).
864 :type enable_statistics: bool
864 :type enable_statistics: bool
865 :param enable_locking: Enable |repo| locking.
865 :param enable_locking: Enable |repo| locking.
866 :type enable_locking: bool
866 :type enable_locking: bool
867 :param enable_downloads: Enable downloads from the |repo|, (True | False).
867 :param enable_downloads: Enable downloads from the |repo|, (True | False).
868 :type enable_downloads: bool
868 :type enable_downloads: bool
869 :param fields: Add extra fields to the |repo|. Use the following
869 :param fields: Add extra fields to the |repo|. Use the following
870 example format: ``field_key=field_val,field_key2=fieldval2``.
870 example format: ``field_key=field_val,field_key2=fieldval2``.
871 Escape ', ' with \,
871 Escape ', ' with \,
872 :type fields: str
872 :type fields: str
873 """
873 """
874
874
875 repo = get_repo_or_error(repoid)
875 repo = get_repo_or_error(repoid)
876
876
877 include_secrets = False
877 include_secrets = False
878 if not has_superadmin_permission(apiuser):
878 if not has_superadmin_permission(apiuser):
879 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
879 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
880 else:
880 else:
881 include_secrets = True
881 include_secrets = True
882
882
883 updates = dict(
883 updates = dict(
884 repo_name=repo_name
884 repo_name=repo_name
885 if not isinstance(repo_name, Optional) else repo.repo_name,
885 if not isinstance(repo_name, Optional) else repo.repo_name,
886
886
887 fork_id=fork_of
887 fork_id=fork_of
888 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
888 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
889
889
890 user=owner
890 user=owner
891 if not isinstance(owner, Optional) else repo.user.username,
891 if not isinstance(owner, Optional) else repo.user.username,
892
892
893 repo_description=description
893 repo_description=description
894 if not isinstance(description, Optional) else repo.description,
894 if not isinstance(description, Optional) else repo.description,
895
895
896 repo_private=private
896 repo_private=private
897 if not isinstance(private, Optional) else repo.private,
897 if not isinstance(private, Optional) else repo.private,
898
898
899 clone_uri=clone_uri
899 clone_uri=clone_uri
900 if not isinstance(clone_uri, Optional) else repo.clone_uri,
900 if not isinstance(clone_uri, Optional) else repo.clone_uri,
901
901
902 repo_landing_rev=landing_rev
902 repo_landing_rev=landing_rev
903 if not isinstance(landing_rev, Optional) else repo._landing_revision,
903 if not isinstance(landing_rev, Optional) else repo._landing_revision,
904
904
905 repo_enable_statistics=enable_statistics
905 repo_enable_statistics=enable_statistics
906 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
906 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
907
907
908 repo_enable_locking=enable_locking
908 repo_enable_locking=enable_locking
909 if not isinstance(enable_locking, Optional) else repo.enable_locking,
909 if not isinstance(enable_locking, Optional) else repo.enable_locking,
910
910
911 repo_enable_downloads=enable_downloads
911 repo_enable_downloads=enable_downloads
912 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
912 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
913
913
914 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
914 ref_choices, _labels = ScmModel().get_repo_landing_revs(
915 request.translate, repo=repo)
915
916
916 old_values = repo.get_api_data()
917 old_values = repo.get_api_data()
917 schema = repo_schema.RepoSchema().bind(
918 schema = repo_schema.RepoSchema().bind(
918 repo_type_options=rhodecode.BACKENDS.keys(),
919 repo_type_options=rhodecode.BACKENDS.keys(),
919 repo_ref_options=ref_choices,
920 repo_ref_options=ref_choices,
920 # user caller
921 # user caller
921 user=apiuser,
922 user=apiuser,
922 old_values=old_values)
923 old_values=old_values)
923 try:
924 try:
924 schema_data = schema.deserialize(dict(
925 schema_data = schema.deserialize(dict(
925 # we save old value, users cannot change type
926 # we save old value, users cannot change type
926 repo_type=repo.repo_type,
927 repo_type=repo.repo_type,
927
928
928 repo_name=updates['repo_name'],
929 repo_name=updates['repo_name'],
929 repo_owner=updates['user'],
930 repo_owner=updates['user'],
930 repo_description=updates['repo_description'],
931 repo_description=updates['repo_description'],
931 repo_clone_uri=updates['clone_uri'],
932 repo_clone_uri=updates['clone_uri'],
932 repo_fork_of=updates['fork_id'],
933 repo_fork_of=updates['fork_id'],
933 repo_private=updates['repo_private'],
934 repo_private=updates['repo_private'],
934 repo_landing_commit_ref=updates['repo_landing_rev'],
935 repo_landing_commit_ref=updates['repo_landing_rev'],
935 repo_enable_statistics=updates['repo_enable_statistics'],
936 repo_enable_statistics=updates['repo_enable_statistics'],
936 repo_enable_downloads=updates['repo_enable_downloads'],
937 repo_enable_downloads=updates['repo_enable_downloads'],
937 repo_enable_locking=updates['repo_enable_locking']))
938 repo_enable_locking=updates['repo_enable_locking']))
938 except validation_schema.Invalid as err:
939 except validation_schema.Invalid as err:
939 raise JSONRPCValidationError(colander_exc=err)
940 raise JSONRPCValidationError(colander_exc=err)
940
941
941 # save validated data back into the updates dict
942 # save validated data back into the updates dict
942 validated_updates = dict(
943 validated_updates = dict(
943 repo_name=schema_data['repo_group']['repo_name_without_group'],
944 repo_name=schema_data['repo_group']['repo_name_without_group'],
944 repo_group=schema_data['repo_group']['repo_group_id'],
945 repo_group=schema_data['repo_group']['repo_group_id'],
945
946
946 user=schema_data['repo_owner'],
947 user=schema_data['repo_owner'],
947 repo_description=schema_data['repo_description'],
948 repo_description=schema_data['repo_description'],
948 repo_private=schema_data['repo_private'],
949 repo_private=schema_data['repo_private'],
949 clone_uri=schema_data['repo_clone_uri'],
950 clone_uri=schema_data['repo_clone_uri'],
950 repo_landing_rev=schema_data['repo_landing_commit_ref'],
951 repo_landing_rev=schema_data['repo_landing_commit_ref'],
951 repo_enable_statistics=schema_data['repo_enable_statistics'],
952 repo_enable_statistics=schema_data['repo_enable_statistics'],
952 repo_enable_locking=schema_data['repo_enable_locking'],
953 repo_enable_locking=schema_data['repo_enable_locking'],
953 repo_enable_downloads=schema_data['repo_enable_downloads'],
954 repo_enable_downloads=schema_data['repo_enable_downloads'],
954 )
955 )
955
956
956 if schema_data['repo_fork_of']:
957 if schema_data['repo_fork_of']:
957 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
958 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
958 validated_updates['fork_id'] = fork_repo.repo_id
959 validated_updates['fork_id'] = fork_repo.repo_id
959
960
960 # extra fields
961 # extra fields
961 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
962 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
962 if fields:
963 if fields:
963 validated_updates.update(fields)
964 validated_updates.update(fields)
964
965
965 try:
966 try:
966 RepoModel().update(repo, **validated_updates)
967 RepoModel().update(repo, **validated_updates)
967 audit_logger.store_api(
968 audit_logger.store_api(
968 'repo.edit', action_data={'old_data': old_values},
969 'repo.edit', action_data={'old_data': old_values},
969 user=apiuser, repo=repo)
970 user=apiuser, repo=repo)
970 Session().commit()
971 Session().commit()
971 return {
972 return {
972 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
973 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
973 'repository': repo.get_api_data(include_secrets=include_secrets)
974 'repository': repo.get_api_data(include_secrets=include_secrets)
974 }
975 }
975 except Exception:
976 except Exception:
976 log.exception(
977 log.exception(
977 u"Exception while trying to update the repository %s",
978 u"Exception while trying to update the repository %s",
978 repoid)
979 repoid)
979 raise JSONRPCError('failed to update repo `%s`' % repoid)
980 raise JSONRPCError('failed to update repo `%s`' % repoid)
980
981
981
982
982 @jsonrpc_method()
983 @jsonrpc_method()
983 def fork_repo(request, apiuser, repoid, fork_name,
984 def fork_repo(request, apiuser, repoid, fork_name,
984 owner=Optional(OAttr('apiuser')),
985 owner=Optional(OAttr('apiuser')),
985 description=Optional(''),
986 description=Optional(''),
986 private=Optional(False),
987 private=Optional(False),
987 clone_uri=Optional(None),
988 clone_uri=Optional(None),
988 landing_rev=Optional('rev:tip'),
989 landing_rev=Optional('rev:tip'),
989 copy_permissions=Optional(False)):
990 copy_permissions=Optional(False)):
990 """
991 """
991 Creates a fork of the specified |repo|.
992 Creates a fork of the specified |repo|.
992
993
993 * If the fork_name contains "/", fork will be created inside
994 * If the fork_name contains "/", fork will be created inside
994 a repository group or nested repository groups
995 a repository group or nested repository groups
995
996
996 For example "foo/bar/fork-repo" will create fork called "fork-repo"
997 For example "foo/bar/fork-repo" will create fork called "fork-repo"
997 inside group "foo/bar". You have to have permissions to access and
998 inside group "foo/bar". You have to have permissions to access and
998 write to the last repository group ("bar" in this example)
999 write to the last repository group ("bar" in this example)
999
1000
1000 This command can only be run using an |authtoken| with minimum
1001 This command can only be run using an |authtoken| with minimum
1001 read permissions of the forked repo, create fork permissions for an user.
1002 read permissions of the forked repo, create fork permissions for an user.
1002
1003
1003 :param apiuser: This is filled automatically from the |authtoken|.
1004 :param apiuser: This is filled automatically from the |authtoken|.
1004 :type apiuser: AuthUser
1005 :type apiuser: AuthUser
1005 :param repoid: Set repository name or repository ID.
1006 :param repoid: Set repository name or repository ID.
1006 :type repoid: str or int
1007 :type repoid: str or int
1007 :param fork_name: Set the fork name, including it's repository group membership.
1008 :param fork_name: Set the fork name, including it's repository group membership.
1008 :type fork_name: str
1009 :type fork_name: str
1009 :param owner: Set the fork owner.
1010 :param owner: Set the fork owner.
1010 :type owner: str
1011 :type owner: str
1011 :param description: Set the fork description.
1012 :param description: Set the fork description.
1012 :type description: str
1013 :type description: str
1013 :param copy_permissions: Copy permissions from parent |repo|. The
1014 :param copy_permissions: Copy permissions from parent |repo|. The
1014 default is False.
1015 default is False.
1015 :type copy_permissions: bool
1016 :type copy_permissions: bool
1016 :param private: Make the fork private. The default is False.
1017 :param private: Make the fork private. The default is False.
1017 :type private: bool
1018 :type private: bool
1018 :param landing_rev: Set the landing revision. The default is tip.
1019 :param landing_rev: Set the landing revision. The default is tip.
1019
1020
1020 Example output:
1021 Example output:
1021
1022
1022 .. code-block:: bash
1023 .. code-block:: bash
1023
1024
1024 id : <id_for_response>
1025 id : <id_for_response>
1025 api_key : "<api_key>"
1026 api_key : "<api_key>"
1026 args: {
1027 args: {
1027 "repoid" : "<reponame or repo_id>",
1028 "repoid" : "<reponame or repo_id>",
1028 "fork_name": "<forkname>",
1029 "fork_name": "<forkname>",
1029 "owner": "<username or user_id = Optional(=apiuser)>",
1030 "owner": "<username or user_id = Optional(=apiuser)>",
1030 "description": "<description>",
1031 "description": "<description>",
1031 "copy_permissions": "<bool>",
1032 "copy_permissions": "<bool>",
1032 "private": "<bool>",
1033 "private": "<bool>",
1033 "landing_rev": "<landing_rev>"
1034 "landing_rev": "<landing_rev>"
1034 }
1035 }
1035
1036
1036 Example error output:
1037 Example error output:
1037
1038
1038 .. code-block:: bash
1039 .. code-block:: bash
1039
1040
1040 id : <id_given_in_input>
1041 id : <id_given_in_input>
1041 result: {
1042 result: {
1042 "msg": "Created fork of `<reponame>` as `<forkname>`",
1043 "msg": "Created fork of `<reponame>` as `<forkname>`",
1043 "success": true,
1044 "success": true,
1044 "task": "<celery task id or None if done sync>"
1045 "task": "<celery task id or None if done sync>"
1045 }
1046 }
1046 error: null
1047 error: null
1047
1048
1048 """
1049 """
1049
1050
1050 repo = get_repo_or_error(repoid)
1051 repo = get_repo_or_error(repoid)
1051 repo_name = repo.repo_name
1052 repo_name = repo.repo_name
1052
1053
1053 if not has_superadmin_permission(apiuser):
1054 if not has_superadmin_permission(apiuser):
1054 # check if we have at least read permission for
1055 # check if we have at least read permission for
1055 # this repo that we fork !
1056 # this repo that we fork !
1056 _perms = (
1057 _perms = (
1057 'repository.admin', 'repository.write', 'repository.read')
1058 'repository.admin', 'repository.write', 'repository.read')
1058 validate_repo_permissions(apiuser, repoid, repo, _perms)
1059 validate_repo_permissions(apiuser, repoid, repo, _perms)
1059
1060
1060 # check if the regular user has at least fork permissions as well
1061 # check if the regular user has at least fork permissions as well
1061 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1062 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1062 raise JSONRPCForbidden()
1063 raise JSONRPCForbidden()
1063
1064
1064 # check if user can set owner parameter
1065 # check if user can set owner parameter
1065 owner = validate_set_owner_permissions(apiuser, owner)
1066 owner = validate_set_owner_permissions(apiuser, owner)
1066
1067
1067 description = Optional.extract(description)
1068 description = Optional.extract(description)
1068 copy_permissions = Optional.extract(copy_permissions)
1069 copy_permissions = Optional.extract(copy_permissions)
1069 clone_uri = Optional.extract(clone_uri)
1070 clone_uri = Optional.extract(clone_uri)
1070 landing_commit_ref = Optional.extract(landing_rev)
1071 landing_commit_ref = Optional.extract(landing_rev)
1071 private = Optional.extract(private)
1072 private = Optional.extract(private)
1072
1073
1073 schema = repo_schema.RepoSchema().bind(
1074 schema = repo_schema.RepoSchema().bind(
1074 repo_type_options=rhodecode.BACKENDS.keys(),
1075 repo_type_options=rhodecode.BACKENDS.keys(),
1075 # user caller
1076 # user caller
1076 user=apiuser)
1077 user=apiuser)
1077
1078
1078 try:
1079 try:
1079 schema_data = schema.deserialize(dict(
1080 schema_data = schema.deserialize(dict(
1080 repo_name=fork_name,
1081 repo_name=fork_name,
1081 repo_type=repo.repo_type,
1082 repo_type=repo.repo_type,
1082 repo_owner=owner.username,
1083 repo_owner=owner.username,
1083 repo_description=description,
1084 repo_description=description,
1084 repo_landing_commit_ref=landing_commit_ref,
1085 repo_landing_commit_ref=landing_commit_ref,
1085 repo_clone_uri=clone_uri,
1086 repo_clone_uri=clone_uri,
1086 repo_private=private,
1087 repo_private=private,
1087 repo_copy_permissions=copy_permissions))
1088 repo_copy_permissions=copy_permissions))
1088 except validation_schema.Invalid as err:
1089 except validation_schema.Invalid as err:
1089 raise JSONRPCValidationError(colander_exc=err)
1090 raise JSONRPCValidationError(colander_exc=err)
1090
1091
1091 try:
1092 try:
1092 data = {
1093 data = {
1093 'fork_parent_id': repo.repo_id,
1094 'fork_parent_id': repo.repo_id,
1094
1095
1095 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1096 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1096 'repo_name_full': schema_data['repo_name'],
1097 'repo_name_full': schema_data['repo_name'],
1097 'repo_group': schema_data['repo_group']['repo_group_id'],
1098 'repo_group': schema_data['repo_group']['repo_group_id'],
1098 'repo_type': schema_data['repo_type'],
1099 'repo_type': schema_data['repo_type'],
1099 'description': schema_data['repo_description'],
1100 'description': schema_data['repo_description'],
1100 'private': schema_data['repo_private'],
1101 'private': schema_data['repo_private'],
1101 'copy_permissions': schema_data['repo_copy_permissions'],
1102 'copy_permissions': schema_data['repo_copy_permissions'],
1102 'landing_rev': schema_data['repo_landing_commit_ref'],
1103 'landing_rev': schema_data['repo_landing_commit_ref'],
1103 }
1104 }
1104
1105
1105 task = RepoModel().create_fork(data, cur_user=owner)
1106 task = RepoModel().create_fork(data, cur_user=owner)
1106 # no commit, it's done in RepoModel, or async via celery
1107 # no commit, it's done in RepoModel, or async via celery
1107 from celery.result import BaseAsyncResult
1108 from celery.result import BaseAsyncResult
1108 task_id = None
1109 task_id = None
1109 if isinstance(task, BaseAsyncResult):
1110 if isinstance(task, BaseAsyncResult):
1110 task_id = task.task_id
1111 task_id = task.task_id
1111 return {
1112 return {
1112 'msg': 'Created fork of `%s` as `%s`' % (
1113 'msg': 'Created fork of `%s` as `%s`' % (
1113 repo.repo_name, schema_data['repo_name']),
1114 repo.repo_name, schema_data['repo_name']),
1114 'success': True, # cannot return the repo data here since fork
1115 'success': True, # cannot return the repo data here since fork
1115 # can be done async
1116 # can be done async
1116 'task': task_id
1117 'task': task_id
1117 }
1118 }
1118 except Exception:
1119 except Exception:
1119 log.exception(
1120 log.exception(
1120 u"Exception while trying to create fork %s",
1121 u"Exception while trying to create fork %s",
1121 schema_data['repo_name'])
1122 schema_data['repo_name'])
1122 raise JSONRPCError(
1123 raise JSONRPCError(
1123 'failed to fork repository `%s` as `%s`' % (
1124 'failed to fork repository `%s` as `%s`' % (
1124 repo_name, schema_data['repo_name']))
1125 repo_name, schema_data['repo_name']))
1125
1126
1126
1127
1127 @jsonrpc_method()
1128 @jsonrpc_method()
1128 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1129 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1129 """
1130 """
1130 Deletes a repository.
1131 Deletes a repository.
1131
1132
1132 * When the `forks` parameter is set it's possible to detach or delete
1133 * When the `forks` parameter is set it's possible to detach or delete
1133 forks of deleted repository.
1134 forks of deleted repository.
1134
1135
1135 This command can only be run using an |authtoken| with admin
1136 This command can only be run using an |authtoken| with admin
1136 permissions on the |repo|.
1137 permissions on the |repo|.
1137
1138
1138 :param apiuser: This is filled automatically from the |authtoken|.
1139 :param apiuser: This is filled automatically from the |authtoken|.
1139 :type apiuser: AuthUser
1140 :type apiuser: AuthUser
1140 :param repoid: Set the repository name or repository ID.
1141 :param repoid: Set the repository name or repository ID.
1141 :type repoid: str or int
1142 :type repoid: str or int
1142 :param forks: Set to `detach` or `delete` forks from the |repo|.
1143 :param forks: Set to `detach` or `delete` forks from the |repo|.
1143 :type forks: Optional(str)
1144 :type forks: Optional(str)
1144
1145
1145 Example error output:
1146 Example error output:
1146
1147
1147 .. code-block:: bash
1148 .. code-block:: bash
1148
1149
1149 id : <id_given_in_input>
1150 id : <id_given_in_input>
1150 result: {
1151 result: {
1151 "msg": "Deleted repository `<reponame>`",
1152 "msg": "Deleted repository `<reponame>`",
1152 "success": true
1153 "success": true
1153 }
1154 }
1154 error: null
1155 error: null
1155 """
1156 """
1156
1157
1157 repo = get_repo_or_error(repoid)
1158 repo = get_repo_or_error(repoid)
1158 repo_name = repo.repo_name
1159 repo_name = repo.repo_name
1159 if not has_superadmin_permission(apiuser):
1160 if not has_superadmin_permission(apiuser):
1160 _perms = ('repository.admin',)
1161 _perms = ('repository.admin',)
1161 validate_repo_permissions(apiuser, repoid, repo, _perms)
1162 validate_repo_permissions(apiuser, repoid, repo, _perms)
1162
1163
1163 try:
1164 try:
1164 handle_forks = Optional.extract(forks)
1165 handle_forks = Optional.extract(forks)
1165 _forks_msg = ''
1166 _forks_msg = ''
1166 _forks = [f for f in repo.forks]
1167 _forks = [f for f in repo.forks]
1167 if handle_forks == 'detach':
1168 if handle_forks == 'detach':
1168 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1169 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1169 elif handle_forks == 'delete':
1170 elif handle_forks == 'delete':
1170 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1171 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1171 elif _forks:
1172 elif _forks:
1172 raise JSONRPCError(
1173 raise JSONRPCError(
1173 'Cannot delete `%s` it still contains attached forks' %
1174 'Cannot delete `%s` it still contains attached forks' %
1174 (repo.repo_name,)
1175 (repo.repo_name,)
1175 )
1176 )
1176 old_data = repo.get_api_data()
1177 old_data = repo.get_api_data()
1177 RepoModel().delete(repo, forks=forks)
1178 RepoModel().delete(repo, forks=forks)
1178
1179
1179 repo = audit_logger.RepoWrap(repo_id=None,
1180 repo = audit_logger.RepoWrap(repo_id=None,
1180 repo_name=repo.repo_name)
1181 repo_name=repo.repo_name)
1181
1182
1182 audit_logger.store_api(
1183 audit_logger.store_api(
1183 'repo.delete', action_data={'old_data': old_data},
1184 'repo.delete', action_data={'old_data': old_data},
1184 user=apiuser, repo=repo)
1185 user=apiuser, repo=repo)
1185
1186
1186 ScmModel().mark_for_invalidation(repo_name, delete=True)
1187 ScmModel().mark_for_invalidation(repo_name, delete=True)
1187 Session().commit()
1188 Session().commit()
1188 return {
1189 return {
1189 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1190 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1190 'success': True
1191 'success': True
1191 }
1192 }
1192 except Exception:
1193 except Exception:
1193 log.exception("Exception occurred while trying to delete repo")
1194 log.exception("Exception occurred while trying to delete repo")
1194 raise JSONRPCError(
1195 raise JSONRPCError(
1195 'failed to delete repository `%s`' % (repo_name,)
1196 'failed to delete repository `%s`' % (repo_name,)
1196 )
1197 )
1197
1198
1198
1199
1199 #TODO: marcink, change name ?
1200 #TODO: marcink, change name ?
1200 @jsonrpc_method()
1201 @jsonrpc_method()
1201 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1202 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1202 """
1203 """
1203 Invalidates the cache for the specified repository.
1204 Invalidates the cache for the specified repository.
1204
1205
1205 This command can only be run using an |authtoken| with admin rights to
1206 This command can only be run using an |authtoken| with admin rights to
1206 the specified repository.
1207 the specified repository.
1207
1208
1208 This command takes the following options:
1209 This command takes the following options:
1209
1210
1210 :param apiuser: This is filled automatically from |authtoken|.
1211 :param apiuser: This is filled automatically from |authtoken|.
1211 :type apiuser: AuthUser
1212 :type apiuser: AuthUser
1212 :param repoid: Sets the repository name or repository ID.
1213 :param repoid: Sets the repository name or repository ID.
1213 :type repoid: str or int
1214 :type repoid: str or int
1214 :param delete_keys: This deletes the invalidated keys instead of
1215 :param delete_keys: This deletes the invalidated keys instead of
1215 just flagging them.
1216 just flagging them.
1216 :type delete_keys: Optional(``True`` | ``False``)
1217 :type delete_keys: Optional(``True`` | ``False``)
1217
1218
1218 Example output:
1219 Example output:
1219
1220
1220 .. code-block:: bash
1221 .. code-block:: bash
1221
1222
1222 id : <id_given_in_input>
1223 id : <id_given_in_input>
1223 result : {
1224 result : {
1224 'msg': Cache for repository `<repository name>` was invalidated,
1225 'msg': Cache for repository `<repository name>` was invalidated,
1225 'repository': <repository name>
1226 'repository': <repository name>
1226 }
1227 }
1227 error : null
1228 error : null
1228
1229
1229 Example error output:
1230 Example error output:
1230
1231
1231 .. code-block:: bash
1232 .. code-block:: bash
1232
1233
1233 id : <id_given_in_input>
1234 id : <id_given_in_input>
1234 result : null
1235 result : null
1235 error : {
1236 error : {
1236 'Error occurred during cache invalidation action'
1237 'Error occurred during cache invalidation action'
1237 }
1238 }
1238
1239
1239 """
1240 """
1240
1241
1241 repo = get_repo_or_error(repoid)
1242 repo = get_repo_or_error(repoid)
1242 if not has_superadmin_permission(apiuser):
1243 if not has_superadmin_permission(apiuser):
1243 _perms = ('repository.admin', 'repository.write',)
1244 _perms = ('repository.admin', 'repository.write',)
1244 validate_repo_permissions(apiuser, repoid, repo, _perms)
1245 validate_repo_permissions(apiuser, repoid, repo, _perms)
1245
1246
1246 delete = Optional.extract(delete_keys)
1247 delete = Optional.extract(delete_keys)
1247 try:
1248 try:
1248 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1249 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1249 return {
1250 return {
1250 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1251 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1251 'repository': repo.repo_name
1252 'repository': repo.repo_name
1252 }
1253 }
1253 except Exception:
1254 except Exception:
1254 log.exception(
1255 log.exception(
1255 "Exception occurred while trying to invalidate repo cache")
1256 "Exception occurred while trying to invalidate repo cache")
1256 raise JSONRPCError(
1257 raise JSONRPCError(
1257 'Error occurred during cache invalidation action'
1258 'Error occurred during cache invalidation action'
1258 )
1259 )
1259
1260
1260
1261
1261 #TODO: marcink, change name ?
1262 #TODO: marcink, change name ?
1262 @jsonrpc_method()
1263 @jsonrpc_method()
1263 def lock(request, apiuser, repoid, locked=Optional(None),
1264 def lock(request, apiuser, repoid, locked=Optional(None),
1264 userid=Optional(OAttr('apiuser'))):
1265 userid=Optional(OAttr('apiuser'))):
1265 """
1266 """
1266 Sets the lock state of the specified |repo| by the given user.
1267 Sets the lock state of the specified |repo| by the given user.
1267 From more information, see :ref:`repo-locking`.
1268 From more information, see :ref:`repo-locking`.
1268
1269
1269 * If the ``userid`` option is not set, the repository is locked to the
1270 * If the ``userid`` option is not set, the repository is locked to the
1270 user who called the method.
1271 user who called the method.
1271 * If the ``locked`` parameter is not set, the current lock state of the
1272 * If the ``locked`` parameter is not set, the current lock state of the
1272 repository is displayed.
1273 repository is displayed.
1273
1274
1274 This command can only be run using an |authtoken| with admin rights to
1275 This command can only be run using an |authtoken| with admin rights to
1275 the specified repository.
1276 the specified repository.
1276
1277
1277 This command takes the following options:
1278 This command takes the following options:
1278
1279
1279 :param apiuser: This is filled automatically from the |authtoken|.
1280 :param apiuser: This is filled automatically from the |authtoken|.
1280 :type apiuser: AuthUser
1281 :type apiuser: AuthUser
1281 :param repoid: Sets the repository name or repository ID.
1282 :param repoid: Sets the repository name or repository ID.
1282 :type repoid: str or int
1283 :type repoid: str or int
1283 :param locked: Sets the lock state.
1284 :param locked: Sets the lock state.
1284 :type locked: Optional(``True`` | ``False``)
1285 :type locked: Optional(``True`` | ``False``)
1285 :param userid: Set the repository lock to this user.
1286 :param userid: Set the repository lock to this user.
1286 :type userid: Optional(str or int)
1287 :type userid: Optional(str or int)
1287
1288
1288 Example error output:
1289 Example error output:
1289
1290
1290 .. code-block:: bash
1291 .. code-block:: bash
1291
1292
1292 id : <id_given_in_input>
1293 id : <id_given_in_input>
1293 result : {
1294 result : {
1294 'repo': '<reponame>',
1295 'repo': '<reponame>',
1295 'locked': <bool: lock state>,
1296 'locked': <bool: lock state>,
1296 'locked_since': <int: lock timestamp>,
1297 'locked_since': <int: lock timestamp>,
1297 'locked_by': <username of person who made the lock>,
1298 'locked_by': <username of person who made the lock>,
1298 'lock_reason': <str: reason for locking>,
1299 'lock_reason': <str: reason for locking>,
1299 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1300 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1300 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1301 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1301 or
1302 or
1302 'msg': 'Repo `<repository name>` not locked.'
1303 'msg': 'Repo `<repository name>` not locked.'
1303 or
1304 or
1304 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1305 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1305 }
1306 }
1306 error : null
1307 error : null
1307
1308
1308 Example error output:
1309 Example error output:
1309
1310
1310 .. code-block:: bash
1311 .. code-block:: bash
1311
1312
1312 id : <id_given_in_input>
1313 id : <id_given_in_input>
1313 result : null
1314 result : null
1314 error : {
1315 error : {
1315 'Error occurred locking repository `<reponame>`'
1316 'Error occurred locking repository `<reponame>`'
1316 }
1317 }
1317 """
1318 """
1318
1319
1319 repo = get_repo_or_error(repoid)
1320 repo = get_repo_or_error(repoid)
1320 if not has_superadmin_permission(apiuser):
1321 if not has_superadmin_permission(apiuser):
1321 # check if we have at least write permission for this repo !
1322 # check if we have at least write permission for this repo !
1322 _perms = ('repository.admin', 'repository.write',)
1323 _perms = ('repository.admin', 'repository.write',)
1323 validate_repo_permissions(apiuser, repoid, repo, _perms)
1324 validate_repo_permissions(apiuser, repoid, repo, _perms)
1324
1325
1325 # make sure normal user does not pass someone else userid,
1326 # make sure normal user does not pass someone else userid,
1326 # he is not allowed to do that
1327 # he is not allowed to do that
1327 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1328 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1328 raise JSONRPCError('userid is not the same as your user')
1329 raise JSONRPCError('userid is not the same as your user')
1329
1330
1330 if isinstance(userid, Optional):
1331 if isinstance(userid, Optional):
1331 userid = apiuser.user_id
1332 userid = apiuser.user_id
1332
1333
1333 user = get_user_or_error(userid)
1334 user = get_user_or_error(userid)
1334
1335
1335 if isinstance(locked, Optional):
1336 if isinstance(locked, Optional):
1336 lockobj = repo.locked
1337 lockobj = repo.locked
1337
1338
1338 if lockobj[0] is None:
1339 if lockobj[0] is None:
1339 _d = {
1340 _d = {
1340 'repo': repo.repo_name,
1341 'repo': repo.repo_name,
1341 'locked': False,
1342 'locked': False,
1342 'locked_since': None,
1343 'locked_since': None,
1343 'locked_by': None,
1344 'locked_by': None,
1344 'lock_reason': None,
1345 'lock_reason': None,
1345 'lock_state_changed': False,
1346 'lock_state_changed': False,
1346 'msg': 'Repo `%s` not locked.' % repo.repo_name
1347 'msg': 'Repo `%s` not locked.' % repo.repo_name
1347 }
1348 }
1348 return _d
1349 return _d
1349 else:
1350 else:
1350 _user_id, _time, _reason = lockobj
1351 _user_id, _time, _reason = lockobj
1351 lock_user = get_user_or_error(userid)
1352 lock_user = get_user_or_error(userid)
1352 _d = {
1353 _d = {
1353 'repo': repo.repo_name,
1354 'repo': repo.repo_name,
1354 'locked': True,
1355 'locked': True,
1355 'locked_since': _time,
1356 'locked_since': _time,
1356 'locked_by': lock_user.username,
1357 'locked_by': lock_user.username,
1357 'lock_reason': _reason,
1358 'lock_reason': _reason,
1358 'lock_state_changed': False,
1359 'lock_state_changed': False,
1359 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1360 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1360 % (repo.repo_name, lock_user.username,
1361 % (repo.repo_name, lock_user.username,
1361 json.dumps(time_to_datetime(_time))))
1362 json.dumps(time_to_datetime(_time))))
1362 }
1363 }
1363 return _d
1364 return _d
1364
1365
1365 # force locked state through a flag
1366 # force locked state through a flag
1366 else:
1367 else:
1367 locked = str2bool(locked)
1368 locked = str2bool(locked)
1368 lock_reason = Repository.LOCK_API
1369 lock_reason = Repository.LOCK_API
1369 try:
1370 try:
1370 if locked:
1371 if locked:
1371 lock_time = time.time()
1372 lock_time = time.time()
1372 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1373 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1373 else:
1374 else:
1374 lock_time = None
1375 lock_time = None
1375 Repository.unlock(repo)
1376 Repository.unlock(repo)
1376 _d = {
1377 _d = {
1377 'repo': repo.repo_name,
1378 'repo': repo.repo_name,
1378 'locked': locked,
1379 'locked': locked,
1379 'locked_since': lock_time,
1380 'locked_since': lock_time,
1380 'locked_by': user.username,
1381 'locked_by': user.username,
1381 'lock_reason': lock_reason,
1382 'lock_reason': lock_reason,
1382 'lock_state_changed': True,
1383 'lock_state_changed': True,
1383 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1384 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1384 % (user.username, repo.repo_name, locked))
1385 % (user.username, repo.repo_name, locked))
1385 }
1386 }
1386 return _d
1387 return _d
1387 except Exception:
1388 except Exception:
1388 log.exception(
1389 log.exception(
1389 "Exception occurred while trying to lock repository")
1390 "Exception occurred while trying to lock repository")
1390 raise JSONRPCError(
1391 raise JSONRPCError(
1391 'Error occurred locking repository `%s`' % repo.repo_name
1392 'Error occurred locking repository `%s`' % repo.repo_name
1392 )
1393 )
1393
1394
1394
1395
1395 @jsonrpc_method()
1396 @jsonrpc_method()
1396 def comment_commit(
1397 def comment_commit(
1397 request, apiuser, repoid, commit_id, message, status=Optional(None),
1398 request, apiuser, repoid, commit_id, message, status=Optional(None),
1398 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1399 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1399 resolves_comment_id=Optional(None),
1400 resolves_comment_id=Optional(None),
1400 userid=Optional(OAttr('apiuser'))):
1401 userid=Optional(OAttr('apiuser'))):
1401 """
1402 """
1402 Set a commit comment, and optionally change the status of the commit.
1403 Set a commit comment, and optionally change the status of the commit.
1403
1404
1404 :param apiuser: This is filled automatically from the |authtoken|.
1405 :param apiuser: This is filled automatically from the |authtoken|.
1405 :type apiuser: AuthUser
1406 :type apiuser: AuthUser
1406 :param repoid: Set the repository name or repository ID.
1407 :param repoid: Set the repository name or repository ID.
1407 :type repoid: str or int
1408 :type repoid: str or int
1408 :param commit_id: Specify the commit_id for which to set a comment.
1409 :param commit_id: Specify the commit_id for which to set a comment.
1409 :type commit_id: str
1410 :type commit_id: str
1410 :param message: The comment text.
1411 :param message: The comment text.
1411 :type message: str
1412 :type message: str
1412 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1413 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1413 'approved', 'rejected', 'under_review'
1414 'approved', 'rejected', 'under_review'
1414 :type status: str
1415 :type status: str
1415 :param comment_type: Comment type, one of: 'note', 'todo'
1416 :param comment_type: Comment type, one of: 'note', 'todo'
1416 :type comment_type: Optional(str), default: 'note'
1417 :type comment_type: Optional(str), default: 'note'
1417 :param userid: Set the user name of the comment creator.
1418 :param userid: Set the user name of the comment creator.
1418 :type userid: Optional(str or int)
1419 :type userid: Optional(str or int)
1419
1420
1420 Example error output:
1421 Example error output:
1421
1422
1422 .. code-block:: bash
1423 .. code-block:: bash
1423
1424
1424 {
1425 {
1425 "id" : <id_given_in_input>,
1426 "id" : <id_given_in_input>,
1426 "result" : {
1427 "result" : {
1427 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1428 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1428 "status_change": null or <status>,
1429 "status_change": null or <status>,
1429 "success": true
1430 "success": true
1430 },
1431 },
1431 "error" : null
1432 "error" : null
1432 }
1433 }
1433
1434
1434 """
1435 """
1435 repo = get_repo_or_error(repoid)
1436 repo = get_repo_or_error(repoid)
1436 if not has_superadmin_permission(apiuser):
1437 if not has_superadmin_permission(apiuser):
1437 _perms = ('repository.read', 'repository.write', 'repository.admin')
1438 _perms = ('repository.read', 'repository.write', 'repository.admin')
1438 validate_repo_permissions(apiuser, repoid, repo, _perms)
1439 validate_repo_permissions(apiuser, repoid, repo, _perms)
1439
1440
1440 try:
1441 try:
1441 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1442 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1442 except Exception as e:
1443 except Exception as e:
1443 log.exception('Failed to fetch commit')
1444 log.exception('Failed to fetch commit')
1444 raise JSONRPCError(e.message)
1445 raise JSONRPCError(e.message)
1445
1446
1446 if isinstance(userid, Optional):
1447 if isinstance(userid, Optional):
1447 userid = apiuser.user_id
1448 userid = apiuser.user_id
1448
1449
1449 user = get_user_or_error(userid)
1450 user = get_user_or_error(userid)
1450 status = Optional.extract(status)
1451 status = Optional.extract(status)
1451 comment_type = Optional.extract(comment_type)
1452 comment_type = Optional.extract(comment_type)
1452 resolves_comment_id = Optional.extract(resolves_comment_id)
1453 resolves_comment_id = Optional.extract(resolves_comment_id)
1453
1454
1454 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1455 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1455 if status and status not in allowed_statuses:
1456 if status and status not in allowed_statuses:
1456 raise JSONRPCError('Bad status, must be on '
1457 raise JSONRPCError('Bad status, must be on '
1457 'of %s got %s' % (allowed_statuses, status,))
1458 'of %s got %s' % (allowed_statuses, status,))
1458
1459
1459 if resolves_comment_id:
1460 if resolves_comment_id:
1460 comment = ChangesetComment.get(resolves_comment_id)
1461 comment = ChangesetComment.get(resolves_comment_id)
1461 if not comment:
1462 if not comment:
1462 raise JSONRPCError(
1463 raise JSONRPCError(
1463 'Invalid resolves_comment_id `%s` for this commit.'
1464 'Invalid resolves_comment_id `%s` for this commit.'
1464 % resolves_comment_id)
1465 % resolves_comment_id)
1465 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1466 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1466 raise JSONRPCError(
1467 raise JSONRPCError(
1467 'Comment `%s` is wrong type for setting status to resolved.'
1468 'Comment `%s` is wrong type for setting status to resolved.'
1468 % resolves_comment_id)
1469 % resolves_comment_id)
1469
1470
1470 try:
1471 try:
1471 rc_config = SettingsModel().get_all_settings()
1472 rc_config = SettingsModel().get_all_settings()
1472 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1473 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1473 status_change_label = ChangesetStatus.get_status_lbl(status)
1474 status_change_label = ChangesetStatus.get_status_lbl(status)
1474 comment = CommentsModel().create(
1475 comment = CommentsModel().create(
1475 message, repo, user, commit_id=commit_id,
1476 message, repo, user, commit_id=commit_id,
1476 status_change=status_change_label,
1477 status_change=status_change_label,
1477 status_change_type=status,
1478 status_change_type=status,
1478 renderer=renderer,
1479 renderer=renderer,
1479 comment_type=comment_type,
1480 comment_type=comment_type,
1480 resolves_comment_id=resolves_comment_id
1481 resolves_comment_id=resolves_comment_id
1481 )
1482 )
1482 if status:
1483 if status:
1483 # also do a status change
1484 # also do a status change
1484 try:
1485 try:
1485 ChangesetStatusModel().set_status(
1486 ChangesetStatusModel().set_status(
1486 repo, status, user, comment, revision=commit_id,
1487 repo, status, user, comment, revision=commit_id,
1487 dont_allow_on_closed_pull_request=True
1488 dont_allow_on_closed_pull_request=True
1488 )
1489 )
1489 except StatusChangeOnClosedPullRequestError:
1490 except StatusChangeOnClosedPullRequestError:
1490 log.exception(
1491 log.exception(
1491 "Exception occurred while trying to change repo commit status")
1492 "Exception occurred while trying to change repo commit status")
1492 msg = ('Changing status on a changeset associated with '
1493 msg = ('Changing status on a changeset associated with '
1493 'a closed pull request is not allowed')
1494 'a closed pull request is not allowed')
1494 raise JSONRPCError(msg)
1495 raise JSONRPCError(msg)
1495
1496
1496 Session().commit()
1497 Session().commit()
1497 return {
1498 return {
1498 'msg': (
1499 'msg': (
1499 'Commented on commit `%s` for repository `%s`' % (
1500 'Commented on commit `%s` for repository `%s`' % (
1500 comment.revision, repo.repo_name)),
1501 comment.revision, repo.repo_name)),
1501 'status_change': status,
1502 'status_change': status,
1502 'success': True,
1503 'success': True,
1503 }
1504 }
1504 except JSONRPCError:
1505 except JSONRPCError:
1505 # catch any inside errors, and re-raise them to prevent from
1506 # catch any inside errors, and re-raise them to prevent from
1506 # below global catch to silence them
1507 # below global catch to silence them
1507 raise
1508 raise
1508 except Exception:
1509 except Exception:
1509 log.exception("Exception occurred while trying to comment on commit")
1510 log.exception("Exception occurred while trying to comment on commit")
1510 raise JSONRPCError(
1511 raise JSONRPCError(
1511 'failed to set comment on repository `%s`' % (repo.repo_name,)
1512 'failed to set comment on repository `%s`' % (repo.repo_name,)
1512 )
1513 )
1513
1514
1514
1515
1515 @jsonrpc_method()
1516 @jsonrpc_method()
1516 def grant_user_permission(request, apiuser, repoid, userid, perm):
1517 def grant_user_permission(request, apiuser, repoid, userid, perm):
1517 """
1518 """
1518 Grant permissions for the specified user on the given repository,
1519 Grant permissions for the specified user on the given repository,
1519 or update existing permissions if found.
1520 or update existing permissions if found.
1520
1521
1521 This command can only be run using an |authtoken| with admin
1522 This command can only be run using an |authtoken| with admin
1522 permissions on the |repo|.
1523 permissions on the |repo|.
1523
1524
1524 :param apiuser: This is filled automatically from the |authtoken|.
1525 :param apiuser: This is filled automatically from the |authtoken|.
1525 :type apiuser: AuthUser
1526 :type apiuser: AuthUser
1526 :param repoid: Set the repository name or repository ID.
1527 :param repoid: Set the repository name or repository ID.
1527 :type repoid: str or int
1528 :type repoid: str or int
1528 :param userid: Set the user name.
1529 :param userid: Set the user name.
1529 :type userid: str
1530 :type userid: str
1530 :param perm: Set the user permissions, using the following format
1531 :param perm: Set the user permissions, using the following format
1531 ``(repository.(none|read|write|admin))``
1532 ``(repository.(none|read|write|admin))``
1532 :type perm: str
1533 :type perm: str
1533
1534
1534 Example output:
1535 Example output:
1535
1536
1536 .. code-block:: bash
1537 .. code-block:: bash
1537
1538
1538 id : <id_given_in_input>
1539 id : <id_given_in_input>
1539 result: {
1540 result: {
1540 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1541 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1541 "success": true
1542 "success": true
1542 }
1543 }
1543 error: null
1544 error: null
1544 """
1545 """
1545
1546
1546 repo = get_repo_or_error(repoid)
1547 repo = get_repo_or_error(repoid)
1547 user = get_user_or_error(userid)
1548 user = get_user_or_error(userid)
1548 perm = get_perm_or_error(perm)
1549 perm = get_perm_or_error(perm)
1549 if not has_superadmin_permission(apiuser):
1550 if not has_superadmin_permission(apiuser):
1550 _perms = ('repository.admin',)
1551 _perms = ('repository.admin',)
1551 validate_repo_permissions(apiuser, repoid, repo, _perms)
1552 validate_repo_permissions(apiuser, repoid, repo, _perms)
1552
1553
1553 try:
1554 try:
1554
1555
1555 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1556 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1556
1557
1557 Session().commit()
1558 Session().commit()
1558 return {
1559 return {
1559 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1560 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1560 perm.permission_name, user.username, repo.repo_name
1561 perm.permission_name, user.username, repo.repo_name
1561 ),
1562 ),
1562 'success': True
1563 'success': True
1563 }
1564 }
1564 except Exception:
1565 except Exception:
1565 log.exception(
1566 log.exception(
1566 "Exception occurred while trying edit permissions for repo")
1567 "Exception occurred while trying edit permissions for repo")
1567 raise JSONRPCError(
1568 raise JSONRPCError(
1568 'failed to edit permission for user: `%s` in repo: `%s`' % (
1569 'failed to edit permission for user: `%s` in repo: `%s`' % (
1569 userid, repoid
1570 userid, repoid
1570 )
1571 )
1571 )
1572 )
1572
1573
1573
1574
1574 @jsonrpc_method()
1575 @jsonrpc_method()
1575 def revoke_user_permission(request, apiuser, repoid, userid):
1576 def revoke_user_permission(request, apiuser, repoid, userid):
1576 """
1577 """
1577 Revoke permission for a user on the specified repository.
1578 Revoke permission for a user on the specified repository.
1578
1579
1579 This command can only be run using an |authtoken| with admin
1580 This command can only be run using an |authtoken| with admin
1580 permissions on the |repo|.
1581 permissions on the |repo|.
1581
1582
1582 :param apiuser: This is filled automatically from the |authtoken|.
1583 :param apiuser: This is filled automatically from the |authtoken|.
1583 :type apiuser: AuthUser
1584 :type apiuser: AuthUser
1584 :param repoid: Set the repository name or repository ID.
1585 :param repoid: Set the repository name or repository ID.
1585 :type repoid: str or int
1586 :type repoid: str or int
1586 :param userid: Set the user name of revoked user.
1587 :param userid: Set the user name of revoked user.
1587 :type userid: str or int
1588 :type userid: str or int
1588
1589
1589 Example error output:
1590 Example error output:
1590
1591
1591 .. code-block:: bash
1592 .. code-block:: bash
1592
1593
1593 id : <id_given_in_input>
1594 id : <id_given_in_input>
1594 result: {
1595 result: {
1595 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1596 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1596 "success": true
1597 "success": true
1597 }
1598 }
1598 error: null
1599 error: null
1599 """
1600 """
1600
1601
1601 repo = get_repo_or_error(repoid)
1602 repo = get_repo_or_error(repoid)
1602 user = get_user_or_error(userid)
1603 user = get_user_or_error(userid)
1603 if not has_superadmin_permission(apiuser):
1604 if not has_superadmin_permission(apiuser):
1604 _perms = ('repository.admin',)
1605 _perms = ('repository.admin',)
1605 validate_repo_permissions(apiuser, repoid, repo, _perms)
1606 validate_repo_permissions(apiuser, repoid, repo, _perms)
1606
1607
1607 try:
1608 try:
1608 RepoModel().revoke_user_permission(repo=repo, user=user)
1609 RepoModel().revoke_user_permission(repo=repo, user=user)
1609 Session().commit()
1610 Session().commit()
1610 return {
1611 return {
1611 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1612 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1612 user.username, repo.repo_name
1613 user.username, repo.repo_name
1613 ),
1614 ),
1614 'success': True
1615 'success': True
1615 }
1616 }
1616 except Exception:
1617 except Exception:
1617 log.exception(
1618 log.exception(
1618 "Exception occurred while trying revoke permissions to repo")
1619 "Exception occurred while trying revoke permissions to repo")
1619 raise JSONRPCError(
1620 raise JSONRPCError(
1620 'failed to edit permission for user: `%s` in repo: `%s`' % (
1621 'failed to edit permission for user: `%s` in repo: `%s`' % (
1621 userid, repoid
1622 userid, repoid
1622 )
1623 )
1623 )
1624 )
1624
1625
1625
1626
1626 @jsonrpc_method()
1627 @jsonrpc_method()
1627 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1628 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1628 """
1629 """
1629 Grant permission for a user group on the specified repository,
1630 Grant permission for a user group on the specified repository,
1630 or update existing permissions.
1631 or update existing permissions.
1631
1632
1632 This command can only be run using an |authtoken| with admin
1633 This command can only be run using an |authtoken| with admin
1633 permissions on the |repo|.
1634 permissions on the |repo|.
1634
1635
1635 :param apiuser: This is filled automatically from the |authtoken|.
1636 :param apiuser: This is filled automatically from the |authtoken|.
1636 :type apiuser: AuthUser
1637 :type apiuser: AuthUser
1637 :param repoid: Set the repository name or repository ID.
1638 :param repoid: Set the repository name or repository ID.
1638 :type repoid: str or int
1639 :type repoid: str or int
1639 :param usergroupid: Specify the ID of the user group.
1640 :param usergroupid: Specify the ID of the user group.
1640 :type usergroupid: str or int
1641 :type usergroupid: str or int
1641 :param perm: Set the user group permissions using the following
1642 :param perm: Set the user group permissions using the following
1642 format: (repository.(none|read|write|admin))
1643 format: (repository.(none|read|write|admin))
1643 :type perm: str
1644 :type perm: str
1644
1645
1645 Example output:
1646 Example output:
1646
1647
1647 .. code-block:: bash
1648 .. code-block:: bash
1648
1649
1649 id : <id_given_in_input>
1650 id : <id_given_in_input>
1650 result : {
1651 result : {
1651 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1652 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1652 "success": true
1653 "success": true
1653
1654
1654 }
1655 }
1655 error : null
1656 error : null
1656
1657
1657 Example error output:
1658 Example error output:
1658
1659
1659 .. code-block:: bash
1660 .. code-block:: bash
1660
1661
1661 id : <id_given_in_input>
1662 id : <id_given_in_input>
1662 result : null
1663 result : null
1663 error : {
1664 error : {
1664 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1665 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1665 }
1666 }
1666
1667
1667 """
1668 """
1668
1669
1669 repo = get_repo_or_error(repoid)
1670 repo = get_repo_or_error(repoid)
1670 perm = get_perm_or_error(perm)
1671 perm = get_perm_or_error(perm)
1671 if not has_superadmin_permission(apiuser):
1672 if not has_superadmin_permission(apiuser):
1672 _perms = ('repository.admin',)
1673 _perms = ('repository.admin',)
1673 validate_repo_permissions(apiuser, repoid, repo, _perms)
1674 validate_repo_permissions(apiuser, repoid, repo, _perms)
1674
1675
1675 user_group = get_user_group_or_error(usergroupid)
1676 user_group = get_user_group_or_error(usergroupid)
1676 if not has_superadmin_permission(apiuser):
1677 if not has_superadmin_permission(apiuser):
1677 # check if we have at least read permission for this user group !
1678 # check if we have at least read permission for this user group !
1678 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1679 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1679 if not HasUserGroupPermissionAnyApi(*_perms)(
1680 if not HasUserGroupPermissionAnyApi(*_perms)(
1680 user=apiuser, user_group_name=user_group.users_group_name):
1681 user=apiuser, user_group_name=user_group.users_group_name):
1681 raise JSONRPCError(
1682 raise JSONRPCError(
1682 'user group `%s` does not exist' % (usergroupid,))
1683 'user group `%s` does not exist' % (usergroupid,))
1683
1684
1684 try:
1685 try:
1685 RepoModel().grant_user_group_permission(
1686 RepoModel().grant_user_group_permission(
1686 repo=repo, group_name=user_group, perm=perm)
1687 repo=repo, group_name=user_group, perm=perm)
1687
1688
1688 Session().commit()
1689 Session().commit()
1689 return {
1690 return {
1690 'msg': 'Granted perm: `%s` for user group: `%s` in '
1691 'msg': 'Granted perm: `%s` for user group: `%s` in '
1691 'repo: `%s`' % (
1692 'repo: `%s`' % (
1692 perm.permission_name, user_group.users_group_name,
1693 perm.permission_name, user_group.users_group_name,
1693 repo.repo_name
1694 repo.repo_name
1694 ),
1695 ),
1695 'success': True
1696 'success': True
1696 }
1697 }
1697 except Exception:
1698 except Exception:
1698 log.exception(
1699 log.exception(
1699 "Exception occurred while trying change permission on repo")
1700 "Exception occurred while trying change permission on repo")
1700 raise JSONRPCError(
1701 raise JSONRPCError(
1701 'failed to edit permission for user group: `%s` in '
1702 'failed to edit permission for user group: `%s` in '
1702 'repo: `%s`' % (
1703 'repo: `%s`' % (
1703 usergroupid, repo.repo_name
1704 usergroupid, repo.repo_name
1704 )
1705 )
1705 )
1706 )
1706
1707
1707
1708
1708 @jsonrpc_method()
1709 @jsonrpc_method()
1709 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1710 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1710 """
1711 """
1711 Revoke the permissions of a user group on a given repository.
1712 Revoke the permissions of a user group on a given repository.
1712
1713
1713 This command can only be run using an |authtoken| with admin
1714 This command can only be run using an |authtoken| with admin
1714 permissions on the |repo|.
1715 permissions on the |repo|.
1715
1716
1716 :param apiuser: This is filled automatically from the |authtoken|.
1717 :param apiuser: This is filled automatically from the |authtoken|.
1717 :type apiuser: AuthUser
1718 :type apiuser: AuthUser
1718 :param repoid: Set the repository name or repository ID.
1719 :param repoid: Set the repository name or repository ID.
1719 :type repoid: str or int
1720 :type repoid: str or int
1720 :param usergroupid: Specify the user group ID.
1721 :param usergroupid: Specify the user group ID.
1721 :type usergroupid: str or int
1722 :type usergroupid: str or int
1722
1723
1723 Example output:
1724 Example output:
1724
1725
1725 .. code-block:: bash
1726 .. code-block:: bash
1726
1727
1727 id : <id_given_in_input>
1728 id : <id_given_in_input>
1728 result: {
1729 result: {
1729 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1730 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1730 "success": true
1731 "success": true
1731 }
1732 }
1732 error: null
1733 error: null
1733 """
1734 """
1734
1735
1735 repo = get_repo_or_error(repoid)
1736 repo = get_repo_or_error(repoid)
1736 if not has_superadmin_permission(apiuser):
1737 if not has_superadmin_permission(apiuser):
1737 _perms = ('repository.admin',)
1738 _perms = ('repository.admin',)
1738 validate_repo_permissions(apiuser, repoid, repo, _perms)
1739 validate_repo_permissions(apiuser, repoid, repo, _perms)
1739
1740
1740 user_group = get_user_group_or_error(usergroupid)
1741 user_group = get_user_group_or_error(usergroupid)
1741 if not has_superadmin_permission(apiuser):
1742 if not has_superadmin_permission(apiuser):
1742 # check if we have at least read permission for this user group !
1743 # check if we have at least read permission for this user group !
1743 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1744 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1744 if not HasUserGroupPermissionAnyApi(*_perms)(
1745 if not HasUserGroupPermissionAnyApi(*_perms)(
1745 user=apiuser, user_group_name=user_group.users_group_name):
1746 user=apiuser, user_group_name=user_group.users_group_name):
1746 raise JSONRPCError(
1747 raise JSONRPCError(
1747 'user group `%s` does not exist' % (usergroupid,))
1748 'user group `%s` does not exist' % (usergroupid,))
1748
1749
1749 try:
1750 try:
1750 RepoModel().revoke_user_group_permission(
1751 RepoModel().revoke_user_group_permission(
1751 repo=repo, group_name=user_group)
1752 repo=repo, group_name=user_group)
1752
1753
1753 Session().commit()
1754 Session().commit()
1754 return {
1755 return {
1755 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1756 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1756 user_group.users_group_name, repo.repo_name
1757 user_group.users_group_name, repo.repo_name
1757 ),
1758 ),
1758 'success': True
1759 'success': True
1759 }
1760 }
1760 except Exception:
1761 except Exception:
1761 log.exception("Exception occurred while trying revoke "
1762 log.exception("Exception occurred while trying revoke "
1762 "user group permission on repo")
1763 "user group permission on repo")
1763 raise JSONRPCError(
1764 raise JSONRPCError(
1764 'failed to edit permission for user group: `%s` in '
1765 'failed to edit permission for user group: `%s` in '
1765 'repo: `%s`' % (
1766 'repo: `%s`' % (
1766 user_group.users_group_name, repo.repo_name
1767 user_group.users_group_name, repo.repo_name
1767 )
1768 )
1768 )
1769 )
1769
1770
1770
1771
1771 @jsonrpc_method()
1772 @jsonrpc_method()
1772 def pull(request, apiuser, repoid):
1773 def pull(request, apiuser, repoid):
1773 """
1774 """
1774 Triggers a pull on the given repository from a remote location. You
1775 Triggers a pull on the given repository from a remote location. You
1775 can use this to keep remote repositories up-to-date.
1776 can use this to keep remote repositories up-to-date.
1776
1777
1777 This command can only be run using an |authtoken| with admin
1778 This command can only be run using an |authtoken| with admin
1778 rights to the specified repository. For more information,
1779 rights to the specified repository. For more information,
1779 see :ref:`config-token-ref`.
1780 see :ref:`config-token-ref`.
1780
1781
1781 This command takes the following options:
1782 This command takes the following options:
1782
1783
1783 :param apiuser: This is filled automatically from the |authtoken|.
1784 :param apiuser: This is filled automatically from the |authtoken|.
1784 :type apiuser: AuthUser
1785 :type apiuser: AuthUser
1785 :param repoid: The repository name or repository ID.
1786 :param repoid: The repository name or repository ID.
1786 :type repoid: str or int
1787 :type repoid: str or int
1787
1788
1788 Example output:
1789 Example output:
1789
1790
1790 .. code-block:: bash
1791 .. code-block:: bash
1791
1792
1792 id : <id_given_in_input>
1793 id : <id_given_in_input>
1793 result : {
1794 result : {
1794 "msg": "Pulled from `<repository name>`"
1795 "msg": "Pulled from `<repository name>`"
1795 "repository": "<repository name>"
1796 "repository": "<repository name>"
1796 }
1797 }
1797 error : null
1798 error : null
1798
1799
1799 Example error output:
1800 Example error output:
1800
1801
1801 .. code-block:: bash
1802 .. code-block:: bash
1802
1803
1803 id : <id_given_in_input>
1804 id : <id_given_in_input>
1804 result : null
1805 result : null
1805 error : {
1806 error : {
1806 "Unable to pull changes from `<reponame>`"
1807 "Unable to pull changes from `<reponame>`"
1807 }
1808 }
1808
1809
1809 """
1810 """
1810
1811
1811 repo = get_repo_or_error(repoid)
1812 repo = get_repo_or_error(repoid)
1812 if not has_superadmin_permission(apiuser):
1813 if not has_superadmin_permission(apiuser):
1813 _perms = ('repository.admin',)
1814 _perms = ('repository.admin',)
1814 validate_repo_permissions(apiuser, repoid, repo, _perms)
1815 validate_repo_permissions(apiuser, repoid, repo, _perms)
1815
1816
1816 try:
1817 try:
1817 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1818 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1818 return {
1819 return {
1819 'msg': 'Pulled from `%s`' % repo.repo_name,
1820 'msg': 'Pulled from `%s`' % repo.repo_name,
1820 'repository': repo.repo_name
1821 'repository': repo.repo_name
1821 }
1822 }
1822 except Exception:
1823 except Exception:
1823 log.exception("Exception occurred while trying to "
1824 log.exception("Exception occurred while trying to "
1824 "pull changes from remote location")
1825 "pull changes from remote location")
1825 raise JSONRPCError(
1826 raise JSONRPCError(
1826 'Unable to pull changes from `%s`' % repo.repo_name
1827 'Unable to pull changes from `%s`' % repo.repo_name
1827 )
1828 )
1828
1829
1829
1830
1830 @jsonrpc_method()
1831 @jsonrpc_method()
1831 def strip(request, apiuser, repoid, revision, branch):
1832 def strip(request, apiuser, repoid, revision, branch):
1832 """
1833 """
1833 Strips the given revision from the specified repository.
1834 Strips the given revision from the specified repository.
1834
1835
1835 * This will remove the revision and all of its decendants.
1836 * This will remove the revision and all of its decendants.
1836
1837
1837 This command can only be run using an |authtoken| with admin rights to
1838 This command can only be run using an |authtoken| with admin rights to
1838 the specified repository.
1839 the specified repository.
1839
1840
1840 This command takes the following options:
1841 This command takes the following options:
1841
1842
1842 :param apiuser: This is filled automatically from the |authtoken|.
1843 :param apiuser: This is filled automatically from the |authtoken|.
1843 :type apiuser: AuthUser
1844 :type apiuser: AuthUser
1844 :param repoid: The repository name or repository ID.
1845 :param repoid: The repository name or repository ID.
1845 :type repoid: str or int
1846 :type repoid: str or int
1846 :param revision: The revision you wish to strip.
1847 :param revision: The revision you wish to strip.
1847 :type revision: str
1848 :type revision: str
1848 :param branch: The branch from which to strip the revision.
1849 :param branch: The branch from which to strip the revision.
1849 :type branch: str
1850 :type branch: str
1850
1851
1851 Example output:
1852 Example output:
1852
1853
1853 .. code-block:: bash
1854 .. code-block:: bash
1854
1855
1855 id : <id_given_in_input>
1856 id : <id_given_in_input>
1856 result : {
1857 result : {
1857 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1858 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1858 "repository": "<repository name>"
1859 "repository": "<repository name>"
1859 }
1860 }
1860 error : null
1861 error : null
1861
1862
1862 Example error output:
1863 Example error output:
1863
1864
1864 .. code-block:: bash
1865 .. code-block:: bash
1865
1866
1866 id : <id_given_in_input>
1867 id : <id_given_in_input>
1867 result : null
1868 result : null
1868 error : {
1869 error : {
1869 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1870 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1870 }
1871 }
1871
1872
1872 """
1873 """
1873
1874
1874 repo = get_repo_or_error(repoid)
1875 repo = get_repo_or_error(repoid)
1875 if not has_superadmin_permission(apiuser):
1876 if not has_superadmin_permission(apiuser):
1876 _perms = ('repository.admin',)
1877 _perms = ('repository.admin',)
1877 validate_repo_permissions(apiuser, repoid, repo, _perms)
1878 validate_repo_permissions(apiuser, repoid, repo, _perms)
1878
1879
1879 try:
1880 try:
1880 ScmModel().strip(repo, revision, branch)
1881 ScmModel().strip(repo, revision, branch)
1881 audit_logger.store_api(
1882 audit_logger.store_api(
1882 'repo.commit.strip', action_data={'commit_id': revision},
1883 'repo.commit.strip', action_data={'commit_id': revision},
1883 repo=repo,
1884 repo=repo,
1884 user=apiuser, commit=True)
1885 user=apiuser, commit=True)
1885
1886
1886 return {
1887 return {
1887 'msg': 'Stripped commit %s from repo `%s`' % (
1888 'msg': 'Stripped commit %s from repo `%s`' % (
1888 revision, repo.repo_name),
1889 revision, repo.repo_name),
1889 'repository': repo.repo_name
1890 'repository': repo.repo_name
1890 }
1891 }
1891 except Exception:
1892 except Exception:
1892 log.exception("Exception while trying to strip")
1893 log.exception("Exception while trying to strip")
1893 raise JSONRPCError(
1894 raise JSONRPCError(
1894 'Unable to strip commit %s from repo `%s`' % (
1895 'Unable to strip commit %s from repo `%s`' % (
1895 revision, repo.repo_name)
1896 revision, repo.repo_name)
1896 )
1897 )
1897
1898
1898
1899
1899 @jsonrpc_method()
1900 @jsonrpc_method()
1900 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1901 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1901 """
1902 """
1902 Returns all settings for a repository. If key is given it only returns the
1903 Returns all settings for a repository. If key is given it only returns the
1903 setting identified by the key or null.
1904 setting identified by the key or null.
1904
1905
1905 :param apiuser: This is filled automatically from the |authtoken|.
1906 :param apiuser: This is filled automatically from the |authtoken|.
1906 :type apiuser: AuthUser
1907 :type apiuser: AuthUser
1907 :param repoid: The repository name or repository id.
1908 :param repoid: The repository name or repository id.
1908 :type repoid: str or int
1909 :type repoid: str or int
1909 :param key: Key of the setting to return.
1910 :param key: Key of the setting to return.
1910 :type: key: Optional(str)
1911 :type: key: Optional(str)
1911
1912
1912 Example output:
1913 Example output:
1913
1914
1914 .. code-block:: bash
1915 .. code-block:: bash
1915
1916
1916 {
1917 {
1917 "error": null,
1918 "error": null,
1918 "id": 237,
1919 "id": 237,
1919 "result": {
1920 "result": {
1920 "extensions_largefiles": true,
1921 "extensions_largefiles": true,
1921 "extensions_evolve": true,
1922 "extensions_evolve": true,
1922 "hooks_changegroup_push_logger": true,
1923 "hooks_changegroup_push_logger": true,
1923 "hooks_changegroup_repo_size": false,
1924 "hooks_changegroup_repo_size": false,
1924 "hooks_outgoing_pull_logger": true,
1925 "hooks_outgoing_pull_logger": true,
1925 "phases_publish": "True",
1926 "phases_publish": "True",
1926 "rhodecode_hg_use_rebase_for_merging": true,
1927 "rhodecode_hg_use_rebase_for_merging": true,
1927 "rhodecode_pr_merge_enabled": true,
1928 "rhodecode_pr_merge_enabled": true,
1928 "rhodecode_use_outdated_comments": true
1929 "rhodecode_use_outdated_comments": true
1929 }
1930 }
1930 }
1931 }
1931 """
1932 """
1932
1933
1933 # Restrict access to this api method to admins only.
1934 # Restrict access to this api method to admins only.
1934 if not has_superadmin_permission(apiuser):
1935 if not has_superadmin_permission(apiuser):
1935 raise JSONRPCForbidden()
1936 raise JSONRPCForbidden()
1936
1937
1937 try:
1938 try:
1938 repo = get_repo_or_error(repoid)
1939 repo = get_repo_or_error(repoid)
1939 settings_model = VcsSettingsModel(repo=repo)
1940 settings_model = VcsSettingsModel(repo=repo)
1940 settings = settings_model.get_global_settings()
1941 settings = settings_model.get_global_settings()
1941 settings.update(settings_model.get_repo_settings())
1942 settings.update(settings_model.get_repo_settings())
1942
1943
1943 # If only a single setting is requested fetch it from all settings.
1944 # If only a single setting is requested fetch it from all settings.
1944 key = Optional.extract(key)
1945 key = Optional.extract(key)
1945 if key is not None:
1946 if key is not None:
1946 settings = settings.get(key, None)
1947 settings = settings.get(key, None)
1947 except Exception:
1948 except Exception:
1948 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1949 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1949 log.exception(msg)
1950 log.exception(msg)
1950 raise JSONRPCError(msg)
1951 raise JSONRPCError(msg)
1951
1952
1952 return settings
1953 return settings
1953
1954
1954
1955
1955 @jsonrpc_method()
1956 @jsonrpc_method()
1956 def set_repo_settings(request, apiuser, repoid, settings):
1957 def set_repo_settings(request, apiuser, repoid, settings):
1957 """
1958 """
1958 Update repository settings. Returns true on success.
1959 Update repository settings. Returns true on success.
1959
1960
1960 :param apiuser: This is filled automatically from the |authtoken|.
1961 :param apiuser: This is filled automatically from the |authtoken|.
1961 :type apiuser: AuthUser
1962 :type apiuser: AuthUser
1962 :param repoid: The repository name or repository id.
1963 :param repoid: The repository name or repository id.
1963 :type repoid: str or int
1964 :type repoid: str or int
1964 :param settings: The new settings for the repository.
1965 :param settings: The new settings for the repository.
1965 :type: settings: dict
1966 :type: settings: dict
1966
1967
1967 Example output:
1968 Example output:
1968
1969
1969 .. code-block:: bash
1970 .. code-block:: bash
1970
1971
1971 {
1972 {
1972 "error": null,
1973 "error": null,
1973 "id": 237,
1974 "id": 237,
1974 "result": true
1975 "result": true
1975 }
1976 }
1976 """
1977 """
1977 # Restrict access to this api method to admins only.
1978 # Restrict access to this api method to admins only.
1978 if not has_superadmin_permission(apiuser):
1979 if not has_superadmin_permission(apiuser):
1979 raise JSONRPCForbidden()
1980 raise JSONRPCForbidden()
1980
1981
1981 if type(settings) is not dict:
1982 if type(settings) is not dict:
1982 raise JSONRPCError('Settings have to be a JSON Object.')
1983 raise JSONRPCError('Settings have to be a JSON Object.')
1983
1984
1984 try:
1985 try:
1985 settings_model = VcsSettingsModel(repo=repoid)
1986 settings_model = VcsSettingsModel(repo=repoid)
1986
1987
1987 # Merge global, repo and incoming settings.
1988 # Merge global, repo and incoming settings.
1988 new_settings = settings_model.get_global_settings()
1989 new_settings = settings_model.get_global_settings()
1989 new_settings.update(settings_model.get_repo_settings())
1990 new_settings.update(settings_model.get_repo_settings())
1990 new_settings.update(settings)
1991 new_settings.update(settings)
1991
1992
1992 # Update the settings.
1993 # Update the settings.
1993 inherit_global_settings = new_settings.get(
1994 inherit_global_settings = new_settings.get(
1994 'inherit_global_settings', False)
1995 'inherit_global_settings', False)
1995 settings_model.create_or_update_repo_settings(
1996 settings_model.create_or_update_repo_settings(
1996 new_settings, inherit_global_settings=inherit_global_settings)
1997 new_settings, inherit_global_settings=inherit_global_settings)
1997 Session().commit()
1998 Session().commit()
1998 except Exception:
1999 except Exception:
1999 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2000 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2000 log.exception(msg)
2001 log.exception(msg)
2001 raise JSONRPCError(msg)
2002 raise JSONRPCError(msg)
2002
2003
2003 # Indicate success.
2004 # Indicate success.
2004 return True
2005 return True
2005
2006
2006
2007
2007 @jsonrpc_method()
2008 @jsonrpc_method()
2008 def maintenance(request, apiuser, repoid):
2009 def maintenance(request, apiuser, repoid):
2009 """
2010 """
2010 Triggers a maintenance on the given repository.
2011 Triggers a maintenance on the given repository.
2011
2012
2012 This command can only be run using an |authtoken| with admin
2013 This command can only be run using an |authtoken| with admin
2013 rights to the specified repository. For more information,
2014 rights to the specified repository. For more information,
2014 see :ref:`config-token-ref`.
2015 see :ref:`config-token-ref`.
2015
2016
2016 This command takes the following options:
2017 This command takes the following options:
2017
2018
2018 :param apiuser: This is filled automatically from the |authtoken|.
2019 :param apiuser: This is filled automatically from the |authtoken|.
2019 :type apiuser: AuthUser
2020 :type apiuser: AuthUser
2020 :param repoid: The repository name or repository ID.
2021 :param repoid: The repository name or repository ID.
2021 :type repoid: str or int
2022 :type repoid: str or int
2022
2023
2023 Example output:
2024 Example output:
2024
2025
2025 .. code-block:: bash
2026 .. code-block:: bash
2026
2027
2027 id : <id_given_in_input>
2028 id : <id_given_in_input>
2028 result : {
2029 result : {
2029 "msg": "executed maintenance command",
2030 "msg": "executed maintenance command",
2030 "executed_actions": [
2031 "executed_actions": [
2031 <action_message>, <action_message2>...
2032 <action_message>, <action_message2>...
2032 ],
2033 ],
2033 "repository": "<repository name>"
2034 "repository": "<repository name>"
2034 }
2035 }
2035 error : null
2036 error : null
2036
2037
2037 Example error output:
2038 Example error output:
2038
2039
2039 .. code-block:: bash
2040 .. code-block:: bash
2040
2041
2041 id : <id_given_in_input>
2042 id : <id_given_in_input>
2042 result : null
2043 result : null
2043 error : {
2044 error : {
2044 "Unable to execute maintenance on `<reponame>`"
2045 "Unable to execute maintenance on `<reponame>`"
2045 }
2046 }
2046
2047
2047 """
2048 """
2048
2049
2049 repo = get_repo_or_error(repoid)
2050 repo = get_repo_or_error(repoid)
2050 if not has_superadmin_permission(apiuser):
2051 if not has_superadmin_permission(apiuser):
2051 _perms = ('repository.admin',)
2052 _perms = ('repository.admin',)
2052 validate_repo_permissions(apiuser, repoid, repo, _perms)
2053 validate_repo_permissions(apiuser, repoid, repo, _perms)
2053
2054
2054 try:
2055 try:
2055 maintenance = repo_maintenance.RepoMaintenance()
2056 maintenance = repo_maintenance.RepoMaintenance()
2056 executed_actions = maintenance.execute(repo)
2057 executed_actions = maintenance.execute(repo)
2057
2058
2058 return {
2059 return {
2059 'msg': 'executed maintenance command',
2060 'msg': 'executed maintenance command',
2060 'executed_actions': executed_actions,
2061 'executed_actions': executed_actions,
2061 'repository': repo.repo_name
2062 'repository': repo.repo_name
2062 }
2063 }
2063 except Exception:
2064 except Exception:
2064 log.exception("Exception occurred while trying to run maintenance")
2065 log.exception("Exception occurred while trying to run maintenance")
2065 raise JSONRPCError(
2066 raise JSONRPCError(
2066 'Unable to execute maintenance on `%s`' % repo.repo_name)
2067 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,201 +1,201 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests import assert_session_flash
23 from rhodecode.tests import assert_session_flash
24 from rhodecode.tests.utils import AssertResponse
24 from rhodecode.tests.utils import AssertResponse
25 from rhodecode.model.db import Session
25 from rhodecode.model.db import Session
26 from rhodecode.model.settings import SettingsModel
26 from rhodecode.model.settings import SettingsModel
27
27
28
28
29 def assert_auth_settings_updated(response):
29 def assert_auth_settings_updated(response):
30 assert response.status_int == 302, 'Expected response HTTP Found 302'
30 assert response.status_int == 302, 'Expected response HTTP Found 302'
31 assert_session_flash(response, 'Auth settings updated successfully')
31 assert_session_flash(response, 'Auth settings updated successfully')
32
32
33
33
34 @pytest.mark.usefixtures("autologin_user", "app")
34 @pytest.mark.usefixtures("autologin_user", "app")
35 class TestAuthSettingsController(object):
35 class TestAuthSettingsView(object):
36
36
37 def _enable_plugins(self, plugins_list, csrf_token, override=None,
37 def _enable_plugins(self, plugins_list, csrf_token, override=None,
38 verify_response=False):
38 verify_response=False):
39 test_url = '/_admin/auth'
39 test_url = '/_admin/auth'
40 params = {
40 params = {
41 'auth_plugins': plugins_list,
41 'auth_plugins': plugins_list,
42 'csrf_token': csrf_token,
42 'csrf_token': csrf_token,
43 }
43 }
44 if override:
44 if override:
45 params.update(override)
45 params.update(override)
46 _enabled_plugins = []
46 _enabled_plugins = []
47 for plugin in plugins_list.split(','):
47 for plugin in plugins_list.split(','):
48 plugin_name = plugin.partition('#')[-1]
48 plugin_name = plugin.partition('#')[-1]
49 enabled_plugin = '%s_enabled' % plugin_name
49 enabled_plugin = '%s_enabled' % plugin_name
50 cache_ttl = '%s_cache_ttl' % plugin_name
50 cache_ttl = '%s_cache_ttl' % plugin_name
51
51
52 # default params that are needed for each plugin,
52 # default params that are needed for each plugin,
53 # `enabled` and `cache_ttl`
53 # `enabled` and `cache_ttl`
54 params.update({
54 params.update({
55 enabled_plugin: True,
55 enabled_plugin: True,
56 cache_ttl: 0
56 cache_ttl: 0
57 })
57 })
58 _enabled_plugins.append(enabled_plugin)
58 _enabled_plugins.append(enabled_plugin)
59
59
60 # we need to clean any enabled plugin before, since they require
60 # we need to clean any enabled plugin before, since they require
61 # form params to be present
61 # form params to be present
62 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
62 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
63 db_plugin.app_settings_value = \
63 db_plugin.app_settings_value = \
64 'egg:rhodecode-enterprise-ce#rhodecode'
64 'egg:rhodecode-enterprise-ce#rhodecode'
65 Session().add(db_plugin)
65 Session().add(db_plugin)
66 Session().commit()
66 Session().commit()
67 for _plugin in _enabled_plugins:
67 for _plugin in _enabled_plugins:
68 db_plugin = SettingsModel().get_setting_by_name(_plugin)
68 db_plugin = SettingsModel().get_setting_by_name(_plugin)
69 if db_plugin:
69 if db_plugin:
70 Session().delete(db_plugin)
70 Session().delete(db_plugin)
71 Session().commit()
71 Session().commit()
72
72
73 response = self.app.post(url=test_url, params=params)
73 response = self.app.post(url=test_url, params=params)
74
74
75 if verify_response:
75 if verify_response:
76 assert_auth_settings_updated(response)
76 assert_auth_settings_updated(response)
77 return params
77 return params
78
78
79 def _post_ldap_settings(self, params, override=None, force=False):
79 def _post_ldap_settings(self, params, override=None, force=False):
80
80
81 params.update({
81 params.update({
82 'filter': 'user',
82 'filter': 'user',
83 'user_member_of': '',
83 'user_member_of': '',
84 'user_search_base': '',
84 'user_search_base': '',
85 'user_search_filter': 'test_filter',
85 'user_search_filter': 'test_filter',
86
86
87 'host': 'dc.example.com',
87 'host': 'dc.example.com',
88 'port': '999',
88 'port': '999',
89 'tls_kind': 'PLAIN',
89 'tls_kind': 'PLAIN',
90 'tls_reqcert': 'NEVER',
90 'tls_reqcert': 'NEVER',
91
91
92 'dn_user': 'test_user',
92 'dn_user': 'test_user',
93 'dn_pass': 'test_pass',
93 'dn_pass': 'test_pass',
94 'base_dn': 'test_base_dn',
94 'base_dn': 'test_base_dn',
95 'search_scope': 'BASE',
95 'search_scope': 'BASE',
96 'attr_login': 'test_attr_login',
96 'attr_login': 'test_attr_login',
97 'attr_firstname': 'ima',
97 'attr_firstname': 'ima',
98 'attr_lastname': 'tester',
98 'attr_lastname': 'tester',
99 'attr_email': 'test@example.com',
99 'attr_email': 'test@example.com',
100 'cache_ttl': '0',
100 'cache_ttl': '0',
101 })
101 })
102 if force:
102 if force:
103 params = {}
103 params = {}
104 params.update(override or {})
104 params.update(override or {})
105
105
106 test_url = '/_admin/auth/ldap/'
106 test_url = '/_admin/auth/ldap/'
107
107
108 response = self.app.post(url=test_url, params=params)
108 response = self.app.post(url=test_url, params=params)
109 return response
109 return response
110
110
111 def test_index(self):
111 def test_index(self):
112 response = self.app.get('/_admin/auth')
112 response = self.app.get('/_admin/auth')
113 response.mustcontain('Authentication Plugins')
113 response.mustcontain('Authentication Plugins')
114
114
115 @pytest.mark.parametrize("disable_plugin, needs_import", [
115 @pytest.mark.parametrize("disable_plugin, needs_import", [
116 ('egg:rhodecode-enterprise-ce#headers', None),
116 ('egg:rhodecode-enterprise-ce#headers', None),
117 ('egg:rhodecode-enterprise-ce#crowd', None),
117 ('egg:rhodecode-enterprise-ce#crowd', None),
118 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
118 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
119 ('egg:rhodecode-enterprise-ce#ldap', None),
119 ('egg:rhodecode-enterprise-ce#ldap', None),
120 ('egg:rhodecode-enterprise-ce#pam', "pam"),
120 ('egg:rhodecode-enterprise-ce#pam', "pam"),
121 ])
121 ])
122 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
122 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
123 # TODO: johbo: "pam" is currently not available on darwin,
123 # TODO: johbo: "pam" is currently not available on darwin,
124 # although the docs state that it should work on darwin.
124 # although the docs state that it should work on darwin.
125 if needs_import:
125 if needs_import:
126 pytest.importorskip(needs_import)
126 pytest.importorskip(needs_import)
127
127
128 self._enable_plugins(
128 self._enable_plugins(
129 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
129 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
130 csrf_token, verify_response=True)
130 csrf_token, verify_response=True)
131
131
132 self._enable_plugins(
132 self._enable_plugins(
133 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
133 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
134 verify_response=True)
134 verify_response=True)
135
135
136 def test_ldap_save_settings(self, csrf_token):
136 def test_ldap_save_settings(self, csrf_token):
137 params = self._enable_plugins(
137 params = self._enable_plugins(
138 'egg:rhodecode-enterprise-ce#rhodecode,'
138 'egg:rhodecode-enterprise-ce#rhodecode,'
139 'egg:rhodecode-enterprise-ce#ldap',
139 'egg:rhodecode-enterprise-ce#ldap',
140 csrf_token)
140 csrf_token)
141 response = self._post_ldap_settings(params)
141 response = self._post_ldap_settings(params)
142 assert_auth_settings_updated(response)
142 assert_auth_settings_updated(response)
143
143
144 new_settings = SettingsModel().get_auth_settings()
144 new_settings = SettingsModel().get_auth_settings()
145 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
145 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
146 'fail db write compare'
146 'fail db write compare'
147
147
148 def test_ldap_error_form_wrong_port_number(self, csrf_token):
148 def test_ldap_error_form_wrong_port_number(self, csrf_token):
149 params = self._enable_plugins(
149 params = self._enable_plugins(
150 'egg:rhodecode-enterprise-ce#rhodecode,'
150 'egg:rhodecode-enterprise-ce#rhodecode,'
151 'egg:rhodecode-enterprise-ce#ldap',
151 'egg:rhodecode-enterprise-ce#ldap',
152 csrf_token)
152 csrf_token)
153 invalid_port_value = 'invalid-port-number'
153 invalid_port_value = 'invalid-port-number'
154 response = self._post_ldap_settings(params, override={
154 response = self._post_ldap_settings(params, override={
155 'port': invalid_port_value,
155 'port': invalid_port_value,
156 })
156 })
157 assertr = AssertResponse(response)
157 assertr = AssertResponse(response)
158 assertr.element_contains(
158 assertr.element_contains(
159 '.form .field #port ~ .error-message',
159 '.form .field #port ~ .error-message',
160 invalid_port_value)
160 invalid_port_value)
161
161
162 def test_ldap_error_form(self, csrf_token):
162 def test_ldap_error_form(self, csrf_token):
163 params = self._enable_plugins(
163 params = self._enable_plugins(
164 'egg:rhodecode-enterprise-ce#rhodecode,'
164 'egg:rhodecode-enterprise-ce#rhodecode,'
165 'egg:rhodecode-enterprise-ce#ldap',
165 'egg:rhodecode-enterprise-ce#ldap',
166 csrf_token)
166 csrf_token)
167 response = self._post_ldap_settings(params, override={
167 response = self._post_ldap_settings(params, override={
168 'attr_login': '',
168 'attr_login': '',
169 })
169 })
170 response.mustcontain("""<span class="error-message">The LDAP Login"""
170 response.mustcontain("""<span class="error-message">The LDAP Login"""
171 """ attribute of the CN must be specified""")
171 """ attribute of the CN must be specified""")
172
172
173 def test_post_ldap_group_settings(self, csrf_token):
173 def test_post_ldap_group_settings(self, csrf_token):
174 params = self._enable_plugins(
174 params = self._enable_plugins(
175 'egg:rhodecode-enterprise-ce#rhodecode,'
175 'egg:rhodecode-enterprise-ce#rhodecode,'
176 'egg:rhodecode-enterprise-ce#ldap',
176 'egg:rhodecode-enterprise-ce#ldap',
177 csrf_token)
177 csrf_token)
178
178
179 response = self._post_ldap_settings(params, override={
179 response = self._post_ldap_settings(params, override={
180 'host': 'dc-legacy.example.com',
180 'host': 'dc-legacy.example.com',
181 'port': '999',
181 'port': '999',
182 'tls_kind': 'PLAIN',
182 'tls_kind': 'PLAIN',
183 'tls_reqcert': 'NEVER',
183 'tls_reqcert': 'NEVER',
184 'dn_user': 'test_user',
184 'dn_user': 'test_user',
185 'dn_pass': 'test_pass',
185 'dn_pass': 'test_pass',
186 'base_dn': 'test_base_dn',
186 'base_dn': 'test_base_dn',
187 'filter': 'test_filter',
187 'filter': 'test_filter',
188 'search_scope': 'BASE',
188 'search_scope': 'BASE',
189 'attr_login': 'test_attr_login',
189 'attr_login': 'test_attr_login',
190 'attr_firstname': 'ima',
190 'attr_firstname': 'ima',
191 'attr_lastname': 'tester',
191 'attr_lastname': 'tester',
192 'attr_email': 'test@example.com',
192 'attr_email': 'test@example.com',
193 'cache_ttl': '60',
193 'cache_ttl': '60',
194 'csrf_token': csrf_token,
194 'csrf_token': csrf_token,
195 }
195 }
196 )
196 )
197 assert_auth_settings_updated(response)
197 assert_auth_settings_updated(response)
198
198
199 new_settings = SettingsModel().get_auth_settings()
199 new_settings = SettingsModel().get_auth_settings()
200 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
200 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
201 'fail db write compare'
201 'fail db write compare'
@@ -1,781 +1,781 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22 from sqlalchemy.orm.exc import NoResultFound
22 from sqlalchemy.orm.exc import NoResultFound
23
23
24 from rhodecode.lib import auth
24 from rhodecode.lib import auth
25 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
26 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
26 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.user import UserModel
28 from rhodecode.model.user import UserModel
29
29
30 from rhodecode.tests import (
30 from rhodecode.tests import (
31 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
31 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
32 from rhodecode.tests.fixture import Fixture
32 from rhodecode.tests.fixture import Fixture
33
33
34 fixture = Fixture()
34 fixture = Fixture()
35
35
36
36
37 def route_path(name, params=None, **kwargs):
37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib
39 from rhodecode.apps._base import ADMIN_PREFIX
39 from rhodecode.apps._base import ADMIN_PREFIX
40
40
41 base_url = {
41 base_url = {
42 'users':
42 'users':
43 ADMIN_PREFIX + '/users',
43 ADMIN_PREFIX + '/users',
44 'users_data':
44 'users_data':
45 ADMIN_PREFIX + '/users_data',
45 ADMIN_PREFIX + '/users_data',
46 'users_create':
46 'users_create':
47 ADMIN_PREFIX + '/users/create',
47 ADMIN_PREFIX + '/users/create',
48 'users_new':
48 'users_new':
49 ADMIN_PREFIX + '/users/new',
49 ADMIN_PREFIX + '/users/new',
50 'user_edit':
50 'user_edit':
51 ADMIN_PREFIX + '/users/{user_id}/edit',
51 ADMIN_PREFIX + '/users/{user_id}/edit',
52 'user_edit_advanced':
52 'user_edit_advanced':
53 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
53 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
54 'user_edit_global_perms':
54 'user_edit_global_perms':
55 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
55 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
56 'user_edit_global_perms_update':
56 'user_edit_global_perms_update':
57 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
57 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
58 'user_update':
58 'user_update':
59 ADMIN_PREFIX + '/users/{user_id}/update',
59 ADMIN_PREFIX + '/users/{user_id}/update',
60 'user_delete':
60 'user_delete':
61 ADMIN_PREFIX + '/users/{user_id}/delete',
61 ADMIN_PREFIX + '/users/{user_id}/delete',
62 'user_force_password_reset':
62 'user_force_password_reset':
63 ADMIN_PREFIX + '/users/{user_id}/password_reset',
63 ADMIN_PREFIX + '/users/{user_id}/password_reset',
64 'user_create_personal_repo_group':
64 'user_create_personal_repo_group':
65 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
65 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
66
66
67 'edit_user_auth_tokens':
67 'edit_user_auth_tokens':
68 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
68 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
69 'edit_user_auth_tokens_add':
69 'edit_user_auth_tokens_add':
70 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
70 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
71 'edit_user_auth_tokens_delete':
71 'edit_user_auth_tokens_delete':
72 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
72 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
73
73
74 'edit_user_emails':
74 'edit_user_emails':
75 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
75 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
76 'edit_user_emails_add':
76 'edit_user_emails_add':
77 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
77 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
78 'edit_user_emails_delete':
78 'edit_user_emails_delete':
79 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
79 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
80
80
81 'edit_user_ips':
81 'edit_user_ips':
82 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
82 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
83 'edit_user_ips_add':
83 'edit_user_ips_add':
84 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
84 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
85 'edit_user_ips_delete':
85 'edit_user_ips_delete':
86 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
86 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
87
87
88 'edit_user_perms_summary':
88 'edit_user_perms_summary':
89 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
89 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
90 'edit_user_perms_summary_json':
90 'edit_user_perms_summary_json':
91 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
91 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
92
92
93 'edit_user_audit_logs':
93 'edit_user_audit_logs':
94 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
94 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
95
95
96 }[name].format(**kwargs)
96 }[name].format(**kwargs)
97
97
98 if params:
98 if params:
99 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
99 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
100 return base_url
100 return base_url
101
101
102
102
103 class TestAdminUsersView(TestController):
103 class TestAdminUsersView(TestController):
104
104
105 def test_show_users(self):
105 def test_show_users(self):
106 self.log_user()
106 self.log_user()
107 self.app.get(route_path('users'))
107 self.app.get(route_path('users'))
108
108
109 def test_show_users_data(self, xhr_header):
109 def test_show_users_data(self, xhr_header):
110 self.log_user()
110 self.log_user()
111 response = self.app.get(route_path(
111 response = self.app.get(route_path(
112 'users_data'), extra_environ=xhr_header)
112 'users_data'), extra_environ=xhr_header)
113
113
114 all_users = User.query().filter(
114 all_users = User.query().filter(
115 User.username != User.DEFAULT_USER).count()
115 User.username != User.DEFAULT_USER).count()
116 assert response.json['recordsTotal'] == all_users
116 assert response.json['recordsTotal'] == all_users
117
117
118 def test_show_users_data_filtered(self, xhr_header):
118 def test_show_users_data_filtered(self, xhr_header):
119 self.log_user()
119 self.log_user()
120 response = self.app.get(route_path(
120 response = self.app.get(route_path(
121 'users_data', params={'search[value]': 'empty_search'}),
121 'users_data', params={'search[value]': 'empty_search'}),
122 extra_environ=xhr_header)
122 extra_environ=xhr_header)
123
123
124 all_users = User.query().filter(
124 all_users = User.query().filter(
125 User.username != User.DEFAULT_USER).count()
125 User.username != User.DEFAULT_USER).count()
126 assert response.json['recordsTotal'] == all_users
126 assert response.json['recordsTotal'] == all_users
127 assert response.json['recordsFiltered'] == 0
127 assert response.json['recordsFiltered'] == 0
128
128
129 def test_auth_tokens_default_user(self):
129 def test_auth_tokens_default_user(self):
130 self.log_user()
130 self.log_user()
131 user = User.get_default_user()
131 user = User.get_default_user()
132 response = self.app.get(
132 response = self.app.get(
133 route_path('edit_user_auth_tokens', user_id=user.user_id),
133 route_path('edit_user_auth_tokens', user_id=user.user_id),
134 status=302)
134 status=302)
135
135
136 def test_auth_tokens(self):
136 def test_auth_tokens(self):
137 self.log_user()
137 self.log_user()
138
138
139 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
139 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
140 response = self.app.get(
140 response = self.app.get(
141 route_path('edit_user_auth_tokens', user_id=user.user_id))
141 route_path('edit_user_auth_tokens', user_id=user.user_id))
142 for token in user.auth_tokens:
142 for token in user.auth_tokens:
143 response.mustcontain(token)
143 response.mustcontain(token)
144 response.mustcontain('never')
144 response.mustcontain('never')
145
145
146 @pytest.mark.parametrize("desc, lifetime", [
146 @pytest.mark.parametrize("desc, lifetime", [
147 ('forever', -1),
147 ('forever', -1),
148 ('5mins', 60*5),
148 ('5mins', 60*5),
149 ('30days', 60*60*24*30),
149 ('30days', 60*60*24*30),
150 ])
150 ])
151 def test_add_auth_token(self, desc, lifetime, user_util):
151 def test_add_auth_token(self, desc, lifetime, user_util):
152 self.log_user()
152 self.log_user()
153 user = user_util.create_user()
153 user = user_util.create_user()
154 user_id = user.user_id
154 user_id = user.user_id
155
155
156 response = self.app.post(
156 response = self.app.post(
157 route_path('edit_user_auth_tokens_add', user_id=user_id),
157 route_path('edit_user_auth_tokens_add', user_id=user_id),
158 {'description': desc, 'lifetime': lifetime,
158 {'description': desc, 'lifetime': lifetime,
159 'csrf_token': self.csrf_token})
159 'csrf_token': self.csrf_token})
160 assert_session_flash(response, 'Auth token successfully created')
160 assert_session_flash(response, 'Auth token successfully created')
161
161
162 response = response.follow()
162 response = response.follow()
163 user = User.get(user_id)
163 user = User.get(user_id)
164 for auth_token in user.auth_tokens:
164 for auth_token in user.auth_tokens:
165 response.mustcontain(auth_token)
165 response.mustcontain(auth_token)
166
166
167 def test_delete_auth_token(self, user_util):
167 def test_delete_auth_token(self, user_util):
168 self.log_user()
168 self.log_user()
169 user = user_util.create_user()
169 user = user_util.create_user()
170 user_id = user.user_id
170 user_id = user.user_id
171 keys = user.auth_tokens
171 keys = user.auth_tokens
172 assert 2 == len(keys)
172 assert 2 == len(keys)
173
173
174 response = self.app.post(
174 response = self.app.post(
175 route_path('edit_user_auth_tokens_add', user_id=user_id),
175 route_path('edit_user_auth_tokens_add', user_id=user_id),
176 {'description': 'desc', 'lifetime': -1,
176 {'description': 'desc', 'lifetime': -1,
177 'csrf_token': self.csrf_token})
177 'csrf_token': self.csrf_token})
178 assert_session_flash(response, 'Auth token successfully created')
178 assert_session_flash(response, 'Auth token successfully created')
179 response.follow()
179 response.follow()
180
180
181 # now delete our key
181 # now delete our key
182 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
182 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
183 assert 3 == len(keys)
183 assert 3 == len(keys)
184
184
185 response = self.app.post(
185 response = self.app.post(
186 route_path('edit_user_auth_tokens_delete', user_id=user_id),
186 route_path('edit_user_auth_tokens_delete', user_id=user_id),
187 {'del_auth_token': keys[0].user_api_key_id,
187 {'del_auth_token': keys[0].user_api_key_id,
188 'csrf_token': self.csrf_token})
188 'csrf_token': self.csrf_token})
189
189
190 assert_session_flash(response, 'Auth token successfully deleted')
190 assert_session_flash(response, 'Auth token successfully deleted')
191 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
191 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
192 assert 2 == len(keys)
192 assert 2 == len(keys)
193
193
194 def test_ips(self):
194 def test_ips(self):
195 self.log_user()
195 self.log_user()
196 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
196 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
197 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
197 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
198 response.mustcontain('All IP addresses are allowed')
198 response.mustcontain('All IP addresses are allowed')
199
199
200 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
200 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
201 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
201 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
202 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
202 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
203 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
203 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
204 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
204 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
205 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
205 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
206 ('127_bad_ip', 'foobar', 'foobar', True),
206 ('127_bad_ip', 'foobar', 'foobar', True),
207 ])
207 ])
208 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
208 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
209 self.log_user()
209 self.log_user()
210 user = user_util.create_user(username=test_name)
210 user = user_util.create_user(username=test_name)
211 user_id = user.user_id
211 user_id = user.user_id
212
212
213 response = self.app.post(
213 response = self.app.post(
214 route_path('edit_user_ips_add', user_id=user_id),
214 route_path('edit_user_ips_add', user_id=user_id),
215 params={'new_ip': ip, 'csrf_token': self.csrf_token})
215 params={'new_ip': ip, 'csrf_token': self.csrf_token})
216
216
217 if failure:
217 if failure:
218 assert_session_flash(
218 assert_session_flash(
219 response, 'Please enter a valid IPv4 or IpV6 address')
219 response, 'Please enter a valid IPv4 or IpV6 address')
220 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
220 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
221
221
222 response.mustcontain(no=[ip])
222 response.mustcontain(no=[ip])
223 response.mustcontain(no=[ip_range])
223 response.mustcontain(no=[ip_range])
224
224
225 else:
225 else:
226 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
226 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
227 response.mustcontain(ip)
227 response.mustcontain(ip)
228 response.mustcontain(ip_range)
228 response.mustcontain(ip_range)
229
229
230 def test_ips_delete(self, user_util):
230 def test_ips_delete(self, user_util):
231 self.log_user()
231 self.log_user()
232 user = user_util.create_user()
232 user = user_util.create_user()
233 user_id = user.user_id
233 user_id = user.user_id
234 ip = '127.0.0.1/32'
234 ip = '127.0.0.1/32'
235 ip_range = '127.0.0.1 - 127.0.0.1'
235 ip_range = '127.0.0.1 - 127.0.0.1'
236 new_ip = UserModel().add_extra_ip(user_id, ip)
236 new_ip = UserModel().add_extra_ip(user_id, ip)
237 Session().commit()
237 Session().commit()
238 new_ip_id = new_ip.ip_id
238 new_ip_id = new_ip.ip_id
239
239
240 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
240 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
241 response.mustcontain(ip)
241 response.mustcontain(ip)
242 response.mustcontain(ip_range)
242 response.mustcontain(ip_range)
243
243
244 self.app.post(
244 self.app.post(
245 route_path('edit_user_ips_delete', user_id=user_id),
245 route_path('edit_user_ips_delete', user_id=user_id),
246 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
246 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
247
247
248 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
248 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
249 response.mustcontain('All IP addresses are allowed')
249 response.mustcontain('All IP addresses are allowed')
250 response.mustcontain(no=[ip])
250 response.mustcontain(no=[ip])
251 response.mustcontain(no=[ip_range])
251 response.mustcontain(no=[ip_range])
252
252
253 def test_emails(self):
253 def test_emails(self):
254 self.log_user()
254 self.log_user()
255 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
255 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
256 response = self.app.get(
256 response = self.app.get(
257 route_path('edit_user_emails', user_id=user.user_id))
257 route_path('edit_user_emails', user_id=user.user_id))
258 response.mustcontain('No additional emails specified')
258 response.mustcontain('No additional emails specified')
259
259
260 def test_emails_add(self, user_util):
260 def test_emails_add(self, user_util):
261 self.log_user()
261 self.log_user()
262 user = user_util.create_user()
262 user = user_util.create_user()
263 user_id = user.user_id
263 user_id = user.user_id
264
264
265 self.app.post(
265 self.app.post(
266 route_path('edit_user_emails_add', user_id=user_id),
266 route_path('edit_user_emails_add', user_id=user_id),
267 params={'new_email': 'example@rhodecode.com',
267 params={'new_email': 'example@rhodecode.com',
268 'csrf_token': self.csrf_token})
268 'csrf_token': self.csrf_token})
269
269
270 response = self.app.get(
270 response = self.app.get(
271 route_path('edit_user_emails', user_id=user_id))
271 route_path('edit_user_emails', user_id=user_id))
272 response.mustcontain('example@rhodecode.com')
272 response.mustcontain('example@rhodecode.com')
273
273
274 def test_emails_add_existing_email(self, user_util, user_regular):
274 def test_emails_add_existing_email(self, user_util, user_regular):
275 existing_email = user_regular.email
275 existing_email = user_regular.email
276
276
277 self.log_user()
277 self.log_user()
278 user = user_util.create_user()
278 user = user_util.create_user()
279 user_id = user.user_id
279 user_id = user.user_id
280
280
281 response = self.app.post(
281 response = self.app.post(
282 route_path('edit_user_emails_add', user_id=user_id),
282 route_path('edit_user_emails_add', user_id=user_id),
283 params={'new_email': existing_email,
283 params={'new_email': existing_email,
284 'csrf_token': self.csrf_token})
284 'csrf_token': self.csrf_token})
285 assert_session_flash(
285 assert_session_flash(
286 response, 'This e-mail address is already taken')
286 response, 'This e-mail address is already taken')
287
287
288 response = self.app.get(
288 response = self.app.get(
289 route_path('edit_user_emails', user_id=user_id))
289 route_path('edit_user_emails', user_id=user_id))
290 response.mustcontain(no=[existing_email])
290 response.mustcontain(no=[existing_email])
291
291
292 def test_emails_delete(self, user_util):
292 def test_emails_delete(self, user_util):
293 self.log_user()
293 self.log_user()
294 user = user_util.create_user()
294 user = user_util.create_user()
295 user_id = user.user_id
295 user_id = user.user_id
296
296
297 self.app.post(
297 self.app.post(
298 route_path('edit_user_emails_add', user_id=user_id),
298 route_path('edit_user_emails_add', user_id=user_id),
299 params={'new_email': 'example@rhodecode.com',
299 params={'new_email': 'example@rhodecode.com',
300 'csrf_token': self.csrf_token})
300 'csrf_token': self.csrf_token})
301
301
302 response = self.app.get(
302 response = self.app.get(
303 route_path('edit_user_emails', user_id=user_id))
303 route_path('edit_user_emails', user_id=user_id))
304 response.mustcontain('example@rhodecode.com')
304 response.mustcontain('example@rhodecode.com')
305
305
306 user_email = UserEmailMap.query()\
306 user_email = UserEmailMap.query()\
307 .filter(UserEmailMap.email == 'example@rhodecode.com') \
307 .filter(UserEmailMap.email == 'example@rhodecode.com') \
308 .filter(UserEmailMap.user_id == user_id)\
308 .filter(UserEmailMap.user_id == user_id)\
309 .one()
309 .one()
310
310
311 del_email_id = user_email.email_id
311 del_email_id = user_email.email_id
312 self.app.post(
312 self.app.post(
313 route_path('edit_user_emails_delete', user_id=user_id),
313 route_path('edit_user_emails_delete', user_id=user_id),
314 params={'del_email_id': del_email_id,
314 params={'del_email_id': del_email_id,
315 'csrf_token': self.csrf_token})
315 'csrf_token': self.csrf_token})
316
316
317 response = self.app.get(
317 response = self.app.get(
318 route_path('edit_user_emails', user_id=user_id))
318 route_path('edit_user_emails', user_id=user_id))
319 response.mustcontain(no=['example@rhodecode.com'])
319 response.mustcontain(no=['example@rhodecode.com'])
320
320
321
321
322 def test_create(self, request, xhr_header):
322 def test_create(self, request, xhr_header):
323 self.log_user()
323 self.log_user()
324 username = 'newtestuser'
324 username = 'newtestuser'
325 password = 'test12'
325 password = 'test12'
326 password_confirmation = password
326 password_confirmation = password
327 name = 'name'
327 name = 'name'
328 lastname = 'lastname'
328 lastname = 'lastname'
329 email = 'mail@mail.com'
329 email = 'mail@mail.com'
330
330
331 self.app.get(route_path('users_new'))
331 self.app.get(route_path('users_new'))
332
332
333 response = self.app.post(route_path('users_create'), params={
333 response = self.app.post(route_path('users_create'), params={
334 'username': username,
334 'username': username,
335 'password': password,
335 'password': password,
336 'password_confirmation': password_confirmation,
336 'password_confirmation': password_confirmation,
337 'firstname': name,
337 'firstname': name,
338 'active': True,
338 'active': True,
339 'lastname': lastname,
339 'lastname': lastname,
340 'extern_name': 'rhodecode',
340 'extern_name': 'rhodecode',
341 'extern_type': 'rhodecode',
341 'extern_type': 'rhodecode',
342 'email': email,
342 'email': email,
343 'csrf_token': self.csrf_token,
343 'csrf_token': self.csrf_token,
344 })
344 })
345 user_link = h.link_to(
345 user_link = h.link_to(
346 username,
346 username,
347 route_path(
347 route_path(
348 'user_edit', user_id=User.get_by_username(username).user_id))
348 'user_edit', user_id=User.get_by_username(username).user_id))
349 assert_session_flash(response, 'Created user %s' % (user_link,))
349 assert_session_flash(response, 'Created user %s' % (user_link,))
350
350
351 @request.addfinalizer
351 @request.addfinalizer
352 def cleanup():
352 def cleanup():
353 fixture.destroy_user(username)
353 fixture.destroy_user(username)
354 Session().commit()
354 Session().commit()
355
355
356 new_user = User.query().filter(User.username == username).one()
356 new_user = User.query().filter(User.username == username).one()
357
357
358 assert new_user.username == username
358 assert new_user.username == username
359 assert auth.check_password(password, new_user.password)
359 assert auth.check_password(password, new_user.password)
360 assert new_user.name == name
360 assert new_user.name == name
361 assert new_user.lastname == lastname
361 assert new_user.lastname == lastname
362 assert new_user.email == email
362 assert new_user.email == email
363
363
364 response = self.app.get(route_path('users_data'),
364 response = self.app.get(route_path('users_data'),
365 extra_environ=xhr_header)
365 extra_environ=xhr_header)
366 response.mustcontain(username)
366 response.mustcontain(username)
367
367
368 def test_create_err(self):
368 def test_create_err(self):
369 self.log_user()
369 self.log_user()
370 username = 'new_user'
370 username = 'new_user'
371 password = ''
371 password = ''
372 name = 'name'
372 name = 'name'
373 lastname = 'lastname'
373 lastname = 'lastname'
374 email = 'errmail.com'
374 email = 'errmail.com'
375
375
376 self.app.get(route_path('users_new'))
376 self.app.get(route_path('users_new'))
377
377
378 response = self.app.post(route_path('users_create'), params={
378 response = self.app.post(route_path('users_create'), params={
379 'username': username,
379 'username': username,
380 'password': password,
380 'password': password,
381 'name': name,
381 'name': name,
382 'active': False,
382 'active': False,
383 'lastname': lastname,
383 'lastname': lastname,
384 'email': email,
384 'email': email,
385 'csrf_token': self.csrf_token,
385 'csrf_token': self.csrf_token,
386 })
386 })
387
387
388 msg = '???'
388 msg = u'Username "%(username)s" is forbidden'
389 msg = h.html_escape(msg % {'username': 'new_user'})
389 msg = h.html_escape(msg % {'username': 'new_user'})
390 response.mustcontain('<span class="error-message">%s</span>' % msg)
390 response.mustcontain('<span class="error-message">%s</span>' % msg)
391 response.mustcontain(
391 response.mustcontain(
392 '<span class="error-message">Please enter a value</span>')
392 '<span class="error-message">Please enter a value</span>')
393 response.mustcontain(
393 response.mustcontain(
394 '<span class="error-message">An email address must contain a'
394 '<span class="error-message">An email address must contain a'
395 ' single @</span>')
395 ' single @</span>')
396
396
397 def get_user():
397 def get_user():
398 Session().query(User).filter(User.username == username).one()
398 Session().query(User).filter(User.username == username).one()
399
399
400 with pytest.raises(NoResultFound):
400 with pytest.raises(NoResultFound):
401 get_user()
401 get_user()
402
402
403 def test_new(self):
403 def test_new(self):
404 self.log_user()
404 self.log_user()
405 self.app.get(route_path('users_new'))
405 self.app.get(route_path('users_new'))
406
406
407 @pytest.mark.parametrize("name, attrs", [
407 @pytest.mark.parametrize("name, attrs", [
408 ('firstname', {'firstname': 'new_username'}),
408 ('firstname', {'firstname': 'new_username'}),
409 ('lastname', {'lastname': 'new_username'}),
409 ('lastname', {'lastname': 'new_username'}),
410 ('admin', {'admin': True}),
410 ('admin', {'admin': True}),
411 ('admin', {'admin': False}),
411 ('admin', {'admin': False}),
412 ('extern_type', {'extern_type': 'ldap'}),
412 ('extern_type', {'extern_type': 'ldap'}),
413 ('extern_type', {'extern_type': None}),
413 ('extern_type', {'extern_type': None}),
414 ('extern_name', {'extern_name': 'test'}),
414 ('extern_name', {'extern_name': 'test'}),
415 ('extern_name', {'extern_name': None}),
415 ('extern_name', {'extern_name': None}),
416 ('active', {'active': False}),
416 ('active', {'active': False}),
417 ('active', {'active': True}),
417 ('active', {'active': True}),
418 ('email', {'email': 'some@email.com'}),
418 ('email', {'email': 'some@email.com'}),
419 ('language', {'language': 'de'}),
419 ('language', {'language': 'de'}),
420 ('language', {'language': 'en'}),
420 ('language', {'language': 'en'}),
421 # ('new_password', {'new_password': 'foobar123',
421 # ('new_password', {'new_password': 'foobar123',
422 # 'password_confirmation': 'foobar123'})
422 # 'password_confirmation': 'foobar123'})
423 ])
423 ])
424 def test_update(self, name, attrs, user_util):
424 def test_update(self, name, attrs, user_util):
425 self.log_user()
425 self.log_user()
426 usr = user_util.create_user(
426 usr = user_util.create_user(
427 password='qweqwe',
427 password='qweqwe',
428 email='testme@rhodecode.org',
428 email='testme@rhodecode.org',
429 extern_type='rhodecode',
429 extern_type='rhodecode',
430 extern_name='xxx',
430 extern_name='xxx',
431 )
431 )
432 user_id = usr.user_id
432 user_id = usr.user_id
433 Session().commit()
433 Session().commit()
434
434
435 params = usr.get_api_data()
435 params = usr.get_api_data()
436 cur_lang = params['language'] or 'en'
436 cur_lang = params['language'] or 'en'
437 params.update({
437 params.update({
438 'password_confirmation': '',
438 'password_confirmation': '',
439 'new_password': '',
439 'new_password': '',
440 'language': cur_lang,
440 'language': cur_lang,
441 'csrf_token': self.csrf_token,
441 'csrf_token': self.csrf_token,
442 })
442 })
443 params.update({'new_password': ''})
443 params.update({'new_password': ''})
444 params.update(attrs)
444 params.update(attrs)
445 if name == 'email':
445 if name == 'email':
446 params['emails'] = [attrs['email']]
446 params['emails'] = [attrs['email']]
447 elif name == 'extern_type':
447 elif name == 'extern_type':
448 # cannot update this via form, expected value is original one
448 # cannot update this via form, expected value is original one
449 params['extern_type'] = "rhodecode"
449 params['extern_type'] = "rhodecode"
450 elif name == 'extern_name':
450 elif name == 'extern_name':
451 # cannot update this via form, expected value is original one
451 # cannot update this via form, expected value is original one
452 params['extern_name'] = 'xxx'
452 params['extern_name'] = 'xxx'
453 # special case since this user is not
453 # special case since this user is not
454 # logged in yet his data is not filled
454 # logged in yet his data is not filled
455 # so we use creation data
455 # so we use creation data
456
456
457 response = self.app.post(
457 response = self.app.post(
458 route_path('user_update', user_id=usr.user_id), params)
458 route_path('user_update', user_id=usr.user_id), params)
459 assert response.status_int == 302
459 assert response.status_int == 302
460 assert_session_flash(response, 'User updated successfully')
460 assert_session_flash(response, 'User updated successfully')
461
461
462 updated_user = User.get(user_id)
462 updated_user = User.get(user_id)
463 updated_params = updated_user.get_api_data()
463 updated_params = updated_user.get_api_data()
464 updated_params.update({'password_confirmation': ''})
464 updated_params.update({'password_confirmation': ''})
465 updated_params.update({'new_password': ''})
465 updated_params.update({'new_password': ''})
466
466
467 del params['csrf_token']
467 del params['csrf_token']
468 assert params == updated_params
468 assert params == updated_params
469
469
470 def test_update_and_migrate_password(
470 def test_update_and_migrate_password(
471 self, autologin_user, real_crypto_backend, user_util):
471 self, autologin_user, real_crypto_backend, user_util):
472
472
473 user = user_util.create_user()
473 user = user_util.create_user()
474 temp_user = user.username
474 temp_user = user.username
475 user.password = auth._RhodeCodeCryptoSha256().hash_create(
475 user.password = auth._RhodeCodeCryptoSha256().hash_create(
476 b'test123')
476 b'test123')
477 Session().add(user)
477 Session().add(user)
478 Session().commit()
478 Session().commit()
479
479
480 params = user.get_api_data()
480 params = user.get_api_data()
481
481
482 params.update({
482 params.update({
483 'password_confirmation': 'qweqwe123',
483 'password_confirmation': 'qweqwe123',
484 'new_password': 'qweqwe123',
484 'new_password': 'qweqwe123',
485 'language': 'en',
485 'language': 'en',
486 'csrf_token': autologin_user.csrf_token,
486 'csrf_token': autologin_user.csrf_token,
487 })
487 })
488
488
489 response = self.app.post(
489 response = self.app.post(
490 route_path('user_update', user_id=user.user_id), params)
490 route_path('user_update', user_id=user.user_id), params)
491 assert response.status_int == 302
491 assert response.status_int == 302
492 assert_session_flash(response, 'User updated successfully')
492 assert_session_flash(response, 'User updated successfully')
493
493
494 # new password should be bcrypted, after log-in and transfer
494 # new password should be bcrypted, after log-in and transfer
495 user = User.get_by_username(temp_user)
495 user = User.get_by_username(temp_user)
496 assert user.password.startswith('$')
496 assert user.password.startswith('$')
497
497
498 updated_user = User.get_by_username(temp_user)
498 updated_user = User.get_by_username(temp_user)
499 updated_params = updated_user.get_api_data()
499 updated_params = updated_user.get_api_data()
500 updated_params.update({'password_confirmation': 'qweqwe123'})
500 updated_params.update({'password_confirmation': 'qweqwe123'})
501 updated_params.update({'new_password': 'qweqwe123'})
501 updated_params.update({'new_password': 'qweqwe123'})
502
502
503 del params['csrf_token']
503 del params['csrf_token']
504 assert params == updated_params
504 assert params == updated_params
505
505
506 def test_delete(self):
506 def test_delete(self):
507 self.log_user()
507 self.log_user()
508 username = 'newtestuserdeleteme'
508 username = 'newtestuserdeleteme'
509
509
510 fixture.create_user(name=username)
510 fixture.create_user(name=username)
511
511
512 new_user = Session().query(User)\
512 new_user = Session().query(User)\
513 .filter(User.username == username).one()
513 .filter(User.username == username).one()
514 response = self.app.post(
514 response = self.app.post(
515 route_path('user_delete', user_id=new_user.user_id),
515 route_path('user_delete', user_id=new_user.user_id),
516 params={'csrf_token': self.csrf_token})
516 params={'csrf_token': self.csrf_token})
517
517
518 assert_session_flash(response, 'Successfully deleted user')
518 assert_session_flash(response, 'Successfully deleted user')
519
519
520 def test_delete_owner_of_repository(self, request, user_util):
520 def test_delete_owner_of_repository(self, request, user_util):
521 self.log_user()
521 self.log_user()
522 obj_name = 'test_repo'
522 obj_name = 'test_repo'
523 usr = user_util.create_user()
523 usr = user_util.create_user()
524 username = usr.username
524 username = usr.username
525 fixture.create_repo(obj_name, cur_user=usr.username)
525 fixture.create_repo(obj_name, cur_user=usr.username)
526
526
527 new_user = Session().query(User)\
527 new_user = Session().query(User)\
528 .filter(User.username == username).one()
528 .filter(User.username == username).one()
529 response = self.app.post(
529 response = self.app.post(
530 route_path('user_delete', user_id=new_user.user_id),
530 route_path('user_delete', user_id=new_user.user_id),
531 params={'csrf_token': self.csrf_token})
531 params={'csrf_token': self.csrf_token})
532
532
533 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
533 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
534 'Switch owners or remove those repositories:%s' % (username,
534 'Switch owners or remove those repositories:%s' % (username,
535 obj_name)
535 obj_name)
536 assert_session_flash(response, msg)
536 assert_session_flash(response, msg)
537 fixture.destroy_repo(obj_name)
537 fixture.destroy_repo(obj_name)
538
538
539 def test_delete_owner_of_repository_detaching(self, request, user_util):
539 def test_delete_owner_of_repository_detaching(self, request, user_util):
540 self.log_user()
540 self.log_user()
541 obj_name = 'test_repo'
541 obj_name = 'test_repo'
542 usr = user_util.create_user(auto_cleanup=False)
542 usr = user_util.create_user(auto_cleanup=False)
543 username = usr.username
543 username = usr.username
544 fixture.create_repo(obj_name, cur_user=usr.username)
544 fixture.create_repo(obj_name, cur_user=usr.username)
545
545
546 new_user = Session().query(User)\
546 new_user = Session().query(User)\
547 .filter(User.username == username).one()
547 .filter(User.username == username).one()
548 response = self.app.post(
548 response = self.app.post(
549 route_path('user_delete', user_id=new_user.user_id),
549 route_path('user_delete', user_id=new_user.user_id),
550 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
550 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
551
551
552 msg = 'Detached 1 repositories'
552 msg = 'Detached 1 repositories'
553 assert_session_flash(response, msg)
553 assert_session_flash(response, msg)
554 fixture.destroy_repo(obj_name)
554 fixture.destroy_repo(obj_name)
555
555
556 def test_delete_owner_of_repository_deleting(self, request, user_util):
556 def test_delete_owner_of_repository_deleting(self, request, user_util):
557 self.log_user()
557 self.log_user()
558 obj_name = 'test_repo'
558 obj_name = 'test_repo'
559 usr = user_util.create_user(auto_cleanup=False)
559 usr = user_util.create_user(auto_cleanup=False)
560 username = usr.username
560 username = usr.username
561 fixture.create_repo(obj_name, cur_user=usr.username)
561 fixture.create_repo(obj_name, cur_user=usr.username)
562
562
563 new_user = Session().query(User)\
563 new_user = Session().query(User)\
564 .filter(User.username == username).one()
564 .filter(User.username == username).one()
565 response = self.app.post(
565 response = self.app.post(
566 route_path('user_delete', user_id=new_user.user_id),
566 route_path('user_delete', user_id=new_user.user_id),
567 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
567 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
568
568
569 msg = 'Deleted 1 repositories'
569 msg = 'Deleted 1 repositories'
570 assert_session_flash(response, msg)
570 assert_session_flash(response, msg)
571
571
572 def test_delete_owner_of_repository_group(self, request, user_util):
572 def test_delete_owner_of_repository_group(self, request, user_util):
573 self.log_user()
573 self.log_user()
574 obj_name = 'test_group'
574 obj_name = 'test_group'
575 usr = user_util.create_user()
575 usr = user_util.create_user()
576 username = usr.username
576 username = usr.username
577 fixture.create_repo_group(obj_name, cur_user=usr.username)
577 fixture.create_repo_group(obj_name, cur_user=usr.username)
578
578
579 new_user = Session().query(User)\
579 new_user = Session().query(User)\
580 .filter(User.username == username).one()
580 .filter(User.username == username).one()
581 response = self.app.post(
581 response = self.app.post(
582 route_path('user_delete', user_id=new_user.user_id),
582 route_path('user_delete', user_id=new_user.user_id),
583 params={'csrf_token': self.csrf_token})
583 params={'csrf_token': self.csrf_token})
584
584
585 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
585 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
586 'Switch owners or remove those repository groups:%s' % (username,
586 'Switch owners or remove those repository groups:%s' % (username,
587 obj_name)
587 obj_name)
588 assert_session_flash(response, msg)
588 assert_session_flash(response, msg)
589 fixture.destroy_repo_group(obj_name)
589 fixture.destroy_repo_group(obj_name)
590
590
591 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
591 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
592 self.log_user()
592 self.log_user()
593 obj_name = 'test_group'
593 obj_name = 'test_group'
594 usr = user_util.create_user(auto_cleanup=False)
594 usr = user_util.create_user(auto_cleanup=False)
595 username = usr.username
595 username = usr.username
596 fixture.create_repo_group(obj_name, cur_user=usr.username)
596 fixture.create_repo_group(obj_name, cur_user=usr.username)
597
597
598 new_user = Session().query(User)\
598 new_user = Session().query(User)\
599 .filter(User.username == username).one()
599 .filter(User.username == username).one()
600 response = self.app.post(
600 response = self.app.post(
601 route_path('user_delete', user_id=new_user.user_id),
601 route_path('user_delete', user_id=new_user.user_id),
602 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
602 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
603
603
604 msg = 'Deleted 1 repository groups'
604 msg = 'Deleted 1 repository groups'
605 assert_session_flash(response, msg)
605 assert_session_flash(response, msg)
606
606
607 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
607 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
608 self.log_user()
608 self.log_user()
609 obj_name = 'test_group'
609 obj_name = 'test_group'
610 usr = user_util.create_user(auto_cleanup=False)
610 usr = user_util.create_user(auto_cleanup=False)
611 username = usr.username
611 username = usr.username
612 fixture.create_repo_group(obj_name, cur_user=usr.username)
612 fixture.create_repo_group(obj_name, cur_user=usr.username)
613
613
614 new_user = Session().query(User)\
614 new_user = Session().query(User)\
615 .filter(User.username == username).one()
615 .filter(User.username == username).one()
616 response = self.app.post(
616 response = self.app.post(
617 route_path('user_delete', user_id=new_user.user_id),
617 route_path('user_delete', user_id=new_user.user_id),
618 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
618 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
619
619
620 msg = 'Detached 1 repository groups'
620 msg = 'Detached 1 repository groups'
621 assert_session_flash(response, msg)
621 assert_session_flash(response, msg)
622 fixture.destroy_repo_group(obj_name)
622 fixture.destroy_repo_group(obj_name)
623
623
624 def test_delete_owner_of_user_group(self, request, user_util):
624 def test_delete_owner_of_user_group(self, request, user_util):
625 self.log_user()
625 self.log_user()
626 obj_name = 'test_user_group'
626 obj_name = 'test_user_group'
627 usr = user_util.create_user()
627 usr = user_util.create_user()
628 username = usr.username
628 username = usr.username
629 fixture.create_user_group(obj_name, cur_user=usr.username)
629 fixture.create_user_group(obj_name, cur_user=usr.username)
630
630
631 new_user = Session().query(User)\
631 new_user = Session().query(User)\
632 .filter(User.username == username).one()
632 .filter(User.username == username).one()
633 response = self.app.post(
633 response = self.app.post(
634 route_path('user_delete', user_id=new_user.user_id),
634 route_path('user_delete', user_id=new_user.user_id),
635 params={'csrf_token': self.csrf_token})
635 params={'csrf_token': self.csrf_token})
636
636
637 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
637 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
638 'Switch owners or remove those user groups:%s' % (username,
638 'Switch owners or remove those user groups:%s' % (username,
639 obj_name)
639 obj_name)
640 assert_session_flash(response, msg)
640 assert_session_flash(response, msg)
641 fixture.destroy_user_group(obj_name)
641 fixture.destroy_user_group(obj_name)
642
642
643 def test_delete_owner_of_user_group_detaching(self, request, user_util):
643 def test_delete_owner_of_user_group_detaching(self, request, user_util):
644 self.log_user()
644 self.log_user()
645 obj_name = 'test_user_group'
645 obj_name = 'test_user_group'
646 usr = user_util.create_user(auto_cleanup=False)
646 usr = user_util.create_user(auto_cleanup=False)
647 username = usr.username
647 username = usr.username
648 fixture.create_user_group(obj_name, cur_user=usr.username)
648 fixture.create_user_group(obj_name, cur_user=usr.username)
649
649
650 new_user = Session().query(User)\
650 new_user = Session().query(User)\
651 .filter(User.username == username).one()
651 .filter(User.username == username).one()
652 try:
652 try:
653 response = self.app.post(
653 response = self.app.post(
654 route_path('user_delete', user_id=new_user.user_id),
654 route_path('user_delete', user_id=new_user.user_id),
655 params={'user_user_groups': 'detach',
655 params={'user_user_groups': 'detach',
656 'csrf_token': self.csrf_token})
656 'csrf_token': self.csrf_token})
657
657
658 msg = 'Detached 1 user groups'
658 msg = 'Detached 1 user groups'
659 assert_session_flash(response, msg)
659 assert_session_flash(response, msg)
660 finally:
660 finally:
661 fixture.destroy_user_group(obj_name)
661 fixture.destroy_user_group(obj_name)
662
662
663 def test_delete_owner_of_user_group_deleting(self, request, user_util):
663 def test_delete_owner_of_user_group_deleting(self, request, user_util):
664 self.log_user()
664 self.log_user()
665 obj_name = 'test_user_group'
665 obj_name = 'test_user_group'
666 usr = user_util.create_user(auto_cleanup=False)
666 usr = user_util.create_user(auto_cleanup=False)
667 username = usr.username
667 username = usr.username
668 fixture.create_user_group(obj_name, cur_user=usr.username)
668 fixture.create_user_group(obj_name, cur_user=usr.username)
669
669
670 new_user = Session().query(User)\
670 new_user = Session().query(User)\
671 .filter(User.username == username).one()
671 .filter(User.username == username).one()
672 response = self.app.post(
672 response = self.app.post(
673 route_path('user_delete', user_id=new_user.user_id),
673 route_path('user_delete', user_id=new_user.user_id),
674 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
674 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
675
675
676 msg = 'Deleted 1 user groups'
676 msg = 'Deleted 1 user groups'
677 assert_session_flash(response, msg)
677 assert_session_flash(response, msg)
678
678
679 def test_edit(self, user_util):
679 def test_edit(self, user_util):
680 self.log_user()
680 self.log_user()
681 user = user_util.create_user()
681 user = user_util.create_user()
682 self.app.get(route_path('user_edit', user_id=user.user_id))
682 self.app.get(route_path('user_edit', user_id=user.user_id))
683
683
684 def test_edit_default_user_redirect(self):
684 def test_edit_default_user_redirect(self):
685 self.log_user()
685 self.log_user()
686 user = User.get_default_user()
686 user = User.get_default_user()
687 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
687 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
688
688
689 @pytest.mark.parametrize(
689 @pytest.mark.parametrize(
690 'repo_create, repo_create_write, user_group_create, repo_group_create,'
690 'repo_create, repo_create_write, user_group_create, repo_group_create,'
691 'fork_create, inherit_default_permissions, expect_error,'
691 'fork_create, inherit_default_permissions, expect_error,'
692 'expect_form_error', [
692 'expect_form_error', [
693 ('hg.create.none', 'hg.create.write_on_repogroup.false',
693 ('hg.create.none', 'hg.create.write_on_repogroup.false',
694 'hg.usergroup.create.false', 'hg.repogroup.create.false',
694 'hg.usergroup.create.false', 'hg.repogroup.create.false',
695 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
695 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
696 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
696 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
697 'hg.usergroup.create.false', 'hg.repogroup.create.false',
697 'hg.usergroup.create.false', 'hg.repogroup.create.false',
698 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
698 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
699 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
699 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
700 'hg.usergroup.create.true', 'hg.repogroup.create.true',
700 'hg.usergroup.create.true', 'hg.repogroup.create.true',
701 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
701 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
702 False),
702 False),
703 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
703 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
704 'hg.usergroup.create.true', 'hg.repogroup.create.true',
704 'hg.usergroup.create.true', 'hg.repogroup.create.true',
705 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
705 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
706 True),
706 True),
707 ('', '', '', '', '', '', True, False),
707 ('', '', '', '', '', '', True, False),
708 ])
708 ])
709 def test_global_perms_on_user(
709 def test_global_perms_on_user(
710 self, repo_create, repo_create_write, user_group_create,
710 self, repo_create, repo_create_write, user_group_create,
711 repo_group_create, fork_create, expect_error, expect_form_error,
711 repo_group_create, fork_create, expect_error, expect_form_error,
712 inherit_default_permissions, user_util):
712 inherit_default_permissions, user_util):
713 self.log_user()
713 self.log_user()
714 user = user_util.create_user()
714 user = user_util.create_user()
715 uid = user.user_id
715 uid = user.user_id
716
716
717 # ENABLE REPO CREATE ON A GROUP
717 # ENABLE REPO CREATE ON A GROUP
718 perm_params = {
718 perm_params = {
719 'inherit_default_permissions': False,
719 'inherit_default_permissions': False,
720 'default_repo_create': repo_create,
720 'default_repo_create': repo_create,
721 'default_repo_create_on_write': repo_create_write,
721 'default_repo_create_on_write': repo_create_write,
722 'default_user_group_create': user_group_create,
722 'default_user_group_create': user_group_create,
723 'default_repo_group_create': repo_group_create,
723 'default_repo_group_create': repo_group_create,
724 'default_fork_create': fork_create,
724 'default_fork_create': fork_create,
725 'default_inherit_default_permissions': inherit_default_permissions,
725 'default_inherit_default_permissions': inherit_default_permissions,
726 'csrf_token': self.csrf_token,
726 'csrf_token': self.csrf_token,
727 }
727 }
728 response = self.app.post(
728 response = self.app.post(
729 route_path('user_edit_global_perms_update', user_id=uid),
729 route_path('user_edit_global_perms_update', user_id=uid),
730 params=perm_params)
730 params=perm_params)
731
731
732 if expect_form_error:
732 if expect_form_error:
733 assert response.status_int == 200
733 assert response.status_int == 200
734 response.mustcontain('Value must be one of')
734 response.mustcontain('Value must be one of')
735 else:
735 else:
736 if expect_error:
736 if expect_error:
737 msg = 'An error occurred during permissions saving'
737 msg = 'An error occurred during permissions saving'
738 else:
738 else:
739 msg = 'User global permissions updated successfully'
739 msg = 'User global permissions updated successfully'
740 ug = User.get(uid)
740 ug = User.get(uid)
741 del perm_params['inherit_default_permissions']
741 del perm_params['inherit_default_permissions']
742 del perm_params['csrf_token']
742 del perm_params['csrf_token']
743 assert perm_params == ug.get_default_perms()
743 assert perm_params == ug.get_default_perms()
744 assert_session_flash(response, msg)
744 assert_session_flash(response, msg)
745
745
746 def test_global_permissions_initial_values(self, user_util):
746 def test_global_permissions_initial_values(self, user_util):
747 self.log_user()
747 self.log_user()
748 user = user_util.create_user()
748 user = user_util.create_user()
749 uid = user.user_id
749 uid = user.user_id
750 response = self.app.get(
750 response = self.app.get(
751 route_path('user_edit_global_perms', user_id=uid))
751 route_path('user_edit_global_perms', user_id=uid))
752 default_user = User.get_default_user()
752 default_user = User.get_default_user()
753 default_permissions = default_user.get_default_perms()
753 default_permissions = default_user.get_default_perms()
754 assert_response = response.assert_response()
754 assert_response = response.assert_response()
755 expected_permissions = (
755 expected_permissions = (
756 'default_repo_create', 'default_repo_create_on_write',
756 'default_repo_create', 'default_repo_create_on_write',
757 'default_fork_create', 'default_repo_group_create',
757 'default_fork_create', 'default_repo_group_create',
758 'default_user_group_create', 'default_inherit_default_permissions')
758 'default_user_group_create', 'default_inherit_default_permissions')
759 for permission in expected_permissions:
759 for permission in expected_permissions:
760 css_selector = '[name={}][checked=checked]'.format(permission)
760 css_selector = '[name={}][checked=checked]'.format(permission)
761 element = assert_response.get_element(css_selector)
761 element = assert_response.get_element(css_selector)
762 assert element.value == default_permissions[permission]
762 assert element.value == default_permissions[permission]
763
763
764 def test_perms_summary_page(self):
764 def test_perms_summary_page(self):
765 user = self.log_user()
765 user = self.log_user()
766 response = self.app.get(
766 response = self.app.get(
767 route_path('edit_user_perms_summary', user_id=user['user_id']))
767 route_path('edit_user_perms_summary', user_id=user['user_id']))
768 for repo in Repository.query().all():
768 for repo in Repository.query().all():
769 response.mustcontain(repo.repo_name)
769 response.mustcontain(repo.repo_name)
770
770
771 def test_perms_summary_page_json(self):
771 def test_perms_summary_page_json(self):
772 user = self.log_user()
772 user = self.log_user()
773 response = self.app.get(
773 response = self.app.get(
774 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
774 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
775 for repo in Repository.query().all():
775 for repo in Repository.query().all():
776 response.mustcontain(repo.repo_name)
776 response.mustcontain(repo.repo_name)
777
777
778 def test_audit_log_page(self):
778 def test_audit_log_page(self):
779 user = self.log_user()
779 user = self.log_user()
780 self.app.get(
780 self.app.get(
781 route_path('edit_user_audit_logs', user_id=user['user_id']))
781 route_path('edit_user_audit_logs', user_id=user['user_id']))
@@ -1,486 +1,484 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22 import logging
22 import logging
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25 import datetime
25 import datetime
26 from pyramid.interfaces import IRoutesMapper
26 from pyramid.interfaces import IRoutesMapper
27
27
28 from pyramid.view import view_config
28 from pyramid.view import view_config
29 from pyramid.httpexceptions import HTTPFound
29 from pyramid.httpexceptions import HTTPFound
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31 from pyramid.response import Response
31 from pyramid.response import Response
32
32
33 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
35 from rhodecode.events import trigger
35 from rhodecode.events import trigger
36
36
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 from rhodecode.lib.utils2 import aslist, safe_unicode
40 from rhodecode.lib.utils2 import aslist, safe_unicode
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 or_, coalesce, User, UserIpMap, UserSshKeys)
42 or_, coalesce, User, UserIpMap, UserSshKeys)
43 from rhodecode.model.forms import (
43 from rhodecode.model.forms import (
44 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
44 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
45 from rhodecode.model.meta import Session
45 from rhodecode.model.meta import Session
46 from rhodecode.model.permission import PermissionModel
46 from rhodecode.model.permission import PermissionModel
47 from rhodecode.model.settings import SettingsModel
47 from rhodecode.model.settings import SettingsModel
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class AdminPermissionsView(BaseAppView, DataGridAppView):
53 class AdminPermissionsView(BaseAppView, DataGridAppView):
54 def load_default_context(self):
54 def load_default_context(self):
55 c = self._get_local_tmpl_context()
55 c = self._get_local_tmpl_context()
56
57
58 PermissionModel().set_global_permission_choices(
56 PermissionModel().set_global_permission_choices(
59 c, gettext_translator=self.request.translate)
57 c, gettext_translator=self.request.translate)
60 return c
58 return c
61
59
62 @LoginRequired()
60 @LoginRequired()
63 @HasPermissionAllDecorator('hg.admin')
61 @HasPermissionAllDecorator('hg.admin')
64 @view_config(
62 @view_config(
65 route_name='admin_permissions_application', request_method='GET',
63 route_name='admin_permissions_application', request_method='GET',
66 renderer='rhodecode:templates/admin/permissions/permissions.mako')
64 renderer='rhodecode:templates/admin/permissions/permissions.mako')
67 def permissions_application(self):
65 def permissions_application(self):
68 c = self.load_default_context()
66 c = self.load_default_context()
69 c.active = 'application'
67 c.active = 'application'
70
68
71 c.user = User.get_default_user(refresh=True)
69 c.user = User.get_default_user(refresh=True)
72
70
73 app_settings = SettingsModel().get_all_settings()
71 app_settings = SettingsModel().get_all_settings()
74 defaults = {
72 defaults = {
75 'anonymous': c.user.active,
73 'anonymous': c.user.active,
76 'default_register_message': app_settings.get(
74 'default_register_message': app_settings.get(
77 'rhodecode_register_message')
75 'rhodecode_register_message')
78 }
76 }
79 defaults.update(c.user.get_default_perms())
77 defaults.update(c.user.get_default_perms())
80
78
81 data = render('rhodecode:templates/admin/permissions/permissions.mako',
79 data = render('rhodecode:templates/admin/permissions/permissions.mako',
82 self._get_template_context(c), self.request)
80 self._get_template_context(c), self.request)
83 html = formencode.htmlfill.render(
81 html = formencode.htmlfill.render(
84 data,
82 data,
85 defaults=defaults,
83 defaults=defaults,
86 encoding="UTF-8",
84 encoding="UTF-8",
87 force_defaults=False
85 force_defaults=False
88 )
86 )
89 return Response(html)
87 return Response(html)
90
88
91 @LoginRequired()
89 @LoginRequired()
92 @HasPermissionAllDecorator('hg.admin')
90 @HasPermissionAllDecorator('hg.admin')
93 @CSRFRequired()
91 @CSRFRequired()
94 @view_config(
92 @view_config(
95 route_name='admin_permissions_application_update', request_method='POST',
93 route_name='admin_permissions_application_update', request_method='POST',
96 renderer='rhodecode:templates/admin/permissions/permissions.mako')
94 renderer='rhodecode:templates/admin/permissions/permissions.mako')
97 def permissions_application_update(self):
95 def permissions_application_update(self):
98 _ = self.request.translate
96 _ = self.request.translate
99 c = self.load_default_context()
97 c = self.load_default_context()
100 c.active = 'application'
98 c.active = 'application'
101
99
102 _form = ApplicationPermissionsForm(
100 _form = ApplicationPermissionsForm(
103 self.request.translate,
101 self.request.translate,
104 [x[0] for x in c.register_choices],
102 [x[0] for x in c.register_choices],
105 [x[0] for x in c.password_reset_choices],
103 [x[0] for x in c.password_reset_choices],
106 [x[0] for x in c.extern_activate_choices])()
104 [x[0] for x in c.extern_activate_choices])()
107
105
108 try:
106 try:
109 form_result = _form.to_python(dict(self.request.POST))
107 form_result = _form.to_python(dict(self.request.POST))
110 form_result.update({'perm_user_name': User.DEFAULT_USER})
108 form_result.update({'perm_user_name': User.DEFAULT_USER})
111 PermissionModel().update_application_permissions(form_result)
109 PermissionModel().update_application_permissions(form_result)
112
110
113 settings = [
111 settings = [
114 ('register_message', 'default_register_message'),
112 ('register_message', 'default_register_message'),
115 ]
113 ]
116 for setting, form_key in settings:
114 for setting, form_key in settings:
117 sett = SettingsModel().create_or_update_setting(
115 sett = SettingsModel().create_or_update_setting(
118 setting, form_result[form_key])
116 setting, form_result[form_key])
119 Session().add(sett)
117 Session().add(sett)
120
118
121 Session().commit()
119 Session().commit()
122 h.flash(_('Application permissions updated successfully'),
120 h.flash(_('Application permissions updated successfully'),
123 category='success')
121 category='success')
124
122
125 except formencode.Invalid as errors:
123 except formencode.Invalid as errors:
126 defaults = errors.value
124 defaults = errors.value
127
125
128 data = render(
126 data = render(
129 'rhodecode:templates/admin/permissions/permissions.mako',
127 'rhodecode:templates/admin/permissions/permissions.mako',
130 self._get_template_context(c), self.request)
128 self._get_template_context(c), self.request)
131 html = formencode.htmlfill.render(
129 html = formencode.htmlfill.render(
132 data,
130 data,
133 defaults=defaults,
131 defaults=defaults,
134 errors=errors.error_dict or {},
132 errors=errors.error_dict or {},
135 prefix_error=False,
133 prefix_error=False,
136 encoding="UTF-8",
134 encoding="UTF-8",
137 force_defaults=False
135 force_defaults=False
138 )
136 )
139 return Response(html)
137 return Response(html)
140
138
141 except Exception:
139 except Exception:
142 log.exception("Exception during update of permissions")
140 log.exception("Exception during update of permissions")
143 h.flash(_('Error occurred during update of permissions'),
141 h.flash(_('Error occurred during update of permissions'),
144 category='error')
142 category='error')
145
143
146 raise HTTPFound(h.route_path('admin_permissions_application'))
144 raise HTTPFound(h.route_path('admin_permissions_application'))
147
145
148 @LoginRequired()
146 @LoginRequired()
149 @HasPermissionAllDecorator('hg.admin')
147 @HasPermissionAllDecorator('hg.admin')
150 @view_config(
148 @view_config(
151 route_name='admin_permissions_object', request_method='GET',
149 route_name='admin_permissions_object', request_method='GET',
152 renderer='rhodecode:templates/admin/permissions/permissions.mako')
150 renderer='rhodecode:templates/admin/permissions/permissions.mako')
153 def permissions_objects(self):
151 def permissions_objects(self):
154 c = self.load_default_context()
152 c = self.load_default_context()
155 c.active = 'objects'
153 c.active = 'objects'
156
154
157 c.user = User.get_default_user(refresh=True)
155 c.user = User.get_default_user(refresh=True)
158 defaults = {}
156 defaults = {}
159 defaults.update(c.user.get_default_perms())
157 defaults.update(c.user.get_default_perms())
160
158
161 data = render(
159 data = render(
162 'rhodecode:templates/admin/permissions/permissions.mako',
160 'rhodecode:templates/admin/permissions/permissions.mako',
163 self._get_template_context(c), self.request)
161 self._get_template_context(c), self.request)
164 html = formencode.htmlfill.render(
162 html = formencode.htmlfill.render(
165 data,
163 data,
166 defaults=defaults,
164 defaults=defaults,
167 encoding="UTF-8",
165 encoding="UTF-8",
168 force_defaults=False
166 force_defaults=False
169 )
167 )
170 return Response(html)
168 return Response(html)
171
169
172 @LoginRequired()
170 @LoginRequired()
173 @HasPermissionAllDecorator('hg.admin')
171 @HasPermissionAllDecorator('hg.admin')
174 @CSRFRequired()
172 @CSRFRequired()
175 @view_config(
173 @view_config(
176 route_name='admin_permissions_object_update', request_method='POST',
174 route_name='admin_permissions_object_update', request_method='POST',
177 renderer='rhodecode:templates/admin/permissions/permissions.mako')
175 renderer='rhodecode:templates/admin/permissions/permissions.mako')
178 def permissions_objects_update(self):
176 def permissions_objects_update(self):
179 _ = self.request.translate
177 _ = self.request.translate
180 c = self.load_default_context()
178 c = self.load_default_context()
181 c.active = 'objects'
179 c.active = 'objects'
182
180
183 _form = ObjectPermissionsForm(
181 _form = ObjectPermissionsForm(
184 self.request.translate,
182 self.request.translate,
185 [x[0] for x in c.repo_perms_choices],
183 [x[0] for x in c.repo_perms_choices],
186 [x[0] for x in c.group_perms_choices],
184 [x[0] for x in c.group_perms_choices],
187 [x[0] for x in c.user_group_perms_choices])()
185 [x[0] for x in c.user_group_perms_choices])()
188
186
189 try:
187 try:
190 form_result = _form.to_python(dict(self.request.POST))
188 form_result = _form.to_python(dict(self.request.POST))
191 form_result.update({'perm_user_name': User.DEFAULT_USER})
189 form_result.update({'perm_user_name': User.DEFAULT_USER})
192 PermissionModel().update_object_permissions(form_result)
190 PermissionModel().update_object_permissions(form_result)
193
191
194 Session().commit()
192 Session().commit()
195 h.flash(_('Object permissions updated successfully'),
193 h.flash(_('Object permissions updated successfully'),
196 category='success')
194 category='success')
197
195
198 except formencode.Invalid as errors:
196 except formencode.Invalid as errors:
199 defaults = errors.value
197 defaults = errors.value
200
198
201 data = render(
199 data = render(
202 'rhodecode:templates/admin/permissions/permissions.mako',
200 'rhodecode:templates/admin/permissions/permissions.mako',
203 self._get_template_context(c), self.request)
201 self._get_template_context(c), self.request)
204 html = formencode.htmlfill.render(
202 html = formencode.htmlfill.render(
205 data,
203 data,
206 defaults=defaults,
204 defaults=defaults,
207 errors=errors.error_dict or {},
205 errors=errors.error_dict or {},
208 prefix_error=False,
206 prefix_error=False,
209 encoding="UTF-8",
207 encoding="UTF-8",
210 force_defaults=False
208 force_defaults=False
211 )
209 )
212 return Response(html)
210 return Response(html)
213 except Exception:
211 except Exception:
214 log.exception("Exception during update of permissions")
212 log.exception("Exception during update of permissions")
215 h.flash(_('Error occurred during update of permissions'),
213 h.flash(_('Error occurred during update of permissions'),
216 category='error')
214 category='error')
217
215
218 raise HTTPFound(h.route_path('admin_permissions_object'))
216 raise HTTPFound(h.route_path('admin_permissions_object'))
219
217
220 @LoginRequired()
218 @LoginRequired()
221 @HasPermissionAllDecorator('hg.admin')
219 @HasPermissionAllDecorator('hg.admin')
222 @view_config(
220 @view_config(
223 route_name='admin_permissions_global', request_method='GET',
221 route_name='admin_permissions_global', request_method='GET',
224 renderer='rhodecode:templates/admin/permissions/permissions.mako')
222 renderer='rhodecode:templates/admin/permissions/permissions.mako')
225 def permissions_global(self):
223 def permissions_global(self):
226 c = self.load_default_context()
224 c = self.load_default_context()
227 c.active = 'global'
225 c.active = 'global'
228
226
229 c.user = User.get_default_user(refresh=True)
227 c.user = User.get_default_user(refresh=True)
230 defaults = {}
228 defaults = {}
231 defaults.update(c.user.get_default_perms())
229 defaults.update(c.user.get_default_perms())
232
230
233 data = render(
231 data = render(
234 'rhodecode:templates/admin/permissions/permissions.mako',
232 'rhodecode:templates/admin/permissions/permissions.mako',
235 self._get_template_context(c), self.request)
233 self._get_template_context(c), self.request)
236 html = formencode.htmlfill.render(
234 html = formencode.htmlfill.render(
237 data,
235 data,
238 defaults=defaults,
236 defaults=defaults,
239 encoding="UTF-8",
237 encoding="UTF-8",
240 force_defaults=False
238 force_defaults=False
241 )
239 )
242 return Response(html)
240 return Response(html)
243
241
244 @LoginRequired()
242 @LoginRequired()
245 @HasPermissionAllDecorator('hg.admin')
243 @HasPermissionAllDecorator('hg.admin')
246 @CSRFRequired()
244 @CSRFRequired()
247 @view_config(
245 @view_config(
248 route_name='admin_permissions_global_update', request_method='POST',
246 route_name='admin_permissions_global_update', request_method='POST',
249 renderer='rhodecode:templates/admin/permissions/permissions.mako')
247 renderer='rhodecode:templates/admin/permissions/permissions.mako')
250 def permissions_global_update(self):
248 def permissions_global_update(self):
251 _ = self.request.translate
249 _ = self.request.translate
252 c = self.load_default_context()
250 c = self.load_default_context()
253 c.active = 'global'
251 c.active = 'global'
254
252
255 _form = UserPermissionsForm(
253 _form = UserPermissionsForm(
256 self.request.translate,
254 self.request.translate,
257 [x[0] for x in c.repo_create_choices],
255 [x[0] for x in c.repo_create_choices],
258 [x[0] for x in c.repo_create_on_write_choices],
256 [x[0] for x in c.repo_create_on_write_choices],
259 [x[0] for x in c.repo_group_create_choices],
257 [x[0] for x in c.repo_group_create_choices],
260 [x[0] for x in c.user_group_create_choices],
258 [x[0] for x in c.user_group_create_choices],
261 [x[0] for x in c.fork_choices],
259 [x[0] for x in c.fork_choices],
262 [x[0] for x in c.inherit_default_permission_choices])()
260 [x[0] for x in c.inherit_default_permission_choices])()
263
261
264 try:
262 try:
265 form_result = _form.to_python(dict(self.request.POST))
263 form_result = _form.to_python(dict(self.request.POST))
266 form_result.update({'perm_user_name': User.DEFAULT_USER})
264 form_result.update({'perm_user_name': User.DEFAULT_USER})
267 PermissionModel().update_user_permissions(form_result)
265 PermissionModel().update_user_permissions(form_result)
268
266
269 Session().commit()
267 Session().commit()
270 h.flash(_('Global permissions updated successfully'),
268 h.flash(_('Global permissions updated successfully'),
271 category='success')
269 category='success')
272
270
273 except formencode.Invalid as errors:
271 except formencode.Invalid as errors:
274 defaults = errors.value
272 defaults = errors.value
275
273
276 data = render(
274 data = render(
277 'rhodecode:templates/admin/permissions/permissions.mako',
275 'rhodecode:templates/admin/permissions/permissions.mako',
278 self._get_template_context(c), self.request)
276 self._get_template_context(c), self.request)
279 html = formencode.htmlfill.render(
277 html = formencode.htmlfill.render(
280 data,
278 data,
281 defaults=defaults,
279 defaults=defaults,
282 errors=errors.error_dict or {},
280 errors=errors.error_dict or {},
283 prefix_error=False,
281 prefix_error=False,
284 encoding="UTF-8",
282 encoding="UTF-8",
285 force_defaults=False
283 force_defaults=False
286 )
284 )
287 return Response(html)
285 return Response(html)
288 except Exception:
286 except Exception:
289 log.exception("Exception during update of permissions")
287 log.exception("Exception during update of permissions")
290 h.flash(_('Error occurred during update of permissions'),
288 h.flash(_('Error occurred during update of permissions'),
291 category='error')
289 category='error')
292
290
293 raise HTTPFound(h.route_path('admin_permissions_global'))
291 raise HTTPFound(h.route_path('admin_permissions_global'))
294
292
295 @LoginRequired()
293 @LoginRequired()
296 @HasPermissionAllDecorator('hg.admin')
294 @HasPermissionAllDecorator('hg.admin')
297 @view_config(
295 @view_config(
298 route_name='admin_permissions_ips', request_method='GET',
296 route_name='admin_permissions_ips', request_method='GET',
299 renderer='rhodecode:templates/admin/permissions/permissions.mako')
297 renderer='rhodecode:templates/admin/permissions/permissions.mako')
300 def permissions_ips(self):
298 def permissions_ips(self):
301 c = self.load_default_context()
299 c = self.load_default_context()
302 c.active = 'ips'
300 c.active = 'ips'
303
301
304 c.user = User.get_default_user(refresh=True)
302 c.user = User.get_default_user(refresh=True)
305 c.user_ip_map = (
303 c.user_ip_map = (
306 UserIpMap.query().filter(UserIpMap.user == c.user).all())
304 UserIpMap.query().filter(UserIpMap.user == c.user).all())
307
305
308 return self._get_template_context(c)
306 return self._get_template_context(c)
309
307
310 @LoginRequired()
308 @LoginRequired()
311 @HasPermissionAllDecorator('hg.admin')
309 @HasPermissionAllDecorator('hg.admin')
312 @view_config(
310 @view_config(
313 route_name='admin_permissions_overview', request_method='GET',
311 route_name='admin_permissions_overview', request_method='GET',
314 renderer='rhodecode:templates/admin/permissions/permissions.mako')
312 renderer='rhodecode:templates/admin/permissions/permissions.mako')
315 def permissions_overview(self):
313 def permissions_overview(self):
316 c = self.load_default_context()
314 c = self.load_default_context()
317 c.active = 'perms'
315 c.active = 'perms'
318
316
319 c.user = User.get_default_user(refresh=True)
317 c.user = User.get_default_user(refresh=True)
320 c.perm_user = c.user.AuthUser()
318 c.perm_user = c.user.AuthUser()
321 return self._get_template_context(c)
319 return self._get_template_context(c)
322
320
323 @LoginRequired()
321 @LoginRequired()
324 @HasPermissionAllDecorator('hg.admin')
322 @HasPermissionAllDecorator('hg.admin')
325 @view_config(
323 @view_config(
326 route_name='admin_permissions_auth_token_access', request_method='GET',
324 route_name='admin_permissions_auth_token_access', request_method='GET',
327 renderer='rhodecode:templates/admin/permissions/permissions.mako')
325 renderer='rhodecode:templates/admin/permissions/permissions.mako')
328 def auth_token_access(self):
326 def auth_token_access(self):
329 from rhodecode import CONFIG
327 from rhodecode import CONFIG
330
328
331 c = self.load_default_context()
329 c = self.load_default_context()
332 c.active = 'auth_token_access'
330 c.active = 'auth_token_access'
333
331
334 c.user = User.get_default_user(refresh=True)
332 c.user = User.get_default_user(refresh=True)
335 c.perm_user = c.user.AuthUser()
333 c.perm_user = c.user.AuthUser()
336
334
337 mapper = self.request.registry.queryUtility(IRoutesMapper)
335 mapper = self.request.registry.queryUtility(IRoutesMapper)
338 c.view_data = []
336 c.view_data = []
339
337
340 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
338 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
341 introspector = self.request.registry.introspector
339 introspector = self.request.registry.introspector
342
340
343 view_intr = {}
341 view_intr = {}
344 for view_data in introspector.get_category('views'):
342 for view_data in introspector.get_category('views'):
345 intr = view_data['introspectable']
343 intr = view_data['introspectable']
346
344
347 if 'route_name' in intr and intr['attr']:
345 if 'route_name' in intr and intr['attr']:
348 view_intr[intr['route_name']] = '{}:{}'.format(
346 view_intr[intr['route_name']] = '{}:{}'.format(
349 str(intr['derived_callable'].func_name), intr['attr']
347 str(intr['derived_callable'].func_name), intr['attr']
350 )
348 )
351
349
352 c.whitelist_key = 'api_access_controllers_whitelist'
350 c.whitelist_key = 'api_access_controllers_whitelist'
353 c.whitelist_file = CONFIG.get('__file__')
351 c.whitelist_file = CONFIG.get('__file__')
354 whitelist_views = aslist(
352 whitelist_views = aslist(
355 CONFIG.get(c.whitelist_key), sep=',')
353 CONFIG.get(c.whitelist_key), sep=',')
356
354
357 for route_info in mapper.get_routes():
355 for route_info in mapper.get_routes():
358 if not route_info.name.startswith('__'):
356 if not route_info.name.startswith('__'):
359 routepath = route_info.pattern
357 routepath = route_info.pattern
360
358
361 def replace(matchobj):
359 def replace(matchobj):
362 if matchobj.group(1):
360 if matchobj.group(1):
363 return "{%s}" % matchobj.group(1).split(':')[0]
361 return "{%s}" % matchobj.group(1).split(':')[0]
364 else:
362 else:
365 return "{%s}" % matchobj.group(2)
363 return "{%s}" % matchobj.group(2)
366
364
367 routepath = _argument_prog.sub(replace, routepath)
365 routepath = _argument_prog.sub(replace, routepath)
368
366
369 if not routepath.startswith('/'):
367 if not routepath.startswith('/'):
370 routepath = '/' + routepath
368 routepath = '/' + routepath
371
369
372 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
370 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
373 active = view_fqn in whitelist_views
371 active = view_fqn in whitelist_views
374 c.view_data.append((route_info.name, view_fqn, routepath, active))
372 c.view_data.append((route_info.name, view_fqn, routepath, active))
375
373
376 c.whitelist_views = whitelist_views
374 c.whitelist_views = whitelist_views
377 return self._get_template_context(c)
375 return self._get_template_context(c)
378
376
379 def ssh_enabled(self):
377 def ssh_enabled(self):
380 return self.request.registry.settings.get(
378 return self.request.registry.settings.get(
381 'ssh.generate_authorized_keyfile')
379 'ssh.generate_authorized_keyfile')
382
380
383 @LoginRequired()
381 @LoginRequired()
384 @HasPermissionAllDecorator('hg.admin')
382 @HasPermissionAllDecorator('hg.admin')
385 @view_config(
383 @view_config(
386 route_name='admin_permissions_ssh_keys', request_method='GET',
384 route_name='admin_permissions_ssh_keys', request_method='GET',
387 renderer='rhodecode:templates/admin/permissions/permissions.mako')
385 renderer='rhodecode:templates/admin/permissions/permissions.mako')
388 def ssh_keys(self):
386 def ssh_keys(self):
389 c = self.load_default_context()
387 c = self.load_default_context()
390 c.active = 'ssh_keys'
388 c.active = 'ssh_keys'
391 c.ssh_enabled = self.ssh_enabled()
389 c.ssh_enabled = self.ssh_enabled()
392 return self._get_template_context(c)
390 return self._get_template_context(c)
393
391
394 @LoginRequired()
392 @LoginRequired()
395 @HasPermissionAllDecorator('hg.admin')
393 @HasPermissionAllDecorator('hg.admin')
396 @view_config(
394 @view_config(
397 route_name='admin_permissions_ssh_keys_data', request_method='GET',
395 route_name='admin_permissions_ssh_keys_data', request_method='GET',
398 renderer='json_ext', xhr=True)
396 renderer='json_ext', xhr=True)
399 def ssh_keys_data(self):
397 def ssh_keys_data(self):
400 _ = self.request.translate
398 _ = self.request.translate
401 self.load_default_context()
399 self.load_default_context()
402 column_map = {
400 column_map = {
403 'fingerprint': 'ssh_key_fingerprint',
401 'fingerprint': 'ssh_key_fingerprint',
404 'username': User.username
402 'username': User.username
405 }
403 }
406 draw, start, limit = self._extract_chunk(self.request)
404 draw, start, limit = self._extract_chunk(self.request)
407 search_q, order_by, order_dir = self._extract_ordering(
405 search_q, order_by, order_dir = self._extract_ordering(
408 self.request, column_map=column_map)
406 self.request, column_map=column_map)
409
407
410 ssh_keys_data_total_count = UserSshKeys.query()\
408 ssh_keys_data_total_count = UserSshKeys.query()\
411 .count()
409 .count()
412
410
413 # json generate
411 # json generate
414 base_q = UserSshKeys.query().join(UserSshKeys.user)
412 base_q = UserSshKeys.query().join(UserSshKeys.user)
415
413
416 if search_q:
414 if search_q:
417 like_expression = u'%{}%'.format(safe_unicode(search_q))
415 like_expression = u'%{}%'.format(safe_unicode(search_q))
418 base_q = base_q.filter(or_(
416 base_q = base_q.filter(or_(
419 User.username.ilike(like_expression),
417 User.username.ilike(like_expression),
420 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
418 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
421 ))
419 ))
422
420
423 users_data_total_filtered_count = base_q.count()
421 users_data_total_filtered_count = base_q.count()
424
422
425 sort_col = self._get_order_col(order_by, UserSshKeys)
423 sort_col = self._get_order_col(order_by, UserSshKeys)
426 if sort_col:
424 if sort_col:
427 if order_dir == 'asc':
425 if order_dir == 'asc':
428 # handle null values properly to order by NULL last
426 # handle null values properly to order by NULL last
429 if order_by in ['created_on']:
427 if order_by in ['created_on']:
430 sort_col = coalesce(sort_col, datetime.date.max)
428 sort_col = coalesce(sort_col, datetime.date.max)
431 sort_col = sort_col.asc()
429 sort_col = sort_col.asc()
432 else:
430 else:
433 # handle null values properly to order by NULL last
431 # handle null values properly to order by NULL last
434 if order_by in ['created_on']:
432 if order_by in ['created_on']:
435 sort_col = coalesce(sort_col, datetime.date.min)
433 sort_col = coalesce(sort_col, datetime.date.min)
436 sort_col = sort_col.desc()
434 sort_col = sort_col.desc()
437
435
438 base_q = base_q.order_by(sort_col)
436 base_q = base_q.order_by(sort_col)
439 base_q = base_q.offset(start).limit(limit)
437 base_q = base_q.offset(start).limit(limit)
440
438
441 ssh_keys = base_q.all()
439 ssh_keys = base_q.all()
442
440
443 ssh_keys_data = []
441 ssh_keys_data = []
444 for ssh_key in ssh_keys:
442 for ssh_key in ssh_keys:
445 ssh_keys_data.append({
443 ssh_keys_data.append({
446 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
444 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
447 "fingerprint": ssh_key.ssh_key_fingerprint,
445 "fingerprint": ssh_key.ssh_key_fingerprint,
448 "description": ssh_key.description,
446 "description": ssh_key.description,
449 "created_on": h.format_date(ssh_key.created_on),
447 "created_on": h.format_date(ssh_key.created_on),
450 "accessed_on": h.format_date(ssh_key.accessed_on),
448 "accessed_on": h.format_date(ssh_key.accessed_on),
451 "action": h.link_to(
449 "action": h.link_to(
452 _('Edit'), h.route_path('edit_user_ssh_keys',
450 _('Edit'), h.route_path('edit_user_ssh_keys',
453 user_id=ssh_key.user.user_id))
451 user_id=ssh_key.user.user_id))
454 })
452 })
455
453
456 data = ({
454 data = ({
457 'draw': draw,
455 'draw': draw,
458 'data': ssh_keys_data,
456 'data': ssh_keys_data,
459 'recordsTotal': ssh_keys_data_total_count,
457 'recordsTotal': ssh_keys_data_total_count,
460 'recordsFiltered': users_data_total_filtered_count,
458 'recordsFiltered': users_data_total_filtered_count,
461 })
459 })
462
460
463 return data
461 return data
464
462
465 @LoginRequired()
463 @LoginRequired()
466 @HasPermissionAllDecorator('hg.admin')
464 @HasPermissionAllDecorator('hg.admin')
467 @CSRFRequired()
465 @CSRFRequired()
468 @view_config(
466 @view_config(
469 route_name='admin_permissions_ssh_keys_update', request_method='POST',
467 route_name='admin_permissions_ssh_keys_update', request_method='POST',
470 renderer='rhodecode:templates/admin/permissions/permissions.mako')
468 renderer='rhodecode:templates/admin/permissions/permissions.mako')
471 def ssh_keys_update(self):
469 def ssh_keys_update(self):
472 _ = self.request.translate
470 _ = self.request.translate
473 self.load_default_context()
471 self.load_default_context()
474
472
475 ssh_enabled = self.ssh_enabled()
473 ssh_enabled = self.ssh_enabled()
476 key_file = self.request.registry.settings.get(
474 key_file = self.request.registry.settings.get(
477 'ssh.authorized_keys_file_path')
475 'ssh.authorized_keys_file_path')
478 if ssh_enabled:
476 if ssh_enabled:
479 trigger(SshKeyFileChangeEvent(), self.request.registry)
477 trigger(SshKeyFileChangeEvent(), self.request.registry)
480 h.flash(_('Updated SSH keys file: {}').format(key_file),
478 h.flash(_('Updated SSH keys file: {}').format(key_file),
481 category='success')
479 category='success')
482 else:
480 else:
483 h.flash(_('SSH key support is disabled in .ini file'),
481 h.flash(_('SSH key support is disabled in .ini file'),
484 category='warning')
482 category='warning')
485
483
486 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
484 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,183 +1,183 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import formencode
22 import formencode
23 import formencode.htmlfill
23 import formencode.htmlfill
24
24
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31
31
32 from rhodecode.lib.ext_json import json
32 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, CSRFRequired, NotAnonymous,
34 LoginRequired, CSRFRequired, NotAnonymous,
35 HasPermissionAny, HasRepoGroupPermissionAny)
35 HasPermissionAny, HasRepoGroupPermissionAny)
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib.utils import repo_name_slug
37 from rhodecode.lib.utils import repo_name_slug
38 from rhodecode.lib.utils2 import safe_int, safe_unicode
38 from rhodecode.lib.utils2 import safe_int, safe_unicode
39 from rhodecode.model.forms import RepoForm
39 from rhodecode.model.forms import RepoForm
40 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
41 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
42 from rhodecode.model.settings import SettingsModel
42 from rhodecode.model.settings import SettingsModel
43 from rhodecode.model.db import Repository, RepoGroup
43 from rhodecode.model.db import Repository, RepoGroup
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class AdminReposView(BaseAppView, DataGridAppView):
48 class AdminReposView(BaseAppView, DataGridAppView):
49
49
50 def load_default_context(self):
50 def load_default_context(self):
51 c = self._get_local_tmpl_context()
51 c = self._get_local_tmpl_context()
52
52
53 return c
53 return c
54
54
55 def _load_form_data(self, c):
55 def _load_form_data(self, c):
56 acl_groups = RepoGroupList(RepoGroup.query().all(),
56 acl_groups = RepoGroupList(RepoGroup.query().all(),
57 perm_set=['group.write', 'group.admin'])
57 perm_set=['group.write', 'group.admin'])
58 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
59 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
59 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
60 c.landing_revs_choices, c.landing_revs = \
60 c.landing_revs_choices, c.landing_revs = \
61 ScmModel().get_repo_landing_revs()
61 ScmModel().get_repo_landing_revs(self.request.translate)
62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
63
63
64 @LoginRequired()
64 @LoginRequired()
65 @NotAnonymous()
65 @NotAnonymous()
66 # perms check inside
66 # perms check inside
67 @view_config(
67 @view_config(
68 route_name='repos', request_method='GET',
68 route_name='repos', request_method='GET',
69 renderer='rhodecode:templates/admin/repos/repos.mako')
69 renderer='rhodecode:templates/admin/repos/repos.mako')
70 def repository_list(self):
70 def repository_list(self):
71 c = self.load_default_context()
71 c = self.load_default_context()
72
72
73 repo_list = Repository.get_all_repos()
73 repo_list = Repository.get_all_repos()
74 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
74 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
75 repos_data = RepoModel().get_repos_as_dict(
75 repos_data = RepoModel().get_repos_as_dict(
76 repo_list=c.repo_list, admin=True, super_user_actions=True)
76 repo_list=c.repo_list, admin=True, super_user_actions=True)
77 # json used to render the grid
77 # json used to render the grid
78 c.data = json.dumps(repos_data)
78 c.data = json.dumps(repos_data)
79
79
80 return self._get_template_context(c)
80 return self._get_template_context(c)
81
81
82 @LoginRequired()
82 @LoginRequired()
83 @NotAnonymous()
83 @NotAnonymous()
84 # perms check inside
84 # perms check inside
85 @view_config(
85 @view_config(
86 route_name='repo_new', request_method='GET',
86 route_name='repo_new', request_method='GET',
87 renderer='rhodecode:templates/admin/repos/repo_add.mako')
87 renderer='rhodecode:templates/admin/repos/repo_add.mako')
88 def repository_new(self):
88 def repository_new(self):
89 c = self.load_default_context()
89 c = self.load_default_context()
90
90
91 new_repo = self.request.GET.get('repo', '')
91 new_repo = self.request.GET.get('repo', '')
92 parent_group = safe_int(self.request.GET.get('parent_group'))
92 parent_group = safe_int(self.request.GET.get('parent_group'))
93 _gr = RepoGroup.get(parent_group)
93 _gr = RepoGroup.get(parent_group)
94
94
95 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
95 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
96 # you're not super admin nor have global create permissions,
96 # you're not super admin nor have global create permissions,
97 # but maybe you have at least write permission to a parent group ?
97 # but maybe you have at least write permission to a parent group ?
98
98
99 gr_name = _gr.group_name if _gr else None
99 gr_name = _gr.group_name if _gr else None
100 # create repositories with write permission on group is set to true
100 # create repositories with write permission on group is set to true
101 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
101 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
102 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
102 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
103 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
103 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
104 if not (group_admin or (group_write and create_on_write)):
104 if not (group_admin or (group_write and create_on_write)):
105 raise HTTPForbidden()
105 raise HTTPForbidden()
106
106
107 self._load_form_data(c)
107 self._load_form_data(c)
108 c.new_repo = repo_name_slug(new_repo)
108 c.new_repo = repo_name_slug(new_repo)
109
109
110 # apply the defaults from defaults page
110 # apply the defaults from defaults page
111 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
111 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
112 # set checkbox to autochecked
112 # set checkbox to autochecked
113 defaults['repo_copy_permissions'] = True
113 defaults['repo_copy_permissions'] = True
114
114
115 parent_group_choice = '-1'
115 parent_group_choice = '-1'
116 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
116 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
117 parent_group_choice = self._rhodecode_user.personal_repo_group
117 parent_group_choice = self._rhodecode_user.personal_repo_group
118
118
119 if parent_group and _gr:
119 if parent_group and _gr:
120 if parent_group in [x[0] for x in c.repo_groups]:
120 if parent_group in [x[0] for x in c.repo_groups]:
121 parent_group_choice = safe_unicode(parent_group)
121 parent_group_choice = safe_unicode(parent_group)
122
122
123 defaults.update({'repo_group': parent_group_choice})
123 defaults.update({'repo_group': parent_group_choice})
124
124
125 data = render('rhodecode:templates/admin/repos/repo_add.mako',
125 data = render('rhodecode:templates/admin/repos/repo_add.mako',
126 self._get_template_context(c), self.request)
126 self._get_template_context(c), self.request)
127 html = formencode.htmlfill.render(
127 html = formencode.htmlfill.render(
128 data,
128 data,
129 defaults=defaults,
129 defaults=defaults,
130 encoding="UTF-8",
130 encoding="UTF-8",
131 force_defaults=False
131 force_defaults=False
132 )
132 )
133 return Response(html)
133 return Response(html)
134
134
135 @LoginRequired()
135 @LoginRequired()
136 @NotAnonymous()
136 @NotAnonymous()
137 @CSRFRequired()
137 @CSRFRequired()
138 # perms check inside
138 # perms check inside
139 @view_config(
139 @view_config(
140 route_name='repo_create', request_method='POST',
140 route_name='repo_create', request_method='POST',
141 renderer='rhodecode:templates/admin/repos/repos.mako')
141 renderer='rhodecode:templates/admin/repos/repos.mako')
142 def repository_create(self):
142 def repository_create(self):
143 c = self.load_default_context()
143 c = self.load_default_context()
144
144
145 form_result = {}
145 form_result = {}
146 task_id = None
146 task_id = None
147 self._load_form_data(c)
147 self._load_form_data(c)
148
148
149 try:
149 try:
150 # CanWriteToGroup validators checks permissions of this POST
150 # CanWriteToGroup validators checks permissions of this POST
151 form = RepoForm(
151 form = RepoForm(
152 self.request.translate, repo_groups=c.repo_groups_choices,
152 self.request.translate, repo_groups=c.repo_groups_choices,
153 landing_revs=c.landing_revs_choices)()
153 landing_revs=c.landing_revs_choices)()
154 form_results = form.to_python(dict(self.request.POST))
154 form_results = form.to_python(dict(self.request.POST))
155
155
156 # create is done sometimes async on celery, db transaction
156 # create is done sometimes async on celery, db transaction
157 # management is handled there.
157 # management is handled there.
158 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
158 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
159 from celery.result import BaseAsyncResult
159 from celery.result import BaseAsyncResult
160 if isinstance(task, BaseAsyncResult):
160 if isinstance(task, BaseAsyncResult):
161 task_id = task.task_id
161 task_id = task.task_id
162 except formencode.Invalid as errors:
162 except formencode.Invalid as errors:
163 data = render('rhodecode:templates/admin/repos/repo_add.mako',
163 data = render('rhodecode:templates/admin/repos/repo_add.mako',
164 self._get_template_context(c), self.request)
164 self._get_template_context(c), self.request)
165 html = formencode.htmlfill.render(
165 html = formencode.htmlfill.render(
166 data,
166 data,
167 defaults=errors.value,
167 defaults=errors.value,
168 errors=errors.error_dict or {},
168 errors=errors.error_dict or {},
169 prefix_error=False,
169 prefix_error=False,
170 encoding="UTF-8",
170 encoding="UTF-8",
171 force_defaults=False
171 force_defaults=False
172 )
172 )
173 return Response(html)
173 return Response(html)
174
174
175 except Exception as e:
175 except Exception as e:
176 msg = self._log_creation_exception(e, form_result.get('repo_name'))
176 msg = self._log_creation_exception(e, form_result.get('repo_name'))
177 h.flash(msg, category='error')
177 h.flash(msg, category='error')
178 raise HTTPFound(h.route_path('home'))
178 raise HTTPFound(h.route_path('home'))
179
179
180 raise HTTPFound(
180 raise HTTPFound(
181 h.route_path('repo_creating',
181 h.route_path('repo_creating',
182 repo_name=form_result['repo_name_full'],
182 repo_name=form_result['repo_name_full'],
183 _query=dict(task_id=task_id)))
183 _query=dict(task_id=task_id)))
@@ -1,762 +1,762 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 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.admin.navigation import navigation_list
36 from rhodecode.apps.admin.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(
178 model.update_global_path_setting(
179 form_result['paths_root_path'])
179 form_result['paths_root_path'])
180
180
181 model.update_global_ssl_setting(form_result['web_push_ssl'])
181 model.update_global_ssl_setting(form_result['web_push_ssl'])
182 model.update_global_hook_settings(form_result)
182 model.update_global_hook_settings(form_result)
183
183
184 model.create_or_update_global_svn_settings(form_result)
184 model.create_or_update_global_svn_settings(form_result)
185 model.create_or_update_global_hg_settings(form_result)
185 model.create_or_update_global_hg_settings(form_result)
186 model.create_or_update_global_git_settings(form_result)
186 model.create_or_update_global_git_settings(form_result)
187 model.create_or_update_global_pr_settings(form_result)
187 model.create_or_update_global_pr_settings(form_result)
188 except Exception:
188 except Exception:
189 log.exception("Exception while updating settings")
189 log.exception("Exception while updating settings")
190 h.flash(_('Error occurred during updating '
190 h.flash(_('Error occurred during updating '
191 'application settings'), category='error')
191 'application settings'), category='error')
192 else:
192 else:
193 Session().commit()
193 Session().commit()
194 h.flash(_('Updated VCS settings'), category='success')
194 h.flash(_('Updated VCS settings'), category='success')
195 raise HTTPFound(h.route_path('admin_settings_vcs'))
195 raise HTTPFound(h.route_path('admin_settings_vcs'))
196
196
197 data = render('rhodecode:templates/admin/settings/settings.mako',
197 data = render('rhodecode:templates/admin/settings/settings.mako',
198 self._get_template_context(c), self.request)
198 self._get_template_context(c), self.request)
199 html = formencode.htmlfill.render(
199 html = formencode.htmlfill.render(
200 data,
200 data,
201 defaults=self._form_defaults(),
201 defaults=self._form_defaults(),
202 encoding="UTF-8",
202 encoding="UTF-8",
203 force_defaults=False
203 force_defaults=False
204 )
204 )
205 return Response(html)
205 return Response(html)
206
206
207 @LoginRequired()
207 @LoginRequired()
208 @HasPermissionAllDecorator('hg.admin')
208 @HasPermissionAllDecorator('hg.admin')
209 @CSRFRequired()
209 @CSRFRequired()
210 @view_config(
210 @view_config(
211 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
211 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
212 renderer='json_ext', xhr=True)
212 renderer='json_ext', xhr=True)
213 def settings_vcs_delete_svn_pattern(self):
213 def settings_vcs_delete_svn_pattern(self):
214 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
214 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
215 model = VcsSettingsModel()
215 model = VcsSettingsModel()
216 try:
216 try:
217 model.delete_global_svn_pattern(delete_pattern_id)
217 model.delete_global_svn_pattern(delete_pattern_id)
218 except SettingNotFound:
218 except SettingNotFound:
219 log.exception(
219 log.exception(
220 'Failed to delete svn_pattern with id %s', delete_pattern_id)
220 'Failed to delete svn_pattern with id %s', delete_pattern_id)
221 raise HTTPNotFound()
221 raise HTTPNotFound()
222
222
223 Session().commit()
223 Session().commit()
224 return True
224 return True
225
225
226 @LoginRequired()
226 @LoginRequired()
227 @HasPermissionAllDecorator('hg.admin')
227 @HasPermissionAllDecorator('hg.admin')
228 @view_config(
228 @view_config(
229 route_name='admin_settings_mapping', request_method='GET',
229 route_name='admin_settings_mapping', request_method='GET',
230 renderer='rhodecode:templates/admin/settings/settings.mako')
230 renderer='rhodecode:templates/admin/settings/settings.mako')
231 def settings_mapping(self):
231 def settings_mapping(self):
232 c = self.load_default_context()
232 c = self.load_default_context()
233 c.active = 'mapping'
233 c.active = 'mapping'
234
234
235 data = render('rhodecode:templates/admin/settings/settings.mako',
235 data = render('rhodecode:templates/admin/settings/settings.mako',
236 self._get_template_context(c), self.request)
236 self._get_template_context(c), self.request)
237 html = formencode.htmlfill.render(
237 html = formencode.htmlfill.render(
238 data,
238 data,
239 defaults=self._form_defaults(),
239 defaults=self._form_defaults(),
240 encoding="UTF-8",
240 encoding="UTF-8",
241 force_defaults=False
241 force_defaults=False
242 )
242 )
243 return Response(html)
243 return Response(html)
244
244
245 @LoginRequired()
245 @LoginRequired()
246 @HasPermissionAllDecorator('hg.admin')
246 @HasPermissionAllDecorator('hg.admin')
247 @CSRFRequired()
247 @CSRFRequired()
248 @view_config(
248 @view_config(
249 route_name='admin_settings_mapping_update', request_method='POST',
249 route_name='admin_settings_mapping_update', request_method='POST',
250 renderer='rhodecode:templates/admin/settings/settings.mako')
250 renderer='rhodecode:templates/admin/settings/settings.mako')
251 def settings_mapping_update(self):
251 def settings_mapping_update(self):
252 _ = self.request.translate
252 _ = self.request.translate
253 c = self.load_default_context()
253 c = self.load_default_context()
254 c.active = 'mapping'
254 c.active = 'mapping'
255 rm_obsolete = self.request.POST.get('destroy', False)
255 rm_obsolete = self.request.POST.get('destroy', False)
256 invalidate_cache = self.request.POST.get('invalidate', False)
256 invalidate_cache = self.request.POST.get('invalidate', False)
257 log.debug(
257 log.debug(
258 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
258 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
259
259
260 if invalidate_cache:
260 if invalidate_cache:
261 log.debug('invalidating all repositories cache')
261 log.debug('invalidating all repositories cache')
262 for repo in Repository.get_all():
262 for repo in Repository.get_all():
263 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
263 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
264
264
265 filesystem_repos = ScmModel().repo_scan()
265 filesystem_repos = ScmModel().repo_scan()
266 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
266 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
267 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
267 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
268 h.flash(_('Repositories successfully '
268 h.flash(_('Repositories successfully '
269 'rescanned added: %s ; removed: %s') %
269 'rescanned added: %s ; removed: %s') %
270 (_repr(added), _repr(removed)),
270 (_repr(added), _repr(removed)),
271 category='success')
271 category='success')
272 raise HTTPFound(h.route_path('admin_settings_mapping'))
272 raise HTTPFound(h.route_path('admin_settings_mapping'))
273
273
274 @LoginRequired()
274 @LoginRequired()
275 @HasPermissionAllDecorator('hg.admin')
275 @HasPermissionAllDecorator('hg.admin')
276 @view_config(
276 @view_config(
277 route_name='admin_settings', request_method='GET',
277 route_name='admin_settings', request_method='GET',
278 renderer='rhodecode:templates/admin/settings/settings.mako')
278 renderer='rhodecode:templates/admin/settings/settings.mako')
279 @view_config(
279 @view_config(
280 route_name='admin_settings_global', request_method='GET',
280 route_name='admin_settings_global', request_method='GET',
281 renderer='rhodecode:templates/admin/settings/settings.mako')
281 renderer='rhodecode:templates/admin/settings/settings.mako')
282 def settings_global(self):
282 def settings_global(self):
283 c = self.load_default_context()
283 c = self.load_default_context()
284 c.active = 'global'
284 c.active = 'global'
285 c.personal_repo_group_default_pattern = RepoGroupModel()\
285 c.personal_repo_group_default_pattern = RepoGroupModel()\
286 .get_personal_group_name_pattern()
286 .get_personal_group_name_pattern()
287
287
288 data = render('rhodecode:templates/admin/settings/settings.mako',
288 data = render('rhodecode:templates/admin/settings/settings.mako',
289 self._get_template_context(c), self.request)
289 self._get_template_context(c), self.request)
290 html = formencode.htmlfill.render(
290 html = formencode.htmlfill.render(
291 data,
291 data,
292 defaults=self._form_defaults(),
292 defaults=self._form_defaults(),
293 encoding="UTF-8",
293 encoding="UTF-8",
294 force_defaults=False
294 force_defaults=False
295 )
295 )
296 return Response(html)
296 return Response(html)
297
297
298 @LoginRequired()
298 @LoginRequired()
299 @HasPermissionAllDecorator('hg.admin')
299 @HasPermissionAllDecorator('hg.admin')
300 @CSRFRequired()
300 @CSRFRequired()
301 @view_config(
301 @view_config(
302 route_name='admin_settings_update', request_method='POST',
302 route_name='admin_settings_update', request_method='POST',
303 renderer='rhodecode:templates/admin/settings/settings.mako')
303 renderer='rhodecode:templates/admin/settings/settings.mako')
304 @view_config(
304 @view_config(
305 route_name='admin_settings_global_update', request_method='POST',
305 route_name='admin_settings_global_update', request_method='POST',
306 renderer='rhodecode:templates/admin/settings/settings.mako')
306 renderer='rhodecode:templates/admin/settings/settings.mako')
307 def settings_global_update(self):
307 def settings_global_update(self):
308 _ = self.request.translate
308 _ = self.request.translate
309 c = self.load_default_context()
309 c = self.load_default_context()
310 c.active = 'global'
310 c.active = 'global'
311 c.personal_repo_group_default_pattern = RepoGroupModel()\
311 c.personal_repo_group_default_pattern = RepoGroupModel()\
312 .get_personal_group_name_pattern()
312 .get_personal_group_name_pattern()
313 application_form = ApplicationSettingsForm(self.request.translate)()
313 application_form = ApplicationSettingsForm(self.request.translate)()
314 try:
314 try:
315 form_result = application_form.to_python(dict(self.request.POST))
315 form_result = application_form.to_python(dict(self.request.POST))
316 except formencode.Invalid as errors:
316 except formencode.Invalid as errors:
317 data = render('rhodecode:templates/admin/settings/settings.mako',
317 data = render('rhodecode:templates/admin/settings/settings.mako',
318 self._get_template_context(c), self.request)
318 self._get_template_context(c), self.request)
319 html = formencode.htmlfill.render(
319 html = formencode.htmlfill.render(
320 data,
320 data,
321 defaults=errors.value,
321 defaults=errors.value,
322 errors=errors.error_dict or {},
322 errors=errors.error_dict or {},
323 prefix_error=False,
323 prefix_error=False,
324 encoding="UTF-8",
324 encoding="UTF-8",
325 force_defaults=False
325 force_defaults=False
326 )
326 )
327 return Response(html)
327 return Response(html)
328
328
329 settings = [
329 settings = [
330 ('title', 'rhodecode_title', 'unicode'),
330 ('title', 'rhodecode_title', 'unicode'),
331 ('realm', 'rhodecode_realm', 'unicode'),
331 ('realm', 'rhodecode_realm', 'unicode'),
332 ('pre_code', 'rhodecode_pre_code', 'unicode'),
332 ('pre_code', 'rhodecode_pre_code', 'unicode'),
333 ('post_code', 'rhodecode_post_code', 'unicode'),
333 ('post_code', 'rhodecode_post_code', 'unicode'),
334 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
334 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
335 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
335 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
336 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
336 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
337 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
337 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
338 ]
338 ]
339 try:
339 try:
340 for setting, form_key, type_ in settings:
340 for setting, form_key, type_ in settings:
341 sett = SettingsModel().create_or_update_setting(
341 sett = SettingsModel().create_or_update_setting(
342 setting, form_result[form_key], type_)
342 setting, form_result[form_key], type_)
343 Session().add(sett)
343 Session().add(sett)
344
344
345 Session().commit()
345 Session().commit()
346 SettingsModel().invalidate_settings_cache()
346 SettingsModel().invalidate_settings_cache()
347 h.flash(_('Updated application settings'), category='success')
347 h.flash(_('Updated application settings'), category='success')
348 except Exception:
348 except Exception:
349 log.exception("Exception while updating application settings")
349 log.exception("Exception while updating application settings")
350 h.flash(
350 h.flash(
351 _('Error occurred during updating application settings'),
351 _('Error occurred during updating application settings'),
352 category='error')
352 category='error')
353
353
354 raise HTTPFound(h.route_path('admin_settings_global'))
354 raise HTTPFound(h.route_path('admin_settings_global'))
355
355
356 @LoginRequired()
356 @LoginRequired()
357 @HasPermissionAllDecorator('hg.admin')
357 @HasPermissionAllDecorator('hg.admin')
358 @view_config(
358 @view_config(
359 route_name='admin_settings_visual', request_method='GET',
359 route_name='admin_settings_visual', request_method='GET',
360 renderer='rhodecode:templates/admin/settings/settings.mako')
360 renderer='rhodecode:templates/admin/settings/settings.mako')
361 def settings_visual(self):
361 def settings_visual(self):
362 c = self.load_default_context()
362 c = self.load_default_context()
363 c.active = 'visual'
363 c.active = 'visual'
364
364
365 data = render('rhodecode:templates/admin/settings/settings.mako',
365 data = render('rhodecode:templates/admin/settings/settings.mako',
366 self._get_template_context(c), self.request)
366 self._get_template_context(c), self.request)
367 html = formencode.htmlfill.render(
367 html = formencode.htmlfill.render(
368 data,
368 data,
369 defaults=self._form_defaults(),
369 defaults=self._form_defaults(),
370 encoding="UTF-8",
370 encoding="UTF-8",
371 force_defaults=False
371 force_defaults=False
372 )
372 )
373 return Response(html)
373 return Response(html)
374
374
375 @LoginRequired()
375 @LoginRequired()
376 @HasPermissionAllDecorator('hg.admin')
376 @HasPermissionAllDecorator('hg.admin')
377 @CSRFRequired()
377 @CSRFRequired()
378 @view_config(
378 @view_config(
379 route_name='admin_settings_visual_update', request_method='POST',
379 route_name='admin_settings_visual_update', request_method='POST',
380 renderer='rhodecode:templates/admin/settings/settings.mako')
380 renderer='rhodecode:templates/admin/settings/settings.mako')
381 def settings_visual_update(self):
381 def settings_visual_update(self):
382 _ = self.request.translate
382 _ = self.request.translate
383 c = self.load_default_context()
383 c = self.load_default_context()
384 c.active = 'visual'
384 c.active = 'visual'
385 application_form = ApplicationVisualisationForm(self.request.translate)()
385 application_form = ApplicationVisualisationForm(self.request.translate)()
386 try:
386 try:
387 form_result = application_form.to_python(dict(self.request.POST))
387 form_result = application_form.to_python(dict(self.request.POST))
388 except formencode.Invalid as errors:
388 except formencode.Invalid as errors:
389 data = render('rhodecode:templates/admin/settings/settings.mako',
389 data = render('rhodecode:templates/admin/settings/settings.mako',
390 self._get_template_context(c), self.request)
390 self._get_template_context(c), self.request)
391 html = formencode.htmlfill.render(
391 html = formencode.htmlfill.render(
392 data,
392 data,
393 defaults=errors.value,
393 defaults=errors.value,
394 errors=errors.error_dict or {},
394 errors=errors.error_dict or {},
395 prefix_error=False,
395 prefix_error=False,
396 encoding="UTF-8",
396 encoding="UTF-8",
397 force_defaults=False
397 force_defaults=False
398 )
398 )
399 return Response(html)
399 return Response(html)
400
400
401 try:
401 try:
402 settings = [
402 settings = [
403 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
403 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
404 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
404 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
405 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
405 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
406 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
406 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
407 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
407 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
408 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
408 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
409 ('show_version', 'rhodecode_show_version', 'bool'),
409 ('show_version', 'rhodecode_show_version', 'bool'),
410 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
410 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
411 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
411 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
412 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
412 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
413 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
413 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
414 ('support_url', 'rhodecode_support_url', 'unicode'),
414 ('support_url', 'rhodecode_support_url', 'unicode'),
415 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
415 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
416 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
416 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
417 ]
417 ]
418 for setting, form_key, type_ in settings:
418 for setting, form_key, type_ in settings:
419 sett = SettingsModel().create_or_update_setting(
419 sett = SettingsModel().create_or_update_setting(
420 setting, form_result[form_key], type_)
420 setting, form_result[form_key], type_)
421 Session().add(sett)
421 Session().add(sett)
422
422
423 Session().commit()
423 Session().commit()
424 SettingsModel().invalidate_settings_cache()
424 SettingsModel().invalidate_settings_cache()
425 h.flash(_('Updated visualisation settings'), category='success')
425 h.flash(_('Updated visualisation settings'), category='success')
426 except Exception:
426 except Exception:
427 log.exception("Exception updating visualization settings")
427 log.exception("Exception updating visualization settings")
428 h.flash(_('Error occurred during updating '
428 h.flash(_('Error occurred during updating '
429 'visualisation settings'),
429 'visualisation settings'),
430 category='error')
430 category='error')
431
431
432 raise HTTPFound(h.route_path('admin_settings_visual'))
432 raise HTTPFound(h.route_path('admin_settings_visual'))
433
433
434 @LoginRequired()
434 @LoginRequired()
435 @HasPermissionAllDecorator('hg.admin')
435 @HasPermissionAllDecorator('hg.admin')
436 @view_config(
436 @view_config(
437 route_name='admin_settings_issuetracker', request_method='GET',
437 route_name='admin_settings_issuetracker', request_method='GET',
438 renderer='rhodecode:templates/admin/settings/settings.mako')
438 renderer='rhodecode:templates/admin/settings/settings.mako')
439 def settings_issuetracker(self):
439 def settings_issuetracker(self):
440 c = self.load_default_context()
440 c = self.load_default_context()
441 c.active = 'issuetracker'
441 c.active = 'issuetracker'
442 defaults = SettingsModel().get_all_settings()
442 defaults = SettingsModel().get_all_settings()
443
443
444 entry_key = 'rhodecode_issuetracker_pat_'
444 entry_key = 'rhodecode_issuetracker_pat_'
445
445
446 c.issuetracker_entries = {}
446 c.issuetracker_entries = {}
447 for k, v in defaults.items():
447 for k, v in defaults.items():
448 if k.startswith(entry_key):
448 if k.startswith(entry_key):
449 uid = k[len(entry_key):]
449 uid = k[len(entry_key):]
450 c.issuetracker_entries[uid] = None
450 c.issuetracker_entries[uid] = None
451
451
452 for uid in c.issuetracker_entries:
452 for uid in c.issuetracker_entries:
453 c.issuetracker_entries[uid] = AttributeDict({
453 c.issuetracker_entries[uid] = AttributeDict({
454 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
454 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
455 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
455 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
456 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
456 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
457 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
457 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
458 })
458 })
459
459
460 return self._get_template_context(c)
460 return self._get_template_context(c)
461
461
462 @LoginRequired()
462 @LoginRequired()
463 @HasPermissionAllDecorator('hg.admin')
463 @HasPermissionAllDecorator('hg.admin')
464 @CSRFRequired()
464 @CSRFRequired()
465 @view_config(
465 @view_config(
466 route_name='admin_settings_issuetracker_test', request_method='POST',
466 route_name='admin_settings_issuetracker_test', request_method='POST',
467 renderer='string', xhr=True)
467 renderer='string', xhr=True)
468 def settings_issuetracker_test(self):
468 def settings_issuetracker_test(self):
469 return h.urlify_commit_message(
469 return h.urlify_commit_message(
470 self.request.POST.get('test_text', ''),
470 self.request.POST.get('test_text', ''),
471 'repo_group/test_repo1')
471 'repo_group/test_repo1')
472
472
473 @LoginRequired()
473 @LoginRequired()
474 @HasPermissionAllDecorator('hg.admin')
474 @HasPermissionAllDecorator('hg.admin')
475 @CSRFRequired()
475 @CSRFRequired()
476 @view_config(
476 @view_config(
477 route_name='admin_settings_issuetracker_update', request_method='POST',
477 route_name='admin_settings_issuetracker_update', request_method='POST',
478 renderer='rhodecode:templates/admin/settings/settings.mako')
478 renderer='rhodecode:templates/admin/settings/settings.mako')
479 def settings_issuetracker_update(self):
479 def settings_issuetracker_update(self):
480 _ = self.request.translate
480 _ = self.request.translate
481 self.load_default_context()
481 self.load_default_context()
482 settings_model = IssueTrackerSettingsModel()
482 settings_model = IssueTrackerSettingsModel()
483
483
484 try:
484 try:
485 form = IssueTrackerPatternsForm(self.request.translate)().to_python(self.request.POST)
485 form = IssueTrackerPatternsForm(self.request.translate)().to_python(self.request.POST)
486 except formencode.Invalid as errors:
486 except formencode.Invalid as errors:
487 log.exception('Failed to add new pattern')
487 log.exception('Failed to add new pattern')
488 error = errors
488 error = errors
489 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
489 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
490 category='error')
490 category='error')
491 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
491 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
492
492
493 if form:
493 if form:
494 for uid in form.get('delete_patterns', []):
494 for uid in form.get('delete_patterns', []):
495 settings_model.delete_entries(uid)
495 settings_model.delete_entries(uid)
496
496
497 for pattern in form.get('patterns', []):
497 for pattern in form.get('patterns', []):
498 for setting, value, type_ in pattern:
498 for setting, value, type_ in pattern:
499 sett = settings_model.create_or_update_setting(
499 sett = settings_model.create_or_update_setting(
500 setting, value, type_)
500 setting, value, type_)
501 Session().add(sett)
501 Session().add(sett)
502
502
503 Session().commit()
503 Session().commit()
504
504
505 SettingsModel().invalidate_settings_cache()
505 SettingsModel().invalidate_settings_cache()
506 h.flash(_('Updated issue tracker entries'), category='success')
506 h.flash(_('Updated issue tracker entries'), category='success')
507 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
507 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
508
508
509 @LoginRequired()
509 @LoginRequired()
510 @HasPermissionAllDecorator('hg.admin')
510 @HasPermissionAllDecorator('hg.admin')
511 @CSRFRequired()
511 @CSRFRequired()
512 @view_config(
512 @view_config(
513 route_name='admin_settings_issuetracker_delete', request_method='POST',
513 route_name='admin_settings_issuetracker_delete', request_method='POST',
514 renderer='rhodecode:templates/admin/settings/settings.mako')
514 renderer='rhodecode:templates/admin/settings/settings.mako')
515 def settings_issuetracker_delete(self):
515 def settings_issuetracker_delete(self):
516 _ = self.request.translate
516 _ = self.request.translate
517 self.load_default_context()
517 self.load_default_context()
518 uid = self.request.POST.get('uid')
518 uid = self.request.POST.get('uid')
519 try:
519 try:
520 IssueTrackerSettingsModel().delete_entries(uid)
520 IssueTrackerSettingsModel().delete_entries(uid)
521 except Exception:
521 except Exception:
522 log.exception('Failed to delete issue tracker setting %s', uid)
522 log.exception('Failed to delete issue tracker setting %s', uid)
523 raise HTTPNotFound()
523 raise HTTPNotFound()
524 h.flash(_('Removed issue tracker entry'), category='success')
524 h.flash(_('Removed issue tracker entry'), category='success')
525 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
525 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
526
526
527 @LoginRequired()
527 @LoginRequired()
528 @HasPermissionAllDecorator('hg.admin')
528 @HasPermissionAllDecorator('hg.admin')
529 @view_config(
529 @view_config(
530 route_name='admin_settings_email', request_method='GET',
530 route_name='admin_settings_email', request_method='GET',
531 renderer='rhodecode:templates/admin/settings/settings.mako')
531 renderer='rhodecode:templates/admin/settings/settings.mako')
532 def settings_email(self):
532 def settings_email(self):
533 c = self.load_default_context()
533 c = self.load_default_context()
534 c.active = 'email'
534 c.active = 'email'
535 c.rhodecode_ini = rhodecode.CONFIG
535 c.rhodecode_ini = rhodecode.CONFIG
536
536
537 data = render('rhodecode:templates/admin/settings/settings.mako',
537 data = render('rhodecode:templates/admin/settings/settings.mako',
538 self._get_template_context(c), self.request)
538 self._get_template_context(c), self.request)
539 html = formencode.htmlfill.render(
539 html = formencode.htmlfill.render(
540 data,
540 data,
541 defaults=self._form_defaults(),
541 defaults=self._form_defaults(),
542 encoding="UTF-8",
542 encoding="UTF-8",
543 force_defaults=False
543 force_defaults=False
544 )
544 )
545 return Response(html)
545 return Response(html)
546
546
547 @LoginRequired()
547 @LoginRequired()
548 @HasPermissionAllDecorator('hg.admin')
548 @HasPermissionAllDecorator('hg.admin')
549 @CSRFRequired()
549 @CSRFRequired()
550 @view_config(
550 @view_config(
551 route_name='admin_settings_email_update', request_method='POST',
551 route_name='admin_settings_email_update', request_method='POST',
552 renderer='rhodecode:templates/admin/settings/settings.mako')
552 renderer='rhodecode:templates/admin/settings/settings.mako')
553 def settings_email_update(self):
553 def settings_email_update(self):
554 _ = self.request.translate
554 _ = self.request.translate
555 c = self.load_default_context()
555 c = self.load_default_context()
556 c.active = 'email'
556 c.active = 'email'
557
557
558 test_email = self.request.POST.get('test_email')
558 test_email = self.request.POST.get('test_email')
559
559
560 if not test_email:
560 if not test_email:
561 h.flash(_('Please enter email address'), category='error')
561 h.flash(_('Please enter email address'), category='error')
562 raise HTTPFound(h.route_path('admin_settings_email'))
562 raise HTTPFound(h.route_path('admin_settings_email'))
563
563
564 email_kwargs = {
564 email_kwargs = {
565 'date': datetime.datetime.now(),
565 'date': datetime.datetime.now(),
566 'user': c.rhodecode_user,
566 'user': c.rhodecode_user,
567 'rhodecode_version': c.rhodecode_version
567 'rhodecode_version': c.rhodecode_version
568 }
568 }
569
569
570 (subject, headers, email_body,
570 (subject, headers, email_body,
571 email_body_plaintext) = EmailNotificationModel().render_email(
571 email_body_plaintext) = EmailNotificationModel().render_email(
572 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
572 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
573
573
574 recipients = [test_email] if test_email else None
574 recipients = [test_email] if test_email else None
575
575
576 run_task(tasks.send_email, recipients, subject,
576 run_task(tasks.send_email, recipients, subject,
577 email_body_plaintext, email_body)
577 email_body_plaintext, email_body)
578
578
579 h.flash(_('Send email task created'), category='success')
579 h.flash(_('Send email task created'), category='success')
580 raise HTTPFound(h.route_path('admin_settings_email'))
580 raise HTTPFound(h.route_path('admin_settings_email'))
581
581
582 @LoginRequired()
582 @LoginRequired()
583 @HasPermissionAllDecorator('hg.admin')
583 @HasPermissionAllDecorator('hg.admin')
584 @view_config(
584 @view_config(
585 route_name='admin_settings_hooks', request_method='GET',
585 route_name='admin_settings_hooks', request_method='GET',
586 renderer='rhodecode:templates/admin/settings/settings.mako')
586 renderer='rhodecode:templates/admin/settings/settings.mako')
587 def settings_hooks(self):
587 def settings_hooks(self):
588 c = self.load_default_context()
588 c = self.load_default_context()
589 c.active = 'hooks'
589 c.active = 'hooks'
590
590
591 model = SettingsModel()
591 model = SettingsModel()
592 c.hooks = model.get_builtin_hooks()
592 c.hooks = model.get_builtin_hooks()
593 c.custom_hooks = model.get_custom_hooks()
593 c.custom_hooks = model.get_custom_hooks()
594
594
595 data = render('rhodecode:templates/admin/settings/settings.mako',
595 data = render('rhodecode:templates/admin/settings/settings.mako',
596 self._get_template_context(c), self.request)
596 self._get_template_context(c), self.request)
597 html = formencode.htmlfill.render(
597 html = formencode.htmlfill.render(
598 data,
598 data,
599 defaults=self._form_defaults(),
599 defaults=self._form_defaults(),
600 encoding="UTF-8",
600 encoding="UTF-8",
601 force_defaults=False
601 force_defaults=False
602 )
602 )
603 return Response(html)
603 return Response(html)
604
604
605 @LoginRequired()
605 @LoginRequired()
606 @HasPermissionAllDecorator('hg.admin')
606 @HasPermissionAllDecorator('hg.admin')
607 @CSRFRequired()
607 @CSRFRequired()
608 @view_config(
608 @view_config(
609 route_name='admin_settings_hooks_update', request_method='POST',
609 route_name='admin_settings_hooks_update', request_method='POST',
610 renderer='rhodecode:templates/admin/settings/settings.mako')
610 renderer='rhodecode:templates/admin/settings/settings.mako')
611 @view_config(
611 @view_config(
612 route_name='admin_settings_hooks_delete', request_method='POST',
612 route_name='admin_settings_hooks_delete', request_method='POST',
613 renderer='rhodecode:templates/admin/settings/settings.mako')
613 renderer='rhodecode:templates/admin/settings/settings.mako')
614 def settings_hooks_update(self):
614 def settings_hooks_update(self):
615 _ = self.request.translate
615 _ = self.request.translate
616 c = self.load_default_context()
616 c = self.load_default_context()
617 c.active = 'hooks'
617 c.active = 'hooks'
618 if c.visual.allow_custom_hooks_settings:
618 if c.visual.allow_custom_hooks_settings:
619 ui_key = self.request.POST.get('new_hook_ui_key')
619 ui_key = self.request.POST.get('new_hook_ui_key')
620 ui_value = self.request.POST.get('new_hook_ui_value')
620 ui_value = self.request.POST.get('new_hook_ui_value')
621
621
622 hook_id = self.request.POST.get('hook_id')
622 hook_id = self.request.POST.get('hook_id')
623 new_hook = False
623 new_hook = False
624
624
625 model = SettingsModel()
625 model = SettingsModel()
626 try:
626 try:
627 if ui_value and ui_key:
627 if ui_value and ui_key:
628 model.create_or_update_hook(ui_key, ui_value)
628 model.create_or_update_hook(ui_key, ui_value)
629 h.flash(_('Added new hook'), category='success')
629 h.flash(_('Added new hook'), category='success')
630 new_hook = True
630 new_hook = True
631 elif hook_id:
631 elif hook_id:
632 RhodeCodeUi.delete(hook_id)
632 RhodeCodeUi.delete(hook_id)
633 Session().commit()
633 Session().commit()
634
634
635 # check for edits
635 # check for edits
636 update = False
636 update = False
637 _d = self.request.POST.dict_of_lists()
637 _d = self.request.POST.dict_of_lists()
638 for k, v in zip(_d.get('hook_ui_key', []),
638 for k, v in zip(_d.get('hook_ui_key', []),
639 _d.get('hook_ui_value_new', [])):
639 _d.get('hook_ui_value_new', [])):
640 model.create_or_update_hook(k, v)
640 model.create_or_update_hook(k, v)
641 update = True
641 update = True
642
642
643 if update and not new_hook:
643 if update and not new_hook:
644 h.flash(_('Updated hooks'), category='success')
644 h.flash(_('Updated hooks'), category='success')
645 Session().commit()
645 Session().commit()
646 except Exception:
646 except Exception:
647 log.exception("Exception during hook creation")
647 log.exception("Exception during hook creation")
648 h.flash(_('Error occurred during hook creation'),
648 h.flash(_('Error occurred during hook creation'),
649 category='error')
649 category='error')
650
650
651 raise HTTPFound(h.route_path('admin_settings_hooks'))
651 raise HTTPFound(h.route_path('admin_settings_hooks'))
652
652
653 @LoginRequired()
653 @LoginRequired()
654 @HasPermissionAllDecorator('hg.admin')
654 @HasPermissionAllDecorator('hg.admin')
655 @view_config(
655 @view_config(
656 route_name='admin_settings_search', request_method='GET',
656 route_name='admin_settings_search', request_method='GET',
657 renderer='rhodecode:templates/admin/settings/settings.mako')
657 renderer='rhodecode:templates/admin/settings/settings.mako')
658 def settings_search(self):
658 def settings_search(self):
659 c = self.load_default_context()
659 c = self.load_default_context()
660 c.active = 'search'
660 c.active = 'search'
661
661
662 searcher = searcher_from_config(self.request.registry.settings)
662 searcher = searcher_from_config(self.request.registry.settings)
663 c.statistics = searcher.statistics()
663 c.statistics = searcher.statistics(self.request.translate)
664
664
665 return self._get_template_context(c)
665 return self._get_template_context(c)
666
666
667 @LoginRequired()
667 @LoginRequired()
668 @HasPermissionAllDecorator('hg.admin')
668 @HasPermissionAllDecorator('hg.admin')
669 @view_config(
669 @view_config(
670 route_name='admin_settings_labs', request_method='GET',
670 route_name='admin_settings_labs', request_method='GET',
671 renderer='rhodecode:templates/admin/settings/settings.mako')
671 renderer='rhodecode:templates/admin/settings/settings.mako')
672 def settings_labs(self):
672 def settings_labs(self):
673 c = self.load_default_context()
673 c = self.load_default_context()
674 if not c.labs_active:
674 if not c.labs_active:
675 raise HTTPFound(h.route_path('admin_settings'))
675 raise HTTPFound(h.route_path('admin_settings'))
676
676
677 c.active = 'labs'
677 c.active = 'labs'
678 c.lab_settings = _LAB_SETTINGS
678 c.lab_settings = _LAB_SETTINGS
679
679
680 data = render('rhodecode:templates/admin/settings/settings.mako',
680 data = render('rhodecode:templates/admin/settings/settings.mako',
681 self._get_template_context(c), self.request)
681 self._get_template_context(c), self.request)
682 html = formencode.htmlfill.render(
682 html = formencode.htmlfill.render(
683 data,
683 data,
684 defaults=self._form_defaults(),
684 defaults=self._form_defaults(),
685 encoding="UTF-8",
685 encoding="UTF-8",
686 force_defaults=False
686 force_defaults=False
687 )
687 )
688 return Response(html)
688 return Response(html)
689
689
690 @LoginRequired()
690 @LoginRequired()
691 @HasPermissionAllDecorator('hg.admin')
691 @HasPermissionAllDecorator('hg.admin')
692 @CSRFRequired()
692 @CSRFRequired()
693 @view_config(
693 @view_config(
694 route_name='admin_settings_labs_update', request_method='POST',
694 route_name='admin_settings_labs_update', request_method='POST',
695 renderer='rhodecode:templates/admin/settings/settings.mako')
695 renderer='rhodecode:templates/admin/settings/settings.mako')
696 def settings_labs_update(self):
696 def settings_labs_update(self):
697 _ = self.request.translate
697 _ = self.request.translate
698 c = self.load_default_context()
698 c = self.load_default_context()
699 c.active = 'labs'
699 c.active = 'labs'
700
700
701 application_form = LabsSettingsForm(self.request.translate)()
701 application_form = LabsSettingsForm(self.request.translate)()
702 try:
702 try:
703 form_result = application_form.to_python(dict(self.request.POST))
703 form_result = application_form.to_python(dict(self.request.POST))
704 except formencode.Invalid as errors:
704 except formencode.Invalid as errors:
705 h.flash(
705 h.flash(
706 _('Some form inputs contain invalid data.'),
706 _('Some form inputs contain invalid data.'),
707 category='error')
707 category='error')
708 data = render('rhodecode:templates/admin/settings/settings.mako',
708 data = render('rhodecode:templates/admin/settings/settings.mako',
709 self._get_template_context(c), self.request)
709 self._get_template_context(c), self.request)
710 html = formencode.htmlfill.render(
710 html = formencode.htmlfill.render(
711 data,
711 data,
712 defaults=errors.value,
712 defaults=errors.value,
713 errors=errors.error_dict or {},
713 errors=errors.error_dict or {},
714 prefix_error=False,
714 prefix_error=False,
715 encoding="UTF-8",
715 encoding="UTF-8",
716 force_defaults=False
716 force_defaults=False
717 )
717 )
718 return Response(html)
718 return Response(html)
719
719
720 try:
720 try:
721 session = Session()
721 session = Session()
722 for setting in _LAB_SETTINGS:
722 for setting in _LAB_SETTINGS:
723 setting_name = setting.key[len('rhodecode_'):]
723 setting_name = setting.key[len('rhodecode_'):]
724 sett = SettingsModel().create_or_update_setting(
724 sett = SettingsModel().create_or_update_setting(
725 setting_name, form_result[setting.key], setting.type)
725 setting_name, form_result[setting.key], setting.type)
726 session.add(sett)
726 session.add(sett)
727
727
728 except Exception:
728 except Exception:
729 log.exception('Exception while updating lab settings')
729 log.exception('Exception while updating lab settings')
730 h.flash(_('Error occurred during updating labs settings'),
730 h.flash(_('Error occurred during updating labs settings'),
731 category='error')
731 category='error')
732 else:
732 else:
733 Session().commit()
733 Session().commit()
734 SettingsModel().invalidate_settings_cache()
734 SettingsModel().invalidate_settings_cache()
735 h.flash(_('Updated Labs settings'), category='success')
735 h.flash(_('Updated Labs settings'), category='success')
736 raise HTTPFound(h.route_path('admin_settings_labs'))
736 raise HTTPFound(h.route_path('admin_settings_labs'))
737
737
738 data = render('rhodecode:templates/admin/settings/settings.mako',
738 data = render('rhodecode:templates/admin/settings/settings.mako',
739 self._get_template_context(c), self.request)
739 self._get_template_context(c), self.request)
740 html = formencode.htmlfill.render(
740 html = formencode.htmlfill.render(
741 data,
741 data,
742 defaults=self._form_defaults(),
742 defaults=self._form_defaults(),
743 encoding="UTF-8",
743 encoding="UTF-8",
744 force_defaults=False
744 force_defaults=False
745 )
745 )
746 return Response(html)
746 return Response(html)
747
747
748
748
749 # :param key: name of the setting including the 'rhodecode_' prefix
749 # :param key: name of the setting including the 'rhodecode_' prefix
750 # :param type: the RhodeCodeSetting type to use.
750 # :param type: the RhodeCodeSetting type to use.
751 # :param group: the i18ned group in which we should dispaly this setting
751 # :param group: the i18ned group in which we should dispaly this setting
752 # :param label: the i18ned label we should display for this setting
752 # :param label: the i18ned label we should display for this setting
753 # :param help: the i18ned help we should dispaly for this setting
753 # :param help: the i18ned help we should dispaly for this setting
754 LabSetting = collections.namedtuple(
754 LabSetting = collections.namedtuple(
755 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
755 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
756
756
757
757
758 # This list has to be kept in sync with the form
758 # This list has to be kept in sync with the form
759 # rhodecode.model.forms.LabsSettingsForm.
759 # rhodecode.model.forms.LabsSettingsForm.
760 _LAB_SETTINGS = [
760 _LAB_SETTINGS = [
761
761
762 ]
762 ]
@@ -1,1190 +1,1189 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.authentication.plugins import auth_rhodecode
33 from rhodecode.authentication.plugins import auth_rhodecode
34 from rhodecode.events import trigger
34 from rhodecode.events import trigger
35
35
36 from rhodecode.lib import audit_logger
36 from rhodecode.lib import audit_logger
37 from rhodecode.lib.exceptions import (
37 from rhodecode.lib.exceptions import (
38 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
38 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
39 UserOwnsUserGroupsException, DefaultUserException)
39 UserOwnsUserGroupsException, DefaultUserException)
40 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.auth import (
41 from rhodecode.lib.auth import (
42 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
42 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
43 from rhodecode.lib import helpers as h
43 from rhodecode.lib import helpers as h
44 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
44 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
45 from rhodecode.model.auth_token import AuthTokenModel
45 from rhodecode.model.auth_token import AuthTokenModel
46 from rhodecode.model.forms import (
46 from rhodecode.model.forms import (
47 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
47 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
48 UserExtraEmailForm, UserExtraIpForm)
48 UserExtraEmailForm, UserExtraIpForm)
49 from rhodecode.model.permission import PermissionModel
49 from rhodecode.model.permission import PermissionModel
50 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.repo_group import RepoGroupModel
51 from rhodecode.model.ssh_key import SshKeyModel
51 from rhodecode.model.ssh_key import SshKeyModel
52 from rhodecode.model.user import UserModel
52 from rhodecode.model.user import UserModel
53 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.user_group import UserGroupModel
54 from rhodecode.model.db import (
54 from rhodecode.model.db import (
55 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
55 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
56 UserApiKeys, UserSshKeys, RepoGroup)
56 UserApiKeys, UserSshKeys, RepoGroup)
57 from rhodecode.model.meta import Session
57 from rhodecode.model.meta import Session
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class AdminUsersView(BaseAppView, DataGridAppView):
62 class AdminUsersView(BaseAppView, DataGridAppView):
63
63
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context()
65 c = self._get_local_tmpl_context()
66 return c
66 return c
67
67
68 @LoginRequired()
68 @LoginRequired()
69 @HasPermissionAllDecorator('hg.admin')
69 @HasPermissionAllDecorator('hg.admin')
70 @view_config(
70 @view_config(
71 route_name='users', request_method='GET',
71 route_name='users', request_method='GET',
72 renderer='rhodecode:templates/admin/users/users.mako')
72 renderer='rhodecode:templates/admin/users/users.mako')
73 def users_list(self):
73 def users_list(self):
74 c = self.load_default_context()
74 c = self.load_default_context()
75 return self._get_template_context(c)
75 return self._get_template_context(c)
76
76
77 @LoginRequired()
77 @LoginRequired()
78 @HasPermissionAllDecorator('hg.admin')
78 @HasPermissionAllDecorator('hg.admin')
79 @view_config(
79 @view_config(
80 # renderer defined below
80 # renderer defined below
81 route_name='users_data', request_method='GET',
81 route_name='users_data', request_method='GET',
82 renderer='json_ext', xhr=True)
82 renderer='json_ext', xhr=True)
83 def users_list_data(self):
83 def users_list_data(self):
84 self.load_default_context()
84 self.load_default_context()
85 column_map = {
85 column_map = {
86 'first_name': 'name',
86 'first_name': 'name',
87 'last_name': 'lastname',
87 'last_name': 'lastname',
88 }
88 }
89 draw, start, limit = self._extract_chunk(self.request)
89 draw, start, limit = self._extract_chunk(self.request)
90 search_q, order_by, order_dir = self._extract_ordering(
90 search_q, order_by, order_dir = self._extract_ordering(
91 self.request, column_map=column_map)
91 self.request, column_map=column_map)
92
92
93 _render = self.request.get_partial_renderer(
93 _render = self.request.get_partial_renderer(
94 'rhodecode:templates/data_table/_dt_elements.mako')
94 'rhodecode:templates/data_table/_dt_elements.mako')
95
95
96 def user_actions(user_id, username):
96 def user_actions(user_id, username):
97 return _render("user_actions", user_id, username)
97 return _render("user_actions", user_id, username)
98
98
99 users_data_total_count = User.query()\
99 users_data_total_count = User.query()\
100 .filter(User.username != User.DEFAULT_USER) \
100 .filter(User.username != User.DEFAULT_USER) \
101 .count()
101 .count()
102
102
103 # json generate
103 # json generate
104 base_q = User.query().filter(User.username != User.DEFAULT_USER)
104 base_q = User.query().filter(User.username != User.DEFAULT_USER)
105
105
106 if search_q:
106 if search_q:
107 like_expression = u'%{}%'.format(safe_unicode(search_q))
107 like_expression = u'%{}%'.format(safe_unicode(search_q))
108 base_q = base_q.filter(or_(
108 base_q = base_q.filter(or_(
109 User.username.ilike(like_expression),
109 User.username.ilike(like_expression),
110 User._email.ilike(like_expression),
110 User._email.ilike(like_expression),
111 User.name.ilike(like_expression),
111 User.name.ilike(like_expression),
112 User.lastname.ilike(like_expression),
112 User.lastname.ilike(like_expression),
113 ))
113 ))
114
114
115 users_data_total_filtered_count = base_q.count()
115 users_data_total_filtered_count = base_q.count()
116
116
117 sort_col = getattr(User, order_by, None)
117 sort_col = getattr(User, order_by, None)
118 if sort_col:
118 if sort_col:
119 if order_dir == 'asc':
119 if order_dir == 'asc':
120 # handle null values properly to order by NULL last
120 # handle null values properly to order by NULL last
121 if order_by in ['last_activity']:
121 if order_by in ['last_activity']:
122 sort_col = coalesce(sort_col, datetime.date.max)
122 sort_col = coalesce(sort_col, datetime.date.max)
123 sort_col = sort_col.asc()
123 sort_col = sort_col.asc()
124 else:
124 else:
125 # handle null values properly to order by NULL last
125 # handle null values properly to order by NULL last
126 if order_by in ['last_activity']:
126 if order_by in ['last_activity']:
127 sort_col = coalesce(sort_col, datetime.date.min)
127 sort_col = coalesce(sort_col, datetime.date.min)
128 sort_col = sort_col.desc()
128 sort_col = sort_col.desc()
129
129
130 base_q = base_q.order_by(sort_col)
130 base_q = base_q.order_by(sort_col)
131 base_q = base_q.offset(start).limit(limit)
131 base_q = base_q.offset(start).limit(limit)
132
132
133 users_list = base_q.all()
133 users_list = base_q.all()
134
134
135 users_data = []
135 users_data = []
136 for user in users_list:
136 for user in users_list:
137 users_data.append({
137 users_data.append({
138 "username": h.gravatar_with_user(self.request, user.username),
138 "username": h.gravatar_with_user(self.request, user.username),
139 "email": user.email,
139 "email": user.email,
140 "first_name": user.first_name,
140 "first_name": user.first_name,
141 "last_name": user.last_name,
141 "last_name": user.last_name,
142 "last_login": h.format_date(user.last_login),
142 "last_login": h.format_date(user.last_login),
143 "last_activity": h.format_date(user.last_activity),
143 "last_activity": h.format_date(user.last_activity),
144 "active": h.bool2icon(user.active),
144 "active": h.bool2icon(user.active),
145 "active_raw": user.active,
145 "active_raw": user.active,
146 "admin": h.bool2icon(user.admin),
146 "admin": h.bool2icon(user.admin),
147 "extern_type": user.extern_type,
147 "extern_type": user.extern_type,
148 "extern_name": user.extern_name,
148 "extern_name": user.extern_name,
149 "action": user_actions(user.user_id, user.username),
149 "action": user_actions(user.user_id, user.username),
150 })
150 })
151
151
152 data = ({
152 data = ({
153 'draw': draw,
153 'draw': draw,
154 'data': users_data,
154 'data': users_data,
155 'recordsTotal': users_data_total_count,
155 'recordsTotal': users_data_total_count,
156 'recordsFiltered': users_data_total_filtered_count,
156 'recordsFiltered': users_data_total_filtered_count,
157 })
157 })
158
158
159 return data
159 return data
160
160
161 def _set_personal_repo_group_template_vars(self, c_obj):
161 def _set_personal_repo_group_template_vars(self, c_obj):
162 DummyUser = AttributeDict({
162 DummyUser = AttributeDict({
163 'username': '${username}',
163 'username': '${username}',
164 'user_id': '${user_id}',
164 'user_id': '${user_id}',
165 })
165 })
166 c_obj.default_create_repo_group = RepoGroupModel() \
166 c_obj.default_create_repo_group = RepoGroupModel() \
167 .get_default_create_personal_repo_group()
167 .get_default_create_personal_repo_group()
168 c_obj.personal_repo_group_name = RepoGroupModel() \
168 c_obj.personal_repo_group_name = RepoGroupModel() \
169 .get_personal_group_name(DummyUser)
169 .get_personal_group_name(DummyUser)
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @HasPermissionAllDecorator('hg.admin')
172 @HasPermissionAllDecorator('hg.admin')
173 @view_config(
173 @view_config(
174 route_name='users_new', request_method='GET',
174 route_name='users_new', request_method='GET',
175 renderer='rhodecode:templates/admin/users/user_add.mako')
175 renderer='rhodecode:templates/admin/users/user_add.mako')
176 def users_new(self):
176 def users_new(self):
177 _ = self.request.translate
177 _ = self.request.translate
178 c = self.load_default_context()
178 c = self.load_default_context()
179 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
179 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
180 self._set_personal_repo_group_template_vars(c)
180 self._set_personal_repo_group_template_vars(c)
181 return self._get_template_context(c)
181 return self._get_template_context(c)
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasPermissionAllDecorator('hg.admin')
184 @HasPermissionAllDecorator('hg.admin')
185 @CSRFRequired()
185 @CSRFRequired()
186 @view_config(
186 @view_config(
187 route_name='users_create', request_method='POST',
187 route_name='users_create', request_method='POST',
188 renderer='rhodecode:templates/admin/users/user_add.mako')
188 renderer='rhodecode:templates/admin/users/user_add.mako')
189 def users_create(self):
189 def users_create(self):
190 _ = self.request.translate
190 _ = self.request.translate
191 c = self.load_default_context()
191 c = self.load_default_context()
192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
193 user_model = UserModel()
193 user_model = UserModel()
194 user_form = UserForm(self.request.translate)()
194 user_form = UserForm(self.request.translate)()
195 try:
195 try:
196 form_result = user_form.to_python(dict(self.request.POST))
196 form_result = user_form.to_python(dict(self.request.POST))
197 user = user_model.create(form_result)
197 user = user_model.create(form_result)
198 Session().flush()
198 Session().flush()
199 creation_data = user.get_api_data()
199 creation_data = user.get_api_data()
200 username = form_result['username']
200 username = form_result['username']
201
201
202 audit_logger.store_web(
202 audit_logger.store_web(
203 'user.create', action_data={'data': creation_data},
203 'user.create', action_data={'data': creation_data},
204 user=c.rhodecode_user)
204 user=c.rhodecode_user)
205
205
206 user_link = h.link_to(
206 user_link = h.link_to(
207 h.escape(username),
207 h.escape(username),
208 h.route_path('user_edit', user_id=user.user_id))
208 h.route_path('user_edit', user_id=user.user_id))
209 h.flash(h.literal(_('Created user %(user_link)s')
209 h.flash(h.literal(_('Created user %(user_link)s')
210 % {'user_link': user_link}), category='success')
210 % {'user_link': user_link}), category='success')
211 Session().commit()
211 Session().commit()
212 except formencode.Invalid as errors:
212 except formencode.Invalid as errors:
213 self._set_personal_repo_group_template_vars(c)
213 self._set_personal_repo_group_template_vars(c)
214 data = render(
214 data = render(
215 'rhodecode:templates/admin/users/user_add.mako',
215 'rhodecode:templates/admin/users/user_add.mako',
216 self._get_template_context(c), self.request)
216 self._get_template_context(c), self.request)
217 html = formencode.htmlfill.render(
217 html = formencode.htmlfill.render(
218 data,
218 data,
219 defaults=errors.value,
219 defaults=errors.value,
220 errors=errors.error_dict or {},
220 errors=errors.error_dict or {},
221 prefix_error=False,
221 prefix_error=False,
222 encoding="UTF-8",
222 encoding="UTF-8",
223 force_defaults=False
223 force_defaults=False
224 )
224 )
225 return Response(html)
225 return Response(html)
226 except UserCreationError as e:
226 except UserCreationError as e:
227 h.flash(e, 'error')
227 h.flash(e, 'error')
228 except Exception:
228 except Exception:
229 log.exception("Exception creation of user")
229 log.exception("Exception creation of user")
230 h.flash(_('Error occurred during creation of user %s')
230 h.flash(_('Error occurred during creation of user %s')
231 % self.request.POST.get('username'), category='error')
231 % self.request.POST.get('username'), category='error')
232 raise HTTPFound(h.route_path('users'))
232 raise HTTPFound(h.route_path('users'))
233
233
234
234
235 class UsersView(UserAppView):
235 class UsersView(UserAppView):
236 ALLOW_SCOPED_TOKENS = False
236 ALLOW_SCOPED_TOKENS = False
237 """
237 """
238 This view has alternative version inside EE, if modified please take a look
238 This view has alternative version inside EE, if modified please take a look
239 in there as well.
239 in there as well.
240 """
240 """
241
241
242 def load_default_context(self):
242 def load_default_context(self):
243 c = self._get_local_tmpl_context()
243 c = self._get_local_tmpl_context()
244 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
244 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
245 c.allowed_languages = [
245 c.allowed_languages = [
246 ('en', 'English (en)'),
246 ('en', 'English (en)'),
247 ('de', 'German (de)'),
247 ('de', 'German (de)'),
248 ('fr', 'French (fr)'),
248 ('fr', 'French (fr)'),
249 ('it', 'Italian (it)'),
249 ('it', 'Italian (it)'),
250 ('ja', 'Japanese (ja)'),
250 ('ja', 'Japanese (ja)'),
251 ('pl', 'Polish (pl)'),
251 ('pl', 'Polish (pl)'),
252 ('pt', 'Portuguese (pt)'),
252 ('pt', 'Portuguese (pt)'),
253 ('ru', 'Russian (ru)'),
253 ('ru', 'Russian (ru)'),
254 ('zh', 'Chinese (zh)'),
254 ('zh', 'Chinese (zh)'),
255 ]
255 ]
256 req = self.request
256 req = self.request
257
257
258 c.available_permissions = req.registry.settings['available_permissions']
258 c.available_permissions = req.registry.settings['available_permissions']
259 PermissionModel().set_global_permission_choices(
259 PermissionModel().set_global_permission_choices(
260 c, gettext_translator=req.translate)
260 c, gettext_translator=req.translate)
261
261
262
263 return c
262 return c
264
263
265 @LoginRequired()
264 @LoginRequired()
266 @HasPermissionAllDecorator('hg.admin')
265 @HasPermissionAllDecorator('hg.admin')
267 @CSRFRequired()
266 @CSRFRequired()
268 @view_config(
267 @view_config(
269 route_name='user_update', request_method='POST',
268 route_name='user_update', request_method='POST',
270 renderer='rhodecode:templates/admin/users/user_edit.mako')
269 renderer='rhodecode:templates/admin/users/user_edit.mako')
271 def user_update(self):
270 def user_update(self):
272 _ = self.request.translate
271 _ = self.request.translate
273 c = self.load_default_context()
272 c = self.load_default_context()
274
273
275 user_id = self.db_user_id
274 user_id = self.db_user_id
276 c.user = self.db_user
275 c.user = self.db_user
277
276
278 c.active = 'profile'
277 c.active = 'profile'
279 c.extern_type = c.user.extern_type
278 c.extern_type = c.user.extern_type
280 c.extern_name = c.user.extern_name
279 c.extern_name = c.user.extern_name
281 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
280 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
282 available_languages = [x[0] for x in c.allowed_languages]
281 available_languages = [x[0] for x in c.allowed_languages]
283 _form = UserForm(self.request.translate, edit=True,
282 _form = UserForm(self.request.translate, edit=True,
284 available_languages=available_languages,
283 available_languages=available_languages,
285 old_data={'user_id': user_id,
284 old_data={'user_id': user_id,
286 'email': c.user.email})()
285 'email': c.user.email})()
287 form_result = {}
286 form_result = {}
288 old_values = c.user.get_api_data()
287 old_values = c.user.get_api_data()
289 try:
288 try:
290 form_result = _form.to_python(dict(self.request.POST))
289 form_result = _form.to_python(dict(self.request.POST))
291 skip_attrs = ['extern_type', 'extern_name']
290 skip_attrs = ['extern_type', 'extern_name']
292 # TODO: plugin should define if username can be updated
291 # TODO: plugin should define if username can be updated
293 if c.extern_type != "rhodecode":
292 if c.extern_type != "rhodecode":
294 # forbid updating username for external accounts
293 # forbid updating username for external accounts
295 skip_attrs.append('username')
294 skip_attrs.append('username')
296
295
297 UserModel().update_user(
296 UserModel().update_user(
298 user_id, skip_attrs=skip_attrs, **form_result)
297 user_id, skip_attrs=skip_attrs, **form_result)
299
298
300 audit_logger.store_web(
299 audit_logger.store_web(
301 'user.edit', action_data={'old_data': old_values},
300 'user.edit', action_data={'old_data': old_values},
302 user=c.rhodecode_user)
301 user=c.rhodecode_user)
303
302
304 Session().commit()
303 Session().commit()
305 h.flash(_('User updated successfully'), category='success')
304 h.flash(_('User updated successfully'), category='success')
306 except formencode.Invalid as errors:
305 except formencode.Invalid as errors:
307 data = render(
306 data = render(
308 'rhodecode:templates/admin/users/user_edit.mako',
307 'rhodecode:templates/admin/users/user_edit.mako',
309 self._get_template_context(c), self.request)
308 self._get_template_context(c), self.request)
310 html = formencode.htmlfill.render(
309 html = formencode.htmlfill.render(
311 data,
310 data,
312 defaults=errors.value,
311 defaults=errors.value,
313 errors=errors.error_dict or {},
312 errors=errors.error_dict or {},
314 prefix_error=False,
313 prefix_error=False,
315 encoding="UTF-8",
314 encoding="UTF-8",
316 force_defaults=False
315 force_defaults=False
317 )
316 )
318 return Response(html)
317 return Response(html)
319 except UserCreationError as e:
318 except UserCreationError as e:
320 h.flash(e, 'error')
319 h.flash(e, 'error')
321 except Exception:
320 except Exception:
322 log.exception("Exception updating user")
321 log.exception("Exception updating user")
323 h.flash(_('Error occurred during update of user %s')
322 h.flash(_('Error occurred during update of user %s')
324 % form_result.get('username'), category='error')
323 % form_result.get('username'), category='error')
325 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
324 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
326
325
327 @LoginRequired()
326 @LoginRequired()
328 @HasPermissionAllDecorator('hg.admin')
327 @HasPermissionAllDecorator('hg.admin')
329 @CSRFRequired()
328 @CSRFRequired()
330 @view_config(
329 @view_config(
331 route_name='user_delete', request_method='POST',
330 route_name='user_delete', request_method='POST',
332 renderer='rhodecode:templates/admin/users/user_edit.mako')
331 renderer='rhodecode:templates/admin/users/user_edit.mako')
333 def user_delete(self):
332 def user_delete(self):
334 _ = self.request.translate
333 _ = self.request.translate
335 c = self.load_default_context()
334 c = self.load_default_context()
336 c.user = self.db_user
335 c.user = self.db_user
337
336
338 _repos = c.user.repositories
337 _repos = c.user.repositories
339 _repo_groups = c.user.repository_groups
338 _repo_groups = c.user.repository_groups
340 _user_groups = c.user.user_groups
339 _user_groups = c.user.user_groups
341
340
342 handle_repos = None
341 handle_repos = None
343 handle_repo_groups = None
342 handle_repo_groups = None
344 handle_user_groups = None
343 handle_user_groups = None
345 # dummy call for flash of handle
344 # dummy call for flash of handle
346 set_handle_flash_repos = lambda: None
345 set_handle_flash_repos = lambda: None
347 set_handle_flash_repo_groups = lambda: None
346 set_handle_flash_repo_groups = lambda: None
348 set_handle_flash_user_groups = lambda: None
347 set_handle_flash_user_groups = lambda: None
349
348
350 if _repos and self.request.POST.get('user_repos'):
349 if _repos and self.request.POST.get('user_repos'):
351 do = self.request.POST['user_repos']
350 do = self.request.POST['user_repos']
352 if do == 'detach':
351 if do == 'detach':
353 handle_repos = 'detach'
352 handle_repos = 'detach'
354 set_handle_flash_repos = lambda: h.flash(
353 set_handle_flash_repos = lambda: h.flash(
355 _('Detached %s repositories') % len(_repos),
354 _('Detached %s repositories') % len(_repos),
356 category='success')
355 category='success')
357 elif do == 'delete':
356 elif do == 'delete':
358 handle_repos = 'delete'
357 handle_repos = 'delete'
359 set_handle_flash_repos = lambda: h.flash(
358 set_handle_flash_repos = lambda: h.flash(
360 _('Deleted %s repositories') % len(_repos),
359 _('Deleted %s repositories') % len(_repos),
361 category='success')
360 category='success')
362
361
363 if _repo_groups and self.request.POST.get('user_repo_groups'):
362 if _repo_groups and self.request.POST.get('user_repo_groups'):
364 do = self.request.POST['user_repo_groups']
363 do = self.request.POST['user_repo_groups']
365 if do == 'detach':
364 if do == 'detach':
366 handle_repo_groups = 'detach'
365 handle_repo_groups = 'detach'
367 set_handle_flash_repo_groups = lambda: h.flash(
366 set_handle_flash_repo_groups = lambda: h.flash(
368 _('Detached %s repository groups') % len(_repo_groups),
367 _('Detached %s repository groups') % len(_repo_groups),
369 category='success')
368 category='success')
370 elif do == 'delete':
369 elif do == 'delete':
371 handle_repo_groups = 'delete'
370 handle_repo_groups = 'delete'
372 set_handle_flash_repo_groups = lambda: h.flash(
371 set_handle_flash_repo_groups = lambda: h.flash(
373 _('Deleted %s repository groups') % len(_repo_groups),
372 _('Deleted %s repository groups') % len(_repo_groups),
374 category='success')
373 category='success')
375
374
376 if _user_groups and self.request.POST.get('user_user_groups'):
375 if _user_groups and self.request.POST.get('user_user_groups'):
377 do = self.request.POST['user_user_groups']
376 do = self.request.POST['user_user_groups']
378 if do == 'detach':
377 if do == 'detach':
379 handle_user_groups = 'detach'
378 handle_user_groups = 'detach'
380 set_handle_flash_user_groups = lambda: h.flash(
379 set_handle_flash_user_groups = lambda: h.flash(
381 _('Detached %s user groups') % len(_user_groups),
380 _('Detached %s user groups') % len(_user_groups),
382 category='success')
381 category='success')
383 elif do == 'delete':
382 elif do == 'delete':
384 handle_user_groups = 'delete'
383 handle_user_groups = 'delete'
385 set_handle_flash_user_groups = lambda: h.flash(
384 set_handle_flash_user_groups = lambda: h.flash(
386 _('Deleted %s user groups') % len(_user_groups),
385 _('Deleted %s user groups') % len(_user_groups),
387 category='success')
386 category='success')
388
387
389 old_values = c.user.get_api_data()
388 old_values = c.user.get_api_data()
390 try:
389 try:
391 UserModel().delete(c.user, handle_repos=handle_repos,
390 UserModel().delete(c.user, handle_repos=handle_repos,
392 handle_repo_groups=handle_repo_groups,
391 handle_repo_groups=handle_repo_groups,
393 handle_user_groups=handle_user_groups)
392 handle_user_groups=handle_user_groups)
394
393
395 audit_logger.store_web(
394 audit_logger.store_web(
396 'user.delete', action_data={'old_data': old_values},
395 'user.delete', action_data={'old_data': old_values},
397 user=c.rhodecode_user)
396 user=c.rhodecode_user)
398
397
399 Session().commit()
398 Session().commit()
400 set_handle_flash_repos()
399 set_handle_flash_repos()
401 set_handle_flash_repo_groups()
400 set_handle_flash_repo_groups()
402 set_handle_flash_user_groups()
401 set_handle_flash_user_groups()
403 h.flash(_('Successfully deleted user'), category='success')
402 h.flash(_('Successfully deleted user'), category='success')
404 except (UserOwnsReposException, UserOwnsRepoGroupsException,
403 except (UserOwnsReposException, UserOwnsRepoGroupsException,
405 UserOwnsUserGroupsException, DefaultUserException) as e:
404 UserOwnsUserGroupsException, DefaultUserException) as e:
406 h.flash(e, category='warning')
405 h.flash(e, category='warning')
407 except Exception:
406 except Exception:
408 log.exception("Exception during deletion of user")
407 log.exception("Exception during deletion of user")
409 h.flash(_('An error occurred during deletion of user'),
408 h.flash(_('An error occurred during deletion of user'),
410 category='error')
409 category='error')
411 raise HTTPFound(h.route_path('users'))
410 raise HTTPFound(h.route_path('users'))
412
411
413 @LoginRequired()
412 @LoginRequired()
414 @HasPermissionAllDecorator('hg.admin')
413 @HasPermissionAllDecorator('hg.admin')
415 @view_config(
414 @view_config(
416 route_name='user_edit', request_method='GET',
415 route_name='user_edit', request_method='GET',
417 renderer='rhodecode:templates/admin/users/user_edit.mako')
416 renderer='rhodecode:templates/admin/users/user_edit.mako')
418 def user_edit(self):
417 def user_edit(self):
419 _ = self.request.translate
418 _ = self.request.translate
420 c = self.load_default_context()
419 c = self.load_default_context()
421 c.user = self.db_user
420 c.user = self.db_user
422
421
423 c.active = 'profile'
422 c.active = 'profile'
424 c.extern_type = c.user.extern_type
423 c.extern_type = c.user.extern_type
425 c.extern_name = c.user.extern_name
424 c.extern_name = c.user.extern_name
426 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
425 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
427
426
428 defaults = c.user.get_dict()
427 defaults = c.user.get_dict()
429 defaults.update({'language': c.user.user_data.get('language')})
428 defaults.update({'language': c.user.user_data.get('language')})
430
429
431 data = render(
430 data = render(
432 'rhodecode:templates/admin/users/user_edit.mako',
431 'rhodecode:templates/admin/users/user_edit.mako',
433 self._get_template_context(c), self.request)
432 self._get_template_context(c), self.request)
434 html = formencode.htmlfill.render(
433 html = formencode.htmlfill.render(
435 data,
434 data,
436 defaults=defaults,
435 defaults=defaults,
437 encoding="UTF-8",
436 encoding="UTF-8",
438 force_defaults=False
437 force_defaults=False
439 )
438 )
440 return Response(html)
439 return Response(html)
441
440
442 @LoginRequired()
441 @LoginRequired()
443 @HasPermissionAllDecorator('hg.admin')
442 @HasPermissionAllDecorator('hg.admin')
444 @view_config(
443 @view_config(
445 route_name='user_edit_advanced', request_method='GET',
444 route_name='user_edit_advanced', request_method='GET',
446 renderer='rhodecode:templates/admin/users/user_edit.mako')
445 renderer='rhodecode:templates/admin/users/user_edit.mako')
447 def user_edit_advanced(self):
446 def user_edit_advanced(self):
448 _ = self.request.translate
447 _ = self.request.translate
449 c = self.load_default_context()
448 c = self.load_default_context()
450
449
451 user_id = self.db_user_id
450 user_id = self.db_user_id
452 c.user = self.db_user
451 c.user = self.db_user
453
452
454 c.active = 'advanced'
453 c.active = 'advanced'
455 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
454 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
456 c.personal_repo_group_name = RepoGroupModel()\
455 c.personal_repo_group_name = RepoGroupModel()\
457 .get_personal_group_name(c.user)
456 .get_personal_group_name(c.user)
458
457
459 c.user_to_review_rules = sorted(
458 c.user_to_review_rules = sorted(
460 (x.user for x in c.user.user_review_rules),
459 (x.user for x in c.user.user_review_rules),
461 key=lambda u: u.username.lower())
460 key=lambda u: u.username.lower())
462
461
463 c.first_admin = User.get_first_super_admin()
462 c.first_admin = User.get_first_super_admin()
464 defaults = c.user.get_dict()
463 defaults = c.user.get_dict()
465
464
466 # Interim workaround if the user participated on any pull requests as a
465 # Interim workaround if the user participated on any pull requests as a
467 # reviewer.
466 # reviewer.
468 has_review = len(c.user.reviewer_pull_requests)
467 has_review = len(c.user.reviewer_pull_requests)
469 c.can_delete_user = not has_review
468 c.can_delete_user = not has_review
470 c.can_delete_user_message = ''
469 c.can_delete_user_message = ''
471 inactive_link = h.link_to(
470 inactive_link = h.link_to(
472 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
471 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
473 if has_review == 1:
472 if has_review == 1:
474 c.can_delete_user_message = h.literal(_(
473 c.can_delete_user_message = h.literal(_(
475 'The user participates as reviewer in {} pull request and '
474 'The user participates as reviewer in {} pull request and '
476 'cannot be deleted. \nYou can set the user to '
475 'cannot be deleted. \nYou can set the user to '
477 '"{}" instead of deleting it.').format(
476 '"{}" instead of deleting it.').format(
478 has_review, inactive_link))
477 has_review, inactive_link))
479 elif has_review:
478 elif has_review:
480 c.can_delete_user_message = h.literal(_(
479 c.can_delete_user_message = h.literal(_(
481 'The user participates as reviewer in {} pull requests and '
480 'The user participates as reviewer in {} pull requests and '
482 'cannot be deleted. \nYou can set the user to '
481 'cannot be deleted. \nYou can set the user to '
483 '"{}" instead of deleting it.').format(
482 '"{}" instead of deleting it.').format(
484 has_review, inactive_link))
483 has_review, inactive_link))
485
484
486 data = render(
485 data = render(
487 'rhodecode:templates/admin/users/user_edit.mako',
486 'rhodecode:templates/admin/users/user_edit.mako',
488 self._get_template_context(c), self.request)
487 self._get_template_context(c), self.request)
489 html = formencode.htmlfill.render(
488 html = formencode.htmlfill.render(
490 data,
489 data,
491 defaults=defaults,
490 defaults=defaults,
492 encoding="UTF-8",
491 encoding="UTF-8",
493 force_defaults=False
492 force_defaults=False
494 )
493 )
495 return Response(html)
494 return Response(html)
496
495
497 @LoginRequired()
496 @LoginRequired()
498 @HasPermissionAllDecorator('hg.admin')
497 @HasPermissionAllDecorator('hg.admin')
499 @view_config(
498 @view_config(
500 route_name='user_edit_global_perms', request_method='GET',
499 route_name='user_edit_global_perms', request_method='GET',
501 renderer='rhodecode:templates/admin/users/user_edit.mako')
500 renderer='rhodecode:templates/admin/users/user_edit.mako')
502 def user_edit_global_perms(self):
501 def user_edit_global_perms(self):
503 _ = self.request.translate
502 _ = self.request.translate
504 c = self.load_default_context()
503 c = self.load_default_context()
505 c.user = self.db_user
504 c.user = self.db_user
506
505
507 c.active = 'global_perms'
506 c.active = 'global_perms'
508
507
509 c.default_user = User.get_default_user()
508 c.default_user = User.get_default_user()
510 defaults = c.user.get_dict()
509 defaults = c.user.get_dict()
511 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
510 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
512 defaults.update(c.default_user.get_default_perms())
511 defaults.update(c.default_user.get_default_perms())
513 defaults.update(c.user.get_default_perms())
512 defaults.update(c.user.get_default_perms())
514
513
515 data = render(
514 data = render(
516 'rhodecode:templates/admin/users/user_edit.mako',
515 'rhodecode:templates/admin/users/user_edit.mako',
517 self._get_template_context(c), self.request)
516 self._get_template_context(c), self.request)
518 html = formencode.htmlfill.render(
517 html = formencode.htmlfill.render(
519 data,
518 data,
520 defaults=defaults,
519 defaults=defaults,
521 encoding="UTF-8",
520 encoding="UTF-8",
522 force_defaults=False
521 force_defaults=False
523 )
522 )
524 return Response(html)
523 return Response(html)
525
524
526 @LoginRequired()
525 @LoginRequired()
527 @HasPermissionAllDecorator('hg.admin')
526 @HasPermissionAllDecorator('hg.admin')
528 @CSRFRequired()
527 @CSRFRequired()
529 @view_config(
528 @view_config(
530 route_name='user_edit_global_perms_update', request_method='POST',
529 route_name='user_edit_global_perms_update', request_method='POST',
531 renderer='rhodecode:templates/admin/users/user_edit.mako')
530 renderer='rhodecode:templates/admin/users/user_edit.mako')
532 def user_edit_global_perms_update(self):
531 def user_edit_global_perms_update(self):
533 _ = self.request.translate
532 _ = self.request.translate
534 c = self.load_default_context()
533 c = self.load_default_context()
535
534
536 user_id = self.db_user_id
535 user_id = self.db_user_id
537 c.user = self.db_user
536 c.user = self.db_user
538
537
539 c.active = 'global_perms'
538 c.active = 'global_perms'
540 try:
539 try:
541 # first stage that verifies the checkbox
540 # first stage that verifies the checkbox
542 _form = UserIndividualPermissionsForm(self.request.translate)
541 _form = UserIndividualPermissionsForm(self.request.translate)
543 form_result = _form.to_python(dict(self.request.POST))
542 form_result = _form.to_python(dict(self.request.POST))
544 inherit_perms = form_result['inherit_default_permissions']
543 inherit_perms = form_result['inherit_default_permissions']
545 c.user.inherit_default_permissions = inherit_perms
544 c.user.inherit_default_permissions = inherit_perms
546 Session().add(c.user)
545 Session().add(c.user)
547
546
548 if not inherit_perms:
547 if not inherit_perms:
549 # only update the individual ones if we un check the flag
548 # only update the individual ones if we un check the flag
550 _form = UserPermissionsForm(
549 _form = UserPermissionsForm(
551 self.request.translate,
550 self.request.translate,
552 [x[0] for x in c.repo_create_choices],
551 [x[0] for x in c.repo_create_choices],
553 [x[0] for x in c.repo_create_on_write_choices],
552 [x[0] for x in c.repo_create_on_write_choices],
554 [x[0] for x in c.repo_group_create_choices],
553 [x[0] for x in c.repo_group_create_choices],
555 [x[0] for x in c.user_group_create_choices],
554 [x[0] for x in c.user_group_create_choices],
556 [x[0] for x in c.fork_choices],
555 [x[0] for x in c.fork_choices],
557 [x[0] for x in c.inherit_default_permission_choices])()
556 [x[0] for x in c.inherit_default_permission_choices])()
558
557
559 form_result = _form.to_python(dict(self.request.POST))
558 form_result = _form.to_python(dict(self.request.POST))
560 form_result.update({'perm_user_id': c.user.user_id})
559 form_result.update({'perm_user_id': c.user.user_id})
561
560
562 PermissionModel().update_user_permissions(form_result)
561 PermissionModel().update_user_permissions(form_result)
563
562
564 # TODO(marcink): implement global permissions
563 # TODO(marcink): implement global permissions
565 # audit_log.store_web('user.edit.permissions')
564 # audit_log.store_web('user.edit.permissions')
566
565
567 Session().commit()
566 Session().commit()
568 h.flash(_('User global permissions updated successfully'),
567 h.flash(_('User global permissions updated successfully'),
569 category='success')
568 category='success')
570
569
571 except formencode.Invalid as errors:
570 except formencode.Invalid as errors:
572 data = render(
571 data = render(
573 'rhodecode:templates/admin/users/user_edit.mako',
572 'rhodecode:templates/admin/users/user_edit.mako',
574 self._get_template_context(c), self.request)
573 self._get_template_context(c), self.request)
575 html = formencode.htmlfill.render(
574 html = formencode.htmlfill.render(
576 data,
575 data,
577 defaults=errors.value,
576 defaults=errors.value,
578 errors=errors.error_dict or {},
577 errors=errors.error_dict or {},
579 prefix_error=False,
578 prefix_error=False,
580 encoding="UTF-8",
579 encoding="UTF-8",
581 force_defaults=False
580 force_defaults=False
582 )
581 )
583 return Response(html)
582 return Response(html)
584 except Exception:
583 except Exception:
585 log.exception("Exception during permissions saving")
584 log.exception("Exception during permissions saving")
586 h.flash(_('An error occurred during permissions saving'),
585 h.flash(_('An error occurred during permissions saving'),
587 category='error')
586 category='error')
588 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
587 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
589
588
590 @LoginRequired()
589 @LoginRequired()
591 @HasPermissionAllDecorator('hg.admin')
590 @HasPermissionAllDecorator('hg.admin')
592 @CSRFRequired()
591 @CSRFRequired()
593 @view_config(
592 @view_config(
594 route_name='user_force_password_reset', request_method='POST',
593 route_name='user_force_password_reset', request_method='POST',
595 renderer='rhodecode:templates/admin/users/user_edit.mako')
594 renderer='rhodecode:templates/admin/users/user_edit.mako')
596 def user_force_password_reset(self):
595 def user_force_password_reset(self):
597 """
596 """
598 toggle reset password flag for this user
597 toggle reset password flag for this user
599 """
598 """
600 _ = self.request.translate
599 _ = self.request.translate
601 c = self.load_default_context()
600 c = self.load_default_context()
602
601
603 user_id = self.db_user_id
602 user_id = self.db_user_id
604 c.user = self.db_user
603 c.user = self.db_user
605
604
606 try:
605 try:
607 old_value = c.user.user_data.get('force_password_change')
606 old_value = c.user.user_data.get('force_password_change')
608 c.user.update_userdata(force_password_change=not old_value)
607 c.user.update_userdata(force_password_change=not old_value)
609
608
610 if old_value:
609 if old_value:
611 msg = _('Force password change disabled for user')
610 msg = _('Force password change disabled for user')
612 audit_logger.store_web(
611 audit_logger.store_web(
613 'user.edit.password_reset.disabled',
612 'user.edit.password_reset.disabled',
614 user=c.rhodecode_user)
613 user=c.rhodecode_user)
615 else:
614 else:
616 msg = _('Force password change enabled for user')
615 msg = _('Force password change enabled for user')
617 audit_logger.store_web(
616 audit_logger.store_web(
618 'user.edit.password_reset.enabled',
617 'user.edit.password_reset.enabled',
619 user=c.rhodecode_user)
618 user=c.rhodecode_user)
620
619
621 Session().commit()
620 Session().commit()
622 h.flash(msg, category='success')
621 h.flash(msg, category='success')
623 except Exception:
622 except Exception:
624 log.exception("Exception during password reset for user")
623 log.exception("Exception during password reset for user")
625 h.flash(_('An error occurred during password reset for user'),
624 h.flash(_('An error occurred during password reset for user'),
626 category='error')
625 category='error')
627
626
628 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
627 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
629
628
630 @LoginRequired()
629 @LoginRequired()
631 @HasPermissionAllDecorator('hg.admin')
630 @HasPermissionAllDecorator('hg.admin')
632 @CSRFRequired()
631 @CSRFRequired()
633 @view_config(
632 @view_config(
634 route_name='user_create_personal_repo_group', request_method='POST',
633 route_name='user_create_personal_repo_group', request_method='POST',
635 renderer='rhodecode:templates/admin/users/user_edit.mako')
634 renderer='rhodecode:templates/admin/users/user_edit.mako')
636 def user_create_personal_repo_group(self):
635 def user_create_personal_repo_group(self):
637 """
636 """
638 Create personal repository group for this user
637 Create personal repository group for this user
639 """
638 """
640 from rhodecode.model.repo_group import RepoGroupModel
639 from rhodecode.model.repo_group import RepoGroupModel
641
640
642 _ = self.request.translate
641 _ = self.request.translate
643 c = self.load_default_context()
642 c = self.load_default_context()
644
643
645 user_id = self.db_user_id
644 user_id = self.db_user_id
646 c.user = self.db_user
645 c.user = self.db_user
647
646
648 personal_repo_group = RepoGroup.get_user_personal_repo_group(
647 personal_repo_group = RepoGroup.get_user_personal_repo_group(
649 c.user.user_id)
648 c.user.user_id)
650 if personal_repo_group:
649 if personal_repo_group:
651 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
650 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
652
651
653 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
652 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
654 c.user)
653 c.user)
655 named_personal_group = RepoGroup.get_by_group_name(
654 named_personal_group = RepoGroup.get_by_group_name(
656 personal_repo_group_name)
655 personal_repo_group_name)
657 try:
656 try:
658
657
659 if named_personal_group and named_personal_group.user_id == c.user.user_id:
658 if named_personal_group and named_personal_group.user_id == c.user.user_id:
660 # migrate the same named group, and mark it as personal
659 # migrate the same named group, and mark it as personal
661 named_personal_group.personal = True
660 named_personal_group.personal = True
662 Session().add(named_personal_group)
661 Session().add(named_personal_group)
663 Session().commit()
662 Session().commit()
664 msg = _('Linked repository group `%s` as personal' % (
663 msg = _('Linked repository group `%s` as personal' % (
665 personal_repo_group_name,))
664 personal_repo_group_name,))
666 h.flash(msg, category='success')
665 h.flash(msg, category='success')
667 elif not named_personal_group:
666 elif not named_personal_group:
668 RepoGroupModel().create_personal_repo_group(c.user)
667 RepoGroupModel().create_personal_repo_group(c.user)
669
668
670 msg = _('Created repository group `%s`' % (
669 msg = _('Created repository group `%s`' % (
671 personal_repo_group_name,))
670 personal_repo_group_name,))
672 h.flash(msg, category='success')
671 h.flash(msg, category='success')
673 else:
672 else:
674 msg = _('Repository group `%s` is already taken' % (
673 msg = _('Repository group `%s` is already taken' % (
675 personal_repo_group_name,))
674 personal_repo_group_name,))
676 h.flash(msg, category='warning')
675 h.flash(msg, category='warning')
677 except Exception:
676 except Exception:
678 log.exception("Exception during repository group creation")
677 log.exception("Exception during repository group creation")
679 msg = _(
678 msg = _(
680 'An error occurred during repository group creation for user')
679 'An error occurred during repository group creation for user')
681 h.flash(msg, category='error')
680 h.flash(msg, category='error')
682 Session().rollback()
681 Session().rollback()
683
682
684 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
683 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
685
684
686 @LoginRequired()
685 @LoginRequired()
687 @HasPermissionAllDecorator('hg.admin')
686 @HasPermissionAllDecorator('hg.admin')
688 @view_config(
687 @view_config(
689 route_name='edit_user_auth_tokens', request_method='GET',
688 route_name='edit_user_auth_tokens', request_method='GET',
690 renderer='rhodecode:templates/admin/users/user_edit.mako')
689 renderer='rhodecode:templates/admin/users/user_edit.mako')
691 def auth_tokens(self):
690 def auth_tokens(self):
692 _ = self.request.translate
691 _ = self.request.translate
693 c = self.load_default_context()
692 c = self.load_default_context()
694 c.user = self.db_user
693 c.user = self.db_user
695
694
696 c.active = 'auth_tokens'
695 c.active = 'auth_tokens'
697
696
698 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
697 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
699 c.role_values = [
698 c.role_values = [
700 (x, AuthTokenModel.cls._get_role_name(x))
699 (x, AuthTokenModel.cls._get_role_name(x))
701 for x in AuthTokenModel.cls.ROLES]
700 for x in AuthTokenModel.cls.ROLES]
702 c.role_options = [(c.role_values, _("Role"))]
701 c.role_options = [(c.role_values, _("Role"))]
703 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
702 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
704 c.user.user_id, show_expired=True)
703 c.user.user_id, show_expired=True)
705 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
704 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
706 return self._get_template_context(c)
705 return self._get_template_context(c)
707
706
708 def maybe_attach_token_scope(self, token):
707 def maybe_attach_token_scope(self, token):
709 # implemented in EE edition
708 # implemented in EE edition
710 pass
709 pass
711
710
712 @LoginRequired()
711 @LoginRequired()
713 @HasPermissionAllDecorator('hg.admin')
712 @HasPermissionAllDecorator('hg.admin')
714 @CSRFRequired()
713 @CSRFRequired()
715 @view_config(
714 @view_config(
716 route_name='edit_user_auth_tokens_add', request_method='POST')
715 route_name='edit_user_auth_tokens_add', request_method='POST')
717 def auth_tokens_add(self):
716 def auth_tokens_add(self):
718 _ = self.request.translate
717 _ = self.request.translate
719 c = self.load_default_context()
718 c = self.load_default_context()
720
719
721 user_id = self.db_user_id
720 user_id = self.db_user_id
722 c.user = self.db_user
721 c.user = self.db_user
723
722
724 user_data = c.user.get_api_data()
723 user_data = c.user.get_api_data()
725 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
724 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
726 description = self.request.POST.get('description')
725 description = self.request.POST.get('description')
727 role = self.request.POST.get('role')
726 role = self.request.POST.get('role')
728
727
729 token = AuthTokenModel().create(
728 token = AuthTokenModel().create(
730 c.user.user_id, description, lifetime, role)
729 c.user.user_id, description, lifetime, role)
731 token_data = token.get_api_data()
730 token_data = token.get_api_data()
732
731
733 self.maybe_attach_token_scope(token)
732 self.maybe_attach_token_scope(token)
734 audit_logger.store_web(
733 audit_logger.store_web(
735 'user.edit.token.add', action_data={
734 'user.edit.token.add', action_data={
736 'data': {'token': token_data, 'user': user_data}},
735 'data': {'token': token_data, 'user': user_data}},
737 user=self._rhodecode_user, )
736 user=self._rhodecode_user, )
738 Session().commit()
737 Session().commit()
739
738
740 h.flash(_("Auth token successfully created"), category='success')
739 h.flash(_("Auth token successfully created"), category='success')
741 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
740 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
742
741
743 @LoginRequired()
742 @LoginRequired()
744 @HasPermissionAllDecorator('hg.admin')
743 @HasPermissionAllDecorator('hg.admin')
745 @CSRFRequired()
744 @CSRFRequired()
746 @view_config(
745 @view_config(
747 route_name='edit_user_auth_tokens_delete', request_method='POST')
746 route_name='edit_user_auth_tokens_delete', request_method='POST')
748 def auth_tokens_delete(self):
747 def auth_tokens_delete(self):
749 _ = self.request.translate
748 _ = self.request.translate
750 c = self.load_default_context()
749 c = self.load_default_context()
751
750
752 user_id = self.db_user_id
751 user_id = self.db_user_id
753 c.user = self.db_user
752 c.user = self.db_user
754
753
755 user_data = c.user.get_api_data()
754 user_data = c.user.get_api_data()
756
755
757 del_auth_token = self.request.POST.get('del_auth_token')
756 del_auth_token = self.request.POST.get('del_auth_token')
758
757
759 if del_auth_token:
758 if del_auth_token:
760 token = UserApiKeys.get_or_404(del_auth_token)
759 token = UserApiKeys.get_or_404(del_auth_token)
761 token_data = token.get_api_data()
760 token_data = token.get_api_data()
762
761
763 AuthTokenModel().delete(del_auth_token, c.user.user_id)
762 AuthTokenModel().delete(del_auth_token, c.user.user_id)
764 audit_logger.store_web(
763 audit_logger.store_web(
765 'user.edit.token.delete', action_data={
764 'user.edit.token.delete', action_data={
766 'data': {'token': token_data, 'user': user_data}},
765 'data': {'token': token_data, 'user': user_data}},
767 user=self._rhodecode_user,)
766 user=self._rhodecode_user,)
768 Session().commit()
767 Session().commit()
769 h.flash(_("Auth token successfully deleted"), category='success')
768 h.flash(_("Auth token successfully deleted"), category='success')
770
769
771 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
770 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
772
771
773 @LoginRequired()
772 @LoginRequired()
774 @HasPermissionAllDecorator('hg.admin')
773 @HasPermissionAllDecorator('hg.admin')
775 @view_config(
774 @view_config(
776 route_name='edit_user_ssh_keys', request_method='GET',
775 route_name='edit_user_ssh_keys', request_method='GET',
777 renderer='rhodecode:templates/admin/users/user_edit.mako')
776 renderer='rhodecode:templates/admin/users/user_edit.mako')
778 def ssh_keys(self):
777 def ssh_keys(self):
779 _ = self.request.translate
778 _ = self.request.translate
780 c = self.load_default_context()
779 c = self.load_default_context()
781 c.user = self.db_user
780 c.user = self.db_user
782
781
783 c.active = 'ssh_keys'
782 c.active = 'ssh_keys'
784 c.default_key = self.request.GET.get('default_key')
783 c.default_key = self.request.GET.get('default_key')
785 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
784 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
786 return self._get_template_context(c)
785 return self._get_template_context(c)
787
786
788 @LoginRequired()
787 @LoginRequired()
789 @HasPermissionAllDecorator('hg.admin')
788 @HasPermissionAllDecorator('hg.admin')
790 @view_config(
789 @view_config(
791 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
790 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
792 renderer='rhodecode:templates/admin/users/user_edit.mako')
791 renderer='rhodecode:templates/admin/users/user_edit.mako')
793 def ssh_keys_generate_keypair(self):
792 def ssh_keys_generate_keypair(self):
794 _ = self.request.translate
793 _ = self.request.translate
795 c = self.load_default_context()
794 c = self.load_default_context()
796
795
797 c.user = self.db_user
796 c.user = self.db_user
798
797
799 c.active = 'ssh_keys_generate'
798 c.active = 'ssh_keys_generate'
800 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
799 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
801 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
800 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
802
801
803 return self._get_template_context(c)
802 return self._get_template_context(c)
804
803
805 @LoginRequired()
804 @LoginRequired()
806 @HasPermissionAllDecorator('hg.admin')
805 @HasPermissionAllDecorator('hg.admin')
807 @CSRFRequired()
806 @CSRFRequired()
808 @view_config(
807 @view_config(
809 route_name='edit_user_ssh_keys_add', request_method='POST')
808 route_name='edit_user_ssh_keys_add', request_method='POST')
810 def ssh_keys_add(self):
809 def ssh_keys_add(self):
811 _ = self.request.translate
810 _ = self.request.translate
812 c = self.load_default_context()
811 c = self.load_default_context()
813
812
814 user_id = self.db_user_id
813 user_id = self.db_user_id
815 c.user = self.db_user
814 c.user = self.db_user
816
815
817 user_data = c.user.get_api_data()
816 user_data = c.user.get_api_data()
818 key_data = self.request.POST.get('key_data')
817 key_data = self.request.POST.get('key_data')
819 description = self.request.POST.get('description')
818 description = self.request.POST.get('description')
820
819
821 try:
820 try:
822 if not key_data:
821 if not key_data:
823 raise ValueError('Please add a valid public key')
822 raise ValueError('Please add a valid public key')
824
823
825 key = SshKeyModel().parse_key(key_data.strip())
824 key = SshKeyModel().parse_key(key_data.strip())
826 fingerprint = key.hash_md5()
825 fingerprint = key.hash_md5()
827
826
828 ssh_key = SshKeyModel().create(
827 ssh_key = SshKeyModel().create(
829 c.user.user_id, fingerprint, key_data, description)
828 c.user.user_id, fingerprint, key_data, description)
830 ssh_key_data = ssh_key.get_api_data()
829 ssh_key_data = ssh_key.get_api_data()
831
830
832 audit_logger.store_web(
831 audit_logger.store_web(
833 'user.edit.ssh_key.add', action_data={
832 'user.edit.ssh_key.add', action_data={
834 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
833 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
835 user=self._rhodecode_user, )
834 user=self._rhodecode_user, )
836 Session().commit()
835 Session().commit()
837
836
838 # Trigger an event on change of keys.
837 # Trigger an event on change of keys.
839 trigger(SshKeyFileChangeEvent(), self.request.registry)
838 trigger(SshKeyFileChangeEvent(), self.request.registry)
840
839
841 h.flash(_("Ssh Key successfully created"), category='success')
840 h.flash(_("Ssh Key successfully created"), category='success')
842
841
843 except IntegrityError:
842 except IntegrityError:
844 log.exception("Exception during ssh key saving")
843 log.exception("Exception during ssh key saving")
845 h.flash(_('An error occurred during ssh key saving: {}').format(
844 h.flash(_('An error occurred during ssh key saving: {}').format(
846 'Such key already exists, please use a different one'),
845 'Such key already exists, please use a different one'),
847 category='error')
846 category='error')
848 except Exception as e:
847 except Exception as e:
849 log.exception("Exception during ssh key saving")
848 log.exception("Exception during ssh key saving")
850 h.flash(_('An error occurred during ssh key saving: {}').format(e),
849 h.flash(_('An error occurred during ssh key saving: {}').format(e),
851 category='error')
850 category='error')
852
851
853 return HTTPFound(
852 return HTTPFound(
854 h.route_path('edit_user_ssh_keys', user_id=user_id))
853 h.route_path('edit_user_ssh_keys', user_id=user_id))
855
854
856 @LoginRequired()
855 @LoginRequired()
857 @HasPermissionAllDecorator('hg.admin')
856 @HasPermissionAllDecorator('hg.admin')
858 @CSRFRequired()
857 @CSRFRequired()
859 @view_config(
858 @view_config(
860 route_name='edit_user_ssh_keys_delete', request_method='POST')
859 route_name='edit_user_ssh_keys_delete', request_method='POST')
861 def ssh_keys_delete(self):
860 def ssh_keys_delete(self):
862 _ = self.request.translate
861 _ = self.request.translate
863 c = self.load_default_context()
862 c = self.load_default_context()
864
863
865 user_id = self.db_user_id
864 user_id = self.db_user_id
866 c.user = self.db_user
865 c.user = self.db_user
867
866
868 user_data = c.user.get_api_data()
867 user_data = c.user.get_api_data()
869
868
870 del_ssh_key = self.request.POST.get('del_ssh_key')
869 del_ssh_key = self.request.POST.get('del_ssh_key')
871
870
872 if del_ssh_key:
871 if del_ssh_key:
873 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
872 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
874 ssh_key_data = ssh_key.get_api_data()
873 ssh_key_data = ssh_key.get_api_data()
875
874
876 SshKeyModel().delete(del_ssh_key, c.user.user_id)
875 SshKeyModel().delete(del_ssh_key, c.user.user_id)
877 audit_logger.store_web(
876 audit_logger.store_web(
878 'user.edit.ssh_key.delete', action_data={
877 'user.edit.ssh_key.delete', action_data={
879 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
878 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
880 user=self._rhodecode_user,)
879 user=self._rhodecode_user,)
881 Session().commit()
880 Session().commit()
882 # Trigger an event on change of keys.
881 # Trigger an event on change of keys.
883 trigger(SshKeyFileChangeEvent(), self.request.registry)
882 trigger(SshKeyFileChangeEvent(), self.request.registry)
884 h.flash(_("Ssh key successfully deleted"), category='success')
883 h.flash(_("Ssh key successfully deleted"), category='success')
885
884
886 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
885 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
887
886
888 @LoginRequired()
887 @LoginRequired()
889 @HasPermissionAllDecorator('hg.admin')
888 @HasPermissionAllDecorator('hg.admin')
890 @view_config(
889 @view_config(
891 route_name='edit_user_emails', request_method='GET',
890 route_name='edit_user_emails', request_method='GET',
892 renderer='rhodecode:templates/admin/users/user_edit.mako')
891 renderer='rhodecode:templates/admin/users/user_edit.mako')
893 def emails(self):
892 def emails(self):
894 _ = self.request.translate
893 _ = self.request.translate
895 c = self.load_default_context()
894 c = self.load_default_context()
896 c.user = self.db_user
895 c.user = self.db_user
897
896
898 c.active = 'emails'
897 c.active = 'emails'
899 c.user_email_map = UserEmailMap.query() \
898 c.user_email_map = UserEmailMap.query() \
900 .filter(UserEmailMap.user == c.user).all()
899 .filter(UserEmailMap.user == c.user).all()
901
900
902 return self._get_template_context(c)
901 return self._get_template_context(c)
903
902
904 @LoginRequired()
903 @LoginRequired()
905 @HasPermissionAllDecorator('hg.admin')
904 @HasPermissionAllDecorator('hg.admin')
906 @CSRFRequired()
905 @CSRFRequired()
907 @view_config(
906 @view_config(
908 route_name='edit_user_emails_add', request_method='POST')
907 route_name='edit_user_emails_add', request_method='POST')
909 def emails_add(self):
908 def emails_add(self):
910 _ = self.request.translate
909 _ = self.request.translate
911 c = self.load_default_context()
910 c = self.load_default_context()
912
911
913 user_id = self.db_user_id
912 user_id = self.db_user_id
914 c.user = self.db_user
913 c.user = self.db_user
915
914
916 email = self.request.POST.get('new_email')
915 email = self.request.POST.get('new_email')
917 user_data = c.user.get_api_data()
916 user_data = c.user.get_api_data()
918 try:
917 try:
919
918
920 form = UserExtraEmailForm(self.request.translate)()
919 form = UserExtraEmailForm(self.request.translate)()
921 data = form.to_python({'email': email})
920 data = form.to_python({'email': email})
922 email = data['email']
921 email = data['email']
923
922
924 UserModel().add_extra_email(c.user.user_id, email)
923 UserModel().add_extra_email(c.user.user_id, email)
925 audit_logger.store_web(
924 audit_logger.store_web(
926 'user.edit.email.add',
925 'user.edit.email.add',
927 action_data={'email': email, 'user': user_data},
926 action_data={'email': email, 'user': user_data},
928 user=self._rhodecode_user)
927 user=self._rhodecode_user)
929 Session().commit()
928 Session().commit()
930 h.flash(_("Added new email address `%s` for user account") % email,
929 h.flash(_("Added new email address `%s` for user account") % email,
931 category='success')
930 category='success')
932 except formencode.Invalid as error:
931 except formencode.Invalid as error:
933 h.flash(h.escape(error.error_dict['email']), category='error')
932 h.flash(h.escape(error.error_dict['email']), category='error')
934 except IntegrityError:
933 except IntegrityError:
935 log.warning("Email %s already exists", email)
934 log.warning("Email %s already exists", email)
936 h.flash(_('Email `{}` is already registered for another user.').format(email),
935 h.flash(_('Email `{}` is already registered for another user.').format(email),
937 category='error')
936 category='error')
938 except Exception:
937 except Exception:
939 log.exception("Exception during email saving")
938 log.exception("Exception during email saving")
940 h.flash(_('An error occurred during email saving'),
939 h.flash(_('An error occurred during email saving'),
941 category='error')
940 category='error')
942 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
941 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
943
942
944 @LoginRequired()
943 @LoginRequired()
945 @HasPermissionAllDecorator('hg.admin')
944 @HasPermissionAllDecorator('hg.admin')
946 @CSRFRequired()
945 @CSRFRequired()
947 @view_config(
946 @view_config(
948 route_name='edit_user_emails_delete', request_method='POST')
947 route_name='edit_user_emails_delete', request_method='POST')
949 def emails_delete(self):
948 def emails_delete(self):
950 _ = self.request.translate
949 _ = self.request.translate
951 c = self.load_default_context()
950 c = self.load_default_context()
952
951
953 user_id = self.db_user_id
952 user_id = self.db_user_id
954 c.user = self.db_user
953 c.user = self.db_user
955
954
956 email_id = self.request.POST.get('del_email_id')
955 email_id = self.request.POST.get('del_email_id')
957 user_model = UserModel()
956 user_model = UserModel()
958
957
959 email = UserEmailMap.query().get(email_id).email
958 email = UserEmailMap.query().get(email_id).email
960 user_data = c.user.get_api_data()
959 user_data = c.user.get_api_data()
961 user_model.delete_extra_email(c.user.user_id, email_id)
960 user_model.delete_extra_email(c.user.user_id, email_id)
962 audit_logger.store_web(
961 audit_logger.store_web(
963 'user.edit.email.delete',
962 'user.edit.email.delete',
964 action_data={'email': email, 'user': user_data},
963 action_data={'email': email, 'user': user_data},
965 user=self._rhodecode_user)
964 user=self._rhodecode_user)
966 Session().commit()
965 Session().commit()
967 h.flash(_("Removed email address from user account"),
966 h.flash(_("Removed email address from user account"),
968 category='success')
967 category='success')
969 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
968 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
970
969
971 @LoginRequired()
970 @LoginRequired()
972 @HasPermissionAllDecorator('hg.admin')
971 @HasPermissionAllDecorator('hg.admin')
973 @view_config(
972 @view_config(
974 route_name='edit_user_ips', request_method='GET',
973 route_name='edit_user_ips', request_method='GET',
975 renderer='rhodecode:templates/admin/users/user_edit.mako')
974 renderer='rhodecode:templates/admin/users/user_edit.mako')
976 def ips(self):
975 def ips(self):
977 _ = self.request.translate
976 _ = self.request.translate
978 c = self.load_default_context()
977 c = self.load_default_context()
979 c.user = self.db_user
978 c.user = self.db_user
980
979
981 c.active = 'ips'
980 c.active = 'ips'
982 c.user_ip_map = UserIpMap.query() \
981 c.user_ip_map = UserIpMap.query() \
983 .filter(UserIpMap.user == c.user).all()
982 .filter(UserIpMap.user == c.user).all()
984
983
985 c.inherit_default_ips = c.user.inherit_default_permissions
984 c.inherit_default_ips = c.user.inherit_default_permissions
986 c.default_user_ip_map = UserIpMap.query() \
985 c.default_user_ip_map = UserIpMap.query() \
987 .filter(UserIpMap.user == User.get_default_user()).all()
986 .filter(UserIpMap.user == User.get_default_user()).all()
988
987
989 return self._get_template_context(c)
988 return self._get_template_context(c)
990
989
991 @LoginRequired()
990 @LoginRequired()
992 @HasPermissionAllDecorator('hg.admin')
991 @HasPermissionAllDecorator('hg.admin')
993 @CSRFRequired()
992 @CSRFRequired()
994 @view_config(
993 @view_config(
995 route_name='edit_user_ips_add', request_method='POST')
994 route_name='edit_user_ips_add', request_method='POST')
996 # NOTE(marcink): this view is allowed for default users, as we can
995 # NOTE(marcink): this view is allowed for default users, as we can
997 # edit their IP white list
996 # edit their IP white list
998 def ips_add(self):
997 def ips_add(self):
999 _ = self.request.translate
998 _ = self.request.translate
1000 c = self.load_default_context()
999 c = self.load_default_context()
1001
1000
1002 user_id = self.db_user_id
1001 user_id = self.db_user_id
1003 c.user = self.db_user
1002 c.user = self.db_user
1004
1003
1005 user_model = UserModel()
1004 user_model = UserModel()
1006 desc = self.request.POST.get('description')
1005 desc = self.request.POST.get('description')
1007 try:
1006 try:
1008 ip_list = user_model.parse_ip_range(
1007 ip_list = user_model.parse_ip_range(
1009 self.request.POST.get('new_ip'))
1008 self.request.POST.get('new_ip'))
1010 except Exception as e:
1009 except Exception as e:
1011 ip_list = []
1010 ip_list = []
1012 log.exception("Exception during ip saving")
1011 log.exception("Exception during ip saving")
1013 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1012 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1014 category='error')
1013 category='error')
1015 added = []
1014 added = []
1016 user_data = c.user.get_api_data()
1015 user_data = c.user.get_api_data()
1017 for ip in ip_list:
1016 for ip in ip_list:
1018 try:
1017 try:
1019 form = UserExtraIpForm(self.request.translate)()
1018 form = UserExtraIpForm(self.request.translate)()
1020 data = form.to_python({'ip': ip})
1019 data = form.to_python({'ip': ip})
1021 ip = data['ip']
1020 ip = data['ip']
1022
1021
1023 user_model.add_extra_ip(c.user.user_id, ip, desc)
1022 user_model.add_extra_ip(c.user.user_id, ip, desc)
1024 audit_logger.store_web(
1023 audit_logger.store_web(
1025 'user.edit.ip.add',
1024 'user.edit.ip.add',
1026 action_data={'ip': ip, 'user': user_data},
1025 action_data={'ip': ip, 'user': user_data},
1027 user=self._rhodecode_user)
1026 user=self._rhodecode_user)
1028 Session().commit()
1027 Session().commit()
1029 added.append(ip)
1028 added.append(ip)
1030 except formencode.Invalid as error:
1029 except formencode.Invalid as error:
1031 msg = error.error_dict['ip']
1030 msg = error.error_dict['ip']
1032 h.flash(msg, category='error')
1031 h.flash(msg, category='error')
1033 except Exception:
1032 except Exception:
1034 log.exception("Exception during ip saving")
1033 log.exception("Exception during ip saving")
1035 h.flash(_('An error occurred during ip saving'),
1034 h.flash(_('An error occurred during ip saving'),
1036 category='error')
1035 category='error')
1037 if added:
1036 if added:
1038 h.flash(
1037 h.flash(
1039 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1038 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1040 category='success')
1039 category='success')
1041 if 'default_user' in self.request.POST:
1040 if 'default_user' in self.request.POST:
1042 # case for editing global IP list we do it for 'DEFAULT' user
1041 # case for editing global IP list we do it for 'DEFAULT' user
1043 raise HTTPFound(h.route_path('admin_permissions_ips'))
1042 raise HTTPFound(h.route_path('admin_permissions_ips'))
1044 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1043 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1045
1044
1046 @LoginRequired()
1045 @LoginRequired()
1047 @HasPermissionAllDecorator('hg.admin')
1046 @HasPermissionAllDecorator('hg.admin')
1048 @CSRFRequired()
1047 @CSRFRequired()
1049 @view_config(
1048 @view_config(
1050 route_name='edit_user_ips_delete', request_method='POST')
1049 route_name='edit_user_ips_delete', request_method='POST')
1051 # NOTE(marcink): this view is allowed for default users, as we can
1050 # NOTE(marcink): this view is allowed for default users, as we can
1052 # edit their IP white list
1051 # edit their IP white list
1053 def ips_delete(self):
1052 def ips_delete(self):
1054 _ = self.request.translate
1053 _ = self.request.translate
1055 c = self.load_default_context()
1054 c = self.load_default_context()
1056
1055
1057 user_id = self.db_user_id
1056 user_id = self.db_user_id
1058 c.user = self.db_user
1057 c.user = self.db_user
1059
1058
1060 ip_id = self.request.POST.get('del_ip_id')
1059 ip_id = self.request.POST.get('del_ip_id')
1061 user_model = UserModel()
1060 user_model = UserModel()
1062 user_data = c.user.get_api_data()
1061 user_data = c.user.get_api_data()
1063 ip = UserIpMap.query().get(ip_id).ip_addr
1062 ip = UserIpMap.query().get(ip_id).ip_addr
1064 user_model.delete_extra_ip(c.user.user_id, ip_id)
1063 user_model.delete_extra_ip(c.user.user_id, ip_id)
1065 audit_logger.store_web(
1064 audit_logger.store_web(
1066 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1065 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1067 user=self._rhodecode_user)
1066 user=self._rhodecode_user)
1068 Session().commit()
1067 Session().commit()
1069 h.flash(_("Removed ip address from user whitelist"), category='success')
1068 h.flash(_("Removed ip address from user whitelist"), category='success')
1070
1069
1071 if 'default_user' in self.request.POST:
1070 if 'default_user' in self.request.POST:
1072 # case for editing global IP list we do it for 'DEFAULT' user
1071 # case for editing global IP list we do it for 'DEFAULT' user
1073 raise HTTPFound(h.route_path('admin_permissions_ips'))
1072 raise HTTPFound(h.route_path('admin_permissions_ips'))
1074 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1073 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1075
1074
1076 @LoginRequired()
1075 @LoginRequired()
1077 @HasPermissionAllDecorator('hg.admin')
1076 @HasPermissionAllDecorator('hg.admin')
1078 @view_config(
1077 @view_config(
1079 route_name='edit_user_groups_management', request_method='GET',
1078 route_name='edit_user_groups_management', request_method='GET',
1080 renderer='rhodecode:templates/admin/users/user_edit.mako')
1079 renderer='rhodecode:templates/admin/users/user_edit.mako')
1081 def groups_management(self):
1080 def groups_management(self):
1082 c = self.load_default_context()
1081 c = self.load_default_context()
1083 c.user = self.db_user
1082 c.user = self.db_user
1084 c.data = c.user.group_member
1083 c.data = c.user.group_member
1085
1084
1086 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1085 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1087 for group in c.user.group_member]
1086 for group in c.user.group_member]
1088 c.groups = json.dumps(groups)
1087 c.groups = json.dumps(groups)
1089 c.active = 'groups'
1088 c.active = 'groups'
1090
1089
1091 return self._get_template_context(c)
1090 return self._get_template_context(c)
1092
1091
1093 @LoginRequired()
1092 @LoginRequired()
1094 @HasPermissionAllDecorator('hg.admin')
1093 @HasPermissionAllDecorator('hg.admin')
1095 @CSRFRequired()
1094 @CSRFRequired()
1096 @view_config(
1095 @view_config(
1097 route_name='edit_user_groups_management_updates', request_method='POST')
1096 route_name='edit_user_groups_management_updates', request_method='POST')
1098 def groups_management_updates(self):
1097 def groups_management_updates(self):
1099 _ = self.request.translate
1098 _ = self.request.translate
1100 c = self.load_default_context()
1099 c = self.load_default_context()
1101
1100
1102 user_id = self.db_user_id
1101 user_id = self.db_user_id
1103 c.user = self.db_user
1102 c.user = self.db_user
1104
1103
1105 user_groups = set(self.request.POST.getall('users_group_id'))
1104 user_groups = set(self.request.POST.getall('users_group_id'))
1106 user_groups_objects = []
1105 user_groups_objects = []
1107
1106
1108 for ugid in user_groups:
1107 for ugid in user_groups:
1109 user_groups_objects.append(
1108 user_groups_objects.append(
1110 UserGroupModel().get_group(safe_int(ugid)))
1109 UserGroupModel().get_group(safe_int(ugid)))
1111 user_group_model = UserGroupModel()
1110 user_group_model = UserGroupModel()
1112 added_to_groups, removed_from_groups = \
1111 added_to_groups, removed_from_groups = \
1113 user_group_model.change_groups(c.user, user_groups_objects)
1112 user_group_model.change_groups(c.user, user_groups_objects)
1114
1113
1115 user_data = c.user.get_api_data()
1114 user_data = c.user.get_api_data()
1116 for user_group_id in added_to_groups:
1115 for user_group_id in added_to_groups:
1117 user_group = UserGroup.get(user_group_id)
1116 user_group = UserGroup.get(user_group_id)
1118 old_values = user_group.get_api_data()
1117 old_values = user_group.get_api_data()
1119 audit_logger.store_web(
1118 audit_logger.store_web(
1120 'user_group.edit.member.add',
1119 'user_group.edit.member.add',
1121 action_data={'user': user_data, 'old_data': old_values},
1120 action_data={'user': user_data, 'old_data': old_values},
1122 user=self._rhodecode_user)
1121 user=self._rhodecode_user)
1123
1122
1124 for user_group_id in removed_from_groups:
1123 for user_group_id in removed_from_groups:
1125 user_group = UserGroup.get(user_group_id)
1124 user_group = UserGroup.get(user_group_id)
1126 old_values = user_group.get_api_data()
1125 old_values = user_group.get_api_data()
1127 audit_logger.store_web(
1126 audit_logger.store_web(
1128 'user_group.edit.member.delete',
1127 'user_group.edit.member.delete',
1129 action_data={'user': user_data, 'old_data': old_values},
1128 action_data={'user': user_data, 'old_data': old_values},
1130 user=self._rhodecode_user)
1129 user=self._rhodecode_user)
1131
1130
1132 Session().commit()
1131 Session().commit()
1133 c.active = 'user_groups_management'
1132 c.active = 'user_groups_management'
1134 h.flash(_("Groups successfully changed"), category='success')
1133 h.flash(_("Groups successfully changed"), category='success')
1135
1134
1136 return HTTPFound(h.route_path(
1135 return HTTPFound(h.route_path(
1137 'edit_user_groups_management', user_id=user_id))
1136 'edit_user_groups_management', user_id=user_id))
1138
1137
1139 @LoginRequired()
1138 @LoginRequired()
1140 @HasPermissionAllDecorator('hg.admin')
1139 @HasPermissionAllDecorator('hg.admin')
1141 @view_config(
1140 @view_config(
1142 route_name='edit_user_audit_logs', request_method='GET',
1141 route_name='edit_user_audit_logs', request_method='GET',
1143 renderer='rhodecode:templates/admin/users/user_edit.mako')
1142 renderer='rhodecode:templates/admin/users/user_edit.mako')
1144 def user_audit_logs(self):
1143 def user_audit_logs(self):
1145 _ = self.request.translate
1144 _ = self.request.translate
1146 c = self.load_default_context()
1145 c = self.load_default_context()
1147 c.user = self.db_user
1146 c.user = self.db_user
1148
1147
1149 c.active = 'audit'
1148 c.active = 'audit'
1150
1149
1151 p = safe_int(self.request.GET.get('page', 1), 1)
1150 p = safe_int(self.request.GET.get('page', 1), 1)
1152
1151
1153 filter_term = self.request.GET.get('filter')
1152 filter_term = self.request.GET.get('filter')
1154 user_log = UserModel().get_user_log(c.user, filter_term)
1153 user_log = UserModel().get_user_log(c.user, filter_term)
1155
1154
1156 def url_generator(**kw):
1155 def url_generator(**kw):
1157 if filter_term:
1156 if filter_term:
1158 kw['filter'] = filter_term
1157 kw['filter'] = filter_term
1159 return self.request.current_route_path(_query=kw)
1158 return self.request.current_route_path(_query=kw)
1160
1159
1161 c.audit_logs = h.Page(
1160 c.audit_logs = h.Page(
1162 user_log, page=p, items_per_page=10, url=url_generator)
1161 user_log, page=p, items_per_page=10, url=url_generator)
1163 c.filter_term = filter_term
1162 c.filter_term = filter_term
1164 return self._get_template_context(c)
1163 return self._get_template_context(c)
1165
1164
1166 @LoginRequired()
1165 @LoginRequired()
1167 @HasPermissionAllDecorator('hg.admin')
1166 @HasPermissionAllDecorator('hg.admin')
1168 @view_config(
1167 @view_config(
1169 route_name='edit_user_perms_summary', request_method='GET',
1168 route_name='edit_user_perms_summary', request_method='GET',
1170 renderer='rhodecode:templates/admin/users/user_edit.mako')
1169 renderer='rhodecode:templates/admin/users/user_edit.mako')
1171 def user_perms_summary(self):
1170 def user_perms_summary(self):
1172 _ = self.request.translate
1171 _ = self.request.translate
1173 c = self.load_default_context()
1172 c = self.load_default_context()
1174 c.user = self.db_user
1173 c.user = self.db_user
1175
1174
1176 c.active = 'perms_summary'
1175 c.active = 'perms_summary'
1177 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1176 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1178
1177
1179 return self._get_template_context(c)
1178 return self._get_template_context(c)
1180
1179
1181 @LoginRequired()
1180 @LoginRequired()
1182 @HasPermissionAllDecorator('hg.admin')
1181 @HasPermissionAllDecorator('hg.admin')
1183 @view_config(
1182 @view_config(
1184 route_name='edit_user_perms_summary_json', request_method='GET',
1183 route_name='edit_user_perms_summary_json', request_method='GET',
1185 renderer='json_ext')
1184 renderer='json_ext')
1186 def user_perms_summary_json(self):
1185 def user_perms_summary_json(self):
1187 self.load_default_context()
1186 self.load_default_context()
1188 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1187 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1189
1188
1190 return perm_user.permissions
1189 return perm_user.permissions
@@ -1,553 +1,559 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import urlparse
21 import urlparse
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 no_newline_id_generator)
28 no_newline_id_generator)
29 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.fixture import Fixture
30 from rhodecode.lib.auth import check_password
30 from rhodecode.lib.auth import check_password
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.model.auth_token import AuthTokenModel
32 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model.db import User, Notification, UserApiKeys
33 from rhodecode.model.db import User, Notification, UserApiKeys
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35
35
36 fixture = Fixture()
36 fixture = Fixture()
37
37
38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39
39
40
40
41 def route_path(name, params=None, **kwargs):
41 def route_path(name, params=None, **kwargs):
42 import urllib
42 import urllib
43 from rhodecode.apps._base import ADMIN_PREFIX
43 from rhodecode.apps._base import ADMIN_PREFIX
44
44
45 base_url = {
45 base_url = {
46 'login': ADMIN_PREFIX + '/login',
46 'login': ADMIN_PREFIX + '/login',
47 'logout': ADMIN_PREFIX + '/logout',
47 'logout': ADMIN_PREFIX + '/logout',
48 'register': ADMIN_PREFIX + '/register',
48 'register': ADMIN_PREFIX + '/register',
49 'reset_password':
49 'reset_password':
50 ADMIN_PREFIX + '/password_reset',
50 ADMIN_PREFIX + '/password_reset',
51 'reset_password_confirmation':
51 'reset_password_confirmation':
52 ADMIN_PREFIX + '/password_reset_confirmation',
52 ADMIN_PREFIX + '/password_reset_confirmation',
53
53
54 'admin_permissions_application':
54 'admin_permissions_application':
55 ADMIN_PREFIX + '/permissions/application',
55 ADMIN_PREFIX + '/permissions/application',
56 'admin_permissions_application_update':
56 'admin_permissions_application_update':
57 ADMIN_PREFIX + '/permissions/application/update',
57 ADMIN_PREFIX + '/permissions/application/update',
58
58
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60
60
61 }[name].format(**kwargs)
61 }[name].format(**kwargs)
62
62
63 if params:
63 if params:
64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
65 return base_url
65 return base_url
66
66
67
67
68 @pytest.mark.usefixtures('app')
68 @pytest.mark.usefixtures('app')
69 class TestLoginController(object):
69 class TestLoginController(object):
70 destroy_users = set()
70 destroy_users = set()
71
71
72 @classmethod
72 @classmethod
73 def teardown_class(cls):
73 def teardown_class(cls):
74 fixture.destroy_users(cls.destroy_users)
74 fixture.destroy_users(cls.destroy_users)
75
75
76 def teardown_method(self, method):
76 def teardown_method(self, method):
77 for n in Notification.query().all():
77 for n in Notification.query().all():
78 Session().delete(n)
78 Session().delete(n)
79
79
80 Session().commit()
80 Session().commit()
81 assert Notification.query().all() == []
81 assert Notification.query().all() == []
82
82
83 def test_index(self):
83 def test_index(self):
84 response = self.app.get(route_path('login'))
84 response = self.app.get(route_path('login'))
85 assert response.status == '200 OK'
85 assert response.status == '200 OK'
86 # Test response...
86 # Test response...
87
87
88 def test_login_admin_ok(self):
88 def test_login_admin_ok(self):
89 response = self.app.post(route_path('login'),
89 response = self.app.post(route_path('login'),
90 {'username': 'test_admin',
90 {'username': 'test_admin',
91 'password': 'test12'})
91 'password': 'test12'}, status=302)
92 assert response.status == '302 Found'
92 response = response.follow()
93 session = response.get_session_from_response()
93 session = response.get_session_from_response()
94 username = session['rhodecode_user'].get('username')
94 username = session['rhodecode_user'].get('username')
95 assert username == 'test_admin'
95 assert username == 'test_admin'
96 response = response.follow()
97 response.mustcontain('/%s' % HG_REPO)
96 response.mustcontain('/%s' % HG_REPO)
98
97
99 def test_login_regular_ok(self):
98 def test_login_regular_ok(self):
100 response = self.app.post(route_path('login'),
99 response = self.app.post(route_path('login'),
101 {'username': 'test_regular',
100 {'username': 'test_regular',
102 'password': 'test12'})
101 'password': 'test12'}, status=302)
103
102
104 assert response.status == '302 Found'
103 response = response.follow()
105 session = response.get_session_from_response()
104 session = response.get_session_from_response()
106 username = session['rhodecode_user'].get('username')
105 username = session['rhodecode_user'].get('username')
107 assert username == 'test_regular'
106 assert username == 'test_regular'
108 response = response.follow()
107
109 response.mustcontain('/%s' % HG_REPO)
108 response.mustcontain('/%s' % HG_REPO)
110
109
111 def test_login_ok_came_from(self):
110 def test_login_ok_came_from(self):
112 test_came_from = '/_admin/users?branch=stable'
111 test_came_from = '/_admin/users?branch=stable'
113 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
112 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
114 response = self.app.post(
113 response = self.app.post(
115 _url, {'username': 'test_admin', 'password': 'test12'})
114 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
116 assert response.status == '302 Found'
115
117 assert 'branch=stable' in response.location
116 assert 'branch=stable' in response.location
118 response = response.follow()
117 response = response.follow()
119
118
120 assert response.status == '200 OK'
119 assert response.status == '200 OK'
121 response.mustcontain('Users administration')
120 response.mustcontain('Users administration')
122
121
123 def test_redirect_to_login_with_get_args(self):
122 def test_redirect_to_login_with_get_args(self):
124 with fixture.anon_access(False):
123 with fixture.anon_access(False):
125 kwargs = {'branch': 'stable'}
124 kwargs = {'branch': 'stable'}
126 response = self.app.get(
125 response = self.app.get(
127 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs))
126 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
128 assert response.status == '302 Found'
127 status=302)
129
128
130 response_query = urlparse.parse_qsl(response.location)
129 response_query = urlparse.parse_qsl(response.location)
131 assert 'branch=stable' in response_query[0][1]
130 assert 'branch=stable' in response_query[0][1]
132
131
133 def test_login_form_with_get_args(self):
132 def test_login_form_with_get_args(self):
134 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
133 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
135 response = self.app.get(_url)
134 response = self.app.get(_url)
136 assert 'branch%3Dstable' in response.form.action
135 assert 'branch%3Dstable' in response.form.action
137
136
138 @pytest.mark.parametrize("url_came_from", [
137 @pytest.mark.parametrize("url_came_from", [
139 'data:text/html,<script>window.alert("xss")</script>',
138 'data:text/html,<script>window.alert("xss")</script>',
140 'mailto:test@rhodecode.org',
139 'mailto:test@rhodecode.org',
141 'file:///etc/passwd',
140 'file:///etc/passwd',
142 'ftp://some.ftp.server',
141 'ftp://some.ftp.server',
143 'http://other.domain',
142 'http://other.domain',
144 '/\r\nX-Forwarded-Host: http://example.org',
143 '/\r\nX-Forwarded-Host: http://example.org',
145 ], ids=no_newline_id_generator)
144 ], ids=no_newline_id_generator)
146 def test_login_bad_came_froms(self, url_came_from):
145 def test_login_bad_came_froms(self, url_came_from):
147 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
146 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
148 response = self.app.post(
147 response = self.app.post(
149 _url,
148 _url,
150 {'username': 'test_admin', 'password': 'test12'})
149 {'username': 'test_admin', 'password': 'test12'})
151 assert response.status == '302 Found'
150 assert response.status == '302 Found'
152 response = response.follow()
151 response = response.follow()
153 assert response.status == '200 OK'
152 assert response.status == '200 OK'
154 assert response.request.path == '/'
153 assert response.request.path == '/'
155
154
156 def test_login_short_password(self):
155 def test_login_short_password(self):
157 response = self.app.post(route_path('login'),
156 response = self.app.post(route_path('login'),
158 {'username': 'test_admin',
157 {'username': 'test_admin',
159 'password': 'as'})
158 'password': 'as'})
160 assert response.status == '200 OK'
159 assert response.status == '200 OK'
161
160
162 response.mustcontain('Enter 3 characters or more')
161 response.mustcontain('Enter 3 characters or more')
163
162
164 def test_login_wrong_non_ascii_password(self, user_regular):
163 def test_login_wrong_non_ascii_password(self, user_regular):
165 response = self.app.post(
164 response = self.app.post(
166 route_path('login'),
165 route_path('login'),
167 {'username': user_regular.username,
166 {'username': user_regular.username,
168 'password': u'invalid-non-asci\xe4'.encode('utf8')})
167 'password': u'invalid-non-asci\xe4'.encode('utf8')})
169
168
170 response.mustcontain('invalid user name')
169 response.mustcontain('invalid user name')
171 response.mustcontain('invalid password')
170 response.mustcontain('invalid password')
172
171
173 def test_login_with_non_ascii_password(self, user_util):
172 def test_login_with_non_ascii_password(self, user_util):
174 password = u'valid-non-ascii\xe4'
173 password = u'valid-non-ascii\xe4'
175 user = user_util.create_user(password=password)
174 user = user_util.create_user(password=password)
176 response = self.app.post(
175 response = self.app.post(
177 route_path('login'),
176 route_path('login'),
178 {'username': user.username,
177 {'username': user.username,
179 'password': password.encode('utf-8')})
178 'password': password.encode('utf-8')})
180 assert response.status_code == 302
179 assert response.status_code == 302
181
180
182 def test_login_wrong_username_password(self):
181 def test_login_wrong_username_password(self):
183 response = self.app.post(route_path('login'),
182 response = self.app.post(route_path('login'),
184 {'username': 'error',
183 {'username': 'error',
185 'password': 'test12'})
184 'password': 'test12'})
186
185
187 response.mustcontain('invalid user name')
186 response.mustcontain('invalid user name')
188 response.mustcontain('invalid password')
187 response.mustcontain('invalid password')
189
188
190 def test_login_admin_ok_password_migration(self, real_crypto_backend):
189 def test_login_admin_ok_password_migration(self, real_crypto_backend):
191 from rhodecode.lib import auth
190 from rhodecode.lib import auth
192
191
193 # create new user, with sha256 password
192 # create new user, with sha256 password
194 temp_user = 'test_admin_sha256'
193 temp_user = 'test_admin_sha256'
195 user = fixture.create_user(temp_user)
194 user = fixture.create_user(temp_user)
196 user.password = auth._RhodeCodeCryptoSha256().hash_create(
195 user.password = auth._RhodeCodeCryptoSha256().hash_create(
197 b'test123')
196 b'test123')
198 Session().add(user)
197 Session().add(user)
199 Session().commit()
198 Session().commit()
200 self.destroy_users.add(temp_user)
199 self.destroy_users.add(temp_user)
201 response = self.app.post(route_path('login'),
200 response = self.app.post(route_path('login'),
202 {'username': temp_user,
201 {'username': temp_user,
203 'password': 'test123'})
202 'password': 'test123'}, status=302)
204
203
205 assert response.status == '302 Found'
204 response = response.follow()
206 session = response.get_session_from_response()
205 session = response.get_session_from_response()
207 username = session['rhodecode_user'].get('username')
206 username = session['rhodecode_user'].get('username')
208 assert username == temp_user
207 assert username == temp_user
209 response = response.follow()
210 response.mustcontain('/%s' % HG_REPO)
208 response.mustcontain('/%s' % HG_REPO)
211
209
212 # new password should be bcrypted, after log-in and transfer
210 # new password should be bcrypted, after log-in and transfer
213 user = User.get_by_username(temp_user)
211 user = User.get_by_username(temp_user)
214 assert user.password.startswith('$')
212 assert user.password.startswith('$')
215
213
216 # REGISTRATIONS
214 # REGISTRATIONS
217 def test_register(self):
215 def test_register(self):
218 response = self.app.get(route_path('register'))
216 response = self.app.get(route_path('register'))
219 response.mustcontain('Create an Account')
217 response.mustcontain('Create an Account')
220
218
221 def test_register_err_same_username(self):
219 def test_register_err_same_username(self):
222 uname = 'test_admin'
220 uname = 'test_admin'
223 response = self.app.post(
221 response = self.app.post(
224 route_path('register'),
222 route_path('register'),
225 {
223 {
226 'username': uname,
224 'username': uname,
227 'password': 'test12',
225 'password': 'test12',
228 'password_confirmation': 'test12',
226 'password_confirmation': 'test12',
229 'email': 'goodmail@domain.com',
227 'email': 'goodmail@domain.com',
230 'firstname': 'test',
228 'firstname': 'test',
231 'lastname': 'test'
229 'lastname': 'test'
232 }
230 }
233 )
231 )
234
232
235 assertr = response.assert_response()
233 assertr = response.assert_response()
236 msg = '???'
234 msg = 'Username "%(username)s" already exists'
237 msg = msg % {'username': uname}
235 msg = msg % {'username': uname}
238 assertr.element_contains('#username+.error-message', msg)
236 assertr.element_contains('#username+.error-message', msg)
239
237
240 def test_register_err_same_email(self):
238 def test_register_err_same_email(self):
241 response = self.app.post(
239 response = self.app.post(
242 route_path('register'),
240 route_path('register'),
243 {
241 {
244 'username': 'test_admin_0',
242 'username': 'test_admin_0',
245 'password': 'test12',
243 'password': 'test12',
246 'password_confirmation': 'test12',
244 'password_confirmation': 'test12',
247 'email': 'test_admin@mail.com',
245 'email': 'test_admin@mail.com',
248 'firstname': 'test',
246 'firstname': 'test',
249 'lastname': 'test'
247 'lastname': 'test'
250 }
248 }
251 )
249 )
252
250
253 assertr = response.assert_response()
251 assertr = response.assert_response()
254 msg = '???'
252 msg = u'This e-mail address is already taken'
255 assertr.element_contains('#email+.error-message', msg)
253 assertr.element_contains('#email+.error-message', msg)
256
254
257 def test_register_err_same_email_case_sensitive(self):
255 def test_register_err_same_email_case_sensitive(self):
258 response = self.app.post(
256 response = self.app.post(
259 route_path('register'),
257 route_path('register'),
260 {
258 {
261 'username': 'test_admin_1',
259 'username': 'test_admin_1',
262 'password': 'test12',
260 'password': 'test12',
263 'password_confirmation': 'test12',
261 'password_confirmation': 'test12',
264 'email': 'TesT_Admin@mail.COM',
262 'email': 'TesT_Admin@mail.COM',
265 'firstname': 'test',
263 'firstname': 'test',
266 'lastname': 'test'
264 'lastname': 'test'
267 }
265 }
268 )
266 )
269 assertr = response.assert_response()
267 assertr = response.assert_response()
270 msg = '???'
268 msg = u'This e-mail address is already taken'
271 assertr.element_contains('#email+.error-message', msg)
269 assertr.element_contains('#email+.error-message', msg)
272
270
273 def test_register_err_wrong_data(self):
271 def test_register_err_wrong_data(self):
274 response = self.app.post(
272 response = self.app.post(
275 route_path('register'),
273 route_path('register'),
276 {
274 {
277 'username': 'xs',
275 'username': 'xs',
278 'password': 'test',
276 'password': 'test',
279 'password_confirmation': 'test',
277 'password_confirmation': 'test',
280 'email': 'goodmailm',
278 'email': 'goodmailm',
281 'firstname': 'test',
279 'firstname': 'test',
282 'lastname': 'test'
280 'lastname': 'test'
283 }
281 }
284 )
282 )
285 assert response.status == '200 OK'
283 assert response.status == '200 OK'
286 response.mustcontain('An email address must contain a single @')
284 response.mustcontain('An email address must contain a single @')
287 response.mustcontain('Enter a value 6 characters long or more')
285 response.mustcontain('Enter a value 6 characters long or more')
288
286
289 def test_register_err_username(self):
287 def test_register_err_username(self):
290 response = self.app.post(
288 response = self.app.post(
291 route_path('register'),
289 route_path('register'),
292 {
290 {
293 'username': 'error user',
291 'username': 'error user',
294 'password': 'test12',
292 'password': 'test12',
295 'password_confirmation': 'test12',
293 'password_confirmation': 'test12',
296 'email': 'goodmailm',
294 'email': 'goodmailm',
297 'firstname': 'test',
295 'firstname': 'test',
298 'lastname': 'test'
296 'lastname': 'test'
299 }
297 }
300 )
298 )
301
299
302 response.mustcontain('An email address must contain a single @')
300 response.mustcontain('An email address must contain a single @')
303 response.mustcontain(
301 response.mustcontain(
304 'Username may only contain '
302 'Username may only contain '
305 'alphanumeric characters underscores, '
303 'alphanumeric characters underscores, '
306 'periods or dashes and must begin with '
304 'periods or dashes and must begin with '
307 'alphanumeric character')
305 'alphanumeric character')
308
306
309 def test_register_err_case_sensitive(self):
307 def test_register_err_case_sensitive(self):
310 usr = 'Test_Admin'
308 usr = 'Test_Admin'
311 response = self.app.post(
309 response = self.app.post(
312 route_path('register'),
310 route_path('register'),
313 {
311 {
314 'username': usr,
312 'username': usr,
315 'password': 'test12',
313 'password': 'test12',
316 'password_confirmation': 'test12',
314 'password_confirmation': 'test12',
317 'email': 'goodmailm',
315 'email': 'goodmailm',
318 'firstname': 'test',
316 'firstname': 'test',
319 'lastname': 'test'
317 'lastname': 'test'
320 }
318 }
321 )
319 )
322
320
323 assertr = response.assert_response()
321 assertr = response.assert_response()
324 msg = '???'
322 msg = u'Username "%(username)s" already exists'
325 msg = msg % {'username': usr}
323 msg = msg % {'username': usr}
326 assertr.element_contains('#username+.error-message', msg)
324 assertr.element_contains('#username+.error-message', msg)
327
325
328 def test_register_special_chars(self):
326 def test_register_special_chars(self):
329 response = self.app.post(
327 response = self.app.post(
330 route_path('register'),
328 route_path('register'),
331 {
329 {
332 'username': 'xxxaxn',
330 'username': 'xxxaxn',
333 'password': 'ąćźżąśśśś',
331 'password': 'ąćźżąśśśś',
334 'password_confirmation': 'ąćźżąśśśś',
332 'password_confirmation': 'ąćźżąśśśś',
335 'email': 'goodmailm@test.plx',
333 'email': 'goodmailm@test.plx',
336 'firstname': 'test',
334 'firstname': 'test',
337 'lastname': 'test'
335 'lastname': 'test'
338 }
336 }
339 )
337 )
340
338
341 msg = '???'
339 msg = u'Invalid characters (non-ascii) in password'
342 response.mustcontain(msg)
340 response.mustcontain(msg)
343
341
344 def test_register_password_mismatch(self):
342 def test_register_password_mismatch(self):
345 response = self.app.post(
343 response = self.app.post(
346 route_path('register'),
344 route_path('register'),
347 {
345 {
348 'username': 'xs',
346 'username': 'xs',
349 'password': '123qwe',
347 'password': '123qwe',
350 'password_confirmation': 'qwe123',
348 'password_confirmation': 'qwe123',
351 'email': 'goodmailm@test.plxa',
349 'email': 'goodmailm@test.plxa',
352 'firstname': 'test',
350 'firstname': 'test',
353 'lastname': 'test'
351 'lastname': 'test'
354 }
352 }
355 )
353 )
356 msg = '???'
354 msg = u'Passwords do not match'
357 response.mustcontain(msg)
355 response.mustcontain(msg)
358
356
359 def test_register_ok(self):
357 def test_register_ok(self):
360 username = 'test_regular4'
358 username = 'test_regular4'
361 password = 'qweqwe'
359 password = 'qweqwe'
362 email = 'marcin@test.com'
360 email = 'marcin@test.com'
363 name = 'testname'
361 name = 'testname'
364 lastname = 'testlastname'
362 lastname = 'testlastname'
365
363
364 # this initializes a session
365 response = self.app.get(route_path('register'))
366 response.mustcontain('Create an Account')
367
368
366 response = self.app.post(
369 response = self.app.post(
367 route_path('register'),
370 route_path('register'),
368 {
371 {
369 'username': username,
372 'username': username,
370 'password': password,
373 'password': password,
371 'password_confirmation': password,
374 'password_confirmation': password,
372 'email': email,
375 'email': email,
373 'firstname': name,
376 'firstname': name,
374 'lastname': lastname,
377 'lastname': lastname,
375 'admin': True
378 'admin': True
376 }
379 },
377 ) # This should be overriden
380 status=302
378 assert response.status == '302 Found'
381 ) # This should be overridden
382
379 assert_session_flash(
383 assert_session_flash(
380 response, 'You have successfully registered with RhodeCode')
384 response, 'You have successfully registered with RhodeCode')
381
385
382 ret = Session().query(User).filter(
386 ret = Session().query(User).filter(
383 User.username == 'test_regular4').one()
387 User.username == 'test_regular4').one()
384 assert ret.username == username
388 assert ret.username == username
385 assert check_password(password, ret.password)
389 assert check_password(password, ret.password)
386 assert ret.email == email
390 assert ret.email == email
387 assert ret.name == name
391 assert ret.name == name
388 assert ret.lastname == lastname
392 assert ret.lastname == lastname
389 assert ret.auth_tokens is not None
393 assert ret.auth_tokens is not None
390 assert not ret.admin
394 assert not ret.admin
391
395
392 def test_forgot_password_wrong_mail(self):
396 def test_forgot_password_wrong_mail(self):
393 bad_email = 'marcin@wrongmail.org'
397 bad_email = 'marcin@wrongmail.org'
398 # this initializes a session
399 self.app.get(route_path('reset_password'))
400
394 response = self.app.post(
401 response = self.app.post(
395 route_path('reset_password'), {'email': bad_email, }
402 route_path('reset_password'), {'email': bad_email, }
396 )
403 )
397 assert_session_flash(response,
404 assert_session_flash(response,
398 'If such email exists, a password reset link was sent to it.')
405 'If such email exists, a password reset link was sent to it.')
399
406
400 def test_forgot_password(self, user_util):
407 def test_forgot_password(self, user_util):
401 response = self.app.get(route_path('reset_password'))
408 # this initializes a session
402 assert response.status == '200 OK'
409 self.app.get(route_path('reset_password'))
403
410
404 user = user_util.create_user()
411 user = user_util.create_user()
405 user_id = user.user_id
412 user_id = user.user_id
406 email = user.email
413 email = user.email
407
414
408 response = self.app.post(route_path('reset_password'), {'email': email, })
415 response = self.app.post(route_path('reset_password'), {'email': email, })
409
416
410 assert_session_flash(response,
417 assert_session_flash(response,
411 'If such email exists, a password reset link was sent to it.')
418 'If such email exists, a password reset link was sent to it.')
412
419
413 # BAD KEY
420 # BAD KEY
414 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
421 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
415 response = self.app.get(confirm_url)
422 response = self.app.get(confirm_url, status=302)
416 assert response.status == '302 Found'
417 assert response.location.endswith(route_path('reset_password'))
423 assert response.location.endswith(route_path('reset_password'))
418 assert_session_flash(response, 'Given reset token is invalid')
424 assert_session_flash(response, 'Given reset token is invalid')
419
425
420 response.follow() # cleanup flash
426 response.follow() # cleanup flash
421
427
422 # GOOD KEY
428 # GOOD KEY
423 key = UserApiKeys.query()\
429 key = UserApiKeys.query()\
424 .filter(UserApiKeys.user_id == user_id)\
430 .filter(UserApiKeys.user_id == user_id)\
425 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
431 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
426 .first()
432 .first()
427
433
428 assert key
434 assert key
429
435
430 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
436 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
431 response = self.app.get(confirm_url)
437 response = self.app.get(confirm_url)
432 assert response.status == '302 Found'
438 assert response.status == '302 Found'
433 assert response.location.endswith(route_path('login'))
439 assert response.location.endswith(route_path('login'))
434
440
435 assert_session_flash(
441 assert_session_flash(
436 response,
442 response,
437 'Your password reset was successful, '
443 'Your password reset was successful, '
438 'a new password has been sent to your email')
444 'a new password has been sent to your email')
439
445
440 response.follow()
446 response.follow()
441
447
442 def _get_api_whitelist(self, values=None):
448 def _get_api_whitelist(self, values=None):
443 config = {'api_access_controllers_whitelist': values or []}
449 config = {'api_access_controllers_whitelist': values or []}
444 return config
450 return config
445
451
446 @pytest.mark.parametrize("test_name, auth_token", [
452 @pytest.mark.parametrize("test_name, auth_token", [
447 ('none', None),
453 ('none', None),
448 ('empty_string', ''),
454 ('empty_string', ''),
449 ('fake_number', '123456'),
455 ('fake_number', '123456'),
450 ('proper_auth_token', None)
456 ('proper_auth_token', None)
451 ])
457 ])
452 def test_access_not_whitelisted_page_via_auth_token(
458 def test_access_not_whitelisted_page_via_auth_token(
453 self, test_name, auth_token, user_admin):
459 self, test_name, auth_token, user_admin):
454
460
455 whitelist = self._get_api_whitelist([])
461 whitelist = self._get_api_whitelist([])
456 with mock.patch.dict('rhodecode.CONFIG', whitelist):
462 with mock.patch.dict('rhodecode.CONFIG', whitelist):
457 assert [] == whitelist['api_access_controllers_whitelist']
463 assert [] == whitelist['api_access_controllers_whitelist']
458 if test_name == 'proper_auth_token':
464 if test_name == 'proper_auth_token':
459 # use builtin if api_key is None
465 # use builtin if api_key is None
460 auth_token = user_admin.api_key
466 auth_token = user_admin.api_key
461
467
462 with fixture.anon_access(False):
468 with fixture.anon_access(False):
463 self.app.get(
469 self.app.get(
464 route_path('repo_commit_raw',
470 route_path('repo_commit_raw',
465 repo_name=HG_REPO, commit_id='tip',
471 repo_name=HG_REPO, commit_id='tip',
466 params=dict(api_key=auth_token)),
472 params=dict(api_key=auth_token)),
467 status=302)
473 status=302)
468
474
469 @pytest.mark.parametrize("test_name, auth_token, code", [
475 @pytest.mark.parametrize("test_name, auth_token, code", [
470 ('none', None, 302),
476 ('none', None, 302),
471 ('empty_string', '', 302),
477 ('empty_string', '', 302),
472 ('fake_number', '123456', 302),
478 ('fake_number', '123456', 302),
473 ('proper_auth_token', None, 200)
479 ('proper_auth_token', None, 200)
474 ])
480 ])
475 def test_access_whitelisted_page_via_auth_token(
481 def test_access_whitelisted_page_via_auth_token(
476 self, test_name, auth_token, code, user_admin):
482 self, test_name, auth_token, code, user_admin):
477
483
478 whitelist = self._get_api_whitelist(whitelist_view)
484 whitelist = self._get_api_whitelist(whitelist_view)
479
485
480 with mock.patch.dict('rhodecode.CONFIG', whitelist):
486 with mock.patch.dict('rhodecode.CONFIG', whitelist):
481 assert whitelist_view == whitelist['api_access_controllers_whitelist']
487 assert whitelist_view == whitelist['api_access_controllers_whitelist']
482
488
483 if test_name == 'proper_auth_token':
489 if test_name == 'proper_auth_token':
484 auth_token = user_admin.api_key
490 auth_token = user_admin.api_key
485 assert auth_token
491 assert auth_token
486
492
487 with fixture.anon_access(False):
493 with fixture.anon_access(False):
488 self.app.get(
494 self.app.get(
489 route_path('repo_commit_raw',
495 route_path('repo_commit_raw',
490 repo_name=HG_REPO, commit_id='tip',
496 repo_name=HG_REPO, commit_id='tip',
491 params=dict(api_key=auth_token)),
497 params=dict(api_key=auth_token)),
492 status=code)
498 status=code)
493
499
494 @pytest.mark.parametrize("test_name, auth_token, code", [
500 @pytest.mark.parametrize("test_name, auth_token, code", [
495 ('proper_auth_token', None, 200),
501 ('proper_auth_token', None, 200),
496 ('wrong_auth_token', '123456', 302),
502 ('wrong_auth_token', '123456', 302),
497 ])
503 ])
498 def test_access_whitelisted_page_via_auth_token_bound_to_token(
504 def test_access_whitelisted_page_via_auth_token_bound_to_token(
499 self, test_name, auth_token, code, user_admin):
505 self, test_name, auth_token, code, user_admin):
500
506
501 expected_token = auth_token
507 expected_token = auth_token
502 if test_name == 'proper_auth_token':
508 if test_name == 'proper_auth_token':
503 auth_token = user_admin.api_key
509 auth_token = user_admin.api_key
504 expected_token = auth_token
510 expected_token = auth_token
505 assert auth_token
511 assert auth_token
506
512
507 whitelist = self._get_api_whitelist([
513 whitelist = self._get_api_whitelist([
508 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
514 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
509
515
510 with mock.patch.dict('rhodecode.CONFIG', whitelist):
516 with mock.patch.dict('rhodecode.CONFIG', whitelist):
511
517
512 with fixture.anon_access(False):
518 with fixture.anon_access(False):
513 self.app.get(
519 self.app.get(
514 route_path('repo_commit_raw',
520 route_path('repo_commit_raw',
515 repo_name=HG_REPO, commit_id='tip',
521 repo_name=HG_REPO, commit_id='tip',
516 params=dict(api_key=auth_token)),
522 params=dict(api_key=auth_token)),
517 status=code)
523 status=code)
518
524
519 def test_access_page_via_extra_auth_token(self):
525 def test_access_page_via_extra_auth_token(self):
520 whitelist = self._get_api_whitelist(whitelist_view)
526 whitelist = self._get_api_whitelist(whitelist_view)
521 with mock.patch.dict('rhodecode.CONFIG', whitelist):
527 with mock.patch.dict('rhodecode.CONFIG', whitelist):
522 assert whitelist_view == \
528 assert whitelist_view == \
523 whitelist['api_access_controllers_whitelist']
529 whitelist['api_access_controllers_whitelist']
524
530
525 new_auth_token = AuthTokenModel().create(
531 new_auth_token = AuthTokenModel().create(
526 TEST_USER_ADMIN_LOGIN, 'test')
532 TEST_USER_ADMIN_LOGIN, 'test')
527 Session().commit()
533 Session().commit()
528 with fixture.anon_access(False):
534 with fixture.anon_access(False):
529 self.app.get(
535 self.app.get(
530 route_path('repo_commit_raw',
536 route_path('repo_commit_raw',
531 repo_name=HG_REPO, commit_id='tip',
537 repo_name=HG_REPO, commit_id='tip',
532 params=dict(api_key=new_auth_token.api_key)),
538 params=dict(api_key=new_auth_token.api_key)),
533 status=200)
539 status=200)
534
540
535 def test_access_page_via_expired_auth_token(self):
541 def test_access_page_via_expired_auth_token(self):
536 whitelist = self._get_api_whitelist(whitelist_view)
542 whitelist = self._get_api_whitelist(whitelist_view)
537 with mock.patch.dict('rhodecode.CONFIG', whitelist):
543 with mock.patch.dict('rhodecode.CONFIG', whitelist):
538 assert whitelist_view == \
544 assert whitelist_view == \
539 whitelist['api_access_controllers_whitelist']
545 whitelist['api_access_controllers_whitelist']
540
546
541 new_auth_token = AuthTokenModel().create(
547 new_auth_token = AuthTokenModel().create(
542 TEST_USER_ADMIN_LOGIN, 'test')
548 TEST_USER_ADMIN_LOGIN, 'test')
543 Session().commit()
549 Session().commit()
544 # patch the api key and make it expired
550 # patch the api key and make it expired
545 new_auth_token.expires = 0
551 new_auth_token.expires = 0
546 Session().add(new_auth_token)
552 Session().add(new_auth_token)
547 Session().commit()
553 Session().commit()
548 with fixture.anon_access(False):
554 with fixture.anon_access(False):
549 self.app.get(
555 self.app.get(
550 route_path('repo_commit_raw',
556 route_path('repo_commit_raw',
551 repo_name=HG_REPO, commit_id='tip',
557 repo_name=HG_REPO, commit_id='tip',
552 params=dict(api_key=new_auth_token.api_key)),
558 params=dict(api_key=new_auth_token.api_key)),
553 status=302)
559 status=302)
@@ -1,133 +1,133 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import mock
22 import mock
23 import pytest
23 import pytest
24
24
25 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.apps.login.views import LoginView, CaptchaData
26 from rhodecode.apps.login.views import LoginView, CaptchaData
27 from rhodecode.model.settings import SettingsModel
27 from rhodecode.model.settings import SettingsModel
28 from rhodecode.lib.utils2 import AttributeDict
28 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.tests.utils import AssertResponse
29 from rhodecode.tests.utils import AssertResponse
30
30
31
31
32 class RhodeCodeSetting(object):
32 class RhodeCodeSetting(object):
33 def __init__(self, name, value):
33 def __init__(self, name, value):
34 self.name = name
34 self.name = name
35 self.value = value
35 self.value = value
36
36
37 def __enter__(self):
37 def __enter__(self):
38 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.settings import SettingsModel
39 model = SettingsModel()
39 model = SettingsModel()
40 self.old_setting = model.get_setting_by_name(self.name)
40 self.old_setting = model.get_setting_by_name(self.name)
41 model.create_or_update_setting(name=self.name, val=self.value)
41 model.create_or_update_setting(name=self.name, val=self.value)
42 return self
42 return self
43
43
44 def __exit__(self, exc_type, exc_val, exc_tb):
44 def __exit__(self, exc_type, exc_val, exc_tb):
45 model = SettingsModel()
45 model = SettingsModel()
46 if self.old_setting:
46 if self.old_setting:
47 model.create_or_update_setting(
47 model.create_or_update_setting(
48 name=self.name, val=self.old_setting.app_settings_value)
48 name=self.name, val=self.old_setting.app_settings_value)
49 else:
49 else:
50 model.create_or_update_setting(name=self.name)
50 model.create_or_update_setting(name=self.name)
51
51
52
52
53 class TestRegisterCaptcha(object):
53 class TestRegisterCaptcha(object):
54
54
55 @pytest.mark.parametrize('private_key, public_key, expected', [
55 @pytest.mark.parametrize('private_key, public_key, expected', [
56 ('', '', CaptchaData(False, '', '')),
56 ('', '', CaptchaData(False, '', '')),
57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
58 ('privkey', '', CaptchaData(True, 'privkey', '')),
58 ('privkey', '', CaptchaData(True, 'privkey', '')),
59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
60 ])
60 ])
61 def test_get_captcha_data(self, private_key, public_key, expected, db,
61 def test_get_captcha_data(self, private_key, public_key, expected,
62 request_stub, user_util):
62 request_stub, user_util):
63 request_stub.user = user_util.create_user().AuthUser()
63 request_stub.user = user_util.create_user().AuthUser()
64 request_stub.matched_route = AttributeDict({'name': 'login'})
64 request_stub.matched_route = AttributeDict({'name': 'login'})
65 login_view = LoginView(mock.Mock(), request_stub)
65 login_view = LoginView(mock.Mock(), request_stub)
66
66
67 with RhodeCodeSetting('captcha_private_key', private_key):
67 with RhodeCodeSetting('captcha_private_key', private_key):
68 with RhodeCodeSetting('captcha_public_key', public_key):
68 with RhodeCodeSetting('captcha_public_key', public_key):
69 captcha = login_view._get_captcha_data()
69 captcha = login_view._get_captcha_data()
70 assert captcha == expected
70 assert captcha == expected
71
71
72 @pytest.mark.parametrize('active', [False, True])
72 @pytest.mark.parametrize('active', [False, True])
73 @mock.patch.object(LoginView, '_get_captcha_data')
73 @mock.patch.object(LoginView, '_get_captcha_data')
74 def test_private_key_does_not_leak_to_html(
74 def test_private_key_does_not_leak_to_html(
75 self, m_get_captcha_data, active, app):
75 self, m_get_captcha_data, active, app):
76 captcha = CaptchaData(
76 captcha = CaptchaData(
77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
78 m_get_captcha_data.return_value = captcha
78 m_get_captcha_data.return_value = captcha
79
79
80 response = app.get(ADMIN_PREFIX + '/register')
80 response = app.get(ADMIN_PREFIX + '/register')
81 assert 'PRIVATE_KEY' not in response
81 assert 'PRIVATE_KEY' not in response
82
82
83 @pytest.mark.parametrize('active', [False, True])
83 @pytest.mark.parametrize('active', [False, True])
84 @mock.patch.object(LoginView, '_get_captcha_data')
84 @mock.patch.object(LoginView, '_get_captcha_data')
85 def test_register_view_renders_captcha(
85 def test_register_view_renders_captcha(
86 self, m_get_captcha_data, active, app):
86 self, m_get_captcha_data, active, app):
87 captcha = CaptchaData(
87 captcha = CaptchaData(
88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
89 m_get_captcha_data.return_value = captcha
89 m_get_captcha_data.return_value = captcha
90
90
91 response = app.get(ADMIN_PREFIX + '/register')
91 response = app.get(ADMIN_PREFIX + '/register')
92
92
93 assertr = AssertResponse(response)
93 assertr = AssertResponse(response)
94 if active:
94 if active:
95 assertr.one_element_exists('#recaptcha_field')
95 assertr.one_element_exists('#recaptcha_field')
96 else:
96 else:
97 assertr.no_element_exists('#recaptcha_field')
97 assertr.no_element_exists('#recaptcha_field')
98
98
99 @pytest.mark.parametrize('valid', [False, True])
99 @pytest.mark.parametrize('valid', [False, True])
100 @mock.patch('rhodecode.apps.login.views.submit')
100 @mock.patch('rhodecode.apps.login.views.submit')
101 @mock.patch.object(LoginView, '_get_captcha_data')
101 @mock.patch.object(LoginView, '_get_captcha_data')
102 def test_register_with_active_captcha(
102 def test_register_with_active_captcha(
103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
104 captcha = CaptchaData(
104 captcha = CaptchaData(
105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
106 m_get_captcha_data.return_value = captcha
106 m_get_captcha_data.return_value = captcha
107 m_response = mock.Mock()
107 m_response = mock.Mock()
108 m_response.is_valid = valid
108 m_response.is_valid = valid
109 m_submit.return_value = m_response
109 m_submit.return_value = m_response
110
110
111 params = {
111 params = {
112 'csrf_token': csrf_token,
112 'csrf_token': csrf_token,
113 'email': 'pytest@example.com',
113 'email': 'pytest@example.com',
114 'firstname': 'pytest-firstname',
114 'firstname': 'pytest-firstname',
115 'lastname': 'pytest-lastname',
115 'lastname': 'pytest-lastname',
116 'password': 'secret',
116 'password': 'secret',
117 'password_confirmation': 'secret',
117 'password_confirmation': 'secret',
118 'username': 'pytest',
118 'username': 'pytest',
119 }
119 }
120 response = app.post(ADMIN_PREFIX + '/register', params=params)
120 response = app.post(ADMIN_PREFIX + '/register', params=params)
121
121
122 if valid:
122 if valid:
123 # If we provided a valid captcha input we expect a successful
123 # If we provided a valid captcha input we expect a successful
124 # registration and redirect to the login page.
124 # registration and redirect to the login page.
125 assert response.status_int == 302
125 assert response.status_int == 302
126 assert 'location' in response.headers
126 assert 'location' in response.headers
127 assert ADMIN_PREFIX + '/login' in response.headers['location']
127 assert ADMIN_PREFIX + '/login' in response.headers['location']
128 else:
128 else:
129 # If captche input is invalid we expect to stay on the registration
129 # If captche input is invalid we expect to stay on the registration
130 # page with an error message displayed.
130 # page with an error message displayed.
131 assertr = AssertResponse(response)
131 assertr = AssertResponse(response)
132 assert response.status_int == 200
132 assert response.status_int == 200
133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
@@ -1,426 +1,428 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import collections
22 import collections
23 import datetime
23 import datetime
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import logging
26 import logging
27 import urlparse
27 import urlparse
28
28
29 from pyramid.httpexceptions import HTTPFound
29 from pyramid.httpexceptions import HTTPFound
30 from pyramid.view import view_config
30 from pyramid.view import view_config
31 from recaptcha.client.captcha import submit
31 from recaptcha.client.captcha import submit
32
32
33 from rhodecode.apps._base import BaseAppView
33 from rhodecode.apps._base import BaseAppView
34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 from rhodecode.events import UserRegistered
35 from rhodecode.events import UserRegistered, trigger
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
40 from rhodecode.lib.base import get_ip_addr
40 from rhodecode.lib.base import get_ip_addr
41 from rhodecode.lib.exceptions import UserCreationError
41 from rhodecode.lib.exceptions import UserCreationError
42 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.model.db import User, UserApiKeys
44 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
44 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
45 from rhodecode.model.meta import Session
45 from rhodecode.model.meta import Session
46 from rhodecode.model.auth_token import AuthTokenModel
46 from rhodecode.model.auth_token import AuthTokenModel
47 from rhodecode.model.settings import SettingsModel
47 from rhodecode.model.settings import SettingsModel
48 from rhodecode.model.user import UserModel
48 from rhodecode.model.user import UserModel
49 from rhodecode.translation import _
49 from rhodecode.translation import _
50
50
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54 CaptchaData = collections.namedtuple(
54 CaptchaData = collections.namedtuple(
55 'CaptchaData', 'active, private_key, public_key')
55 'CaptchaData', 'active, private_key, public_key')
56
56
57
57
58 def _store_user_in_session(session, username, remember=False):
58 def _store_user_in_session(session, username, remember=False):
59 user = User.get_by_username(username, case_insensitive=True)
59 user = User.get_by_username(username, case_insensitive=True)
60 auth_user = AuthUser(user.user_id)
60 auth_user = AuthUser(user.user_id)
61 auth_user.set_authenticated()
61 auth_user.set_authenticated()
62 cs = auth_user.get_cookie_store()
62 cs = auth_user.get_cookie_store()
63 session['rhodecode_user'] = cs
63 session['rhodecode_user'] = cs
64 user.update_lastlogin()
64 user.update_lastlogin()
65 Session().commit()
65 Session().commit()
66
66
67 # If they want to be remembered, update the cookie
67 # If they want to be remembered, update the cookie
68 if remember:
68 if remember:
69 _year = (datetime.datetime.now() +
69 _year = (datetime.datetime.now() +
70 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 datetime.timedelta(seconds=60 * 60 * 24 * 365))
71 session._set_cookie_expires(_year)
71 session._set_cookie_expires(_year)
72
72
73 session.save()
73 session.save()
74
74
75 safe_cs = cs.copy()
75 safe_cs = cs.copy()
76 safe_cs['password'] = '****'
76 safe_cs['password'] = '****'
77 log.info('user %s is now authenticated and stored in '
77 log.info('user %s is now authenticated and stored in '
78 'session, session attrs %s', username, safe_cs)
78 'session, session attrs %s', username, safe_cs)
79
79
80 # dumps session attrs back to cookie
80 # dumps session attrs back to cookie
81 session._update_cookie_out()
81 session._update_cookie_out()
82 # we set new cookie
82 # we set new cookie
83 headers = None
83 headers = None
84 if session.request['set_cookie']:
84 if session.request['set_cookie']:
85 # send set-cookie headers back to response to update cookie
85 # send set-cookie headers back to response to update cookie
86 headers = [('Set-Cookie', session.request['cookie_out'])]
86 headers = [('Set-Cookie', session.request['cookie_out'])]
87 return headers
87 return headers
88
88
89
89
90 def get_came_from(request):
90 def get_came_from(request):
91 came_from = safe_str(request.GET.get('came_from', ''))
91 came_from = safe_str(request.GET.get('came_from', ''))
92 parsed = urlparse.urlparse(came_from)
92 parsed = urlparse.urlparse(came_from)
93 allowed_schemes = ['http', 'https']
93 allowed_schemes = ['http', 'https']
94 default_came_from = h.route_path('home')
94 default_came_from = h.route_path('home')
95 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 if parsed.scheme and parsed.scheme not in allowed_schemes:
96 log.error('Suspicious URL scheme detected %s for url %s' %
96 log.error('Suspicious URL scheme detected %s for url %s' %
97 (parsed.scheme, parsed))
97 (parsed.scheme, parsed))
98 came_from = default_came_from
98 came_from = default_came_from
99 elif parsed.netloc and request.host != parsed.netloc:
99 elif parsed.netloc and request.host != parsed.netloc:
100 log.error('Suspicious NETLOC detected %s for url %s server url '
100 log.error('Suspicious NETLOC detected %s for url %s server url '
101 'is: %s' % (parsed.netloc, parsed, request.host))
101 'is: %s' % (parsed.netloc, parsed, request.host))
102 came_from = default_came_from
102 came_from = default_came_from
103 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
104 log.error('Header injection detected `%s` for url %s server url ' %
104 log.error('Header injection detected `%s` for url %s server url ' %
105 (parsed.path, parsed))
105 (parsed.path, parsed))
106 came_from = default_came_from
106 came_from = default_came_from
107
107
108 return came_from or default_came_from
108 return came_from or default_came_from
109
109
110
110
111 class LoginView(BaseAppView):
111 class LoginView(BaseAppView):
112
112
113 def load_default_context(self):
113 def load_default_context(self):
114 c = self._get_local_tmpl_context()
114 c = self._get_local_tmpl_context()
115 c.came_from = get_came_from(self.request)
115 c.came_from = get_came_from(self.request)
116
116
117 return c
117 return c
118
118
119 def _get_captcha_data(self):
119 def _get_captcha_data(self):
120 settings = SettingsModel().get_all_settings()
120 settings = SettingsModel().get_all_settings()
121 private_key = settings.get('rhodecode_captcha_private_key')
121 private_key = settings.get('rhodecode_captcha_private_key')
122 public_key = settings.get('rhodecode_captcha_public_key')
122 public_key = settings.get('rhodecode_captcha_public_key')
123 active = bool(private_key)
123 active = bool(private_key)
124 return CaptchaData(
124 return CaptchaData(
125 active=active, private_key=private_key, public_key=public_key)
125 active=active, private_key=private_key, public_key=public_key)
126
126
127 @view_config(
127 @view_config(
128 route_name='login', request_method='GET',
128 route_name='login', request_method='GET',
129 renderer='rhodecode:templates/login.mako')
129 renderer='rhodecode:templates/login.mako')
130 def login(self):
130 def login(self):
131 c = self.load_default_context()
131 c = self.load_default_context()
132 auth_user = self._rhodecode_user
132 auth_user = self._rhodecode_user
133
133
134 # redirect if already logged in
134 # redirect if already logged in
135 if (auth_user.is_authenticated and
135 if (auth_user.is_authenticated and
136 not auth_user.is_default and auth_user.ip_allowed):
136 not auth_user.is_default and auth_user.ip_allowed):
137 raise HTTPFound(c.came_from)
137 raise HTTPFound(c.came_from)
138
138
139 # check if we use headers plugin, and try to login using it.
139 # check if we use headers plugin, and try to login using it.
140 try:
140 try:
141 log.debug('Running PRE-AUTH for headers based authentication')
141 log.debug('Running PRE-AUTH for headers based authentication')
142 auth_info = authenticate(
142 auth_info = authenticate(
143 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
143 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
144 if auth_info:
144 if auth_info:
145 headers = _store_user_in_session(
145 headers = _store_user_in_session(
146 self.session, auth_info.get('username'))
146 self.session, auth_info.get('username'))
147 raise HTTPFound(c.came_from, headers=headers)
147 raise HTTPFound(c.came_from, headers=headers)
148 except UserCreationError as e:
148 except UserCreationError as e:
149 log.error(e)
149 log.error(e)
150 self.session.flash(e, queue='error')
150 h.flash(e, category='error')
151
151
152 return self._get_template_context(c)
152 return self._get_template_context(c)
153
153
154 @view_config(
154 @view_config(
155 route_name='login', request_method='POST',
155 route_name='login', request_method='POST',
156 renderer='rhodecode:templates/login.mako')
156 renderer='rhodecode:templates/login.mako')
157 def login_post(self):
157 def login_post(self):
158 c = self.load_default_context()
158 c = self.load_default_context()
159
159
160 login_form = LoginForm(self.request.translate)()
160 login_form = LoginForm(self.request.translate)()
161
161
162 try:
162 try:
163 self.session.invalidate()
163 self.session.invalidate()
164 form_result = login_form.to_python(self.request.POST)
164 form_result = login_form.to_python(self.request.POST)
165 # form checks for username/password, now we're authenticated
165 # form checks for username/password, now we're authenticated
166 headers = _store_user_in_session(
166 headers = _store_user_in_session(
167 self.session,
167 self.session,
168 username=form_result['username'],
168 username=form_result['username'],
169 remember=form_result['remember'])
169 remember=form_result['remember'])
170 log.debug('Redirecting to "%s" after login.', c.came_from)
170 log.debug('Redirecting to "%s" after login.', c.came_from)
171
171
172 audit_user = audit_logger.UserWrap(
172 audit_user = audit_logger.UserWrap(
173 username=self.request.POST.get('username'),
173 username=self.request.POST.get('username'),
174 ip_addr=self.request.remote_addr)
174 ip_addr=self.request.remote_addr)
175 action_data = {'user_agent': self.request.user_agent}
175 action_data = {'user_agent': self.request.user_agent}
176 audit_logger.store_web(
176 audit_logger.store_web(
177 'user.login.success', action_data=action_data,
177 'user.login.success', action_data=action_data,
178 user=audit_user, commit=True)
178 user=audit_user, commit=True)
179
179
180 raise HTTPFound(c.came_from, headers=headers)
180 raise HTTPFound(c.came_from, headers=headers)
181 except formencode.Invalid as errors:
181 except formencode.Invalid as errors:
182 defaults = errors.value
182 defaults = errors.value
183 # remove password from filling in form again
183 # remove password from filling in form again
184 defaults.pop('password', None)
184 defaults.pop('password', None)
185 render_ctx = {
185 render_ctx = {
186 'errors': errors.error_dict,
186 'errors': errors.error_dict,
187 'defaults': defaults,
187 'defaults': defaults,
188 }
188 }
189
189
190 audit_user = audit_logger.UserWrap(
190 audit_user = audit_logger.UserWrap(
191 username=self.request.POST.get('username'),
191 username=self.request.POST.get('username'),
192 ip_addr=self.request.remote_addr)
192 ip_addr=self.request.remote_addr)
193 action_data = {'user_agent': self.request.user_agent}
193 action_data = {'user_agent': self.request.user_agent}
194 audit_logger.store_web(
194 audit_logger.store_web(
195 'user.login.failure', action_data=action_data,
195 'user.login.failure', action_data=action_data,
196 user=audit_user, commit=True)
196 user=audit_user, commit=True)
197 return self._get_template_context(c, **render_ctx)
197 return self._get_template_context(c, **render_ctx)
198
198
199 except UserCreationError as e:
199 except UserCreationError as e:
200 # headers auth or other auth functions that create users on
200 # headers auth or other auth functions that create users on
201 # the fly can throw this exception signaling that there's issue
201 # the fly can throw this exception signaling that there's issue
202 # with user creation, explanation should be provided in
202 # with user creation, explanation should be provided in
203 # Exception itself
203 # Exception itself
204 self.session.flash(e, queue='error')
204 h.flash(e, category='error')
205 return self._get_template_context(c)
205 return self._get_template_context(c)
206
206
207 @CSRFRequired()
207 @CSRFRequired()
208 @view_config(route_name='logout', request_method='POST')
208 @view_config(route_name='logout', request_method='POST')
209 def logout(self):
209 def logout(self):
210 auth_user = self._rhodecode_user
210 auth_user = self._rhodecode_user
211 log.info('Deleting session for user: `%s`', auth_user)
211 log.info('Deleting session for user: `%s`', auth_user)
212
212
213 action_data = {'user_agent': self.request.user_agent}
213 action_data = {'user_agent': self.request.user_agent}
214 audit_logger.store_web(
214 audit_logger.store_web(
215 'user.logout', action_data=action_data,
215 'user.logout', action_data=action_data,
216 user=auth_user, commit=True)
216 user=auth_user, commit=True)
217 self.session.delete()
217 self.session.delete()
218 return HTTPFound(h.route_path('home'))
218 return HTTPFound(h.route_path('home'))
219
219
220 @HasPermissionAnyDecorator(
220 @HasPermissionAnyDecorator(
221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
222 @view_config(
222 @view_config(
223 route_name='register', request_method='GET',
223 route_name='register', request_method='GET',
224 renderer='rhodecode:templates/register.mako',)
224 renderer='rhodecode:templates/register.mako',)
225 def register(self, defaults=None, errors=None):
225 def register(self, defaults=None, errors=None):
226 c = self.load_default_context()
226 c = self.load_default_context()
227 defaults = defaults or {}
227 defaults = defaults or {}
228 errors = errors or {}
228 errors = errors or {}
229
229
230 settings = SettingsModel().get_all_settings()
230 settings = SettingsModel().get_all_settings()
231 register_message = settings.get('rhodecode_register_message') or ''
231 register_message = settings.get('rhodecode_register_message') or ''
232 captcha = self._get_captcha_data()
232 captcha = self._get_captcha_data()
233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 .AuthUser().permissions['global']
234 .AuthUser().permissions['global']
235
235
236 render_ctx = self._get_template_context(c)
236 render_ctx = self._get_template_context(c)
237 render_ctx.update({
237 render_ctx.update({
238 'defaults': defaults,
238 'defaults': defaults,
239 'errors': errors,
239 'errors': errors,
240 'auto_active': auto_active,
240 'auto_active': auto_active,
241 'captcha_active': captcha.active,
241 'captcha_active': captcha.active,
242 'captcha_public_key': captcha.public_key,
242 'captcha_public_key': captcha.public_key,
243 'register_message': register_message,
243 'register_message': register_message,
244 })
244 })
245 return render_ctx
245 return render_ctx
246
246
247 @HasPermissionAnyDecorator(
247 @HasPermissionAnyDecorator(
248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
249 @view_config(
249 @view_config(
250 route_name='register', request_method='POST',
250 route_name='register', request_method='POST',
251 renderer='rhodecode:templates/register.mako')
251 renderer='rhodecode:templates/register.mako')
252 def register_post(self):
252 def register_post(self):
253 self.load_default_context()
253 captcha = self._get_captcha_data()
254 captcha = self._get_captcha_data()
254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 .AuthUser().permissions['global']
256 .AuthUser().permissions['global']
256
257
257 register_form = RegisterForm(self.request.translate)()
258 register_form = RegisterForm(self.request.translate)()
258 try:
259 try:
259
260
260 form_result = register_form.to_python(self.request.POST)
261 form_result = register_form.to_python(self.request.POST)
261 form_result['active'] = auto_active
262 form_result['active'] = auto_active
262
263
263 if captcha.active:
264 if captcha.active:
264 response = submit(
265 response = submit(
265 self.request.POST.get('recaptcha_challenge_field'),
266 self.request.POST.get('recaptcha_challenge_field'),
266 self.request.POST.get('recaptcha_response_field'),
267 self.request.POST.get('recaptcha_response_field'),
267 private_key=captcha.private_key,
268 private_key=captcha.private_key,
268 remoteip=get_ip_addr(self.request.environ))
269 remoteip=get_ip_addr(self.request.environ))
269 if not response.is_valid:
270 if not response.is_valid:
270 _value = form_result
271 _value = form_result
271 _msg = _('Bad captcha')
272 _msg = _('Bad captcha')
272 error_dict = {'recaptcha_field': _msg}
273 error_dict = {'recaptcha_field': _msg}
273 raise formencode.Invalid(_msg, _value, None,
274 raise formencode.Invalid(_msg, _value, None,
274 error_dict=error_dict)
275 error_dict=error_dict)
275
276
276 new_user = UserModel().create_registration(form_result)
277 new_user = UserModel().create_registration(form_result)
277 event = UserRegistered(user=new_user, session=self.session)
278 event = UserRegistered(user=new_user, session=self.session)
278 self.request.registry.notify(event)
279 trigger(event)
279 self.session.flash(
280 h.flash(
280 _('You have successfully registered with RhodeCode'),
281 _('You have successfully registered with RhodeCode'),
281 queue='success')
282 category='success')
282 Session().commit()
283 Session().commit()
283
284
284 redirect_ro = self.request.route_path('login')
285 redirect_ro = self.request.route_path('login')
285 raise HTTPFound(redirect_ro)
286 raise HTTPFound(redirect_ro)
286
287
287 except formencode.Invalid as errors:
288 except formencode.Invalid as errors:
288 errors.value.pop('password', None)
289 errors.value.pop('password', None)
289 errors.value.pop('password_confirmation', None)
290 errors.value.pop('password_confirmation', None)
290 return self.register(
291 return self.register(
291 defaults=errors.value, errors=errors.error_dict)
292 defaults=errors.value, errors=errors.error_dict)
292
293
293 except UserCreationError as e:
294 except UserCreationError as e:
294 # container auth or other auth functions that create users on
295 # container auth or other auth functions that create users on
295 # the fly can throw this exception signaling that there's issue
296 # the fly can throw this exception signaling that there's issue
296 # with user creation, explanation should be provided in
297 # with user creation, explanation should be provided in
297 # Exception itself
298 # Exception itself
298 self.session.flash(e, queue='error')
299 h.flash(e, category='error')
299 return self.register()
300 return self.register()
300
301
301 @view_config(
302 @view_config(
302 route_name='reset_password', request_method=('GET', 'POST'),
303 route_name='reset_password', request_method=('GET', 'POST'),
303 renderer='rhodecode:templates/password_reset.mako')
304 renderer='rhodecode:templates/password_reset.mako')
304 def password_reset(self):
305 def password_reset(self):
306 c = self.load_default_context()
305 captcha = self._get_captcha_data()
307 captcha = self._get_captcha_data()
306
308
307 render_ctx = {
309 template_context = {
308 'captcha_active': captcha.active,
310 'captcha_active': captcha.active,
309 'captcha_public_key': captcha.public_key,
311 'captcha_public_key': captcha.public_key,
310 'defaults': {},
312 'defaults': {},
311 'errors': {},
313 'errors': {},
312 }
314 }
313
315
314 # always send implicit message to prevent from discovery of
316 # always send implicit message to prevent from discovery of
315 # matching emails
317 # matching emails
316 msg = _('If such email exists, a password reset link was sent to it.')
318 msg = _('If such email exists, a password reset link was sent to it.')
317
319
318 if self.request.POST:
320 if self.request.POST:
319 if h.HasPermissionAny('hg.password_reset.disabled')():
321 if h.HasPermissionAny('hg.password_reset.disabled')():
320 _email = self.request.POST.get('email', '')
322 _email = self.request.POST.get('email', '')
321 log.error('Failed attempt to reset password for `%s`.', _email)
323 log.error('Failed attempt to reset password for `%s`.', _email)
322 self.session.flash(_('Password reset has been disabled.'),
324 h.flash(_('Password reset has been disabled.'),
323 queue='error')
325 category='error')
324 return HTTPFound(self.request.route_path('reset_password'))
326 return HTTPFound(self.request.route_path('reset_password'))
325
327
326 password_reset_form = PasswordResetForm(self.request.translate)()
328 password_reset_form = PasswordResetForm(self.request.translate)()
327 try:
329 try:
328 form_result = password_reset_form.to_python(
330 form_result = password_reset_form.to_python(
329 self.request.POST)
331 self.request.POST)
330 user_email = form_result['email']
332 user_email = form_result['email']
331
333
332 if captcha.active:
334 if captcha.active:
333 response = submit(
335 response = submit(
334 self.request.POST.get('recaptcha_challenge_field'),
336 self.request.POST.get('recaptcha_challenge_field'),
335 self.request.POST.get('recaptcha_response_field'),
337 self.request.POST.get('recaptcha_response_field'),
336 private_key=captcha.private_key,
338 private_key=captcha.private_key,
337 remoteip=get_ip_addr(self.request.environ))
339 remoteip=get_ip_addr(self.request.environ))
338 if not response.is_valid:
340 if not response.is_valid:
339 _value = form_result
341 _value = form_result
340 _msg = _('Bad captcha')
342 _msg = _('Bad captcha')
341 error_dict = {'recaptcha_field': _msg}
343 error_dict = {'recaptcha_field': _msg}
342 raise formencode.Invalid(
344 raise formencode.Invalid(
343 _msg, _value, None, error_dict=error_dict)
345 _msg, _value, None, error_dict=error_dict)
344
346
345 # Generate reset URL and send mail.
347 # Generate reset URL and send mail.
346 user = User.get_by_email(user_email)
348 user = User.get_by_email(user_email)
347
349
348 # generate password reset token that expires in 10minutes
350 # generate password reset token that expires in 10minutes
349 desc = 'Generated token for password reset from {}'.format(
351 desc = 'Generated token for password reset from {}'.format(
350 datetime.datetime.now().isoformat())
352 datetime.datetime.now().isoformat())
351 reset_token = AuthTokenModel().create(
353 reset_token = AuthTokenModel().create(
352 user, lifetime=10,
354 user, lifetime=10,
353 description=desc,
355 description=desc,
354 role=UserApiKeys.ROLE_PASSWORD_RESET)
356 role=UserApiKeys.ROLE_PASSWORD_RESET)
355 Session().commit()
357 Session().commit()
356
358
357 log.debug('Successfully created password recovery token')
359 log.debug('Successfully created password recovery token')
358 password_reset_url = self.request.route_url(
360 password_reset_url = self.request.route_url(
359 'reset_password_confirmation',
361 'reset_password_confirmation',
360 _query={'key': reset_token.api_key})
362 _query={'key': reset_token.api_key})
361 UserModel().reset_password_link(
363 UserModel().reset_password_link(
362 form_result, password_reset_url)
364 form_result, password_reset_url)
363 # Display success message and redirect.
365 # Display success message and redirect.
364 self.session.flash(msg, queue='success')
366 h.flash(msg, category='success')
365
367
366 action_data = {'email': user_email,
368 action_data = {'email': user_email,
367 'user_agent': self.request.user_agent}
369 'user_agent': self.request.user_agent}
368 audit_logger.store_web(
370 audit_logger.store_web(
369 'user.password.reset_request', action_data=action_data,
371 'user.password.reset_request', action_data=action_data,
370 user=self._rhodecode_user, commit=True)
372 user=self._rhodecode_user, commit=True)
371 return HTTPFound(self.request.route_path('reset_password'))
373 return HTTPFound(self.request.route_path('reset_password'))
372
374
373 except formencode.Invalid as errors:
375 except formencode.Invalid as errors:
374 render_ctx.update({
376 template_context.update({
375 'defaults': errors.value,
377 'defaults': errors.value,
376 'errors': errors.error_dict,
378 'errors': errors.error_dict,
377 })
379 })
378 if not self.request.POST.get('email'):
380 if not self.request.POST.get('email'):
379 # case of empty email, we want to report that
381 # case of empty email, we want to report that
380 return render_ctx
382 return self._get_template_context(c, **template_context)
381
383
382 if 'recaptcha_field' in errors.error_dict:
384 if 'recaptcha_field' in errors.error_dict:
383 # case of failed captcha
385 # case of failed captcha
384 return render_ctx
386 return self._get_template_context(c, **template_context)
385
387
386 log.debug('faking response on invalid password reset')
388 log.debug('faking response on invalid password reset')
387 # make this take 2s, to prevent brute forcing.
389 # make this take 2s, to prevent brute forcing.
388 time.sleep(2)
390 time.sleep(2)
389 self.session.flash(msg, queue='success')
391 h.flash(msg, category='success')
390 return HTTPFound(self.request.route_path('reset_password'))
392 return HTTPFound(self.request.route_path('reset_password'))
391
393
392 return render_ctx
394 return self._get_template_context(c, **template_context)
393
395
394 @view_config(route_name='reset_password_confirmation',
396 @view_config(route_name='reset_password_confirmation',
395 request_method='GET')
397 request_method='GET')
396 def password_reset_confirmation(self):
398 def password_reset_confirmation(self):
397
399 self.load_default_context()
398 if self.request.GET and self.request.GET.get('key'):
400 if self.request.GET and self.request.GET.get('key'):
399 # make this take 2s, to prevent brute forcing.
401 # make this take 2s, to prevent brute forcing.
400 time.sleep(2)
402 time.sleep(2)
401
403
402 token = AuthTokenModel().get_auth_token(
404 token = AuthTokenModel().get_auth_token(
403 self.request.GET.get('key'))
405 self.request.GET.get('key'))
404
406
405 # verify token is the correct role
407 # verify token is the correct role
406 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
408 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
407 log.debug('Got token with role:%s expected is %s',
409 log.debug('Got token with role:%s expected is %s',
408 getattr(token, 'role', 'EMPTY_TOKEN'),
410 getattr(token, 'role', 'EMPTY_TOKEN'),
409 UserApiKeys.ROLE_PASSWORD_RESET)
411 UserApiKeys.ROLE_PASSWORD_RESET)
410 self.session.flash(
412 h.flash(
411 _('Given reset token is invalid'), queue='error')
413 _('Given reset token is invalid'), category='error')
412 return HTTPFound(self.request.route_path('reset_password'))
414 return HTTPFound(self.request.route_path('reset_password'))
413
415
414 try:
416 try:
415 owner = token.user
417 owner = token.user
416 data = {'email': owner.email, 'token': token.api_key}
418 data = {'email': owner.email, 'token': token.api_key}
417 UserModel().reset_password(data)
419 UserModel().reset_password(data)
418 self.session.flash(
420 h.flash(
419 _('Your password reset was successful, '
421 _('Your password reset was successful, '
420 'a new password has been sent to your email'),
422 'a new password has been sent to your email'),
421 queue='success')
423 category='success')
422 except Exception as e:
424 except Exception as e:
423 log.error(e)
425 log.error(e)
424 return HTTPFound(self.request.route_path('reset_password'))
426 return HTTPFound(self.request.route_path('reset_password'))
425
427
426 return HTTPFound(self.request.route_path('login'))
428 return HTTPFound(self.request.route_path('login'))
@@ -1,203 +1,203 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 # -*- coding: utf-8 -*-
20 # -*- coding: utf-8 -*-
21
21
22 # Copyright (C) 2016-2017 RhodeCode GmbH
22 # Copyright (C) 2016-2017 RhodeCode GmbH
23 #
23 #
24 # This program is free software: you can redistribute it and/or modify
24 # This program is free software: you can redistribute it and/or modify
25 # it under the terms of the GNU Affero General Public License, version 3
25 # it under the terms of the GNU Affero General Public License, version 3
26 # (only), as published by the Free Software Foundation.
26 # (only), as published by the Free Software Foundation.
27 #
27 #
28 # This program is distributed in the hope that it will be useful,
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
31 # GNU General Public License for more details.
32 #
32 #
33 # You should have received a copy of the GNU Affero General Public License
33 # You should have received a copy of the GNU Affero General Public License
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 #
35 #
36 # This program is dual-licensed. If you wish to learn more about the
36 # This program is dual-licensed. If you wish to learn more about the
37 # RhodeCode Enterprise Edition, including its added features, Support services,
37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39
39
40 import pytest
40 import pytest
41
41
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.tests import TestController, assert_session_flash
43 from rhodecode.tests import TestController, assert_session_flash
44 from rhodecode.lib import helpers as h
44 from rhodecode.lib import helpers as h
45
45
46
46
47 def route_path(name, params=None, **kwargs):
47 def route_path(name, params=None, **kwargs):
48 import urllib
48 import urllib
49 from rhodecode.apps._base import ADMIN_PREFIX
49 from rhodecode.apps._base import ADMIN_PREFIX
50
50
51 base_url = {
51 base_url = {
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 }[name].format(**kwargs)
56 }[name].format(**kwargs)
57
57
58 if params:
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 return base_url
60 return base_url
61
61
62
62
63 class TestMyAccountEdit(TestController):
63 class TestMyAccountEdit(TestController):
64
64
65 def test_my_account_edit(self):
65 def test_my_account_edit(self):
66 self.log_user()
66 self.log_user()
67 response = self.app.get(route_path('my_account_edit'))
67 response = self.app.get(route_path('my_account_edit'))
68
68
69 response.mustcontain('value="test_admin')
69 response.mustcontain('value="test_admin')
70
70
71 @pytest.mark.backends("git", "hg")
71 @pytest.mark.backends("git", "hg")
72 def test_my_account_my_pullrequests(self, pr_util):
72 def test_my_account_my_pullrequests(self, pr_util):
73 self.log_user()
73 self.log_user()
74 response = self.app.get(route_path('my_account_pullrequests'))
74 response = self.app.get(route_path('my_account_pullrequests'))
75 response.mustcontain('There are currently no open pull '
75 response.mustcontain('There are currently no open pull '
76 'requests requiring your participation.')
76 'requests requiring your participation.')
77
77
78 @pytest.mark.backends("git", "hg")
78 @pytest.mark.backends("git", "hg")
79 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header):
79 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header):
80 self.log_user()
80 self.log_user()
81 response = self.app.get(route_path('my_account_pullrequests_data'),
81 response = self.app.get(route_path('my_account_pullrequests_data'),
82 extra_environ=xhr_header)
82 extra_environ=xhr_header)
83 assert response.json == {
83 assert response.json == {
84 u'data': [], u'draw': None,
84 u'data': [], u'draw': None,
85 u'recordsFiltered': 0, u'recordsTotal': 0}
85 u'recordsFiltered': 0, u'recordsTotal': 0}
86
86
87 pr = pr_util.create_pull_request(title='TestMyAccountPR')
87 pr = pr_util.create_pull_request(title='TestMyAccountPR')
88 expected = {
88 expected = {
89 'author_raw': 'RhodeCode Admin',
89 'author_raw': 'RhodeCode Admin',
90 'name_raw': pr.pull_request_id
90 'name_raw': pr.pull_request_id
91 }
91 }
92 response = self.app.get(route_path('my_account_pullrequests_data'),
92 response = self.app.get(route_path('my_account_pullrequests_data'),
93 extra_environ=xhr_header)
93 extra_environ=xhr_header)
94 assert response.json['recordsTotal'] == 1
94 assert response.json['recordsTotal'] == 1
95 assert response.json['data'][0]['author_raw'] == expected['author_raw']
95 assert response.json['data'][0]['author_raw'] == expected['author_raw']
96
96
97 assert response.json['data'][0]['author_raw'] == expected['author_raw']
97 assert response.json['data'][0]['author_raw'] == expected['author_raw']
98 assert response.json['data'][0]['name_raw'] == expected['name_raw']
98 assert response.json['data'][0]['name_raw'] == expected['name_raw']
99
99
100 @pytest.mark.parametrize(
100 @pytest.mark.parametrize(
101 "name, attrs", [
101 "name, attrs", [
102 ('firstname', {'firstname': 'new_username'}),
102 ('firstname', {'firstname': 'new_username'}),
103 ('lastname', {'lastname': 'new_username'}),
103 ('lastname', {'lastname': 'new_username'}),
104 ('admin', {'admin': True}),
104 ('admin', {'admin': True}),
105 ('admin', {'admin': False}),
105 ('admin', {'admin': False}),
106 ('extern_type', {'extern_type': 'ldap'}),
106 ('extern_type', {'extern_type': 'ldap'}),
107 ('extern_type', {'extern_type': None}),
107 ('extern_type', {'extern_type': None}),
108 # ('extern_name', {'extern_name': 'test'}),
108 # ('extern_name', {'extern_name': 'test'}),
109 # ('extern_name', {'extern_name': None}),
109 # ('extern_name', {'extern_name': None}),
110 ('active', {'active': False}),
110 ('active', {'active': False}),
111 ('active', {'active': True}),
111 ('active', {'active': True}),
112 ('email', {'email': 'some@email.com'}),
112 ('email', {'email': 'some@email.com'}),
113 ])
113 ])
114 def test_my_account_update(self, name, attrs, user_util):
114 def test_my_account_update(self, name, attrs, user_util):
115 usr = user_util.create_user(password='qweqwe')
115 usr = user_util.create_user(password='qweqwe')
116 params = usr.get_api_data() # current user data
116 params = usr.get_api_data() # current user data
117 user_id = usr.user_id
117 user_id = usr.user_id
118 self.log_user(
118 self.log_user(
119 username=usr.username, password='qweqwe')
119 username=usr.username, password='qweqwe')
120
120
121 params.update({'password_confirmation': ''})
121 params.update({'password_confirmation': ''})
122 params.update({'new_password': ''})
122 params.update({'new_password': ''})
123 params.update({'extern_type': 'rhodecode'})
123 params.update({'extern_type': 'rhodecode'})
124 params.update({'extern_name': 'rhodecode'})
124 params.update({'extern_name': 'rhodecode'})
125 params.update({'csrf_token': self.csrf_token})
125 params.update({'csrf_token': self.csrf_token})
126
126
127 params.update(attrs)
127 params.update(attrs)
128 # my account page cannot set language param yet, only for admins
128 # my account page cannot set language param yet, only for admins
129 del params['language']
129 del params['language']
130 response = self.app.post(route_path('my_account_update'), params)
130 response = self.app.post(route_path('my_account_update'), params)
131
131
132 assert_session_flash(
132 assert_session_flash(
133 response, 'Your account was updated successfully')
133 response, 'Your account was updated successfully')
134
134
135 del params['csrf_token']
135 del params['csrf_token']
136
136
137 updated_user = User.get(user_id)
137 updated_user = User.get(user_id)
138 updated_params = updated_user.get_api_data()
138 updated_params = updated_user.get_api_data()
139 updated_params.update({'password_confirmation': ''})
139 updated_params.update({'password_confirmation': ''})
140 updated_params.update({'new_password': ''})
140 updated_params.update({'new_password': ''})
141
141
142 params['last_login'] = updated_params['last_login']
142 params['last_login'] = updated_params['last_login']
143 params['last_activity'] = updated_params['last_activity']
143 params['last_activity'] = updated_params['last_activity']
144 # my account page cannot set language param yet, only for admins
144 # my account page cannot set language param yet, only for admins
145 # but we get this info from API anyway
145 # but we get this info from API anyway
146 params['language'] = updated_params['language']
146 params['language'] = updated_params['language']
147
147
148 if name == 'email':
148 if name == 'email':
149 params['emails'] = [attrs['email']]
149 params['emails'] = [attrs['email']]
150 if name == 'extern_type':
150 if name == 'extern_type':
151 # cannot update this via form, expected value is original one
151 # cannot update this via form, expected value is original one
152 params['extern_type'] = "rhodecode"
152 params['extern_type'] = "rhodecode"
153 if name == 'extern_name':
153 if name == 'extern_name':
154 # cannot update this via form, expected value is original one
154 # cannot update this via form, expected value is original one
155 params['extern_name'] = str(user_id)
155 params['extern_name'] = str(user_id)
156 if name == 'active':
156 if name == 'active':
157 # my account cannot deactivate account
157 # my account cannot deactivate account
158 params['active'] = True
158 params['active'] = True
159 if name == 'admin':
159 if name == 'admin':
160 # my account cannot make you an admin !
160 # my account cannot make you an admin !
161 params['admin'] = False
161 params['admin'] = False
162
162
163 assert params == updated_params
163 assert params == updated_params
164
164
165 def test_my_account_update_err_email_exists(self):
165 def test_my_account_update_err_email_exists(self):
166 self.log_user()
166 self.log_user()
167
167
168 new_email = 'test_regular@mail.com' # already existing email
168 new_email = 'test_regular@mail.com' # already existing email
169 params = {
169 params = {
170 'username': 'test_admin',
170 'username': 'test_admin',
171 'new_password': 'test12',
171 'new_password': 'test12',
172 'password_confirmation': 'test122',
172 'password_confirmation': 'test122',
173 'firstname': 'NewName',
173 'firstname': 'NewName',
174 'lastname': 'NewLastname',
174 'lastname': 'NewLastname',
175 'email': new_email,
175 'email': new_email,
176 'csrf_token': self.csrf_token,
176 'csrf_token': self.csrf_token,
177 }
177 }
178
178
179 response = self.app.post(route_path('my_account_update'),
179 response = self.app.post(route_path('my_account_update'),
180 params=params)
180 params=params)
181
181
182 response.mustcontain('This e-mail address is already taken')
182 response.mustcontain('This e-mail address is already taken')
183
183
184 def test_my_account_update_bad_email_address(self):
184 def test_my_account_update_bad_email_address(self):
185 self.log_user('test_regular2', 'test12')
185 self.log_user('test_regular2', 'test12')
186
186
187 new_email = 'newmail.pl'
187 new_email = 'newmail.pl'
188 params = {
188 params = {
189 'username': 'test_admin',
189 'username': 'test_admin',
190 'new_password': 'test12',
190 'new_password': 'test12',
191 'password_confirmation': 'test122',
191 'password_confirmation': 'test122',
192 'firstname': 'NewName',
192 'firstname': 'NewName',
193 'lastname': 'NewLastname',
193 'lastname': 'NewLastname',
194 'email': new_email,
194 'email': new_email,
195 'csrf_token': self.csrf_token,
195 'csrf_token': self.csrf_token,
196 }
196 }
197 response = self.app.post(route_path('my_account_update'),
197 response = self.app.post(route_path('my_account_update'),
198 params=params)
198 params=params)
199
199
200 response.mustcontain('An email address must contain a single @')
200 response.mustcontain('An email address must contain a single @')
201 msg = '???'
201 msg = u'Username "%(username)s" already exists'
202 msg = h.html_escape(msg % {'username': 'test_admin'})
202 msg = h.html_escape(msg % {'username': 'test_admin'})
203 response.mustcontain(u"%s" % msg)
203 response.mustcontain(u"%s" % msg)
@@ -1,584 +1,585 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode import forms
32 from rhodecode import forms
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
37 from rhodecode.lib.channelstream import (
37 from rhodecode.lib.channelstream import (
38 channelstream_request, ChannelstreamException)
38 channelstream_request, ChannelstreamException)
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.db import (
42 from rhodecode.model.db import (
43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
44 PullRequest)
44 PullRequest)
45 from rhodecode.model.forms import UserForm, UserExtraEmailForm
45 from rhodecode.model.forms import UserForm, UserExtraEmailForm
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.scm import RepoList
48 from rhodecode.model.scm import RepoList
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.validation_schema.schemas import user_schema
51 from rhodecode.model.validation_schema.schemas import user_schema
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class MyAccountView(BaseAppView, DataGridAppView):
56 class MyAccountView(BaseAppView, DataGridAppView):
57 ALLOW_SCOPED_TOKENS = False
57 ALLOW_SCOPED_TOKENS = False
58 """
58 """
59 This view has alternative version inside EE, if modified please take a look
59 This view has alternative version inside EE, if modified please take a look
60 in there as well.
60 in there as well.
61 """
61 """
62
62
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context()
64 c = self._get_local_tmpl_context()
65 c.user = c.auth_user.get_instance()
65 c.user = c.auth_user.get_instance()
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67
67
68 return c
68 return c
69
69
70 @LoginRequired()
70 @LoginRequired()
71 @NotAnonymous()
71 @NotAnonymous()
72 @view_config(
72 @view_config(
73 route_name='my_account_profile', request_method='GET',
73 route_name='my_account_profile', request_method='GET',
74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 def my_account_profile(self):
75 def my_account_profile(self):
76 c = self.load_default_context()
76 c = self.load_default_context()
77 c.active = 'profile'
77 c.active = 'profile'
78 return self._get_template_context(c)
78 return self._get_template_context(c)
79
79
80 @LoginRequired()
80 @LoginRequired()
81 @NotAnonymous()
81 @NotAnonymous()
82 @view_config(
82 @view_config(
83 route_name='my_account_password', request_method='GET',
83 route_name='my_account_password', request_method='GET',
84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
85 def my_account_password(self):
85 def my_account_password(self):
86 c = self.load_default_context()
86 c = self.load_default_context()
87 c.active = 'password'
87 c.active = 'password'
88 c.extern_type = c.user.extern_type
88 c.extern_type = c.user.extern_type
89
89
90 schema = user_schema.ChangePasswordSchema().bind(
90 schema = user_schema.ChangePasswordSchema().bind(
91 username=c.user.username)
91 username=c.user.username)
92
92
93 form = forms.Form(
93 form = forms.Form(
94 schema,
94 schema,
95 action=h.route_path('my_account_password_update'),
95 action=h.route_path('my_account_password_update'),
96 buttons=(forms.buttons.save, forms.buttons.reset))
96 buttons=(forms.buttons.save, forms.buttons.reset))
97
97
98 c.form = form
98 c.form = form
99 return self._get_template_context(c)
99 return self._get_template_context(c)
100
100
101 @LoginRequired()
101 @LoginRequired()
102 @NotAnonymous()
102 @NotAnonymous()
103 @CSRFRequired()
103 @CSRFRequired()
104 @view_config(
104 @view_config(
105 route_name='my_account_password_update', request_method='POST',
105 route_name='my_account_password_update', request_method='POST',
106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
107 def my_account_password_update(self):
107 def my_account_password_update(self):
108 _ = self.request.translate
108 _ = self.request.translate
109 c = self.load_default_context()
109 c = self.load_default_context()
110 c.active = 'password'
110 c.active = 'password'
111 c.extern_type = c.user.extern_type
111 c.extern_type = c.user.extern_type
112
112
113 schema = user_schema.ChangePasswordSchema().bind(
113 schema = user_schema.ChangePasswordSchema().bind(
114 username=c.user.username)
114 username=c.user.username)
115
115
116 form = forms.Form(
116 form = forms.Form(
117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
118
118
119 if c.extern_type != 'rhodecode':
119 if c.extern_type != 'rhodecode':
120 raise HTTPFound(self.request.route_path('my_account_password'))
120 raise HTTPFound(self.request.route_path('my_account_password'))
121
121
122 controls = self.request.POST.items()
122 controls = self.request.POST.items()
123 try:
123 try:
124 valid_data = form.validate(controls)
124 valid_data = form.validate(controls)
125 UserModel().update_user(c.user.user_id, **valid_data)
125 UserModel().update_user(c.user.user_id, **valid_data)
126 c.user.update_userdata(force_password_change=False)
126 c.user.update_userdata(force_password_change=False)
127 Session().commit()
127 Session().commit()
128 except forms.ValidationFailure as e:
128 except forms.ValidationFailure as e:
129 c.form = e
129 c.form = e
130 return self._get_template_context(c)
130 return self._get_template_context(c)
131
131
132 except Exception:
132 except Exception:
133 log.exception("Exception updating password")
133 log.exception("Exception updating password")
134 h.flash(_('Error occurred during update of user password'),
134 h.flash(_('Error occurred during update of user password'),
135 category='error')
135 category='error')
136 else:
136 else:
137 instance = c.auth_user.get_instance()
137 instance = c.auth_user.get_instance()
138 self.session.setdefault('rhodecode_user', {}).update(
138 self.session.setdefault('rhodecode_user', {}).update(
139 {'password': md5(instance.password)})
139 {'password': md5(instance.password)})
140 self.session.save()
140 self.session.save()
141 h.flash(_("Successfully updated password"), category='success')
141 h.flash(_("Successfully updated password"), category='success')
142
142
143 raise HTTPFound(self.request.route_path('my_account_password'))
143 raise HTTPFound(self.request.route_path('my_account_password'))
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @view_config(
147 @view_config(
148 route_name='my_account_auth_tokens', request_method='GET',
148 route_name='my_account_auth_tokens', request_method='GET',
149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
150 def my_account_auth_tokens(self):
150 def my_account_auth_tokens(self):
151 _ = self.request.translate
151 _ = self.request.translate
152
152
153 c = self.load_default_context()
153 c = self.load_default_context()
154 c.active = 'auth_tokens'
154 c.active = 'auth_tokens'
155 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
155 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
156 c.role_values = [
156 c.role_values = [
157 (x, AuthTokenModel.cls._get_role_name(x))
157 (x, AuthTokenModel.cls._get_role_name(x))
158 for x in AuthTokenModel.cls.ROLES]
158 for x in AuthTokenModel.cls.ROLES]
159 c.role_options = [(c.role_values, _("Role"))]
159 c.role_options = [(c.role_values, _("Role"))]
160 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
160 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
161 c.user.user_id, show_expired=True)
161 c.user.user_id, show_expired=True)
162 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
162 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
163 return self._get_template_context(c)
163 return self._get_template_context(c)
164
164
165 def maybe_attach_token_scope(self, token):
165 def maybe_attach_token_scope(self, token):
166 # implemented in EE edition
166 # implemented in EE edition
167 pass
167 pass
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @NotAnonymous()
170 @NotAnonymous()
171 @CSRFRequired()
171 @CSRFRequired()
172 @view_config(
172 @view_config(
173 route_name='my_account_auth_tokens_add', request_method='POST',)
173 route_name='my_account_auth_tokens_add', request_method='POST',)
174 def my_account_auth_tokens_add(self):
174 def my_account_auth_tokens_add(self):
175 _ = self.request.translate
175 _ = self.request.translate
176 c = self.load_default_context()
176 c = self.load_default_context()
177
177
178 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
178 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
179 description = self.request.POST.get('description')
179 description = self.request.POST.get('description')
180 role = self.request.POST.get('role')
180 role = self.request.POST.get('role')
181
181
182 token = AuthTokenModel().create(
182 token = AuthTokenModel().create(
183 c.user.user_id, description, lifetime, role)
183 c.user.user_id, description, lifetime, role)
184 token_data = token.get_api_data()
184 token_data = token.get_api_data()
185
185
186 self.maybe_attach_token_scope(token)
186 self.maybe_attach_token_scope(token)
187 audit_logger.store_web(
187 audit_logger.store_web(
188 'user.edit.token.add', action_data={
188 'user.edit.token.add', action_data={
189 'data': {'token': token_data, 'user': 'self'}},
189 'data': {'token': token_data, 'user': 'self'}},
190 user=self._rhodecode_user, )
190 user=self._rhodecode_user, )
191 Session().commit()
191 Session().commit()
192
192
193 h.flash(_("Auth token successfully created"), category='success')
193 h.flash(_("Auth token successfully created"), category='success')
194 return HTTPFound(h.route_path('my_account_auth_tokens'))
194 return HTTPFound(h.route_path('my_account_auth_tokens'))
195
195
196 @LoginRequired()
196 @LoginRequired()
197 @NotAnonymous()
197 @NotAnonymous()
198 @CSRFRequired()
198 @CSRFRequired()
199 @view_config(
199 @view_config(
200 route_name='my_account_auth_tokens_delete', request_method='POST')
200 route_name='my_account_auth_tokens_delete', request_method='POST')
201 def my_account_auth_tokens_delete(self):
201 def my_account_auth_tokens_delete(self):
202 _ = self.request.translate
202 _ = self.request.translate
203 c = self.load_default_context()
203 c = self.load_default_context()
204
204
205 del_auth_token = self.request.POST.get('del_auth_token')
205 del_auth_token = self.request.POST.get('del_auth_token')
206
206
207 if del_auth_token:
207 if del_auth_token:
208 token = UserApiKeys.get_or_404(del_auth_token)
208 token = UserApiKeys.get_or_404(del_auth_token)
209 token_data = token.get_api_data()
209 token_data = token.get_api_data()
210
210
211 AuthTokenModel().delete(del_auth_token, c.user.user_id)
211 AuthTokenModel().delete(del_auth_token, c.user.user_id)
212 audit_logger.store_web(
212 audit_logger.store_web(
213 'user.edit.token.delete', action_data={
213 'user.edit.token.delete', action_data={
214 'data': {'token': token_data, 'user': 'self'}},
214 'data': {'token': token_data, 'user': 'self'}},
215 user=self._rhodecode_user,)
215 user=self._rhodecode_user,)
216 Session().commit()
216 Session().commit()
217 h.flash(_("Auth token successfully deleted"), category='success')
217 h.flash(_("Auth token successfully deleted"), category='success')
218
218
219 return HTTPFound(h.route_path('my_account_auth_tokens'))
219 return HTTPFound(h.route_path('my_account_auth_tokens'))
220
220
221 @LoginRequired()
221 @LoginRequired()
222 @NotAnonymous()
222 @NotAnonymous()
223 @view_config(
223 @view_config(
224 route_name='my_account_emails', request_method='GET',
224 route_name='my_account_emails', request_method='GET',
225 renderer='rhodecode:templates/admin/my_account/my_account.mako')
225 renderer='rhodecode:templates/admin/my_account/my_account.mako')
226 def my_account_emails(self):
226 def my_account_emails(self):
227 _ = self.request.translate
227 _ = self.request.translate
228
228
229 c = self.load_default_context()
229 c = self.load_default_context()
230 c.active = 'emails'
230 c.active = 'emails'
231
231
232 c.user_email_map = UserEmailMap.query()\
232 c.user_email_map = UserEmailMap.query()\
233 .filter(UserEmailMap.user == c.user).all()
233 .filter(UserEmailMap.user == c.user).all()
234 return self._get_template_context(c)
234 return self._get_template_context(c)
235
235
236 @LoginRequired()
236 @LoginRequired()
237 @NotAnonymous()
237 @NotAnonymous()
238 @CSRFRequired()
238 @CSRFRequired()
239 @view_config(
239 @view_config(
240 route_name='my_account_emails_add', request_method='POST')
240 route_name='my_account_emails_add', request_method='POST')
241 def my_account_emails_add(self):
241 def my_account_emails_add(self):
242 _ = self.request.translate
242 _ = self.request.translate
243 c = self.load_default_context()
243 c = self.load_default_context()
244
244
245 email = self.request.POST.get('new_email')
245 email = self.request.POST.get('new_email')
246
246
247 try:
247 try:
248 form = UserExtraEmailForm(self.request.translate)()
248 form = UserExtraEmailForm(self.request.translate)()
249 data = form.to_python({'email': email})
249 data = form.to_python({'email': email})
250 email = data['email']
250 email = data['email']
251
251
252 UserModel().add_extra_email(c.user.user_id, email)
252 UserModel().add_extra_email(c.user.user_id, email)
253 audit_logger.store_web(
253 audit_logger.store_web(
254 'user.edit.email.add', action_data={
254 'user.edit.email.add', action_data={
255 'data': {'email': email, 'user': 'self'}},
255 'data': {'email': email, 'user': 'self'}},
256 user=self._rhodecode_user,)
256 user=self._rhodecode_user,)
257
257
258 Session().commit()
258 Session().commit()
259 h.flash(_("Added new email address `%s` for user account") % email,
259 h.flash(_("Added new email address `%s` for user account") % email,
260 category='success')
260 category='success')
261 except formencode.Invalid as error:
261 except formencode.Invalid as error:
262 h.flash(h.escape(error.error_dict['email']), category='error')
262 h.flash(h.escape(error.error_dict['email']), category='error')
263 except Exception:
263 except Exception:
264 log.exception("Exception in my_account_emails")
264 log.exception("Exception in my_account_emails")
265 h.flash(_('An error occurred during email saving'),
265 h.flash(_('An error occurred during email saving'),
266 category='error')
266 category='error')
267 return HTTPFound(h.route_path('my_account_emails'))
267 return HTTPFound(h.route_path('my_account_emails'))
268
268
269 @LoginRequired()
269 @LoginRequired()
270 @NotAnonymous()
270 @NotAnonymous()
271 @CSRFRequired()
271 @CSRFRequired()
272 @view_config(
272 @view_config(
273 route_name='my_account_emails_delete', request_method='POST')
273 route_name='my_account_emails_delete', request_method='POST')
274 def my_account_emails_delete(self):
274 def my_account_emails_delete(self):
275 _ = self.request.translate
275 _ = self.request.translate
276 c = self.load_default_context()
276 c = self.load_default_context()
277
277
278 del_email_id = self.request.POST.get('del_email_id')
278 del_email_id = self.request.POST.get('del_email_id')
279 if del_email_id:
279 if del_email_id:
280 email = UserEmailMap.get_or_404(del_email_id).email
280 email = UserEmailMap.get_or_404(del_email_id).email
281 UserModel().delete_extra_email(c.user.user_id, del_email_id)
281 UserModel().delete_extra_email(c.user.user_id, del_email_id)
282 audit_logger.store_web(
282 audit_logger.store_web(
283 'user.edit.email.delete', action_data={
283 'user.edit.email.delete', action_data={
284 'data': {'email': email, 'user': 'self'}},
284 'data': {'email': email, 'user': 'self'}},
285 user=self._rhodecode_user,)
285 user=self._rhodecode_user,)
286 Session().commit()
286 Session().commit()
287 h.flash(_("Email successfully deleted"),
287 h.flash(_("Email successfully deleted"),
288 category='success')
288 category='success')
289 return HTTPFound(h.route_path('my_account_emails'))
289 return HTTPFound(h.route_path('my_account_emails'))
290
290
291 @LoginRequired()
291 @LoginRequired()
292 @NotAnonymous()
292 @NotAnonymous()
293 @CSRFRequired()
293 @CSRFRequired()
294 @view_config(
294 @view_config(
295 route_name='my_account_notifications_test_channelstream',
295 route_name='my_account_notifications_test_channelstream',
296 request_method='POST', renderer='json_ext')
296 request_method='POST', renderer='json_ext')
297 def my_account_notifications_test_channelstream(self):
297 def my_account_notifications_test_channelstream(self):
298 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
298 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
299 self._rhodecode_user.username, datetime.datetime.now())
299 self._rhodecode_user.username, datetime.datetime.now())
300 payload = {
300 payload = {
301 # 'channel': 'broadcast',
301 # 'channel': 'broadcast',
302 'type': 'message',
302 'type': 'message',
303 'timestamp': datetime.datetime.utcnow(),
303 'timestamp': datetime.datetime.utcnow(),
304 'user': 'system',
304 'user': 'system',
305 'pm_users': [self._rhodecode_user.username],
305 'pm_users': [self._rhodecode_user.username],
306 'message': {
306 'message': {
307 'message': message,
307 'message': message,
308 'level': 'info',
308 'level': 'info',
309 'topic': '/notifications'
309 'topic': '/notifications'
310 }
310 }
311 }
311 }
312
312
313 registry = self.request.registry
313 registry = self.request.registry
314 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
314 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
315 channelstream_config = rhodecode_plugins.get('channelstream', {})
315 channelstream_config = rhodecode_plugins.get('channelstream', {})
316
316
317 try:
317 try:
318 channelstream_request(channelstream_config, [payload], '/message')
318 channelstream_request(channelstream_config, [payload], '/message')
319 except ChannelstreamException as e:
319 except ChannelstreamException as e:
320 log.exception('Failed to send channelstream data')
320 log.exception('Failed to send channelstream data')
321 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
321 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
322 return {"response": 'Channelstream data sent. '
322 return {"response": 'Channelstream data sent. '
323 'You should see a new live message now.'}
323 'You should see a new live message now.'}
324
324
325 def _load_my_repos_data(self, watched=False):
325 def _load_my_repos_data(self, watched=False):
326 if watched:
326 if watched:
327 admin = False
327 admin = False
328 follows_repos = Session().query(UserFollowing)\
328 follows_repos = Session().query(UserFollowing)\
329 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
329 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
330 .options(joinedload(UserFollowing.follows_repository))\
330 .options(joinedload(UserFollowing.follows_repository))\
331 .all()
331 .all()
332 repo_list = [x.follows_repository for x in follows_repos]
332 repo_list = [x.follows_repository for x in follows_repos]
333 else:
333 else:
334 admin = True
334 admin = True
335 repo_list = Repository.get_all_repos(
335 repo_list = Repository.get_all_repos(
336 user_id=self._rhodecode_user.user_id)
336 user_id=self._rhodecode_user.user_id)
337 repo_list = RepoList(repo_list, perm_set=[
337 repo_list = RepoList(repo_list, perm_set=[
338 'repository.read', 'repository.write', 'repository.admin'])
338 'repository.read', 'repository.write', 'repository.admin'])
339
339
340 repos_data = RepoModel().get_repos_as_dict(
340 repos_data = RepoModel().get_repos_as_dict(
341 repo_list=repo_list, admin=admin)
341 repo_list=repo_list, admin=admin)
342 # json used to render the grid
342 # json used to render the grid
343 return json.dumps(repos_data)
343 return json.dumps(repos_data)
344
344
345 @LoginRequired()
345 @LoginRequired()
346 @NotAnonymous()
346 @NotAnonymous()
347 @view_config(
347 @view_config(
348 route_name='my_account_repos', request_method='GET',
348 route_name='my_account_repos', request_method='GET',
349 renderer='rhodecode:templates/admin/my_account/my_account.mako')
349 renderer='rhodecode:templates/admin/my_account/my_account.mako')
350 def my_account_repos(self):
350 def my_account_repos(self):
351 c = self.load_default_context()
351 c = self.load_default_context()
352 c.active = 'repos'
352 c.active = 'repos'
353
353
354 # json used to render the grid
354 # json used to render the grid
355 c.data = self._load_my_repos_data()
355 c.data = self._load_my_repos_data()
356 return self._get_template_context(c)
356 return self._get_template_context(c)
357
357
358 @LoginRequired()
358 @LoginRequired()
359 @NotAnonymous()
359 @NotAnonymous()
360 @view_config(
360 @view_config(
361 route_name='my_account_watched', request_method='GET',
361 route_name='my_account_watched', request_method='GET',
362 renderer='rhodecode:templates/admin/my_account/my_account.mako')
362 renderer='rhodecode:templates/admin/my_account/my_account.mako')
363 def my_account_watched(self):
363 def my_account_watched(self):
364 c = self.load_default_context()
364 c = self.load_default_context()
365 c.active = 'watched'
365 c.active = 'watched'
366
366
367 # json used to render the grid
367 # json used to render the grid
368 c.data = self._load_my_repos_data(watched=True)
368 c.data = self._load_my_repos_data(watched=True)
369 return self._get_template_context(c)
369 return self._get_template_context(c)
370
370
371 @LoginRequired()
371 @LoginRequired()
372 @NotAnonymous()
372 @NotAnonymous()
373 @view_config(
373 @view_config(
374 route_name='my_account_perms', request_method='GET',
374 route_name='my_account_perms', request_method='GET',
375 renderer='rhodecode:templates/admin/my_account/my_account.mako')
375 renderer='rhodecode:templates/admin/my_account/my_account.mako')
376 def my_account_perms(self):
376 def my_account_perms(self):
377 c = self.load_default_context()
377 c = self.load_default_context()
378 c.active = 'perms'
378 c.active = 'perms'
379
379
380 c.perm_user = c.auth_user
380 c.perm_user = c.auth_user
381 return self._get_template_context(c)
381 return self._get_template_context(c)
382
382
383 @LoginRequired()
383 @LoginRequired()
384 @NotAnonymous()
384 @NotAnonymous()
385 @view_config(
385 @view_config(
386 route_name='my_account_notifications', request_method='GET',
386 route_name='my_account_notifications', request_method='GET',
387 renderer='rhodecode:templates/admin/my_account/my_account.mako')
387 renderer='rhodecode:templates/admin/my_account/my_account.mako')
388 def my_notifications(self):
388 def my_notifications(self):
389 c = self.load_default_context()
389 c = self.load_default_context()
390 c.active = 'notifications'
390 c.active = 'notifications'
391
391
392 return self._get_template_context(c)
392 return self._get_template_context(c)
393
393
394 @LoginRequired()
394 @LoginRequired()
395 @NotAnonymous()
395 @NotAnonymous()
396 @CSRFRequired()
396 @CSRFRequired()
397 @view_config(
397 @view_config(
398 route_name='my_account_notifications_toggle_visibility',
398 route_name='my_account_notifications_toggle_visibility',
399 request_method='POST', renderer='json_ext')
399 request_method='POST', renderer='json_ext')
400 def my_notifications_toggle_visibility(self):
400 def my_notifications_toggle_visibility(self):
401 user = self._rhodecode_db_user
401 user = self._rhodecode_db_user
402 new_status = not user.user_data.get('notification_status', True)
402 new_status = not user.user_data.get('notification_status', True)
403 user.update_userdata(notification_status=new_status)
403 user.update_userdata(notification_status=new_status)
404 Session().commit()
404 Session().commit()
405 return user.user_data['notification_status']
405 return user.user_data['notification_status']
406
406
407 @LoginRequired()
407 @LoginRequired()
408 @NotAnonymous()
408 @NotAnonymous()
409 @view_config(
409 @view_config(
410 route_name='my_account_edit',
410 route_name='my_account_edit',
411 request_method='GET',
411 request_method='GET',
412 renderer='rhodecode:templates/admin/my_account/my_account.mako')
412 renderer='rhodecode:templates/admin/my_account/my_account.mako')
413 def my_account_edit(self):
413 def my_account_edit(self):
414 c = self.load_default_context()
414 c = self.load_default_context()
415 c.active = 'profile_edit'
415 c.active = 'profile_edit'
416
416
417 c.perm_user = c.auth_user
417 c.perm_user = c.auth_user
418 c.extern_type = c.user.extern_type
418 c.extern_type = c.user.extern_type
419 c.extern_name = c.user.extern_name
419 c.extern_name = c.user.extern_name
420
420
421 defaults = c.user.get_dict()
421 defaults = c.user.get_dict()
422
422
423 data = render('rhodecode:templates/admin/my_account/my_account.mako',
423 data = render('rhodecode:templates/admin/my_account/my_account.mako',
424 self._get_template_context(c), self.request)
424 self._get_template_context(c), self.request)
425 html = formencode.htmlfill.render(
425 html = formencode.htmlfill.render(
426 data,
426 data,
427 defaults=defaults,
427 defaults=defaults,
428 encoding="UTF-8",
428 encoding="UTF-8",
429 force_defaults=False
429 force_defaults=False
430 )
430 )
431 return Response(html)
431 return Response(html)
432
432
433 @LoginRequired()
433 @LoginRequired()
434 @NotAnonymous()
434 @NotAnonymous()
435 @CSRFRequired()
435 @CSRFRequired()
436 @view_config(
436 @view_config(
437 route_name='my_account_update',
437 route_name='my_account_update',
438 request_method='POST',
438 request_method='POST',
439 renderer='rhodecode:templates/admin/my_account/my_account.mako')
439 renderer='rhodecode:templates/admin/my_account/my_account.mako')
440 def my_account_update(self):
440 def my_account_update(self):
441 _ = self.request.translate
441 _ = self.request.translate
442 c = self.load_default_context()
442 c = self.load_default_context()
443 c.active = 'profile_edit'
443 c.active = 'profile_edit'
444
444
445 c.perm_user = c.auth_user
445 c.perm_user = c.auth_user
446 c.extern_type = c.user.extern_type
446 c.extern_type = c.user.extern_type
447 c.extern_name = c.user.extern_name
447 c.extern_name = c.user.extern_name
448
448
449 _form = UserForm(self.request.translate, edit=True,
449 _form = UserForm(self.request.translate, edit=True,
450 old_data={'user_id': self._rhodecode_user.user_id,
450 old_data={'user_id': self._rhodecode_user.user_id,
451 'email': self._rhodecode_user.email})()
451 'email': self._rhodecode_user.email})()
452 form_result = {}
452 form_result = {}
453 try:
453 try:
454 post_data = dict(self.request.POST)
454 post_data = dict(self.request.POST)
455 post_data['new_password'] = ''
455 post_data['new_password'] = ''
456 post_data['password_confirmation'] = ''
456 post_data['password_confirmation'] = ''
457 form_result = _form.to_python(post_data)
457 form_result = _form.to_python(post_data)
458 # skip updating those attrs for my account
458 # skip updating those attrs for my account
459 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
459 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
460 'new_password', 'password_confirmation']
460 'new_password', 'password_confirmation']
461 # TODO: plugin should define if username can be updated
461 # TODO: plugin should define if username can be updated
462 if c.extern_type != "rhodecode":
462 if c.extern_type != "rhodecode":
463 # forbid updating username for external accounts
463 # forbid updating username for external accounts
464 skip_attrs.append('username')
464 skip_attrs.append('username')
465
465
466 UserModel().update_user(
466 UserModel().update_user(
467 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
467 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
468 **form_result)
468 **form_result)
469 h.flash(_('Your account was updated successfully'),
469 h.flash(_('Your account was updated successfully'),
470 category='success')
470 category='success')
471 Session().commit()
471 Session().commit()
472
472
473 except formencode.Invalid as errors:
473 except formencode.Invalid as errors:
474 data = render(
474 data = render(
475 'rhodecode:templates/admin/my_account/my_account.mako',
475 'rhodecode:templates/admin/my_account/my_account.mako',
476 self._get_template_context(c), self.request)
476 self._get_template_context(c), self.request)
477
477
478 html = formencode.htmlfill.render(
478 html = formencode.htmlfill.render(
479 data,
479 data,
480 defaults=errors.value,
480 defaults=errors.value,
481 errors=errors.error_dict or {},
481 errors=errors.error_dict or {},
482 prefix_error=False,
482 prefix_error=False,
483 encoding="UTF-8",
483 encoding="UTF-8",
484 force_defaults=False)
484 force_defaults=False)
485 return Response(html)
485 return Response(html)
486
486
487 except Exception:
487 except Exception:
488 log.exception("Exception updating user")
488 log.exception("Exception updating user")
489 h.flash(_('Error occurred during update of user %s')
489 h.flash(_('Error occurred during update of user %s')
490 % form_result.get('username'), category='error')
490 % form_result.get('username'), category='error')
491 raise HTTPFound(h.route_path('my_account_profile'))
491 raise HTTPFound(h.route_path('my_account_profile'))
492
492
493 raise HTTPFound(h.route_path('my_account_profile'))
493 raise HTTPFound(h.route_path('my_account_profile'))
494
494
495 def _get_pull_requests_list(self, statuses):
495 def _get_pull_requests_list(self, statuses):
496 draw, start, limit = self._extract_chunk(self.request)
496 draw, start, limit = self._extract_chunk(self.request)
497 search_q, order_by, order_dir = self._extract_ordering(self.request)
497 search_q, order_by, order_dir = self._extract_ordering(self.request)
498 _render = self.request.get_partial_renderer(
498 _render = self.request.get_partial_renderer(
499 'rhodecode:templates/data_table/_dt_elements.mako')
499 'rhodecode:templates/data_table/_dt_elements.mako')
500
500
501 pull_requests = PullRequestModel().get_im_participating_in(
501 pull_requests = PullRequestModel().get_im_participating_in(
502 user_id=self._rhodecode_user.user_id,
502 user_id=self._rhodecode_user.user_id,
503 statuses=statuses,
503 statuses=statuses,
504 offset=start, length=limit, order_by=order_by,
504 offset=start, length=limit, order_by=order_by,
505 order_dir=order_dir)
505 order_dir=order_dir)
506
506
507 pull_requests_total_count = PullRequestModel().count_im_participating_in(
507 pull_requests_total_count = PullRequestModel().count_im_participating_in(
508 user_id=self._rhodecode_user.user_id, statuses=statuses)
508 user_id=self._rhodecode_user.user_id, statuses=statuses)
509
509
510 data = []
510 data = []
511 comments_model = CommentsModel()
511 comments_model = CommentsModel()
512 for pr in pull_requests:
512 for pr in pull_requests:
513 repo_id = pr.target_repo_id
513 repo_id = pr.target_repo_id
514 comments = comments_model.get_all_comments(
514 comments = comments_model.get_all_comments(
515 repo_id, pull_request=pr)
515 repo_id, pull_request=pr)
516 owned = pr.user_id == self._rhodecode_user.user_id
516 owned = pr.user_id == self._rhodecode_user.user_id
517
517
518 data.append({
518 data.append({
519 'target_repo': _render('pullrequest_target_repo',
519 'target_repo': _render('pullrequest_target_repo',
520 pr.target_repo.repo_name),
520 pr.target_repo.repo_name),
521 'name': _render('pullrequest_name',
521 'name': _render('pullrequest_name',
522 pr.pull_request_id, pr.target_repo.repo_name,
522 pr.pull_request_id, pr.target_repo.repo_name,
523 short=True),
523 short=True),
524 'name_raw': pr.pull_request_id,
524 'name_raw': pr.pull_request_id,
525 'status': _render('pullrequest_status',
525 'status': _render('pullrequest_status',
526 pr.calculated_review_status()),
526 pr.calculated_review_status()),
527 'title': _render(
527 'title': _render(
528 'pullrequest_title', pr.title, pr.description),
528 'pullrequest_title', pr.title, pr.description),
529 'description': h.escape(pr.description),
529 'description': h.escape(pr.description),
530 'updated_on': _render('pullrequest_updated_on',
530 'updated_on': _render('pullrequest_updated_on',
531 h.datetime_to_time(pr.updated_on)),
531 h.datetime_to_time(pr.updated_on)),
532 'updated_on_raw': h.datetime_to_time(pr.updated_on),
532 'updated_on_raw': h.datetime_to_time(pr.updated_on),
533 'created_on': _render('pullrequest_updated_on',
533 'created_on': _render('pullrequest_updated_on',
534 h.datetime_to_time(pr.created_on)),
534 h.datetime_to_time(pr.created_on)),
535 'created_on_raw': h.datetime_to_time(pr.created_on),
535 'created_on_raw': h.datetime_to_time(pr.created_on),
536 'author': _render('pullrequest_author',
536 'author': _render('pullrequest_author',
537 pr.author.full_contact, ),
537 pr.author.full_contact, ),
538 'author_raw': pr.author.full_name,
538 'author_raw': pr.author.full_name,
539 'comments': _render('pullrequest_comments', len(comments)),
539 'comments': _render('pullrequest_comments', len(comments)),
540 'comments_raw': len(comments),
540 'comments_raw': len(comments),
541 'closed': pr.is_closed(),
541 'closed': pr.is_closed(),
542 'owned': owned
542 'owned': owned
543 })
543 })
544
544
545 # json used to render the grid
545 # json used to render the grid
546 data = ({
546 data = ({
547 'draw': draw,
547 'draw': draw,
548 'data': data,
548 'data': data,
549 'recordsTotal': pull_requests_total_count,
549 'recordsTotal': pull_requests_total_count,
550 'recordsFiltered': pull_requests_total_count,
550 'recordsFiltered': pull_requests_total_count,
551 })
551 })
552 return data
552 return data
553
553
554 @LoginRequired()
554 @LoginRequired()
555 @NotAnonymous()
555 @NotAnonymous()
556 @view_config(
556 @view_config(
557 route_name='my_account_pullrequests',
557 route_name='my_account_pullrequests',
558 request_method='GET',
558 request_method='GET',
559 renderer='rhodecode:templates/admin/my_account/my_account.mako')
559 renderer='rhodecode:templates/admin/my_account/my_account.mako')
560 def my_account_pullrequests(self):
560 def my_account_pullrequests(self):
561 c = self.load_default_context()
561 c = self.load_default_context()
562 c.active = 'pullrequests'
562 c.active = 'pullrequests'
563 req_get = self.request.GET
563 req_get = self.request.GET
564
564
565 c.closed = str2bool(req_get.get('pr_show_closed'))
565 c.closed = str2bool(req_get.get('pr_show_closed'))
566
566
567 return self._get_template_context(c)
567 return self._get_template_context(c)
568
568
569 @LoginRequired()
569 @LoginRequired()
570 @NotAnonymous()
570 @NotAnonymous()
571 @view_config(
571 @view_config(
572 route_name='my_account_pullrequests_data',
572 route_name='my_account_pullrequests_data',
573 request_method='GET', renderer='json_ext')
573 request_method='GET', renderer='json_ext')
574 def my_account_pullrequests_data(self):
574 def my_account_pullrequests_data(self):
575 self.load_default_context()
575 req_get = self.request.GET
576 req_get = self.request.GET
576 closed = str2bool(req_get.get('closed'))
577 closed = str2bool(req_get.get('closed'))
577
578
578 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
579 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
579 if closed:
580 if closed:
580 statuses += [PullRequest.STATUS_CLOSED]
581 statuses += [PullRequest.STATUS_CLOSED]
581
582
582 data = self._get_pull_requests_list(statuses=statuses)
583 data = self._get_pull_requests_list(statuses=statuses)
583 return data
584 return data
584
585
@@ -1,1064 +1,1068 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.compat import OrderedDict
28 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.ext_json import json
29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.vcs import nodes
30 from rhodecode.lib.vcs import nodes
31
31
32 from rhodecode.lib.vcs.conf import settings
32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.tests import assert_session_flash
33 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests.fixture import Fixture
34 from rhodecode.tests.fixture import Fixture
35
35
36 fixture = Fixture()
36 fixture = Fixture()
37
37
38
38
39 def get_node_history(backend_type):
39 def get_node_history(backend_type):
40 return {
40 return {
41 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
41 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
42 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
42 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
43 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
43 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
44 }[backend_type]
44 }[backend_type]
45
45
46
46
47 def route_path(name, params=None, **kwargs):
47 def route_path(name, params=None, **kwargs):
48 import urllib
48 import urllib
49
49
50 base_url = {
50 base_url = {
51 'repo_summary': '/{repo_name}',
51 'repo_archivefile': '/{repo_name}/archive/{fname}',
52 'repo_archivefile': '/{repo_name}/archive/{fname}',
52 'repo_files_diff': '/{repo_name}/diff/{f_path}',
53 'repo_files_diff': '/{repo_name}/diff/{f_path}',
53 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
54 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
54 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
55 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
55 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
56 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
56 'repo_files:default_commit': '/{repo_name}/files',
57 'repo_files:default_commit': '/{repo_name}/files',
57 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
58 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
58 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
59 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
59 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
60 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
60 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
61 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
61 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
62 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
62 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
63 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
63 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
64 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
64 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
65 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
65 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
66 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
66 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
67 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
67 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
68 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
68 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
69 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
69 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
70 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
70 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
71 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
71 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
72 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
72 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
73 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
73 }[name].format(**kwargs)
74 }[name].format(**kwargs)
74
75
75 if params:
76 if params:
76 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
77 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
77 return base_url
78 return base_url
78
79
79
80
80 def assert_files_in_response(response, files, params):
81 def assert_files_in_response(response, files, params):
81 template = (
82 template = (
82 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
83 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
83 _assert_items_in_response(response, files, template, params)
84 _assert_items_in_response(response, files, template, params)
84
85
85
86
86 def assert_dirs_in_response(response, dirs, params):
87 def assert_dirs_in_response(response, dirs, params):
87 template = (
88 template = (
88 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
89 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
89 _assert_items_in_response(response, dirs, template, params)
90 _assert_items_in_response(response, dirs, template, params)
90
91
91
92
92 def _assert_items_in_response(response, items, template, params):
93 def _assert_items_in_response(response, items, template, params):
93 for item in items:
94 for item in items:
94 item_params = {'name': item}
95 item_params = {'name': item}
95 item_params.update(params)
96 item_params.update(params)
96 response.mustcontain(template % item_params)
97 response.mustcontain(template % item_params)
97
98
98
99
99 def assert_timeago_in_response(response, items, params):
100 def assert_timeago_in_response(response, items, params):
100 for item in items:
101 for item in items:
101 response.mustcontain(h.age_component(params['date']))
102 response.mustcontain(h.age_component(params['date']))
102
103
103
104
104 @pytest.mark.usefixtures("app")
105 @pytest.mark.usefixtures("app")
105 class TestFilesViews(object):
106 class TestFilesViews(object):
106
107
107 def test_show_files(self, backend):
108 def test_show_files(self, backend):
108 response = self.app.get(
109 response = self.app.get(
109 route_path('repo_files',
110 route_path('repo_files',
110 repo_name=backend.repo_name,
111 repo_name=backend.repo_name,
111 commit_id='tip', f_path='/'))
112 commit_id='tip', f_path='/'))
112 commit = backend.repo.get_commit()
113 commit = backend.repo.get_commit()
113
114
114 params = {
115 params = {
115 'repo_name': backend.repo_name,
116 'repo_name': backend.repo_name,
116 'commit_id': commit.raw_id,
117 'commit_id': commit.raw_id,
117 'date': commit.date
118 'date': commit.date
118 }
119 }
119 assert_dirs_in_response(response, ['docs', 'vcs'], params)
120 assert_dirs_in_response(response, ['docs', 'vcs'], params)
120 files = [
121 files = [
121 '.gitignore',
122 '.gitignore',
122 '.hgignore',
123 '.hgignore',
123 '.hgtags',
124 '.hgtags',
124 # TODO: missing in Git
125 # TODO: missing in Git
125 # '.travis.yml',
126 # '.travis.yml',
126 'MANIFEST.in',
127 'MANIFEST.in',
127 'README.rst',
128 'README.rst',
128 # TODO: File is missing in svn repository
129 # TODO: File is missing in svn repository
129 # 'run_test_and_report.sh',
130 # 'run_test_and_report.sh',
130 'setup.cfg',
131 'setup.cfg',
131 'setup.py',
132 'setup.py',
132 'test_and_report.sh',
133 'test_and_report.sh',
133 'tox.ini',
134 'tox.ini',
134 ]
135 ]
135 assert_files_in_response(response, files, params)
136 assert_files_in_response(response, files, params)
136 assert_timeago_in_response(response, files, params)
137 assert_timeago_in_response(response, files, params)
137
138
138 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
139 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
139 repo = backend_hg['subrepos']
140 repo = backend_hg['subrepos']
140 response = self.app.get(
141 response = self.app.get(
141 route_path('repo_files',
142 route_path('repo_files',
142 repo_name=repo.repo_name,
143 repo_name=repo.repo_name,
143 commit_id='tip', f_path='/'))
144 commit_id='tip', f_path='/'))
144 assert_response = response.assert_response()
145 assert_response = response.assert_response()
145 assert_response.contains_one_link(
146 assert_response.contains_one_link(
146 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
147 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
147
148
148 def test_show_files_links_submodules_with_absolute_url_subpaths(
149 def test_show_files_links_submodules_with_absolute_url_subpaths(
149 self, backend_hg):
150 self, backend_hg):
150 repo = backend_hg['subrepos']
151 repo = backend_hg['subrepos']
151 response = self.app.get(
152 response = self.app.get(
152 route_path('repo_files',
153 route_path('repo_files',
153 repo_name=repo.repo_name,
154 repo_name=repo.repo_name,
154 commit_id='tip', f_path='/'))
155 commit_id='tip', f_path='/'))
155 assert_response = response.assert_response()
156 assert_response = response.assert_response()
156 assert_response.contains_one_link(
157 assert_response.contains_one_link(
157 'subpaths-path @ 000000000000',
158 'subpaths-path @ 000000000000',
158 'http://sub-base.example.com/subpaths-path')
159 'http://sub-base.example.com/subpaths-path')
159
160
160 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
161 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
161 def test_files_menu(self, backend):
162 def test_files_menu(self, backend):
162 new_branch = "temp_branch_name"
163 new_branch = "temp_branch_name"
163 commits = [
164 commits = [
164 {'message': 'a'},
165 {'message': 'a'},
165 {'message': 'b', 'branch': new_branch}
166 {'message': 'b', 'branch': new_branch}
166 ]
167 ]
167 backend.create_repo(commits)
168 backend.create_repo(commits)
168
169
169 backend.repo.landing_rev = "branch:%s" % new_branch
170 backend.repo.landing_rev = "branch:%s" % new_branch
170
171
171 # get response based on tip and not new commit
172 # get response based on tip and not new commit
172 response = self.app.get(
173 response = self.app.get(
173 route_path('repo_files',
174 route_path('repo_files',
174 repo_name=backend.repo_name,
175 repo_name=backend.repo_name,
175 commit_id='tip', f_path='/'))
176 commit_id='tip', f_path='/'))
176
177
177 # make sure Files menu url is not tip but new commit
178 # make sure Files menu url is not tip but new commit
178 landing_rev = backend.repo.landing_rev[1]
179 landing_rev = backend.repo.landing_rev[1]
179 files_url = route_path('repo_files:default_path',
180 files_url = route_path('repo_files:default_path',
180 repo_name=backend.repo_name,
181 repo_name=backend.repo_name,
181 commit_id=landing_rev)
182 commit_id=landing_rev)
182
183
183 assert landing_rev != 'tip'
184 assert landing_rev != 'tip'
184 response.mustcontain(
185 response.mustcontain(
185 '<li class="active"><a class="menulink" href="%s">' % files_url)
186 '<li class="active"><a class="menulink" href="%s">' % files_url)
186
187
187 def test_show_files_commit(self, backend):
188 def test_show_files_commit(self, backend):
188 commit = backend.repo.get_commit(commit_idx=32)
189 commit = backend.repo.get_commit(commit_idx=32)
189
190
190 response = self.app.get(
191 response = self.app.get(
191 route_path('repo_files',
192 route_path('repo_files',
192 repo_name=backend.repo_name,
193 repo_name=backend.repo_name,
193 commit_id=commit.raw_id, f_path='/'))
194 commit_id=commit.raw_id, f_path='/'))
194
195
195 dirs = ['docs', 'tests']
196 dirs = ['docs', 'tests']
196 files = ['README.rst']
197 files = ['README.rst']
197 params = {
198 params = {
198 'repo_name': backend.repo_name,
199 'repo_name': backend.repo_name,
199 'commit_id': commit.raw_id,
200 'commit_id': commit.raw_id,
200 }
201 }
201 assert_dirs_in_response(response, dirs, params)
202 assert_dirs_in_response(response, dirs, params)
202 assert_files_in_response(response, files, params)
203 assert_files_in_response(response, files, params)
203
204
204 def test_show_files_different_branch(self, backend):
205 def test_show_files_different_branch(self, backend):
205 branches = dict(
206 branches = dict(
206 hg=(150, ['git']),
207 hg=(150, ['git']),
207 # TODO: Git test repository does not contain other branches
208 # TODO: Git test repository does not contain other branches
208 git=(633, ['master']),
209 git=(633, ['master']),
209 # TODO: Branch support in Subversion
210 # TODO: Branch support in Subversion
210 svn=(150, [])
211 svn=(150, [])
211 )
212 )
212 idx, branches = branches[backend.alias]
213 idx, branches = branches[backend.alias]
213 commit = backend.repo.get_commit(commit_idx=idx)
214 commit = backend.repo.get_commit(commit_idx=idx)
214 response = self.app.get(
215 response = self.app.get(
215 route_path('repo_files',
216 route_path('repo_files',
216 repo_name=backend.repo_name,
217 repo_name=backend.repo_name,
217 commit_id=commit.raw_id, f_path='/'))
218 commit_id=commit.raw_id, f_path='/'))
218
219
219 assert_response = response.assert_response()
220 assert_response = response.assert_response()
220 for branch in branches:
221 for branch in branches:
221 assert_response.element_contains('.tags .branchtag', branch)
222 assert_response.element_contains('.tags .branchtag', branch)
222
223
223 def test_show_files_paging(self, backend):
224 def test_show_files_paging(self, backend):
224 repo = backend.repo
225 repo = backend.repo
225 indexes = [73, 92, 109, 1, 0]
226 indexes = [73, 92, 109, 1, 0]
226 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
227 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
227 for rev in indexes]
228 for rev in indexes]
228
229
229 for idx in idx_map:
230 for idx in idx_map:
230 response = self.app.get(
231 response = self.app.get(
231 route_path('repo_files',
232 route_path('repo_files',
232 repo_name=backend.repo_name,
233 repo_name=backend.repo_name,
233 commit_id=idx[1], f_path='/'))
234 commit_id=idx[1], f_path='/'))
234
235
235 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
236 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
236
237
237 def test_file_source(self, backend):
238 def test_file_source(self, backend):
238 commit = backend.repo.get_commit(commit_idx=167)
239 commit = backend.repo.get_commit(commit_idx=167)
239 response = self.app.get(
240 response = self.app.get(
240 route_path('repo_files',
241 route_path('repo_files',
241 repo_name=backend.repo_name,
242 repo_name=backend.repo_name,
242 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
243 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
243
244
244 msgbox = """<div class="commit right-content">%s</div>"""
245 msgbox = """<div class="commit right-content">%s</div>"""
245 response.mustcontain(msgbox % (commit.message, ))
246 response.mustcontain(msgbox % (commit.message, ))
246
247
247 assert_response = response.assert_response()
248 assert_response = response.assert_response()
248 if commit.branch:
249 if commit.branch:
249 assert_response.element_contains(
250 assert_response.element_contains(
250 '.tags.tags-main .branchtag', commit.branch)
251 '.tags.tags-main .branchtag', commit.branch)
251 if commit.tags:
252 if commit.tags:
252 for tag in commit.tags:
253 for tag in commit.tags:
253 assert_response.element_contains('.tags.tags-main .tagtag', tag)
254 assert_response.element_contains('.tags.tags-main .tagtag', tag)
254
255
255 def test_file_source_annotated(self, backend):
256 def test_file_source_annotated(self, backend):
256 response = self.app.get(
257 response = self.app.get(
257 route_path('repo_files:annotated',
258 route_path('repo_files:annotated',
258 repo_name=backend.repo_name,
259 repo_name=backend.repo_name,
259 commit_id='tip', f_path='vcs/nodes.py'))
260 commit_id='tip', f_path='vcs/nodes.py'))
260 expected_commits = {
261 expected_commits = {
261 'hg': 'r356',
262 'hg': 'r356',
262 'git': 'r345',
263 'git': 'r345',
263 'svn': 'r208',
264 'svn': 'r208',
264 }
265 }
265 response.mustcontain(expected_commits[backend.alias])
266 response.mustcontain(expected_commits[backend.alias])
266
267
267 def test_file_source_authors(self, backend):
268 def test_file_source_authors(self, backend):
268 response = self.app.get(
269 response = self.app.get(
269 route_path('repo_file_authors',
270 route_path('repo_file_authors',
270 repo_name=backend.repo_name,
271 repo_name=backend.repo_name,
271 commit_id='tip', f_path='vcs/nodes.py'))
272 commit_id='tip', f_path='vcs/nodes.py'))
272 expected_authors = {
273 expected_authors = {
273 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
274 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
274 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
275 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
275 'svn': ('marcin', 'lukasz'),
276 'svn': ('marcin', 'lukasz'),
276 }
277 }
277
278
278 for author in expected_authors[backend.alias]:
279 for author in expected_authors[backend.alias]:
279 response.mustcontain(author)
280 response.mustcontain(author)
280
281
281 def test_file_source_authors_with_annotation(self, backend):
282 def test_file_source_authors_with_annotation(self, backend):
282 response = self.app.get(
283 response = self.app.get(
283 route_path('repo_file_authors',
284 route_path('repo_file_authors',
284 repo_name=backend.repo_name,
285 repo_name=backend.repo_name,
285 commit_id='tip', f_path='vcs/nodes.py',
286 commit_id='tip', f_path='vcs/nodes.py',
286 params=dict(annotate=1)))
287 params=dict(annotate=1)))
287 expected_authors = {
288 expected_authors = {
288 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
289 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
289 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
290 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
290 'svn': ('marcin', 'lukasz'),
291 'svn': ('marcin', 'lukasz'),
291 }
292 }
292
293
293 for author in expected_authors[backend.alias]:
294 for author in expected_authors[backend.alias]:
294 response.mustcontain(author)
295 response.mustcontain(author)
295
296
296 def test_file_source_history(self, backend, xhr_header):
297 def test_file_source_history(self, backend, xhr_header):
297 response = self.app.get(
298 response = self.app.get(
298 route_path('repo_file_history',
299 route_path('repo_file_history',
299 repo_name=backend.repo_name,
300 repo_name=backend.repo_name,
300 commit_id='tip', f_path='vcs/nodes.py'),
301 commit_id='tip', f_path='vcs/nodes.py'),
301 extra_environ=xhr_header)
302 extra_environ=xhr_header)
302 assert get_node_history(backend.alias) == json.loads(response.body)
303 assert get_node_history(backend.alias) == json.loads(response.body)
303
304
304 def test_file_source_history_svn(self, backend_svn, xhr_header):
305 def test_file_source_history_svn(self, backend_svn, xhr_header):
305 simple_repo = backend_svn['svn-simple-layout']
306 simple_repo = backend_svn['svn-simple-layout']
306 response = self.app.get(
307 response = self.app.get(
307 route_path('repo_file_history',
308 route_path('repo_file_history',
308 repo_name=simple_repo.repo_name,
309 repo_name=simple_repo.repo_name,
309 commit_id='tip', f_path='trunk/example.py'),
310 commit_id='tip', f_path='trunk/example.py'),
310 extra_environ=xhr_header)
311 extra_environ=xhr_header)
311
312
312 expected_data = json.loads(
313 expected_data = json.loads(
313 fixture.load_resource('svn_node_history_branches.json'))
314 fixture.load_resource('svn_node_history_branches.json'))
314 assert expected_data == response.json
315 assert expected_data == response.json
315
316
316 def test_file_source_history_with_annotation(self, backend, xhr_header):
317 def test_file_source_history_with_annotation(self, backend, xhr_header):
317 response = self.app.get(
318 response = self.app.get(
318 route_path('repo_file_history',
319 route_path('repo_file_history',
319 repo_name=backend.repo_name,
320 repo_name=backend.repo_name,
320 commit_id='tip', f_path='vcs/nodes.py',
321 commit_id='tip', f_path='vcs/nodes.py',
321 params=dict(annotate=1)),
322 params=dict(annotate=1)),
322
323
323 extra_environ=xhr_header)
324 extra_environ=xhr_header)
324 assert get_node_history(backend.alias) == json.loads(response.body)
325 assert get_node_history(backend.alias) == json.loads(response.body)
325
326
326 def test_tree_search_top_level(self, backend, xhr_header):
327 def test_tree_search_top_level(self, backend, xhr_header):
327 commit = backend.repo.get_commit(commit_idx=173)
328 commit = backend.repo.get_commit(commit_idx=173)
328 response = self.app.get(
329 response = self.app.get(
329 route_path('repo_files_nodelist',
330 route_path('repo_files_nodelist',
330 repo_name=backend.repo_name,
331 repo_name=backend.repo_name,
331 commit_id=commit.raw_id, f_path='/'),
332 commit_id=commit.raw_id, f_path='/'),
332 extra_environ=xhr_header)
333 extra_environ=xhr_header)
333 assert 'nodes' in response.json
334 assert 'nodes' in response.json
334 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
335 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
335
336
336 def test_tree_search_missing_xhr(self, backend):
337 def test_tree_search_missing_xhr(self, backend):
337 self.app.get(
338 self.app.get(
338 route_path('repo_files_nodelist',
339 route_path('repo_files_nodelist',
339 repo_name=backend.repo_name,
340 repo_name=backend.repo_name,
340 commit_id='tip', f_path='/'),
341 commit_id='tip', f_path='/'),
341 status=404)
342 status=404)
342
343
343 def test_tree_search_at_path(self, backend, xhr_header):
344 def test_tree_search_at_path(self, backend, xhr_header):
344 commit = backend.repo.get_commit(commit_idx=173)
345 commit = backend.repo.get_commit(commit_idx=173)
345 response = self.app.get(
346 response = self.app.get(
346 route_path('repo_files_nodelist',
347 route_path('repo_files_nodelist',
347 repo_name=backend.repo_name,
348 repo_name=backend.repo_name,
348 commit_id=commit.raw_id, f_path='/docs'),
349 commit_id=commit.raw_id, f_path='/docs'),
349 extra_environ=xhr_header)
350 extra_environ=xhr_header)
350 assert 'nodes' in response.json
351 assert 'nodes' in response.json
351 nodes = response.json['nodes']
352 nodes = response.json['nodes']
352 assert {'name': 'docs/api', 'type': 'dir'} in nodes
353 assert {'name': 'docs/api', 'type': 'dir'} in nodes
353 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
354 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
354
355
355 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
356 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
356 commit = backend.repo.get_commit(commit_idx=173)
357 commit = backend.repo.get_commit(commit_idx=173)
357 response = self.app.get(
358 response = self.app.get(
358 route_path('repo_files_nodelist',
359 route_path('repo_files_nodelist',
359 repo_name=backend.repo_name,
360 repo_name=backend.repo_name,
360 commit_id=commit.raw_id, f_path='/docs/api'),
361 commit_id=commit.raw_id, f_path='/docs/api'),
361 extra_environ=xhr_header)
362 extra_environ=xhr_header)
362 assert 'nodes' in response.json
363 assert 'nodes' in response.json
363 nodes = response.json['nodes']
364 nodes = response.json['nodes']
364 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
365 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
365
366
366 def test_tree_search_at_path_missing_xhr(self, backend):
367 def test_tree_search_at_path_missing_xhr(self, backend):
367 self.app.get(
368 self.app.get(
368 route_path('repo_files_nodelist',
369 route_path('repo_files_nodelist',
369 repo_name=backend.repo_name,
370 repo_name=backend.repo_name,
370 commit_id='tip', f_path='/docs'),
371 commit_id='tip', f_path='/docs'),
371 status=404)
372 status=404)
372
373
373 def test_nodetree(self, backend, xhr_header):
374 def test_nodetree(self, backend, xhr_header):
374 commit = backend.repo.get_commit(commit_idx=173)
375 commit = backend.repo.get_commit(commit_idx=173)
375 response = self.app.get(
376 response = self.app.get(
376 route_path('repo_nodetree_full',
377 route_path('repo_nodetree_full',
377 repo_name=backend.repo_name,
378 repo_name=backend.repo_name,
378 commit_id=commit.raw_id, f_path='/'),
379 commit_id=commit.raw_id, f_path='/'),
379 extra_environ=xhr_header)
380 extra_environ=xhr_header)
380
381
381 assert_response = response.assert_response()
382 assert_response = response.assert_response()
382
383
383 for attr in ['data-commit-id', 'data-date', 'data-author']:
384 for attr in ['data-commit-id', 'data-date', 'data-author']:
384 elements = assert_response.get_elements('[{}]'.format(attr))
385 elements = assert_response.get_elements('[{}]'.format(attr))
385 assert len(elements) > 1
386 assert len(elements) > 1
386
387
387 for element in elements:
388 for element in elements:
388 assert element.get(attr)
389 assert element.get(attr)
389
390
390 def test_nodetree_if_file(self, backend, xhr_header):
391 def test_nodetree_if_file(self, backend, xhr_header):
391 commit = backend.repo.get_commit(commit_idx=173)
392 commit = backend.repo.get_commit(commit_idx=173)
392 response = self.app.get(
393 response = self.app.get(
393 route_path('repo_nodetree_full',
394 route_path('repo_nodetree_full',
394 repo_name=backend.repo_name,
395 repo_name=backend.repo_name,
395 commit_id=commit.raw_id, f_path='README.rst'),
396 commit_id=commit.raw_id, f_path='README.rst'),
396 extra_environ=xhr_header)
397 extra_environ=xhr_header)
397 assert response.body == ''
398 assert response.body == ''
398
399
399 def test_nodetree_wrong_path(self, backend, xhr_header):
400 def test_nodetree_wrong_path(self, backend, xhr_header):
400 commit = backend.repo.get_commit(commit_idx=173)
401 commit = backend.repo.get_commit(commit_idx=173)
401 response = self.app.get(
402 response = self.app.get(
402 route_path('repo_nodetree_full',
403 route_path('repo_nodetree_full',
403 repo_name=backend.repo_name,
404 repo_name=backend.repo_name,
404 commit_id=commit.raw_id, f_path='/dont-exist'),
405 commit_id=commit.raw_id, f_path='/dont-exist'),
405 extra_environ=xhr_header)
406 extra_environ=xhr_header)
406
407
407 err = 'error: There is no file nor ' \
408 err = 'error: There is no file nor ' \
408 'directory at the given path'
409 'directory at the given path'
409 assert err in response.body
410 assert err in response.body
410
411
411 def test_nodetree_missing_xhr(self, backend):
412 def test_nodetree_missing_xhr(self, backend):
412 self.app.get(
413 self.app.get(
413 route_path('repo_nodetree_full',
414 route_path('repo_nodetree_full',
414 repo_name=backend.repo_name,
415 repo_name=backend.repo_name,
415 commit_id='tip', f_path='/'),
416 commit_id='tip', f_path='/'),
416 status=404)
417 status=404)
417
418
418
419
419 @pytest.mark.usefixtures("app", "autologin_user")
420 @pytest.mark.usefixtures("app", "autologin_user")
420 class TestRawFileHandling(object):
421 class TestRawFileHandling(object):
421
422
422 def test_download_file(self, backend):
423 def test_download_file(self, backend):
423 commit = backend.repo.get_commit(commit_idx=173)
424 commit = backend.repo.get_commit(commit_idx=173)
424 response = self.app.get(
425 response = self.app.get(
425 route_path('repo_file_download',
426 route_path('repo_file_download',
426 repo_name=backend.repo_name,
427 repo_name=backend.repo_name,
427 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
428 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
428
429
429 assert response.content_disposition == "attachment; filename=nodes.py"
430 assert response.content_disposition == "attachment; filename=nodes.py"
430 assert response.content_type == "text/x-python"
431 assert response.content_type == "text/x-python"
431
432
432 def test_download_file_wrong_cs(self, backend):
433 def test_download_file_wrong_cs(self, backend):
433 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
434 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
434
435
435 response = self.app.get(
436 response = self.app.get(
436 route_path('repo_file_download',
437 route_path('repo_file_download',
437 repo_name=backend.repo_name,
438 repo_name=backend.repo_name,
438 commit_id=raw_id, f_path='vcs/nodes.svg'),
439 commit_id=raw_id, f_path='vcs/nodes.svg'),
439 status=404)
440 status=404)
440
441
441 msg = """No such commit exists for this repository"""
442 msg = """No such commit exists for this repository"""
442 response.mustcontain(msg)
443 response.mustcontain(msg)
443
444
444 def test_download_file_wrong_f_path(self, backend):
445 def test_download_file_wrong_f_path(self, backend):
445 commit = backend.repo.get_commit(commit_idx=173)
446 commit = backend.repo.get_commit(commit_idx=173)
446 f_path = 'vcs/ERRORnodes.py'
447 f_path = 'vcs/ERRORnodes.py'
447
448
448 response = self.app.get(
449 response = self.app.get(
449 route_path('repo_file_download',
450 route_path('repo_file_download',
450 repo_name=backend.repo_name,
451 repo_name=backend.repo_name,
451 commit_id=commit.raw_id, f_path=f_path),
452 commit_id=commit.raw_id, f_path=f_path),
452 status=404)
453 status=404)
453
454
454 msg = (
455 msg = (
455 "There is no file nor directory at the given path: "
456 "There is no file nor directory at the given path: "
456 "`%s` at commit %s" % (f_path, commit.short_id))
457 "`%s` at commit %s" % (f_path, commit.short_id))
457 response.mustcontain(msg)
458 response.mustcontain(msg)
458
459
459 def test_file_raw(self, backend):
460 def test_file_raw(self, backend):
460 commit = backend.repo.get_commit(commit_idx=173)
461 commit = backend.repo.get_commit(commit_idx=173)
461 response = self.app.get(
462 response = self.app.get(
462 route_path('repo_file_raw',
463 route_path('repo_file_raw',
463 repo_name=backend.repo_name,
464 repo_name=backend.repo_name,
464 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
465 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
465
466
466 assert response.content_type == "text/plain"
467 assert response.content_type == "text/plain"
467
468
468 def test_file_raw_binary(self, backend):
469 def test_file_raw_binary(self, backend):
469 commit = backend.repo.get_commit()
470 commit = backend.repo.get_commit()
470 response = self.app.get(
471 response = self.app.get(
471 route_path('repo_file_raw',
472 route_path('repo_file_raw',
472 repo_name=backend.repo_name,
473 repo_name=backend.repo_name,
473 commit_id=commit.raw_id,
474 commit_id=commit.raw_id,
474 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
475 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
475
476
476 assert response.content_disposition == 'inline'
477 assert response.content_disposition == 'inline'
477
478
478 def test_raw_file_wrong_cs(self, backend):
479 def test_raw_file_wrong_cs(self, backend):
479 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
480 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
480
481
481 response = self.app.get(
482 response = self.app.get(
482 route_path('repo_file_raw',
483 route_path('repo_file_raw',
483 repo_name=backend.repo_name,
484 repo_name=backend.repo_name,
484 commit_id=raw_id, f_path='vcs/nodes.svg'),
485 commit_id=raw_id, f_path='vcs/nodes.svg'),
485 status=404)
486 status=404)
486
487
487 msg = """No such commit exists for this repository"""
488 msg = """No such commit exists for this repository"""
488 response.mustcontain(msg)
489 response.mustcontain(msg)
489
490
490 def test_raw_wrong_f_path(self, backend):
491 def test_raw_wrong_f_path(self, backend):
491 commit = backend.repo.get_commit(commit_idx=173)
492 commit = backend.repo.get_commit(commit_idx=173)
492 f_path = 'vcs/ERRORnodes.py'
493 f_path = 'vcs/ERRORnodes.py'
493 response = self.app.get(
494 response = self.app.get(
494 route_path('repo_file_raw',
495 route_path('repo_file_raw',
495 repo_name=backend.repo_name,
496 repo_name=backend.repo_name,
496 commit_id=commit.raw_id, f_path=f_path),
497 commit_id=commit.raw_id, f_path=f_path),
497 status=404)
498 status=404)
498
499
499 msg = (
500 msg = (
500 "There is no file nor directory at the given path: "
501 "There is no file nor directory at the given path: "
501 "`%s` at commit %s" % (f_path, commit.short_id))
502 "`%s` at commit %s" % (f_path, commit.short_id))
502 response.mustcontain(msg)
503 response.mustcontain(msg)
503
504
504 def test_raw_svg_should_not_be_rendered(self, backend):
505 def test_raw_svg_should_not_be_rendered(self, backend):
505 backend.create_repo()
506 backend.create_repo()
506 backend.ensure_file("xss.svg")
507 backend.ensure_file("xss.svg")
507 response = self.app.get(
508 response = self.app.get(
508 route_path('repo_file_raw',
509 route_path('repo_file_raw',
509 repo_name=backend.repo_name,
510 repo_name=backend.repo_name,
510 commit_id='tip', f_path='xss.svg'),)
511 commit_id='tip', f_path='xss.svg'),)
511 # If the content type is image/svg+xml then it allows to render HTML
512 # If the content type is image/svg+xml then it allows to render HTML
512 # and malicious SVG.
513 # and malicious SVG.
513 assert response.content_type == "text/plain"
514 assert response.content_type == "text/plain"
514
515
515
516
516 @pytest.mark.usefixtures("app")
517 @pytest.mark.usefixtures("app")
517 class TestRepositoryArchival(object):
518 class TestRepositoryArchival(object):
518
519
519 def test_archival(self, backend):
520 def test_archival(self, backend):
520 backend.enable_downloads()
521 backend.enable_downloads()
521 commit = backend.repo.get_commit(commit_idx=173)
522 commit = backend.repo.get_commit(commit_idx=173)
522 for archive, info in settings.ARCHIVE_SPECS.items():
523 for archive, info in settings.ARCHIVE_SPECS.items():
523 mime_type, arch_ext = info
524 mime_type, arch_ext = info
524 short = commit.short_id + arch_ext
525 short = commit.short_id + arch_ext
525 fname = commit.raw_id + arch_ext
526 fname = commit.raw_id + arch_ext
526 filename = '%s-%s' % (backend.repo_name, short)
527 filename = '%s-%s' % (backend.repo_name, short)
527 response = self.app.get(
528 response = self.app.get(
528 route_path('repo_archivefile',
529 route_path('repo_archivefile',
529 repo_name=backend.repo_name,
530 repo_name=backend.repo_name,
530 fname=fname))
531 fname=fname))
531
532
532 assert response.status == '200 OK'
533 assert response.status == '200 OK'
533 headers = [
534 headers = [
534 ('Content-Disposition', 'attachment; filename=%s' % filename),
535 ('Content-Disposition', 'attachment; filename=%s' % filename),
535 ('Content-Type', '%s' % mime_type),
536 ('Content-Type', '%s' % mime_type),
536 ]
537 ]
537
538
538 for header in headers:
539 for header in headers:
539 assert header in response.headers.items()
540 assert header in response.headers.items()
540
541
541 @pytest.mark.parametrize('arch_ext',[
542 @pytest.mark.parametrize('arch_ext',[
542 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
543 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
543 def test_archival_wrong_ext(self, backend, arch_ext):
544 def test_archival_wrong_ext(self, backend, arch_ext):
544 backend.enable_downloads()
545 backend.enable_downloads()
545 commit = backend.repo.get_commit(commit_idx=173)
546 commit = backend.repo.get_commit(commit_idx=173)
546
547
547 fname = commit.raw_id + '.' + arch_ext
548 fname = commit.raw_id + '.' + arch_ext
548
549
549 response = self.app.get(
550 response = self.app.get(
550 route_path('repo_archivefile',
551 route_path('repo_archivefile',
551 repo_name=backend.repo_name,
552 repo_name=backend.repo_name,
552 fname=fname))
553 fname=fname))
553 response.mustcontain(
554 response.mustcontain(
554 'Unknown archive type for: `{}`'.format(fname))
555 'Unknown archive type for: `{}`'.format(fname))
555
556
556 @pytest.mark.parametrize('commit_id', [
557 @pytest.mark.parametrize('commit_id', [
557 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
558 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
558 def test_archival_wrong_commit_id(self, backend, commit_id):
559 def test_archival_wrong_commit_id(self, backend, commit_id):
559 backend.enable_downloads()
560 backend.enable_downloads()
560 fname = '%s.zip' % commit_id
561 fname = '%s.zip' % commit_id
561
562
562 response = self.app.get(
563 response = self.app.get(
563 route_path('repo_archivefile',
564 route_path('repo_archivefile',
564 repo_name=backend.repo_name,
565 repo_name=backend.repo_name,
565 fname=fname))
566 fname=fname))
566 response.mustcontain('Unknown commit_id')
567 response.mustcontain('Unknown commit_id')
567
568
568
569
569 @pytest.mark.usefixtures("app")
570 @pytest.mark.usefixtures("app")
570 class TestFilesDiff(object):
571 class TestFilesDiff(object):
571
572
572 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
573 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
573 def test_file_full_diff(self, backend, diff):
574 def test_file_full_diff(self, backend, diff):
574 commit1 = backend.repo.get_commit(commit_idx=-1)
575 commit1 = backend.repo.get_commit(commit_idx=-1)
575 commit2 = backend.repo.get_commit(commit_idx=-2)
576 commit2 = backend.repo.get_commit(commit_idx=-2)
576
577
577 response = self.app.get(
578 response = self.app.get(
578 route_path('repo_files_diff',
579 route_path('repo_files_diff',
579 repo_name=backend.repo_name,
580 repo_name=backend.repo_name,
580 f_path='README'),
581 f_path='README'),
581 params={
582 params={
582 'diff1': commit2.raw_id,
583 'diff1': commit2.raw_id,
583 'diff2': commit1.raw_id,
584 'diff2': commit1.raw_id,
584 'fulldiff': '1',
585 'fulldiff': '1',
585 'diff': diff,
586 'diff': diff,
586 })
587 })
587
588
588 if diff == 'diff':
589 if diff == 'diff':
589 # use redirect since this is OLD view redirecting to compare page
590 # use redirect since this is OLD view redirecting to compare page
590 response = response.follow()
591 response = response.follow()
591
592
592 # It's a symlink to README.rst
593 # It's a symlink to README.rst
593 response.mustcontain('README.rst')
594 response.mustcontain('README.rst')
594 response.mustcontain('No newline at end of file')
595 response.mustcontain('No newline at end of file')
595
596
596 def test_file_binary_diff(self, backend):
597 def test_file_binary_diff(self, backend):
597 commits = [
598 commits = [
598 {'message': 'First commit'},
599 {'message': 'First commit'},
599 {'message': 'Commit with binary',
600 {'message': 'Commit with binary',
600 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
601 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
601 ]
602 ]
602 repo = backend.create_repo(commits=commits)
603 repo = backend.create_repo(commits=commits)
603
604
604 response = self.app.get(
605 response = self.app.get(
605 route_path('repo_files_diff',
606 route_path('repo_files_diff',
606 repo_name=backend.repo_name,
607 repo_name=backend.repo_name,
607 f_path='file.bin'),
608 f_path='file.bin'),
608 params={
609 params={
609 'diff1': repo.get_commit(commit_idx=0).raw_id,
610 'diff1': repo.get_commit(commit_idx=0).raw_id,
610 'diff2': repo.get_commit(commit_idx=1).raw_id,
611 'diff2': repo.get_commit(commit_idx=1).raw_id,
611 'fulldiff': '1',
612 'fulldiff': '1',
612 'diff': 'diff',
613 'diff': 'diff',
613 })
614 })
614 # use redirect since this is OLD view redirecting to compare page
615 # use redirect since this is OLD view redirecting to compare page
615 response = response.follow()
616 response = response.follow()
616 response.mustcontain('Expand 1 commit')
617 response.mustcontain('Expand 1 commit')
617 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
618 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
618
619
619 if backend.alias == 'svn':
620 if backend.alias == 'svn':
620 response.mustcontain('new file 10644')
621 response.mustcontain('new file 10644')
621 # TODO(marcink): SVN doesn't yet detect binary changes
622 # TODO(marcink): SVN doesn't yet detect binary changes
622 else:
623 else:
623 response.mustcontain('new file 100644')
624 response.mustcontain('new file 100644')
624 response.mustcontain('binary diff hidden')
625 response.mustcontain('binary diff hidden')
625
626
626 def test_diff_2way(self, backend):
627 def test_diff_2way(self, backend):
627 commit1 = backend.repo.get_commit(commit_idx=-1)
628 commit1 = backend.repo.get_commit(commit_idx=-1)
628 commit2 = backend.repo.get_commit(commit_idx=-2)
629 commit2 = backend.repo.get_commit(commit_idx=-2)
629 response = self.app.get(
630 response = self.app.get(
630 route_path('repo_files_diff_2way_redirect',
631 route_path('repo_files_diff_2way_redirect',
631 repo_name=backend.repo_name,
632 repo_name=backend.repo_name,
632 f_path='README'),
633 f_path='README'),
633 params={
634 params={
634 'diff1': commit2.raw_id,
635 'diff1': commit2.raw_id,
635 'diff2': commit1.raw_id,
636 'diff2': commit1.raw_id,
636 })
637 })
637 # use redirect since this is OLD view redirecting to compare page
638 # use redirect since this is OLD view redirecting to compare page
638 response = response.follow()
639 response = response.follow()
639
640
640 # It's a symlink to README.rst
641 # It's a symlink to README.rst
641 response.mustcontain('README.rst')
642 response.mustcontain('README.rst')
642 response.mustcontain('No newline at end of file')
643 response.mustcontain('No newline at end of file')
643
644
644 def test_requires_one_commit_id(self, backend, autologin_user):
645 def test_requires_one_commit_id(self, backend, autologin_user):
645 response = self.app.get(
646 response = self.app.get(
646 route_path('repo_files_diff',
647 route_path('repo_files_diff',
647 repo_name=backend.repo_name,
648 repo_name=backend.repo_name,
648 f_path='README.rst'),
649 f_path='README.rst'),
649 status=400)
650 status=400)
650 response.mustcontain(
651 response.mustcontain(
651 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
652 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
652
653
653 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
654 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
654 repo = vcsbackend.repo
655 repo = vcsbackend.repo
655 response = self.app.get(
656 response = self.app.get(
656 route_path('repo_files_diff',
657 route_path('repo_files_diff',
657 repo_name=repo.name,
658 repo_name=repo.name,
658 f_path='does-not-exist-in-any-commit'),
659 f_path='does-not-exist-in-any-commit'),
659 params={
660 params={
660 'diff1': repo[0].raw_id,
661 'diff1': repo[0].raw_id,
661 'diff2': repo[1].raw_id
662 'diff2': repo[1].raw_id
662 })
663 })
663
664
664 response = response.follow()
665 response = response.follow()
665 response.mustcontain('No files')
666 response.mustcontain('No files')
666
667
667 def test_returns_redirect_if_file_not_changed(self, backend):
668 def test_returns_redirect_if_file_not_changed(self, backend):
668 commit = backend.repo.get_commit(commit_idx=-1)
669 commit = backend.repo.get_commit(commit_idx=-1)
669 response = self.app.get(
670 response = self.app.get(
670 route_path('repo_files_diff_2way_redirect',
671 route_path('repo_files_diff_2way_redirect',
671 repo_name=backend.repo_name,
672 repo_name=backend.repo_name,
672 f_path='README'),
673 f_path='README'),
673 params={
674 params={
674 'diff1': commit.raw_id,
675 'diff1': commit.raw_id,
675 'diff2': commit.raw_id,
676 'diff2': commit.raw_id,
676 })
677 })
677
678
678 response = response.follow()
679 response = response.follow()
679 response.mustcontain('No files')
680 response.mustcontain('No files')
680 response.mustcontain('No commits in this compare')
681 response.mustcontain('No commits in this compare')
681
682
682 def test_supports_diff_to_different_path_svn(self, backend_svn):
683 def test_supports_diff_to_different_path_svn(self, backend_svn):
683 #TODO: check this case
684 #TODO: check this case
684 return
685 return
685
686
686 repo = backend_svn['svn-simple-layout'].scm_instance()
687 repo = backend_svn['svn-simple-layout'].scm_instance()
687 commit_id_1 = '24'
688 commit_id_1 = '24'
688 commit_id_2 = '26'
689 commit_id_2 = '26'
689
690
690 response = self.app.get(
691 response = self.app.get(
691 route_path('repo_files_diff',
692 route_path('repo_files_diff',
692 repo_name=backend_svn.repo_name,
693 repo_name=backend_svn.repo_name,
693 f_path='trunk/example.py'),
694 f_path='trunk/example.py'),
694 params={
695 params={
695 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
696 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
696 'diff2': commit_id_2,
697 'diff2': commit_id_2,
697 })
698 })
698
699
699 response = response.follow()
700 response = response.follow()
700 response.mustcontain(
701 response.mustcontain(
701 # diff contains this
702 # diff contains this
702 "Will print out a useful message on invocation.")
703 "Will print out a useful message on invocation.")
703
704
704 # Note: Expecting that we indicate the user what's being compared
705 # Note: Expecting that we indicate the user what's being compared
705 response.mustcontain("trunk/example.py")
706 response.mustcontain("trunk/example.py")
706 response.mustcontain("tags/v0.2/example.py")
707 response.mustcontain("tags/v0.2/example.py")
707
708
708 def test_show_rev_redirects_to_svn_path(self, backend_svn):
709 def test_show_rev_redirects_to_svn_path(self, backend_svn):
709 #TODO: check this case
710 #TODO: check this case
710 return
711 return
711
712
712 repo = backend_svn['svn-simple-layout'].scm_instance()
713 repo = backend_svn['svn-simple-layout'].scm_instance()
713 commit_id = repo[-1].raw_id
714 commit_id = repo[-1].raw_id
714
715
715 response = self.app.get(
716 response = self.app.get(
716 route_path('repo_files_diff',
717 route_path('repo_files_diff',
717 repo_name=backend_svn.repo_name,
718 repo_name=backend_svn.repo_name,
718 f_path='trunk/example.py'),
719 f_path='trunk/example.py'),
719 params={
720 params={
720 'diff1': 'branches/argparse/example.py@' + commit_id,
721 'diff1': 'branches/argparse/example.py@' + commit_id,
721 'diff2': commit_id,
722 'diff2': commit_id,
722 },
723 },
723 status=302)
724 status=302)
724 response = response.follow()
725 response = response.follow()
725 assert response.headers['Location'].endswith(
726 assert response.headers['Location'].endswith(
726 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
727 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
727
728
728 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
729 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
729 #TODO: check this case
730 #TODO: check this case
730 return
731 return
731
732
732 repo = backend_svn['svn-simple-layout'].scm_instance()
733 repo = backend_svn['svn-simple-layout'].scm_instance()
733 commit_id = repo[-1].raw_id
734 commit_id = repo[-1].raw_id
734 response = self.app.get(
735 response = self.app.get(
735 route_path('repo_files_diff',
736 route_path('repo_files_diff',
736 repo_name=backend_svn.repo_name,
737 repo_name=backend_svn.repo_name,
737 f_path='trunk/example.py'),
738 f_path='trunk/example.py'),
738 params={
739 params={
739 'diff1': 'branches/argparse/example.py@' + commit_id,
740 'diff1': 'branches/argparse/example.py@' + commit_id,
740 'diff2': commit_id,
741 'diff2': commit_id,
741 'show_rev': 'Show at Revision',
742 'show_rev': 'Show at Revision',
742 'annotate': 'true',
743 'annotate': 'true',
743 },
744 },
744 status=302)
745 status=302)
745 response = response.follow()
746 response = response.follow()
746 assert response.headers['Location'].endswith(
747 assert response.headers['Location'].endswith(
747 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
748 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
748
749
749
750
750 @pytest.mark.usefixtures("app", "autologin_user")
751 @pytest.mark.usefixtures("app", "autologin_user")
751 class TestModifyFilesWithWebInterface(object):
752 class TestModifyFilesWithWebInterface(object):
752
753
753 def test_add_file_view(self, backend):
754 def test_add_file_view(self, backend):
754 self.app.get(
755 self.app.get(
755 route_path('repo_files_add_file',
756 route_path('repo_files_add_file',
756 repo_name=backend.repo_name,
757 repo_name=backend.repo_name,
757 commit_id='tip', f_path='/')
758 commit_id='tip', f_path='/')
758 )
759 )
759
760
760 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
761 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
761 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
762 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
762 repo = backend.create_repo()
763 repo = backend.create_repo()
763 filename = 'init.py'
764 filename = 'init.py'
764 response = self.app.post(
765 response = self.app.post(
765 route_path('repo_files_create_file',
766 route_path('repo_files_create_file',
766 repo_name=backend.repo_name,
767 repo_name=backend.repo_name,
767 commit_id='tip', f_path='/'),
768 commit_id='tip', f_path='/'),
768 params={
769 params={
769 'content': "",
770 'content': "",
770 'filename': filename,
771 'filename': filename,
771 'location': "",
772 'location': "",
772 'csrf_token': csrf_token,
773 'csrf_token': csrf_token,
773 },
774 },
774 status=302)
775 status=302)
775 assert_session_flash(response,
776 assert_session_flash(response,
776 'Successfully committed new file `{}`'.format(
777 'Successfully committed new file `{}`'.format(
777 os.path.join(filename)))
778 os.path.join(filename)))
778
779
779 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
780 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
780 response = self.app.post(
781 response = self.app.post(
781 route_path('repo_files_create_file',
782 route_path('repo_files_create_file',
782 repo_name=backend.repo_name,
783 repo_name=backend.repo_name,
783 commit_id='tip', f_path='/'),
784 commit_id='tip', f_path='/'),
784 params={
785 params={
785 'content': "foo",
786 'content': "foo",
786 'csrf_token': csrf_token,
787 'csrf_token': csrf_token,
787 },
788 },
788 status=302)
789 status=302)
789
790
790 assert_session_flash(response, 'No filename')
791 assert_session_flash(response, 'No filename')
791
792
792 def test_add_file_into_repo_errors_and_no_commits(
793 def test_add_file_into_repo_errors_and_no_commits(
793 self, backend, csrf_token):
794 self, backend, csrf_token):
794 repo = backend.create_repo()
795 repo = backend.create_repo()
795 # Create a file with no filename, it will display an error but
796 # Create a file with no filename, it will display an error but
796 # the repo has no commits yet
797 # the repo has no commits yet
797 response = self.app.post(
798 response = self.app.post(
798 route_path('repo_files_create_file',
799 route_path('repo_files_create_file',
799 repo_name=repo.repo_name,
800 repo_name=repo.repo_name,
800 commit_id='tip', f_path='/'),
801 commit_id='tip', f_path='/'),
801 params={
802 params={
802 'content': "foo",
803 'content': "foo",
803 'csrf_token': csrf_token,
804 'csrf_token': csrf_token,
804 },
805 },
805 status=302)
806 status=302)
806
807
807 assert_session_flash(response, 'No filename')
808 assert_session_flash(response, 'No filename')
808
809
809 # Not allowed, redirect to the summary
810 # Not allowed, redirect to the summary
810 redirected = response.follow()
811 redirected = response.follow()
811 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
812 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
812
813
813 # As there are no commits, displays the summary page with the error of
814 # As there are no commits, displays the summary page with the error of
814 # creating a file with no filename
815 # creating a file with no filename
815
816
816 assert redirected.request.path == summary_url
817 assert redirected.request.path == summary_url
817
818
818 @pytest.mark.parametrize("location, filename", [
819 @pytest.mark.parametrize("location, filename", [
819 ('/abs', 'foo'),
820 ('/abs', 'foo'),
820 ('../rel', 'foo'),
821 ('../rel', 'foo'),
821 ('file/../foo', 'foo'),
822 ('file/../foo', 'foo'),
822 ])
823 ])
823 def test_add_file_into_repo_bad_filenames(
824 def test_add_file_into_repo_bad_filenames(
824 self, location, filename, backend, csrf_token):
825 self, location, filename, backend, csrf_token):
825 response = self.app.post(
826 response = self.app.post(
826 route_path('repo_files_create_file',
827 route_path('repo_files_create_file',
827 repo_name=backend.repo_name,
828 repo_name=backend.repo_name,
828 commit_id='tip', f_path='/'),
829 commit_id='tip', f_path='/'),
829 params={
830 params={
830 'content': "foo",
831 'content': "foo",
831 'filename': filename,
832 'filename': filename,
832 'location': location,
833 'location': location,
833 'csrf_token': csrf_token,
834 'csrf_token': csrf_token,
834 },
835 },
835 status=302)
836 status=302)
836
837
837 assert_session_flash(
838 assert_session_flash(
838 response,
839 response,
839 'The location specified must be a relative path and must not '
840 'The location specified must be a relative path and must not '
840 'contain .. in the path')
841 'contain .. in the path')
841
842
842 @pytest.mark.parametrize("cnt, location, filename", [
843 @pytest.mark.parametrize("cnt, location, filename", [
843 (1, '', 'foo.txt'),
844 (1, '', 'foo.txt'),
844 (2, 'dir', 'foo.rst'),
845 (2, 'dir', 'foo.rst'),
845 (3, 'rel/dir', 'foo.bar'),
846 (3, 'rel/dir', 'foo.bar'),
846 ])
847 ])
847 def test_add_file_into_repo(self, cnt, location, filename, backend,
848 def test_add_file_into_repo(self, cnt, location, filename, backend,
848 csrf_token):
849 csrf_token):
849 repo = backend.create_repo()
850 repo = backend.create_repo()
850 response = self.app.post(
851 response = self.app.post(
851 route_path('repo_files_create_file',
852 route_path('repo_files_create_file',
852 repo_name=repo.repo_name,
853 repo_name=repo.repo_name,
853 commit_id='tip', f_path='/'),
854 commit_id='tip', f_path='/'),
854 params={
855 params={
855 'content': "foo",
856 'content': "foo",
856 'filename': filename,
857 'filename': filename,
857 'location': location,
858 'location': location,
858 'csrf_token': csrf_token,
859 'csrf_token': csrf_token,
859 },
860 },
860 status=302)
861 status=302)
861 assert_session_flash(response,
862 assert_session_flash(response,
862 'Successfully committed new file `{}`'.format(
863 'Successfully committed new file `{}`'.format(
863 os.path.join(location, filename)))
864 os.path.join(location, filename)))
864
865
865 def test_edit_file_view(self, backend):
866 def test_edit_file_view(self, backend):
866 response = self.app.get(
867 response = self.app.get(
867 route_path('repo_files_edit_file',
868 route_path('repo_files_edit_file',
868 repo_name=backend.repo_name,
869 repo_name=backend.repo_name,
869 commit_id=backend.default_head_id,
870 commit_id=backend.default_head_id,
870 f_path='vcs/nodes.py'),
871 f_path='vcs/nodes.py'),
871 status=200)
872 status=200)
872 response.mustcontain("Module holding everything related to vcs nodes.")
873 response.mustcontain("Module holding everything related to vcs nodes.")
873
874
874 def test_edit_file_view_not_on_branch(self, backend):
875 def test_edit_file_view_not_on_branch(self, backend):
875 repo = backend.create_repo()
876 repo = backend.create_repo()
876 backend.ensure_file("vcs/nodes.py")
877 backend.ensure_file("vcs/nodes.py")
877
878
878 response = self.app.get(
879 response = self.app.get(
879 route_path('repo_files_edit_file',
880 route_path('repo_files_edit_file',
880 repo_name=repo.repo_name,
881 repo_name=repo.repo_name,
881 commit_id='tip',
882 commit_id='tip',
882 f_path='vcs/nodes.py'),
883 f_path='vcs/nodes.py'),
883 status=302)
884 status=302)
884 assert_session_flash(
885 assert_session_flash(
885 response,
886 response,
886 'You can only edit files with commit being a valid branch')
887 'You can only edit files with commit being a valid branch')
887
888
888 def test_edit_file_view_commit_changes(self, backend, csrf_token):
889 def test_edit_file_view_commit_changes(self, backend, csrf_token):
889 repo = backend.create_repo()
890 repo = backend.create_repo()
890 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
891 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
891
892
892 response = self.app.post(
893 response = self.app.post(
893 route_path('repo_files_update_file',
894 route_path('repo_files_update_file',
894 repo_name=repo.repo_name,
895 repo_name=repo.repo_name,
895 commit_id=backend.default_head_id,
896 commit_id=backend.default_head_id,
896 f_path='vcs/nodes.py'),
897 f_path='vcs/nodes.py'),
897 params={
898 params={
898 'content': "print 'hello world'",
899 'content': "print 'hello world'",
899 'message': 'I committed',
900 'message': 'I committed',
900 'filename': "vcs/nodes.py",
901 'filename': "vcs/nodes.py",
901 'csrf_token': csrf_token,
902 'csrf_token': csrf_token,
902 },
903 },
903 status=302)
904 status=302)
904 assert_session_flash(
905 assert_session_flash(
905 response, 'Successfully committed changes to file `vcs/nodes.py`')
906 response, 'Successfully committed changes to file `vcs/nodes.py`')
906 tip = repo.get_commit(commit_idx=-1)
907 tip = repo.get_commit(commit_idx=-1)
907 assert tip.message == 'I committed'
908 assert tip.message == 'I committed'
908
909
909 def test_edit_file_view_commit_changes_default_message(self, backend,
910 def test_edit_file_view_commit_changes_default_message(self, backend,
910 csrf_token):
911 csrf_token):
911 repo = backend.create_repo()
912 repo = backend.create_repo()
912 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
913 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
913
914
914 commit_id = (
915 commit_id = (
915 backend.default_branch_name or
916 backend.default_branch_name or
916 backend.repo.scm_instance().commit_ids[-1])
917 backend.repo.scm_instance().commit_ids[-1])
917
918
918 response = self.app.post(
919 response = self.app.post(
919 route_path('repo_files_update_file',
920 route_path('repo_files_update_file',
920 repo_name=repo.repo_name,
921 repo_name=repo.repo_name,
921 commit_id=commit_id,
922 commit_id=commit_id,
922 f_path='vcs/nodes.py'),
923 f_path='vcs/nodes.py'),
923 params={
924 params={
924 'content': "print 'hello world'",
925 'content': "print 'hello world'",
925 'message': '',
926 'message': '',
926 'filename': "vcs/nodes.py",
927 'filename': "vcs/nodes.py",
927 'csrf_token': csrf_token,
928 'csrf_token': csrf_token,
928 },
929 },
929 status=302)
930 status=302)
930 assert_session_flash(
931 assert_session_flash(
931 response, 'Successfully committed changes to file `vcs/nodes.py`')
932 response, 'Successfully committed changes to file `vcs/nodes.py`')
932 tip = repo.get_commit(commit_idx=-1)
933 tip = repo.get_commit(commit_idx=-1)
933 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
934 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
934
935
935 def test_delete_file_view(self, backend):
936 def test_delete_file_view(self, backend):
936 self.app.get(
937 self.app.get(
937 route_path('repo_files_remove_file',
938 route_path('repo_files_remove_file',
938 repo_name=backend.repo_name,
939 repo_name=backend.repo_name,
939 commit_id=backend.default_head_id,
940 commit_id=backend.default_head_id,
940 f_path='vcs/nodes.py'),
941 f_path='vcs/nodes.py'),
941 status=200)
942 status=200)
942
943
943 def test_delete_file_view_not_on_branch(self, backend):
944 def test_delete_file_view_not_on_branch(self, backend):
944 repo = backend.create_repo()
945 repo = backend.create_repo()
945 backend.ensure_file('vcs/nodes.py')
946 backend.ensure_file('vcs/nodes.py')
946
947
947 response = self.app.get(
948 response = self.app.get(
948 route_path('repo_files_remove_file',
949 route_path('repo_files_remove_file',
949 repo_name=repo.repo_name,
950 repo_name=repo.repo_name,
950 commit_id='tip',
951 commit_id='tip',
951 f_path='vcs/nodes.py'),
952 f_path='vcs/nodes.py'),
952 status=302)
953 status=302)
953 assert_session_flash(
954 assert_session_flash(
954 response,
955 response,
955 'You can only delete files with commit being a valid branch')
956 'You can only delete files with commit being a valid branch')
956
957
957 def test_delete_file_view_commit_changes(self, backend, csrf_token):
958 def test_delete_file_view_commit_changes(self, backend, csrf_token):
958 repo = backend.create_repo()
959 repo = backend.create_repo()
959 backend.ensure_file("vcs/nodes.py")
960 backend.ensure_file("vcs/nodes.py")
960
961
961 response = self.app.post(
962 response = self.app.post(
962 route_path('repo_files_delete_file',
963 route_path('repo_files_delete_file',
963 repo_name=repo.repo_name,
964 repo_name=repo.repo_name,
964 commit_id=backend.default_head_id,
965 commit_id=backend.default_head_id,
965 f_path='vcs/nodes.py'),
966 f_path='vcs/nodes.py'),
966 params={
967 params={
967 'message': 'i commited',
968 'message': 'i commited',
968 'csrf_token': csrf_token,
969 'csrf_token': csrf_token,
969 },
970 },
970 status=302)
971 status=302)
971 assert_session_flash(
972 assert_session_flash(
972 response, 'Successfully deleted file `vcs/nodes.py`')
973 response, 'Successfully deleted file `vcs/nodes.py`')
973
974
974
975
975 @pytest.mark.usefixtures("app")
976 @pytest.mark.usefixtures("app")
976 class TestFilesViewOtherCases(object):
977 class TestFilesViewOtherCases(object):
977
978
978 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
979 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
979 self, backend_stub, autologin_regular_user, user_regular,
980 self, backend_stub, autologin_regular_user, user_regular,
980 user_util):
981 user_util):
981
982
982 repo = backend_stub.create_repo()
983 repo = backend_stub.create_repo()
983 user_util.grant_user_permission_to_repo(
984 user_util.grant_user_permission_to_repo(
984 repo, user_regular, 'repository.write')
985 repo, user_regular, 'repository.write')
985 response = self.app.get(
986 response = self.app.get(
986 route_path('repo_files',
987 route_path('repo_files',
987 repo_name=repo.repo_name,
988 repo_name=repo.repo_name,
988 commit_id='tip', f_path='/'))
989 commit_id='tip', f_path='/'))
989
990
990 repo_file_add_url = route_path(
991 repo_file_add_url = route_path(
991 'repo_files_add_file',
992 'repo_files_add_file',
992 repo_name=repo.repo_name,
993 repo_name=repo.repo_name,
993 commit_id=0, f_path='') + '#edit'
994 commit_id=0, f_path='') + '#edit'
994
995
995 assert_session_flash(
996 assert_session_flash(
996 response,
997 response,
997 'There are no files yet. <a class="alert-link" '
998 'There are no files yet. <a class="alert-link" '
998 'href="{}">Click here to add a new file.</a>'
999 'href="{}">Click here to add a new file.</a>'
999 .format(repo_file_add_url))
1000 .format(repo_file_add_url))
1000
1001
1001 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1002 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1002 self, backend_stub, user_util):
1003 self, backend_stub, autologin_regular_user):
1003 repo = backend_stub.create_repo()
1004 repo = backend_stub.create_repo()
1005 # init session for anon user
1006 route_path('repo_summary', repo_name=repo.repo_name)
1007
1004 repo_file_add_url = route_path(
1008 repo_file_add_url = route_path(
1005 'repo_files_add_file',
1009 'repo_files_add_file',
1006 repo_name=repo.repo_name,
1010 repo_name=repo.repo_name,
1007 commit_id=0, f_path='') + '#edit'
1011 commit_id=0, f_path='') + '#edit'
1008
1012
1009 response = self.app.get(
1013 response = self.app.get(
1010 route_path('repo_files',
1014 route_path('repo_files',
1011 repo_name=repo.repo_name,
1015 repo_name=repo.repo_name,
1012 commit_id='tip', f_path='/'))
1016 commit_id='tip', f_path='/'))
1013
1017
1014 assert_session_flash(response, no_=repo_file_add_url)
1018 assert_session_flash(response, no_=repo_file_add_url)
1015
1019
1016 @pytest.mark.parametrize('file_node', [
1020 @pytest.mark.parametrize('file_node', [
1017 'archive/file.zip',
1021 'archive/file.zip',
1018 'diff/my-file.txt',
1022 'diff/my-file.txt',
1019 'render.py',
1023 'render.py',
1020 'render',
1024 'render',
1021 'remove_file',
1025 'remove_file',
1022 'remove_file/to-delete.txt',
1026 'remove_file/to-delete.txt',
1023 ])
1027 ])
1024 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1028 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1025 backend.create_repo()
1029 backend.create_repo()
1026 backend.ensure_file(file_node)
1030 backend.ensure_file(file_node)
1027
1031
1028 self.app.get(
1032 self.app.get(
1029 route_path('repo_files',
1033 route_path('repo_files',
1030 repo_name=backend.repo_name,
1034 repo_name=backend.repo_name,
1031 commit_id='tip', f_path=file_node),
1035 commit_id='tip', f_path=file_node),
1032 status=200)
1036 status=200)
1033
1037
1034
1038
1035 class TestAdjustFilePathForSvn(object):
1039 class TestAdjustFilePathForSvn(object):
1036 """
1040 """
1037 SVN specific adjustments of node history in RepoFilesView.
1041 SVN specific adjustments of node history in RepoFilesView.
1038 """
1042 """
1039
1043
1040 def test_returns_path_relative_to_matched_reference(self):
1044 def test_returns_path_relative_to_matched_reference(self):
1041 repo = self._repo(branches=['trunk'])
1045 repo = self._repo(branches=['trunk'])
1042 self.assert_file_adjustment('trunk/file', 'file', repo)
1046 self.assert_file_adjustment('trunk/file', 'file', repo)
1043
1047
1044 def test_does_not_modify_file_if_no_reference_matches(self):
1048 def test_does_not_modify_file_if_no_reference_matches(self):
1045 repo = self._repo(branches=['trunk'])
1049 repo = self._repo(branches=['trunk'])
1046 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1050 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1047
1051
1048 def test_does_not_adjust_partial_directory_names(self):
1052 def test_does_not_adjust_partial_directory_names(self):
1049 repo = self._repo(branches=['trun'])
1053 repo = self._repo(branches=['trun'])
1050 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1054 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1051
1055
1052 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1056 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1053 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1057 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1054 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1058 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1055
1059
1056 def assert_file_adjustment(self, f_path, expected, repo):
1060 def assert_file_adjustment(self, f_path, expected, repo):
1057 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1061 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1058 assert result == expected
1062 assert result == expected
1059
1063
1060 def _repo(self, branches=None):
1064 def _repo(self, branches=None):
1061 repo = mock.Mock()
1065 repo = mock.Mock()
1062 repo.branches = OrderedDict((name, '0') for name in branches or [])
1066 repo.branches = OrderedDict((name, '0') for name in branches or [])
1063 repo.tags = {}
1067 repo.tags = {}
1064 return repo
1068 return repo
@@ -1,258 +1,259 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode.apps._base import RepoAppView, DataGridAppView
31 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
33 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
33 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
34 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
34 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
35 import rhodecode.lib.helpers as h
35 import rhodecode.lib.helpers as h
36 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
36 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
37 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.forms import RepoForkForm
38 from rhodecode.model.forms import RepoForkForm
39 from rhodecode.model.scm import ScmModel, RepoGroupList
39 from rhodecode.model.scm import ScmModel, RepoGroupList
40 from rhodecode.lib.utils2 import safe_int, safe_unicode
40 from rhodecode.lib.utils2 import safe_int, safe_unicode
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class RepoForksView(RepoAppView, DataGridAppView):
45 class RepoForksView(RepoAppView, DataGridAppView):
46
46
47 def load_default_context(self):
47 def load_default_context(self):
48 c = self._get_local_tmpl_context(include_app_defaults=True)
48 c = self._get_local_tmpl_context(include_app_defaults=True)
49 c.rhodecode_repo = self.rhodecode_vcs_repo
49 c.rhodecode_repo = self.rhodecode_vcs_repo
50
50
51 acl_groups = RepoGroupList(
51 acl_groups = RepoGroupList(
52 RepoGroup.query().all(),
52 RepoGroup.query().all(),
53 perm_set=['group.write', 'group.admin'])
53 perm_set=['group.write', 'group.admin'])
54 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
54 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
55 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
55 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
56 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
56 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
57 self.request.translate)
57 c.landing_revs_choices = choices
58 c.landing_revs_choices = choices
58 c.personal_repo_group = c.rhodecode_user.personal_repo_group
59 c.personal_repo_group = c.rhodecode_user.personal_repo_group
59
60
60
61 return c
61 return c
62
62
63 @LoginRequired()
63 @LoginRequired()
64 @HasRepoPermissionAnyDecorator(
64 @HasRepoPermissionAnyDecorator(
65 'repository.read', 'repository.write', 'repository.admin')
65 'repository.read', 'repository.write', 'repository.admin')
66 @view_config(
66 @view_config(
67 route_name='repo_forks_show_all', request_method='GET',
67 route_name='repo_forks_show_all', request_method='GET',
68 renderer='rhodecode:templates/forks/forks.mako')
68 renderer='rhodecode:templates/forks/forks.mako')
69 def repo_forks_show_all(self):
69 def repo_forks_show_all(self):
70 c = self.load_default_context()
70 c = self.load_default_context()
71 return self._get_template_context(c)
71 return self._get_template_context(c)
72
72
73 @LoginRequired()
73 @LoginRequired()
74 @HasRepoPermissionAnyDecorator(
74 @HasRepoPermissionAnyDecorator(
75 'repository.read', 'repository.write', 'repository.admin')
75 'repository.read', 'repository.write', 'repository.admin')
76 @view_config(
76 @view_config(
77 route_name='repo_forks_data', request_method='GET',
77 route_name='repo_forks_data', request_method='GET',
78 renderer='json_ext', xhr=True)
78 renderer='json_ext', xhr=True)
79 def repo_forks_data(self):
79 def repo_forks_data(self):
80 _ = self.request.translate
80 _ = self.request.translate
81 self.load_default_context()
81 column_map = {
82 column_map = {
82 'fork_name': 'repo_name',
83 'fork_name': 'repo_name',
83 'fork_date': 'created_on',
84 'fork_date': 'created_on',
84 'last_activity': 'updated_on'
85 'last_activity': 'updated_on'
85 }
86 }
86 draw, start, limit = self._extract_chunk(self.request)
87 draw, start, limit = self._extract_chunk(self.request)
87 search_q, order_by, order_dir = self._extract_ordering(
88 search_q, order_by, order_dir = self._extract_ordering(
88 self.request, column_map=column_map)
89 self.request, column_map=column_map)
89
90
90 acl_check = HasRepoPermissionAny(
91 acl_check = HasRepoPermissionAny(
91 'repository.read', 'repository.write', 'repository.admin')
92 'repository.read', 'repository.write', 'repository.admin')
92 repo_id = self.db_repo.repo_id
93 repo_id = self.db_repo.repo_id
93 allowed_ids = [-1]
94 allowed_ids = [-1]
94 for f in Repository.query().filter(Repository.fork_id == repo_id):
95 for f in Repository.query().filter(Repository.fork_id == repo_id):
95 if acl_check(f.repo_name, 'get forks check'):
96 if acl_check(f.repo_name, 'get forks check'):
96 allowed_ids.append(f.repo_id)
97 allowed_ids.append(f.repo_id)
97
98
98 forks_data_total_count = Repository.query()\
99 forks_data_total_count = Repository.query()\
99 .filter(Repository.fork_id == repo_id)\
100 .filter(Repository.fork_id == repo_id)\
100 .filter(Repository.repo_id.in_(allowed_ids))\
101 .filter(Repository.repo_id.in_(allowed_ids))\
101 .count()
102 .count()
102
103
103 # json generate
104 # json generate
104 base_q = Repository.query()\
105 base_q = Repository.query()\
105 .filter(Repository.fork_id == repo_id)\
106 .filter(Repository.fork_id == repo_id)\
106 .filter(Repository.repo_id.in_(allowed_ids))\
107 .filter(Repository.repo_id.in_(allowed_ids))\
107
108
108 if search_q:
109 if search_q:
109 like_expression = u'%{}%'.format(safe_unicode(search_q))
110 like_expression = u'%{}%'.format(safe_unicode(search_q))
110 base_q = base_q.filter(or_(
111 base_q = base_q.filter(or_(
111 Repository.repo_name.ilike(like_expression),
112 Repository.repo_name.ilike(like_expression),
112 Repository.description.ilike(like_expression),
113 Repository.description.ilike(like_expression),
113 ))
114 ))
114
115
115 forks_data_total_filtered_count = base_q.count()
116 forks_data_total_filtered_count = base_q.count()
116
117
117 sort_col = getattr(Repository, order_by, None)
118 sort_col = getattr(Repository, order_by, None)
118 if sort_col:
119 if sort_col:
119 if order_dir == 'asc':
120 if order_dir == 'asc':
120 # handle null values properly to order by NULL last
121 # handle null values properly to order by NULL last
121 if order_by in ['last_activity']:
122 if order_by in ['last_activity']:
122 sort_col = coalesce(sort_col, datetime.date.max)
123 sort_col = coalesce(sort_col, datetime.date.max)
123 sort_col = sort_col.asc()
124 sort_col = sort_col.asc()
124 else:
125 else:
125 # handle null values properly to order by NULL last
126 # handle null values properly to order by NULL last
126 if order_by in ['last_activity']:
127 if order_by in ['last_activity']:
127 sort_col = coalesce(sort_col, datetime.date.min)
128 sort_col = coalesce(sort_col, datetime.date.min)
128 sort_col = sort_col.desc()
129 sort_col = sort_col.desc()
129
130
130 base_q = base_q.order_by(sort_col)
131 base_q = base_q.order_by(sort_col)
131 base_q = base_q.offset(start).limit(limit)
132 base_q = base_q.offset(start).limit(limit)
132
133
133 fork_list = base_q.all()
134 fork_list = base_q.all()
134
135
135 def fork_actions(fork):
136 def fork_actions(fork):
136 url_link = h.route_path(
137 url_link = h.route_path(
137 'repo_compare',
138 'repo_compare',
138 repo_name=fork.repo_name,
139 repo_name=fork.repo_name,
139 source_ref_type=self.db_repo.landing_rev[0],
140 source_ref_type=self.db_repo.landing_rev[0],
140 source_ref=self.db_repo.landing_rev[1],
141 source_ref=self.db_repo.landing_rev[1],
141 target_ref_type=self.db_repo.landing_rev[0],
142 target_ref_type=self.db_repo.landing_rev[0],
142 target_ref=self.db_repo.landing_rev[1],
143 target_ref=self.db_repo.landing_rev[1],
143 _query=dict(merge=1, target_repo=f.repo_name))
144 _query=dict(merge=1, target_repo=f.repo_name))
144 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
145 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
145
146
146 def fork_name(fork):
147 def fork_name(fork):
147 return h.link_to(fork.repo_name,
148 return h.link_to(fork.repo_name,
148 h.route_path('repo_summary', repo_name=fork.repo_name))
149 h.route_path('repo_summary', repo_name=fork.repo_name))
149
150
150 forks_data = []
151 forks_data = []
151 for fork in fork_list:
152 for fork in fork_list:
152 forks_data.append({
153 forks_data.append({
153 "username": h.gravatar_with_user(self.request, fork.user.username),
154 "username": h.gravatar_with_user(self.request, fork.user.username),
154 "fork_name": fork_name(fork),
155 "fork_name": fork_name(fork),
155 "description": fork.description,
156 "description": fork.description,
156 "fork_date": h.age_component(fork.created_on, time_is_local=True),
157 "fork_date": h.age_component(fork.created_on, time_is_local=True),
157 "last_activity": h.format_date(fork.updated_on),
158 "last_activity": h.format_date(fork.updated_on),
158 "action": fork_actions(fork),
159 "action": fork_actions(fork),
159 })
160 })
160
161
161 data = ({
162 data = ({
162 'draw': draw,
163 'draw': draw,
163 'data': forks_data,
164 'data': forks_data,
164 'recordsTotal': forks_data_total_count,
165 'recordsTotal': forks_data_total_count,
165 'recordsFiltered': forks_data_total_filtered_count,
166 'recordsFiltered': forks_data_total_filtered_count,
166 })
167 })
167
168
168 return data
169 return data
169
170
170 @LoginRequired()
171 @LoginRequired()
171 @NotAnonymous()
172 @NotAnonymous()
172 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
173 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
173 @HasRepoPermissionAnyDecorator(
174 @HasRepoPermissionAnyDecorator(
174 'repository.read', 'repository.write', 'repository.admin')
175 'repository.read', 'repository.write', 'repository.admin')
175 @view_config(
176 @view_config(
176 route_name='repo_fork_new', request_method='GET',
177 route_name='repo_fork_new', request_method='GET',
177 renderer='rhodecode:templates/forks/forks.mako')
178 renderer='rhodecode:templates/forks/forks.mako')
178 def repo_fork_new(self):
179 def repo_fork_new(self):
179 c = self.load_default_context()
180 c = self.load_default_context()
180
181
181 defaults = RepoModel()._get_defaults(self.db_repo_name)
182 defaults = RepoModel()._get_defaults(self.db_repo_name)
182 # alter the description to indicate a fork
183 # alter the description to indicate a fork
183 defaults['description'] = (
184 defaults['description'] = (
184 'fork of repository: %s \n%s' % (
185 'fork of repository: %s \n%s' % (
185 defaults['repo_name'], defaults['description']))
186 defaults['repo_name'], defaults['description']))
186 # add suffix to fork
187 # add suffix to fork
187 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
188 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
188
189
189 data = render('rhodecode:templates/forks/fork.mako',
190 data = render('rhodecode:templates/forks/fork.mako',
190 self._get_template_context(c), self.request)
191 self._get_template_context(c), self.request)
191 html = formencode.htmlfill.render(
192 html = formencode.htmlfill.render(
192 data,
193 data,
193 defaults=defaults,
194 defaults=defaults,
194 encoding="UTF-8",
195 encoding="UTF-8",
195 force_defaults=False
196 force_defaults=False
196 )
197 )
197 return Response(html)
198 return Response(html)
198
199
199 @LoginRequired()
200 @LoginRequired()
200 @NotAnonymous()
201 @NotAnonymous()
201 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
202 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
202 @HasRepoPermissionAnyDecorator(
203 @HasRepoPermissionAnyDecorator(
203 'repository.read', 'repository.write', 'repository.admin')
204 'repository.read', 'repository.write', 'repository.admin')
204 @CSRFRequired()
205 @CSRFRequired()
205 @view_config(
206 @view_config(
206 route_name='repo_fork_create', request_method='POST',
207 route_name='repo_fork_create', request_method='POST',
207 renderer='rhodecode:templates/forks/fork.mako')
208 renderer='rhodecode:templates/forks/fork.mako')
208 def repo_fork_create(self):
209 def repo_fork_create(self):
209 _ = self.request.translate
210 _ = self.request.translate
210 c = self.load_default_context()
211 c = self.load_default_context()
211
212
212 _form = RepoForkForm(self.request.translate, old_data={'repo_type': self.db_repo.repo_type},
213 _form = RepoForkForm(self.request.translate, old_data={'repo_type': self.db_repo.repo_type},
213 repo_groups=c.repo_groups_choices,
214 repo_groups=c.repo_groups_choices,
214 landing_revs=c.landing_revs_choices)()
215 landing_revs=c.landing_revs_choices)()
215 post_data = dict(self.request.POST)
216 post_data = dict(self.request.POST)
216
217
217 # forbid injecting other repo by forging a request
218 # forbid injecting other repo by forging a request
218 post_data['fork_parent_id'] = self.db_repo.repo_id
219 post_data['fork_parent_id'] = self.db_repo.repo_id
219
220
220 form_result = {}
221 form_result = {}
221 task_id = None
222 task_id = None
222 try:
223 try:
223 form_result = _form.to_python(post_data)
224 form_result = _form.to_python(post_data)
224 # create fork is done sometimes async on celery, db transaction
225 # create fork is done sometimes async on celery, db transaction
225 # management is handled there.
226 # management is handled there.
226 task = RepoModel().create_fork(
227 task = RepoModel().create_fork(
227 form_result, c.rhodecode_user.user_id)
228 form_result, c.rhodecode_user.user_id)
228 from celery.result import BaseAsyncResult
229 from celery.result import BaseAsyncResult
229 if isinstance(task, BaseAsyncResult):
230 if isinstance(task, BaseAsyncResult):
230 task_id = task.task_id
231 task_id = task.task_id
231 except formencode.Invalid as errors:
232 except formencode.Invalid as errors:
232 c.rhodecode_db_repo = self.db_repo
233 c.rhodecode_db_repo = self.db_repo
233
234
234 data = render('rhodecode:templates/forks/fork.mako',
235 data = render('rhodecode:templates/forks/fork.mako',
235 self._get_template_context(c), self.request)
236 self._get_template_context(c), self.request)
236 html = formencode.htmlfill.render(
237 html = formencode.htmlfill.render(
237 data,
238 data,
238 defaults=errors.value,
239 defaults=errors.value,
239 errors=errors.error_dict or {},
240 errors=errors.error_dict or {},
240 prefix_error=False,
241 prefix_error=False,
241 encoding="UTF-8",
242 encoding="UTF-8",
242 force_defaults=False
243 force_defaults=False
243 )
244 )
244 return Response(html)
245 return Response(html)
245 except Exception:
246 except Exception:
246 log.exception(
247 log.exception(
247 u'Exception while trying to fork the repository %s',
248 u'Exception while trying to fork the repository %s',
248 self.db_repo_name)
249 self.db_repo_name)
249 msg = (
250 msg = (
250 _('An error occurred during repository forking %s') % (
251 _('An error occurred during repository forking %s') % (
251 self.db_repo_name, ))
252 self.db_repo_name, ))
252 h.flash(msg, category='error')
253 h.flash(msg, category='error')
253
254
254 repo_name = form_result.get('repo_name_full', self.db_repo_name)
255 repo_name = form_result.get('repo_name_full', self.db_repo_name)
255 raise HTTPFound(
256 raise HTTPFound(
256 h.route_path('repo_creating',
257 h.route_path('repo_creating',
257 repo_name=repo_name,
258 repo_name=repo_name,
258 _query=dict(task_id=task_id)))
259 _query=dict(task_id=task_id)))
@@ -1,1238 +1,1244 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 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 import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
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, NodeDoesNotExistError, EmptyRepositoryError)
44 RepositoryRequirementError, NodeDoesNotExistError, 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
63
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'rhodecode:templates/data_table/_dt_elements.mako')
72 'rhodecode:templates/data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176 self.load_default_context()
176
177
177 # additional filters
178 # additional filters
178 req_get = self.request.GET
179 req_get = self.request.GET
179 source = str2bool(req_get.get('source'))
180 source = str2bool(req_get.get('source'))
180 closed = str2bool(req_get.get('closed'))
181 closed = str2bool(req_get.get('closed'))
181 my = str2bool(req_get.get('my'))
182 my = str2bool(req_get.get('my'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184
185
185 filter_type = 'awaiting_review' if awaiting_review \
186 filter_type = 'awaiting_review' if awaiting_review \
186 else 'awaiting_my_review' if awaiting_my_review \
187 else 'awaiting_my_review' if awaiting_my_review \
187 else None
188 else None
188
189
189 opened_by = None
190 opened_by = None
190 if my:
191 if my:
191 opened_by = [self._rhodecode_user.user_id]
192 opened_by = [self._rhodecode_user.user_id]
192
193
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 if closed:
195 if closed:
195 statuses = [PullRequest.STATUS_CLOSED]
196 statuses = [PullRequest.STATUS_CLOSED]
196
197
197 data = self._get_pull_requests_list(
198 data = self._get_pull_requests_list(
198 repo_name=self.db_repo_name, source=source,
199 repo_name=self.db_repo_name, source=source,
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200
201
201 return data
202 return data
202
203
203 def _get_pr_version(self, pull_request_id, version=None):
204 def _get_pr_version(self, pull_request_id, version=None):
204 at_version = None
205 at_version = None
205
206
206 if version and version == 'latest':
207 if version and version == 'latest':
207 pull_request_ver = PullRequest.get(pull_request_id)
208 pull_request_ver = PullRequest.get(pull_request_id)
208 pull_request_obj = pull_request_ver
209 pull_request_obj = pull_request_ver
209 _org_pull_request_obj = pull_request_obj
210 _org_pull_request_obj = pull_request_obj
210 at_version = 'latest'
211 at_version = 'latest'
211 elif version:
212 elif version:
212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 pull_request_ver = PullRequestVersion.get_or_404(version)
213 pull_request_obj = pull_request_ver
214 pull_request_obj = pull_request_ver
214 _org_pull_request_obj = pull_request_ver.pull_request
215 _org_pull_request_obj = pull_request_ver.pull_request
215 at_version = pull_request_ver.pull_request_version_id
216 at_version = pull_request_ver.pull_request_version_id
216 else:
217 else:
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 pull_request_id)
219 pull_request_id)
219
220
220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 pull_request_display_obj = PullRequest.get_pr_display_object(
221 pull_request_obj, _org_pull_request_obj)
222 pull_request_obj, _org_pull_request_obj)
222
223
223 return _org_pull_request_obj, pull_request_obj, \
224 return _org_pull_request_obj, pull_request_obj, \
224 pull_request_display_obj, at_version
225 pull_request_display_obj, at_version
225
226
226 def _get_diffset(self, source_repo_name, source_repo,
227 def _get_diffset(self, source_repo_name, source_repo,
227 source_ref_id, target_ref_id,
228 source_ref_id, target_ref_id,
228 target_commit, source_commit, diff_limit, fulldiff,
229 target_commit, source_commit, diff_limit, fulldiff,
229 file_limit, display_inline_comments):
230 file_limit, display_inline_comments):
230
231
231 vcs_diff = PullRequestModel().get_diff(
232 vcs_diff = PullRequestModel().get_diff(
232 source_repo, source_ref_id, target_ref_id)
233 source_repo, source_ref_id, target_ref_id)
233
234
234 diff_processor = diffs.DiffProcessor(
235 diff_processor = diffs.DiffProcessor(
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 file_limit=file_limit, show_full_diff=fulldiff)
237 file_limit=file_limit, show_full_diff=fulldiff)
237
238
238 _parsed = diff_processor.prepare()
239 _parsed = diff_processor.prepare()
239
240
240 def _node_getter(commit):
241 def _node_getter(commit):
241 def get_node(fname):
242 def get_node(fname):
242 try:
243 try:
243 return commit.get_node(fname)
244 return commit.get_node(fname)
244 except NodeDoesNotExistError:
245 except NodeDoesNotExistError:
245 return None
246 return None
246
247
247 return get_node
248 return get_node
248
249
249 diffset = codeblocks.DiffSet(
250 diffset = codeblocks.DiffSet(
250 repo_name=self.db_repo_name,
251 repo_name=self.db_repo_name,
251 source_repo_name=source_repo_name,
252 source_repo_name=source_repo_name,
252 source_node_getter=_node_getter(target_commit),
253 source_node_getter=_node_getter(target_commit),
253 target_node_getter=_node_getter(source_commit),
254 target_node_getter=_node_getter(source_commit),
254 comments=display_inline_comments
255 comments=display_inline_comments
255 )
256 )
256 diffset = diffset.render_patchset(
257 diffset = diffset.render_patchset(
257 _parsed, target_commit.raw_id, source_commit.raw_id)
258 _parsed, target_commit.raw_id, source_commit.raw_id)
258
259
259 return diffset
260 return diffset
260
261
261 @LoginRequired()
262 @LoginRequired()
262 @HasRepoPermissionAnyDecorator(
263 @HasRepoPermissionAnyDecorator(
263 'repository.read', 'repository.write', 'repository.admin')
264 'repository.read', 'repository.write', 'repository.admin')
264 @view_config(
265 @view_config(
265 route_name='pullrequest_show', request_method='GET',
266 route_name='pullrequest_show', request_method='GET',
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 def pull_request_show(self):
268 def pull_request_show(self):
268 pull_request_id = self.request.matchdict['pull_request_id']
269 pull_request_id = self.request.matchdict['pull_request_id']
269
270
270 c = self.load_default_context()
271 c = self.load_default_context()
271
272
272 version = self.request.GET.get('version')
273 version = self.request.GET.get('version')
273 from_version = self.request.GET.get('from_version') or version
274 from_version = self.request.GET.get('from_version') or version
274 merge_checks = self.request.GET.get('merge_checks')
275 merge_checks = self.request.GET.get('merge_checks')
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276
277
277 (pull_request_latest,
278 (pull_request_latest,
278 pull_request_at_ver,
279 pull_request_at_ver,
279 pull_request_display_obj,
280 pull_request_display_obj,
280 at_version) = self._get_pr_version(
281 at_version) = self._get_pr_version(
281 pull_request_id, version=version)
282 pull_request_id, version=version)
282 pr_closed = pull_request_latest.is_closed()
283 pr_closed = pull_request_latest.is_closed()
283
284
284 if pr_closed and (version or from_version):
285 if pr_closed and (version or from_version):
285 # not allow to browse versions
286 # not allow to browse versions
286 raise HTTPFound(h.route_path(
287 raise HTTPFound(h.route_path(
287 'pullrequest_show', repo_name=self.db_repo_name,
288 'pullrequest_show', repo_name=self.db_repo_name,
288 pull_request_id=pull_request_id))
289 pull_request_id=pull_request_id))
289
290
290 versions = pull_request_display_obj.versions()
291 versions = pull_request_display_obj.versions()
291
292
292 c.at_version = at_version
293 c.at_version = at_version
293 c.at_version_num = (at_version
294 c.at_version_num = (at_version
294 if at_version and at_version != 'latest'
295 if at_version and at_version != 'latest'
295 else None)
296 else None)
296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 c.at_version_pos = ChangesetComment.get_index_from_version(
297 c.at_version_num, versions)
298 c.at_version_num, versions)
298
299
299 (prev_pull_request_latest,
300 (prev_pull_request_latest,
300 prev_pull_request_at_ver,
301 prev_pull_request_at_ver,
301 prev_pull_request_display_obj,
302 prev_pull_request_display_obj,
302 prev_at_version) = self._get_pr_version(
303 prev_at_version) = self._get_pr_version(
303 pull_request_id, version=from_version)
304 pull_request_id, version=from_version)
304
305
305 c.from_version = prev_at_version
306 c.from_version = prev_at_version
306 c.from_version_num = (prev_at_version
307 c.from_version_num = (prev_at_version
307 if prev_at_version and prev_at_version != 'latest'
308 if prev_at_version and prev_at_version != 'latest'
308 else None)
309 else None)
309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 c.from_version_pos = ChangesetComment.get_index_from_version(
310 c.from_version_num, versions)
311 c.from_version_num, versions)
311
312
312 # define if we're in COMPARE mode or VIEW at version mode
313 # define if we're in COMPARE mode or VIEW at version mode
313 compare = at_version != prev_at_version
314 compare = at_version != prev_at_version
314
315
315 # pull_requests repo_name we opened it against
316 # pull_requests repo_name we opened it against
316 # ie. target_repo must match
317 # ie. target_repo must match
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 raise HTTPNotFound()
319 raise HTTPNotFound()
319
320
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 pull_request_at_ver)
322 pull_request_at_ver)
322
323
323 c.pull_request = pull_request_display_obj
324 c.pull_request = pull_request_display_obj
324 c.pull_request_latest = pull_request_latest
325 c.pull_request_latest = pull_request_latest
325
326
326 if compare or (at_version and not at_version == 'latest'):
327 if compare or (at_version and not at_version == 'latest'):
327 c.allowed_to_change_status = False
328 c.allowed_to_change_status = False
328 c.allowed_to_update = False
329 c.allowed_to_update = False
329 c.allowed_to_merge = False
330 c.allowed_to_merge = False
330 c.allowed_to_delete = False
331 c.allowed_to_delete = False
331 c.allowed_to_comment = False
332 c.allowed_to_comment = False
332 c.allowed_to_close = False
333 c.allowed_to_close = False
333 else:
334 else:
334 can_change_status = PullRequestModel().check_user_change_status(
335 can_change_status = PullRequestModel().check_user_change_status(
335 pull_request_at_ver, self._rhodecode_user)
336 pull_request_at_ver, self._rhodecode_user)
336 c.allowed_to_change_status = can_change_status and not pr_closed
337 c.allowed_to_change_status = can_change_status and not pr_closed
337
338
338 c.allowed_to_update = PullRequestModel().check_user_update(
339 c.allowed_to_update = PullRequestModel().check_user_update(
339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 pull_request_latest, self._rhodecode_user) and not pr_closed
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 pull_request_latest, self._rhodecode_user) and not pr_closed
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 pull_request_latest, self._rhodecode_user) and not pr_closed
344 c.allowed_to_comment = not pr_closed
345 c.allowed_to_comment = not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346
347
347 c.forbid_adding_reviewers = False
348 c.forbid_adding_reviewers = False
348 c.forbid_author_to_review = False
349 c.forbid_author_to_review = False
349 c.forbid_commit_author_to_review = False
350 c.forbid_commit_author_to_review = False
350
351
351 if pull_request_latest.reviewer_data and \
352 if pull_request_latest.reviewer_data and \
352 'rules' in pull_request_latest.reviewer_data:
353 'rules' in pull_request_latest.reviewer_data:
353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 rules = pull_request_latest.reviewer_data['rules'] or {}
354 try:
355 try:
355 c.forbid_adding_reviewers = rules.get(
356 c.forbid_adding_reviewers = rules.get(
356 'forbid_adding_reviewers')
357 'forbid_adding_reviewers')
357 c.forbid_author_to_review = rules.get(
358 c.forbid_author_to_review = rules.get(
358 'forbid_author_to_review')
359 'forbid_author_to_review')
359 c.forbid_commit_author_to_review = rules.get(
360 c.forbid_commit_author_to_review = rules.get(
360 'forbid_commit_author_to_review')
361 'forbid_commit_author_to_review')
361 except Exception:
362 except Exception:
362 pass
363 pass
363
364
364 # check merge capabilities
365 # check merge capabilities
365 _merge_check = MergeCheck.validate(
366 _merge_check = MergeCheck.validate(
366 pull_request_latest, user=self._rhodecode_user,
367 pull_request_latest, user=self._rhodecode_user,
367 translator=self.request.translate)
368 translator=self.request.translate)
368 c.pr_merge_errors = _merge_check.error_details
369 c.pr_merge_errors = _merge_check.error_details
369 c.pr_merge_possible = not _merge_check.failed
370 c.pr_merge_possible = not _merge_check.failed
370 c.pr_merge_message = _merge_check.merge_msg
371 c.pr_merge_message = _merge_check.merge_msg
371
372
372 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 pull_request_latest, translator=self.request.translate)
374 pull_request_latest, translator=self.request.translate)
374
375
375 c.pull_request_review_status = _merge_check.review_status
376 c.pull_request_review_status = _merge_check.review_status
376 if merge_checks:
377 if merge_checks:
377 self.request.override_renderer = \
378 self.request.override_renderer = \
378 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 return self._get_template_context(c)
380 return self._get_template_context(c)
380
381
381 comments_model = CommentsModel()
382 comments_model = CommentsModel()
382
383
383 # reviewers and statuses
384 # reviewers and statuses
384 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386
387
387 # GENERAL COMMENTS with versions #
388 # GENERAL COMMENTS with versions #
388 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 q = q.order_by(ChangesetComment.comment_id.asc())
390 q = q.order_by(ChangesetComment.comment_id.asc())
390 general_comments = q
391 general_comments = q
391
392
392 # pick comments we want to render at current version
393 # pick comments we want to render at current version
393 c.comment_versions = comments_model.aggregate_comments(
394 c.comment_versions = comments_model.aggregate_comments(
394 general_comments, versions, c.at_version_num)
395 general_comments, versions, c.at_version_num)
395 c.comments = c.comment_versions[c.at_version_num]['until']
396 c.comments = c.comment_versions[c.at_version_num]['until']
396
397
397 # INLINE COMMENTS with versions #
398 # INLINE COMMENTS with versions #
398 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 q = q.order_by(ChangesetComment.comment_id.asc())
400 q = q.order_by(ChangesetComment.comment_id.asc())
400 inline_comments = q
401 inline_comments = q
401
402
402 c.inline_versions = comments_model.aggregate_comments(
403 c.inline_versions = comments_model.aggregate_comments(
403 inline_comments, versions, c.at_version_num, inline=True)
404 inline_comments, versions, c.at_version_num, inline=True)
404
405
405 # inject latest version
406 # inject latest version
406 latest_ver = PullRequest.get_pr_display_object(
407 latest_ver = PullRequest.get_pr_display_object(
407 pull_request_latest, pull_request_latest)
408 pull_request_latest, pull_request_latest)
408
409
409 c.versions = versions + [latest_ver]
410 c.versions = versions + [latest_ver]
410
411
411 # if we use version, then do not show later comments
412 # if we use version, then do not show later comments
412 # than current version
413 # than current version
413 display_inline_comments = collections.defaultdict(
414 display_inline_comments = collections.defaultdict(
414 lambda: collections.defaultdict(list))
415 lambda: collections.defaultdict(list))
415 for co in inline_comments:
416 for co in inline_comments:
416 if c.at_version_num:
417 if c.at_version_num:
417 # pick comments that are at least UPTO given version, so we
418 # pick comments that are at least UPTO given version, so we
418 # don't render comments for higher version
419 # don't render comments for higher version
419 should_render = co.pull_request_version_id and \
420 should_render = co.pull_request_version_id and \
420 co.pull_request_version_id <= c.at_version_num
421 co.pull_request_version_id <= c.at_version_num
421 else:
422 else:
422 # showing all, for 'latest'
423 # showing all, for 'latest'
423 should_render = True
424 should_render = True
424
425
425 if should_render:
426 if should_render:
426 display_inline_comments[co.f_path][co.line_no].append(co)
427 display_inline_comments[co.f_path][co.line_no].append(co)
427
428
428 # load diff data into template context, if we use compare mode then
429 # load diff data into template context, if we use compare mode then
429 # diff is calculated based on changes between versions of PR
430 # diff is calculated based on changes between versions of PR
430
431
431 source_repo = pull_request_at_ver.source_repo
432 source_repo = pull_request_at_ver.source_repo
432 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433
434
434 target_repo = pull_request_at_ver.target_repo
435 target_repo = pull_request_at_ver.target_repo
435 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436
437
437 if compare:
438 if compare:
438 # in compare switch the diff base to latest commit from prev version
439 # in compare switch the diff base to latest commit from prev version
439 target_ref_id = prev_pull_request_display_obj.revisions[0]
440 target_ref_id = prev_pull_request_display_obj.revisions[0]
440
441
441 # despite opening commits for bookmarks/branches/tags, we always
442 # despite opening commits for bookmarks/branches/tags, we always
442 # convert this to rev to prevent changes after bookmark or branch change
443 # convert this to rev to prevent changes after bookmark or branch change
443 c.source_ref_type = 'rev'
444 c.source_ref_type = 'rev'
444 c.source_ref = source_ref_id
445 c.source_ref = source_ref_id
445
446
446 c.target_ref_type = 'rev'
447 c.target_ref_type = 'rev'
447 c.target_ref = target_ref_id
448 c.target_ref = target_ref_id
448
449
449 c.source_repo = source_repo
450 c.source_repo = source_repo
450 c.target_repo = target_repo
451 c.target_repo = target_repo
451
452
452 c.commit_ranges = []
453 c.commit_ranges = []
453 source_commit = EmptyCommit()
454 source_commit = EmptyCommit()
454 target_commit = EmptyCommit()
455 target_commit = EmptyCommit()
455 c.missing_requirements = False
456 c.missing_requirements = False
456
457
457 source_scm = source_repo.scm_instance()
458 source_scm = source_repo.scm_instance()
458 target_scm = target_repo.scm_instance()
459 target_scm = target_repo.scm_instance()
459
460
460 # try first shadow repo, fallback to regular repo
461 # try first shadow repo, fallback to regular repo
461 try:
462 try:
462 commits_source_repo = pull_request_latest.get_shadow_repo()
463 commits_source_repo = pull_request_latest.get_shadow_repo()
463 except Exception:
464 except Exception:
464 log.debug('Failed to get shadow repo', exc_info=True)
465 log.debug('Failed to get shadow repo', exc_info=True)
465 commits_source_repo = source_scm
466 commits_source_repo = source_scm
466
467
467 c.commits_source_repo = commits_source_repo
468 c.commits_source_repo = commits_source_repo
468 commit_cache = {}
469 commit_cache = {}
469 try:
470 try:
470 pre_load = ["author", "branch", "date", "message"]
471 pre_load = ["author", "branch", "date", "message"]
471 show_revs = pull_request_at_ver.revisions
472 show_revs = pull_request_at_ver.revisions
472 for rev in show_revs:
473 for rev in show_revs:
473 comm = commits_source_repo.get_commit(
474 comm = commits_source_repo.get_commit(
474 commit_id=rev, pre_load=pre_load)
475 commit_id=rev, pre_load=pre_load)
475 c.commit_ranges.append(comm)
476 c.commit_ranges.append(comm)
476 commit_cache[comm.raw_id] = comm
477 commit_cache[comm.raw_id] = comm
477
478
478 # Order here matters, we first need to get target, and then
479 # Order here matters, we first need to get target, and then
479 # the source
480 # the source
480 target_commit = commits_source_repo.get_commit(
481 target_commit = commits_source_repo.get_commit(
481 commit_id=safe_str(target_ref_id))
482 commit_id=safe_str(target_ref_id))
482
483
483 source_commit = commits_source_repo.get_commit(
484 source_commit = commits_source_repo.get_commit(
484 commit_id=safe_str(source_ref_id))
485 commit_id=safe_str(source_ref_id))
485
486
486 except CommitDoesNotExistError:
487 except CommitDoesNotExistError:
487 log.warning(
488 log.warning(
488 'Failed to get commit from `{}` repo'.format(
489 'Failed to get commit from `{}` repo'.format(
489 commits_source_repo), exc_info=True)
490 commits_source_repo), exc_info=True)
490 except RepositoryRequirementError:
491 except RepositoryRequirementError:
491 log.warning(
492 log.warning(
492 'Failed to get all required data from repo', exc_info=True)
493 'Failed to get all required data from repo', exc_info=True)
493 c.missing_requirements = True
494 c.missing_requirements = True
494
495
495 c.ancestor = None # set it to None, to hide it from PR view
496 c.ancestor = None # set it to None, to hide it from PR view
496
497
497 try:
498 try:
498 ancestor_id = source_scm.get_common_ancestor(
499 ancestor_id = source_scm.get_common_ancestor(
499 source_commit.raw_id, target_commit.raw_id, target_scm)
500 source_commit.raw_id, target_commit.raw_id, target_scm)
500 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 except Exception:
502 except Exception:
502 c.ancestor_commit = None
503 c.ancestor_commit = None
503
504
504 c.statuses = source_repo.statuses(
505 c.statuses = source_repo.statuses(
505 [x.raw_id for x in c.commit_ranges])
506 [x.raw_id for x in c.commit_ranges])
506
507
507 # auto collapse if we have more than limit
508 # auto collapse if we have more than limit
508 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 c.compare_mode = compare
511 c.compare_mode = compare
511
512
512 # diff_limit is the old behavior, will cut off the whole diff
513 # diff_limit is the old behavior, will cut off the whole diff
513 # if the limit is applied otherwise will just hide the
514 # if the limit is applied otherwise will just hide the
514 # big files from the front-end
515 # big files from the front-end
515 diff_limit = c.visual.cut_off_limit_diff
516 diff_limit = c.visual.cut_off_limit_diff
516 file_limit = c.visual.cut_off_limit_file
517 file_limit = c.visual.cut_off_limit_file
517
518
518 c.missing_commits = False
519 c.missing_commits = False
519 if (c.missing_requirements
520 if (c.missing_requirements
520 or isinstance(source_commit, EmptyCommit)
521 or isinstance(source_commit, EmptyCommit)
521 or source_commit == target_commit):
522 or source_commit == target_commit):
522
523
523 c.missing_commits = True
524 c.missing_commits = True
524 else:
525 else:
525
526
526 c.diffset = self._get_diffset(
527 c.diffset = self._get_diffset(
527 c.source_repo.repo_name, commits_source_repo,
528 c.source_repo.repo_name, commits_source_repo,
528 source_ref_id, target_ref_id,
529 source_ref_id, target_ref_id,
529 target_commit, source_commit,
530 target_commit, source_commit,
530 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531
532
532 c.limited_diff = c.diffset.limited_diff
533 c.limited_diff = c.diffset.limited_diff
533
534
534 # calculate removed files that are bound to comments
535 # calculate removed files that are bound to comments
535 comment_deleted_files = [
536 comment_deleted_files = [
536 fname for fname in display_inline_comments
537 fname for fname in display_inline_comments
537 if fname not in c.diffset.file_stats]
538 if fname not in c.diffset.file_stats]
538
539
539 c.deleted_files_comments = collections.defaultdict(dict)
540 c.deleted_files_comments = collections.defaultdict(dict)
540 for fname, per_line_comments in display_inline_comments.items():
541 for fname, per_line_comments in display_inline_comments.items():
541 if fname in comment_deleted_files:
542 if fname in comment_deleted_files:
542 c.deleted_files_comments[fname]['stats'] = 0
543 c.deleted_files_comments[fname]['stats'] = 0
543 c.deleted_files_comments[fname]['comments'] = list()
544 c.deleted_files_comments[fname]['comments'] = list()
544 for lno, comments in per_line_comments.items():
545 for lno, comments in per_line_comments.items():
545 c.deleted_files_comments[fname]['comments'].extend(
546 c.deleted_files_comments[fname]['comments'].extend(
546 comments)
547 comments)
547
548
548 # this is a hack to properly display links, when creating PR, the
549 # this is a hack to properly display links, when creating PR, the
549 # compare view and others uses different notation, and
550 # compare view and others uses different notation, and
550 # compare_commits.mako renders links based on the target_repo.
551 # compare_commits.mako renders links based on the target_repo.
551 # We need to swap that here to generate it properly on the html side
552 # We need to swap that here to generate it properly on the html side
552 c.target_repo = c.source_repo
553 c.target_repo = c.source_repo
553
554
554 c.commit_statuses = ChangesetStatus.STATUSES
555 c.commit_statuses = ChangesetStatus.STATUSES
555
556
556 c.show_version_changes = not pr_closed
557 c.show_version_changes = not pr_closed
557 if c.show_version_changes:
558 if c.show_version_changes:
558 cur_obj = pull_request_at_ver
559 cur_obj = pull_request_at_ver
559 prev_obj = prev_pull_request_at_ver
560 prev_obj = prev_pull_request_at_ver
560
561
561 old_commit_ids = prev_obj.revisions
562 old_commit_ids = prev_obj.revisions
562 new_commit_ids = cur_obj.revisions
563 new_commit_ids = cur_obj.revisions
563 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 old_commit_ids, new_commit_ids)
565 old_commit_ids, new_commit_ids)
565 c.commit_changes_summary = commit_changes
566 c.commit_changes_summary = commit_changes
566
567
567 # calculate the diff for commits between versions
568 # calculate the diff for commits between versions
568 c.commit_changes = []
569 c.commit_changes = []
569 mark = lambda cs, fw: list(
570 mark = lambda cs, fw: list(
570 h.itertools.izip_longest([], cs, fillvalue=fw))
571 h.itertools.izip_longest([], cs, fillvalue=fw))
571 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 + mark(commit_changes.removed, 'r') \
573 + mark(commit_changes.removed, 'r') \
573 + mark(commit_changes.common, 'c'):
574 + mark(commit_changes.common, 'c'):
574
575
575 if raw_id in commit_cache:
576 if raw_id in commit_cache:
576 commit = commit_cache[raw_id]
577 commit = commit_cache[raw_id]
577 else:
578 else:
578 try:
579 try:
579 commit = commits_source_repo.get_commit(raw_id)
580 commit = commits_source_repo.get_commit(raw_id)
580 except CommitDoesNotExistError:
581 except CommitDoesNotExistError:
581 # in case we fail extracting still use "dummy" commit
582 # in case we fail extracting still use "dummy" commit
582 # for display in commit diff
583 # for display in commit diff
583 commit = h.AttributeDict(
584 commit = h.AttributeDict(
584 {'raw_id': raw_id,
585 {'raw_id': raw_id,
585 'message': 'EMPTY or MISSING COMMIT'})
586 'message': 'EMPTY or MISSING COMMIT'})
586 c.commit_changes.append([c_type, commit])
587 c.commit_changes.append([c_type, commit])
587
588
588 # current user review statuses for each version
589 # current user review statuses for each version
589 c.review_versions = {}
590 c.review_versions = {}
590 if self._rhodecode_user.user_id in allowed_reviewers:
591 if self._rhodecode_user.user_id in allowed_reviewers:
591 for co in general_comments:
592 for co in general_comments:
592 if co.author.user_id == self._rhodecode_user.user_id:
593 if co.author.user_id == self._rhodecode_user.user_id:
593 # each comment has a status change
594 # each comment has a status change
594 status = co.status_change
595 status = co.status_change
595 if status:
596 if status:
596 _ver_pr = status[0].comment.pull_request_version_id
597 _ver_pr = status[0].comment.pull_request_version_id
597 c.review_versions[_ver_pr] = status[0]
598 c.review_versions[_ver_pr] = status[0]
598
599
599 return self._get_template_context(c)
600 return self._get_template_context(c)
600
601
601 def assure_not_empty_repo(self):
602 def assure_not_empty_repo(self):
602 _ = self.request.translate
603 _ = self.request.translate
603
604
604 try:
605 try:
605 self.db_repo.scm_instance().get_commit()
606 self.db_repo.scm_instance().get_commit()
606 except EmptyRepositoryError:
607 except EmptyRepositoryError:
607 h.flash(h.literal(_('There are no commits yet')),
608 h.flash(h.literal(_('There are no commits yet')),
608 category='warning')
609 category='warning')
609 raise HTTPFound(
610 raise HTTPFound(
610 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611
612
612 @LoginRequired()
613 @LoginRequired()
613 @NotAnonymous()
614 @NotAnonymous()
614 @HasRepoPermissionAnyDecorator(
615 @HasRepoPermissionAnyDecorator(
615 'repository.read', 'repository.write', 'repository.admin')
616 'repository.read', 'repository.write', 'repository.admin')
616 @view_config(
617 @view_config(
617 route_name='pullrequest_new', request_method='GET',
618 route_name='pullrequest_new', request_method='GET',
618 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 def pull_request_new(self):
620 def pull_request_new(self):
620 _ = self.request.translate
621 _ = self.request.translate
621 c = self.load_default_context()
622 c = self.load_default_context()
622
623
623 self.assure_not_empty_repo()
624 self.assure_not_empty_repo()
624 source_repo = self.db_repo
625 source_repo = self.db_repo
625
626
626 commit_id = self.request.GET.get('commit')
627 commit_id = self.request.GET.get('commit')
627 branch_ref = self.request.GET.get('branch')
628 branch_ref = self.request.GET.get('branch')
628 bookmark_ref = self.request.GET.get('bookmark')
629 bookmark_ref = self.request.GET.get('bookmark')
629
630
630 try:
631 try:
631 source_repo_data = PullRequestModel().generate_repo_data(
632 source_repo_data = PullRequestModel().generate_repo_data(
632 source_repo, commit_id=commit_id,
633 source_repo, commit_id=commit_id,
633 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 except CommitDoesNotExistError as e:
635 except CommitDoesNotExistError as e:
635 log.exception(e)
636 log.exception(e)
636 h.flash(_('Commit does not exist'), 'error')
637 h.flash(_('Commit does not exist'), 'error')
637 raise HTTPFound(
638 raise HTTPFound(
638 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639
640
640 default_target_repo = source_repo
641 default_target_repo = source_repo
641
642
642 if source_repo.parent:
643 if source_repo.parent:
643 parent_vcs_obj = source_repo.parent.scm_instance()
644 parent_vcs_obj = source_repo.parent.scm_instance()
644 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 # change default if we have a parent repo
646 # change default if we have a parent repo
646 default_target_repo = source_repo.parent
647 default_target_repo = source_repo.parent
647
648
648 target_repo_data = PullRequestModel().generate_repo_data(
649 target_repo_data = PullRequestModel().generate_repo_data(
649 default_target_repo, translator=self.request.translate)
650 default_target_repo, translator=self.request.translate)
650
651
651 selected_source_ref = source_repo_data['refs']['selected_ref']
652 selected_source_ref = source_repo_data['refs']['selected_ref']
652
653
653 title_source_ref = selected_source_ref.split(':', 2)[1]
654 title_source_ref = selected_source_ref.split(':', 2)[1]
654 c.default_title = PullRequestModel().generate_pullrequest_title(
655 c.default_title = PullRequestModel().generate_pullrequest_title(
655 source=source_repo.repo_name,
656 source=source_repo.repo_name,
656 source_ref=title_source_ref,
657 source_ref=title_source_ref,
657 target=default_target_repo.repo_name
658 target=default_target_repo.repo_name
658 )
659 )
659
660
660 c.default_repo_data = {
661 c.default_repo_data = {
661 'source_repo_name': source_repo.repo_name,
662 'source_repo_name': source_repo.repo_name,
662 'source_refs_json': json.dumps(source_repo_data),
663 'source_refs_json': json.dumps(source_repo_data),
663 'target_repo_name': default_target_repo.repo_name,
664 'target_repo_name': default_target_repo.repo_name,
664 'target_refs_json': json.dumps(target_repo_data),
665 'target_refs_json': json.dumps(target_repo_data),
665 }
666 }
666 c.default_source_ref = selected_source_ref
667 c.default_source_ref = selected_source_ref
667
668
668 return self._get_template_context(c)
669 return self._get_template_context(c)
669
670
670 @LoginRequired()
671 @LoginRequired()
671 @NotAnonymous()
672 @NotAnonymous()
672 @HasRepoPermissionAnyDecorator(
673 @HasRepoPermissionAnyDecorator(
673 'repository.read', 'repository.write', 'repository.admin')
674 'repository.read', 'repository.write', 'repository.admin')
674 @view_config(
675 @view_config(
675 route_name='pullrequest_repo_refs', request_method='GET',
676 route_name='pullrequest_repo_refs', request_method='GET',
676 renderer='json_ext', xhr=True)
677 renderer='json_ext', xhr=True)
677 def pull_request_repo_refs(self):
678 def pull_request_repo_refs(self):
679 self.load_default_context()
678 target_repo_name = self.request.matchdict['target_repo_name']
680 target_repo_name = self.request.matchdict['target_repo_name']
679 repo = Repository.get_by_repo_name(target_repo_name)
681 repo = Repository.get_by_repo_name(target_repo_name)
680 if not repo:
682 if not repo:
681 raise HTTPNotFound()
683 raise HTTPNotFound()
682 return PullRequestModel().generate_repo_data(
684 return PullRequestModel().generate_repo_data(
683 repo, translator=self.request.translate)
685 repo, translator=self.request.translate)
684
686
685 @LoginRequired()
687 @LoginRequired()
686 @NotAnonymous()
688 @NotAnonymous()
687 @HasRepoPermissionAnyDecorator(
689 @HasRepoPermissionAnyDecorator(
688 'repository.read', 'repository.write', 'repository.admin')
690 'repository.read', 'repository.write', 'repository.admin')
689 @view_config(
691 @view_config(
690 route_name='pullrequest_repo_destinations', request_method='GET',
692 route_name='pullrequest_repo_destinations', request_method='GET',
691 renderer='json_ext', xhr=True)
693 renderer='json_ext', xhr=True)
692 def pull_request_repo_destinations(self):
694 def pull_request_repo_destinations(self):
693 _ = self.request.translate
695 _ = self.request.translate
694 filter_query = self.request.GET.get('query')
696 filter_query = self.request.GET.get('query')
695
697
696 query = Repository.query() \
698 query = Repository.query() \
697 .order_by(func.length(Repository.repo_name)) \
699 .order_by(func.length(Repository.repo_name)) \
698 .filter(
700 .filter(
699 or_(Repository.repo_name == self.db_repo.repo_name,
701 or_(Repository.repo_name == self.db_repo.repo_name,
700 Repository.fork_id == self.db_repo.repo_id))
702 Repository.fork_id == self.db_repo.repo_id))
701
703
702 if filter_query:
704 if filter_query:
703 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
705 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
704 query = query.filter(
706 query = query.filter(
705 Repository.repo_name.ilike(ilike_expression))
707 Repository.repo_name.ilike(ilike_expression))
706
708
707 add_parent = False
709 add_parent = False
708 if self.db_repo.parent:
710 if self.db_repo.parent:
709 if filter_query in self.db_repo.parent.repo_name:
711 if filter_query in self.db_repo.parent.repo_name:
710 parent_vcs_obj = self.db_repo.parent.scm_instance()
712 parent_vcs_obj = self.db_repo.parent.scm_instance()
711 if parent_vcs_obj and not parent_vcs_obj.is_empty():
713 if parent_vcs_obj and not parent_vcs_obj.is_empty():
712 add_parent = True
714 add_parent = True
713
715
714 limit = 20 - 1 if add_parent else 20
716 limit = 20 - 1 if add_parent else 20
715 all_repos = query.limit(limit).all()
717 all_repos = query.limit(limit).all()
716 if add_parent:
718 if add_parent:
717 all_repos += [self.db_repo.parent]
719 all_repos += [self.db_repo.parent]
718
720
719 repos = []
721 repos = []
720 for obj in ScmModel().get_repos(all_repos):
722 for obj in ScmModel().get_repos(all_repos):
721 repos.append({
723 repos.append({
722 'id': obj['name'],
724 'id': obj['name'],
723 'text': obj['name'],
725 'text': obj['name'],
724 'type': 'repo',
726 'type': 'repo',
725 'obj': obj['dbrepo']
727 'obj': obj['dbrepo']
726 })
728 })
727
729
728 data = {
730 data = {
729 'more': False,
731 'more': False,
730 'results': [{
732 'results': [{
731 'text': _('Repositories'),
733 'text': _('Repositories'),
732 'children': repos
734 'children': repos
733 }] if repos else []
735 }] if repos else []
734 }
736 }
735 return data
737 return data
736
738
737 @LoginRequired()
739 @LoginRequired()
738 @NotAnonymous()
740 @NotAnonymous()
739 @HasRepoPermissionAnyDecorator(
741 @HasRepoPermissionAnyDecorator(
740 'repository.read', 'repository.write', 'repository.admin')
742 'repository.read', 'repository.write', 'repository.admin')
741 @CSRFRequired()
743 @CSRFRequired()
742 @view_config(
744 @view_config(
743 route_name='pullrequest_create', request_method='POST',
745 route_name='pullrequest_create', request_method='POST',
744 renderer=None)
746 renderer=None)
745 def pull_request_create(self):
747 def pull_request_create(self):
746 _ = self.request.translate
748 _ = self.request.translate
747 self.assure_not_empty_repo()
749 self.assure_not_empty_repo()
750 self.load_default_context()
748
751
749 controls = peppercorn.parse(self.request.POST.items())
752 controls = peppercorn.parse(self.request.POST.items())
750
753
751 try:
754 try:
752 form = PullRequestForm(
755 form = PullRequestForm(
753 self.request.translate, self.db_repo.repo_id)()
756 self.request.translate, self.db_repo.repo_id)()
754 _form = form.to_python(controls)
757 _form = form.to_python(controls)
755 except formencode.Invalid as errors:
758 except formencode.Invalid as errors:
756 if errors.error_dict.get('revisions'):
759 if errors.error_dict.get('revisions'):
757 msg = 'Revisions: %s' % errors.error_dict['revisions']
760 msg = 'Revisions: %s' % errors.error_dict['revisions']
758 elif errors.error_dict.get('pullrequest_title'):
761 elif errors.error_dict.get('pullrequest_title'):
759 msg = _('Pull request requires a title with min. 3 chars')
762 msg = _('Pull request requires a title with min. 3 chars')
760 else:
763 else:
761 msg = _('Error creating pull request: {}').format(errors)
764 msg = _('Error creating pull request: {}').format(errors)
762 log.exception(msg)
765 log.exception(msg)
763 h.flash(msg, 'error')
766 h.flash(msg, 'error')
764
767
765 # would rather just go back to form ...
768 # would rather just go back to form ...
766 raise HTTPFound(
769 raise HTTPFound(
767 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
770 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
768
771
769 source_repo = _form['source_repo']
772 source_repo = _form['source_repo']
770 source_ref = _form['source_ref']
773 source_ref = _form['source_ref']
771 target_repo = _form['target_repo']
774 target_repo = _form['target_repo']
772 target_ref = _form['target_ref']
775 target_ref = _form['target_ref']
773 commit_ids = _form['revisions'][::-1]
776 commit_ids = _form['revisions'][::-1]
774
777
775 # find the ancestor for this pr
778 # find the ancestor for this pr
776 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
779 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
777 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
780 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
778
781
779 # re-check permissions again here
782 # re-check permissions again here
780 # source_repo we must have read permissions
783 # source_repo we must have read permissions
781
784
782 source_perm = HasRepoPermissionAny(
785 source_perm = HasRepoPermissionAny(
783 'repository.read',
786 'repository.read',
784 'repository.write', 'repository.admin')(source_db_repo.repo_name)
787 'repository.write', 'repository.admin')(source_db_repo.repo_name)
785 if not source_perm:
788 if not source_perm:
786 msg = _('Not Enough permissions to source repo `{}`.'.format(
789 msg = _('Not Enough permissions to source repo `{}`.'.format(
787 source_db_repo.repo_name))
790 source_db_repo.repo_name))
788 h.flash(msg, category='error')
791 h.flash(msg, category='error')
789 # copy the args back to redirect
792 # copy the args back to redirect
790 org_query = self.request.GET.mixed()
793 org_query = self.request.GET.mixed()
791 raise HTTPFound(
794 raise HTTPFound(
792 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
795 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
793 _query=org_query))
796 _query=org_query))
794
797
795 # target repo we must have read permissions, and also later on
798 # target repo we must have read permissions, and also later on
796 # we want to check branch permissions here
799 # we want to check branch permissions here
797 target_perm = HasRepoPermissionAny(
800 target_perm = HasRepoPermissionAny(
798 'repository.read',
801 'repository.read',
799 'repository.write', 'repository.admin')(target_db_repo.repo_name)
802 'repository.write', 'repository.admin')(target_db_repo.repo_name)
800 if not target_perm:
803 if not target_perm:
801 msg = _('Not Enough permissions to target repo `{}`.'.format(
804 msg = _('Not Enough permissions to target repo `{}`.'.format(
802 target_db_repo.repo_name))
805 target_db_repo.repo_name))
803 h.flash(msg, category='error')
806 h.flash(msg, category='error')
804 # copy the args back to redirect
807 # copy the args back to redirect
805 org_query = self.request.GET.mixed()
808 org_query = self.request.GET.mixed()
806 raise HTTPFound(
809 raise HTTPFound(
807 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
810 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
808 _query=org_query))
811 _query=org_query))
809
812
810 source_scm = source_db_repo.scm_instance()
813 source_scm = source_db_repo.scm_instance()
811 target_scm = target_db_repo.scm_instance()
814 target_scm = target_db_repo.scm_instance()
812
815
813 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
816 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
814 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
817 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
815
818
816 ancestor = source_scm.get_common_ancestor(
819 ancestor = source_scm.get_common_ancestor(
817 source_commit.raw_id, target_commit.raw_id, target_scm)
820 source_commit.raw_id, target_commit.raw_id, target_scm)
818
821
819 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
822 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
820 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
823 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
821
824
822 pullrequest_title = _form['pullrequest_title']
825 pullrequest_title = _form['pullrequest_title']
823 title_source_ref = source_ref.split(':', 2)[1]
826 title_source_ref = source_ref.split(':', 2)[1]
824 if not pullrequest_title:
827 if not pullrequest_title:
825 pullrequest_title = PullRequestModel().generate_pullrequest_title(
828 pullrequest_title = PullRequestModel().generate_pullrequest_title(
826 source=source_repo,
829 source=source_repo,
827 source_ref=title_source_ref,
830 source_ref=title_source_ref,
828 target=target_repo
831 target=target_repo
829 )
832 )
830
833
831 description = _form['pullrequest_desc']
834 description = _form['pullrequest_desc']
832
835
833 get_default_reviewers_data, validate_default_reviewers = \
836 get_default_reviewers_data, validate_default_reviewers = \
834 PullRequestModel().get_reviewer_functions()
837 PullRequestModel().get_reviewer_functions()
835
838
836 # recalculate reviewers logic, to make sure we can validate this
839 # recalculate reviewers logic, to make sure we can validate this
837 reviewer_rules = get_default_reviewers_data(
840 reviewer_rules = get_default_reviewers_data(
838 self._rhodecode_db_user, source_db_repo,
841 self._rhodecode_db_user, source_db_repo,
839 source_commit, target_db_repo, target_commit)
842 source_commit, target_db_repo, target_commit)
840
843
841 given_reviewers = _form['review_members']
844 given_reviewers = _form['review_members']
842 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
845 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
843
846
844 try:
847 try:
845 pull_request = PullRequestModel().create(
848 pull_request = PullRequestModel().create(
846 self._rhodecode_user.user_id, source_repo, source_ref,
849 self._rhodecode_user.user_id, source_repo, source_ref,
847 target_repo, target_ref, commit_ids, reviewers,
850 target_repo, target_ref, commit_ids, reviewers,
848 pullrequest_title, description, reviewer_rules
851 pullrequest_title, description, reviewer_rules
849 )
852 )
850 Session().commit()
853 Session().commit()
851
854
852 h.flash(_('Successfully opened new pull request'),
855 h.flash(_('Successfully opened new pull request'),
853 category='success')
856 category='success')
854 except Exception:
857 except Exception:
855 msg = _('Error occurred during creation of this pull request.')
858 msg = _('Error occurred during creation of this pull request.')
856 log.exception(msg)
859 log.exception(msg)
857 h.flash(msg, category='error')
860 h.flash(msg, category='error')
858
861
859 # copy the args back to redirect
862 # copy the args back to redirect
860 org_query = self.request.GET.mixed()
863 org_query = self.request.GET.mixed()
861 raise HTTPFound(
864 raise HTTPFound(
862 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
865 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
863 _query=org_query))
866 _query=org_query))
864
867
865 raise HTTPFound(
868 raise HTTPFound(
866 h.route_path('pullrequest_show', repo_name=target_repo,
869 h.route_path('pullrequest_show', repo_name=target_repo,
867 pull_request_id=pull_request.pull_request_id))
870 pull_request_id=pull_request.pull_request_id))
868
871
869 @LoginRequired()
872 @LoginRequired()
870 @NotAnonymous()
873 @NotAnonymous()
871 @HasRepoPermissionAnyDecorator(
874 @HasRepoPermissionAnyDecorator(
872 'repository.read', 'repository.write', 'repository.admin')
875 'repository.read', 'repository.write', 'repository.admin')
873 @CSRFRequired()
876 @CSRFRequired()
874 @view_config(
877 @view_config(
875 route_name='pullrequest_update', request_method='POST',
878 route_name='pullrequest_update', request_method='POST',
876 renderer='json_ext')
879 renderer='json_ext')
877 def pull_request_update(self):
880 def pull_request_update(self):
878 pull_request = PullRequest.get_or_404(
881 pull_request = PullRequest.get_or_404(
879 self.request.matchdict['pull_request_id'])
882 self.request.matchdict['pull_request_id'])
880
883
884 self.load_default_context()
881 # only owner or admin can update it
885 # only owner or admin can update it
882 allowed_to_update = PullRequestModel().check_user_update(
886 allowed_to_update = PullRequestModel().check_user_update(
883 pull_request, self._rhodecode_user)
887 pull_request, self._rhodecode_user)
884 if allowed_to_update:
888 if allowed_to_update:
885 controls = peppercorn.parse(self.request.POST.items())
889 controls = peppercorn.parse(self.request.POST.items())
886
890
887 if 'review_members' in controls:
891 if 'review_members' in controls:
888 self._update_reviewers(
892 self._update_reviewers(
889 pull_request, controls['review_members'],
893 pull_request, controls['review_members'],
890 pull_request.reviewer_data)
894 pull_request.reviewer_data)
891 elif str2bool(self.request.POST.get('update_commits', 'false')):
895 elif str2bool(self.request.POST.get('update_commits', 'false')):
892 self._update_commits(pull_request)
896 self._update_commits(pull_request)
893 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
897 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
894 self._edit_pull_request(pull_request)
898 self._edit_pull_request(pull_request)
895 else:
899 else:
896 raise HTTPBadRequest()
900 raise HTTPBadRequest()
897 return True
901 return True
898 raise HTTPForbidden()
902 raise HTTPForbidden()
899
903
900 def _edit_pull_request(self, pull_request):
904 def _edit_pull_request(self, pull_request):
901 _ = self.request.translate
905 _ = self.request.translate
902 try:
906 try:
903 PullRequestModel().edit(
907 PullRequestModel().edit(
904 pull_request, self.request.POST.get('title'),
908 pull_request, self.request.POST.get('title'),
905 self.request.POST.get('description'), self._rhodecode_user)
909 self.request.POST.get('description'), self._rhodecode_user)
906 except ValueError:
910 except ValueError:
907 msg = _(u'Cannot update closed pull requests.')
911 msg = _(u'Cannot update closed pull requests.')
908 h.flash(msg, category='error')
912 h.flash(msg, category='error')
909 return
913 return
910 else:
914 else:
911 Session().commit()
915 Session().commit()
912
916
913 msg = _(u'Pull request title & description updated.')
917 msg = _(u'Pull request title & description updated.')
914 h.flash(msg, category='success')
918 h.flash(msg, category='success')
915 return
919 return
916
920
917 def _update_commits(self, pull_request):
921 def _update_commits(self, pull_request):
918 _ = self.request.translate
922 _ = self.request.translate
919 resp = PullRequestModel().update_commits(pull_request)
923 resp = PullRequestModel().update_commits(pull_request)
920
924
921 if resp.executed:
925 if resp.executed:
922
926
923 if resp.target_changed and resp.source_changed:
927 if resp.target_changed and resp.source_changed:
924 changed = 'target and source repositories'
928 changed = 'target and source repositories'
925 elif resp.target_changed and not resp.source_changed:
929 elif resp.target_changed and not resp.source_changed:
926 changed = 'target repository'
930 changed = 'target repository'
927 elif not resp.target_changed and resp.source_changed:
931 elif not resp.target_changed and resp.source_changed:
928 changed = 'source repository'
932 changed = 'source repository'
929 else:
933 else:
930 changed = 'nothing'
934 changed = 'nothing'
931
935
932 msg = _(
936 msg = _(
933 u'Pull request updated to "{source_commit_id}" with '
937 u'Pull request updated to "{source_commit_id}" with '
934 u'{count_added} added, {count_removed} removed commits. '
938 u'{count_added} added, {count_removed} removed commits. '
935 u'Source of changes: {change_source}')
939 u'Source of changes: {change_source}')
936 msg = msg.format(
940 msg = msg.format(
937 source_commit_id=pull_request.source_ref_parts.commit_id,
941 source_commit_id=pull_request.source_ref_parts.commit_id,
938 count_added=len(resp.changes.added),
942 count_added=len(resp.changes.added),
939 count_removed=len(resp.changes.removed),
943 count_removed=len(resp.changes.removed),
940 change_source=changed)
944 change_source=changed)
941 h.flash(msg, category='success')
945 h.flash(msg, category='success')
942
946
943 channel = '/repo${}$/pr/{}'.format(
947 channel = '/repo${}$/pr/{}'.format(
944 pull_request.target_repo.repo_name,
948 pull_request.target_repo.repo_name,
945 pull_request.pull_request_id)
949 pull_request.pull_request_id)
946 message = msg + (
950 message = msg + (
947 ' - <a onclick="window.location.reload()">'
951 ' - <a onclick="window.location.reload()">'
948 '<strong>{}</strong></a>'.format(_('Reload page')))
952 '<strong>{}</strong></a>'.format(_('Reload page')))
949 channelstream.post_message(
953 channelstream.post_message(
950 channel, message, self._rhodecode_user.username,
954 channel, message, self._rhodecode_user.username,
951 registry=self.request.registry)
955 registry=self.request.registry)
952 else:
956 else:
953 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
957 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
954 warning_reasons = [
958 warning_reasons = [
955 UpdateFailureReason.NO_CHANGE,
959 UpdateFailureReason.NO_CHANGE,
956 UpdateFailureReason.WRONG_REF_TYPE,
960 UpdateFailureReason.WRONG_REF_TYPE,
957 ]
961 ]
958 category = 'warning' if resp.reason in warning_reasons else 'error'
962 category = 'warning' if resp.reason in warning_reasons else 'error'
959 h.flash(msg, category=category)
963 h.flash(msg, category=category)
960
964
961 @LoginRequired()
965 @LoginRequired()
962 @NotAnonymous()
966 @NotAnonymous()
963 @HasRepoPermissionAnyDecorator(
967 @HasRepoPermissionAnyDecorator(
964 'repository.read', 'repository.write', 'repository.admin')
968 'repository.read', 'repository.write', 'repository.admin')
965 @CSRFRequired()
969 @CSRFRequired()
966 @view_config(
970 @view_config(
967 route_name='pullrequest_merge', request_method='POST',
971 route_name='pullrequest_merge', request_method='POST',
968 renderer='json_ext')
972 renderer='json_ext')
969 def pull_request_merge(self):
973 def pull_request_merge(self):
970 """
974 """
971 Merge will perform a server-side merge of the specified
975 Merge will perform a server-side merge of the specified
972 pull request, if the pull request is approved and mergeable.
976 pull request, if the pull request is approved and mergeable.
973 After successful merging, the pull request is automatically
977 After successful merging, the pull request is automatically
974 closed, with a relevant comment.
978 closed, with a relevant comment.
975 """
979 """
976 pull_request = PullRequest.get_or_404(
980 pull_request = PullRequest.get_or_404(
977 self.request.matchdict['pull_request_id'])
981 self.request.matchdict['pull_request_id'])
978
982
983 self.load_default_context()
979 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
984 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
980 translator=self.request.translate)
985 translator=self.request.translate)
981 merge_possible = not check.failed
986 merge_possible = not check.failed
982
987
983 for err_type, error_msg in check.errors:
988 for err_type, error_msg in check.errors:
984 h.flash(error_msg, category=err_type)
989 h.flash(error_msg, category=err_type)
985
990
986 if merge_possible:
991 if merge_possible:
987 log.debug("Pre-conditions checked, trying to merge.")
992 log.debug("Pre-conditions checked, trying to merge.")
988 extras = vcs_operation_context(
993 extras = vcs_operation_context(
989 self.request.environ, repo_name=pull_request.target_repo.repo_name,
994 self.request.environ, repo_name=pull_request.target_repo.repo_name,
990 username=self._rhodecode_db_user.username, action='push',
995 username=self._rhodecode_db_user.username, action='push',
991 scm=pull_request.target_repo.repo_type)
996 scm=pull_request.target_repo.repo_type)
992 self._merge_pull_request(
997 self._merge_pull_request(
993 pull_request, self._rhodecode_db_user, extras)
998 pull_request, self._rhodecode_db_user, extras)
994 else:
999 else:
995 log.debug("Pre-conditions failed, NOT merging.")
1000 log.debug("Pre-conditions failed, NOT merging.")
996
1001
997 raise HTTPFound(
1002 raise HTTPFound(
998 h.route_path('pullrequest_show',
1003 h.route_path('pullrequest_show',
999 repo_name=pull_request.target_repo.repo_name,
1004 repo_name=pull_request.target_repo.repo_name,
1000 pull_request_id=pull_request.pull_request_id))
1005 pull_request_id=pull_request.pull_request_id))
1001
1006
1002 def _merge_pull_request(self, pull_request, user, extras):
1007 def _merge_pull_request(self, pull_request, user, extras):
1003 _ = self.request.translate
1008 _ = self.request.translate
1004 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1009 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1005
1010
1006 if merge_resp.executed:
1011 if merge_resp.executed:
1007 log.debug("The merge was successful, closing the pull request.")
1012 log.debug("The merge was successful, closing the pull request.")
1008 PullRequestModel().close_pull_request(
1013 PullRequestModel().close_pull_request(
1009 pull_request.pull_request_id, user)
1014 pull_request.pull_request_id, user)
1010 Session().commit()
1015 Session().commit()
1011 msg = _('Pull request was successfully merged and closed.')
1016 msg = _('Pull request was successfully merged and closed.')
1012 h.flash(msg, category='success')
1017 h.flash(msg, category='success')
1013 else:
1018 else:
1014 log.debug(
1019 log.debug(
1015 "The merge was not successful. Merge response: %s",
1020 "The merge was not successful. Merge response: %s",
1016 merge_resp)
1021 merge_resp)
1017 msg = PullRequestModel().merge_status_message(
1022 msg = PullRequestModel().merge_status_message(
1018 merge_resp.failure_reason)
1023 merge_resp.failure_reason)
1019 h.flash(msg, category='error')
1024 h.flash(msg, category='error')
1020
1025
1021 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1026 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1022 _ = self.request.translate
1027 _ = self.request.translate
1023 get_default_reviewers_data, validate_default_reviewers = \
1028 get_default_reviewers_data, validate_default_reviewers = \
1024 PullRequestModel().get_reviewer_functions()
1029 PullRequestModel().get_reviewer_functions()
1025
1030
1026 try:
1031 try:
1027 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1032 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1028 except ValueError as e:
1033 except ValueError as e:
1029 log.error('Reviewers Validation: {}'.format(e))
1034 log.error('Reviewers Validation: {}'.format(e))
1030 h.flash(e, category='error')
1035 h.flash(e, category='error')
1031 return
1036 return
1032
1037
1033 PullRequestModel().update_reviewers(
1038 PullRequestModel().update_reviewers(
1034 pull_request, reviewers, self._rhodecode_user)
1039 pull_request, reviewers, self._rhodecode_user)
1035 h.flash(_('Pull request reviewers updated.'), category='success')
1040 h.flash(_('Pull request reviewers updated.'), category='success')
1036 Session().commit()
1041 Session().commit()
1037
1042
1038 @LoginRequired()
1043 @LoginRequired()
1039 @NotAnonymous()
1044 @NotAnonymous()
1040 @HasRepoPermissionAnyDecorator(
1045 @HasRepoPermissionAnyDecorator(
1041 'repository.read', 'repository.write', 'repository.admin')
1046 'repository.read', 'repository.write', 'repository.admin')
1042 @CSRFRequired()
1047 @CSRFRequired()
1043 @view_config(
1048 @view_config(
1044 route_name='pullrequest_delete', request_method='POST',
1049 route_name='pullrequest_delete', request_method='POST',
1045 renderer='json_ext')
1050 renderer='json_ext')
1046 def pull_request_delete(self):
1051 def pull_request_delete(self):
1047 _ = self.request.translate
1052 _ = self.request.translate
1048
1053
1049 pull_request = PullRequest.get_or_404(
1054 pull_request = PullRequest.get_or_404(
1050 self.request.matchdict['pull_request_id'])
1055 self.request.matchdict['pull_request_id'])
1056 self.load_default_context()
1051
1057
1052 pr_closed = pull_request.is_closed()
1058 pr_closed = pull_request.is_closed()
1053 allowed_to_delete = PullRequestModel().check_user_delete(
1059 allowed_to_delete = PullRequestModel().check_user_delete(
1054 pull_request, self._rhodecode_user) and not pr_closed
1060 pull_request, self._rhodecode_user) and not pr_closed
1055
1061
1056 # only owner can delete it !
1062 # only owner can delete it !
1057 if allowed_to_delete:
1063 if allowed_to_delete:
1058 PullRequestModel().delete(pull_request, self._rhodecode_user)
1064 PullRequestModel().delete(pull_request, self._rhodecode_user)
1059 Session().commit()
1065 Session().commit()
1060 h.flash(_('Successfully deleted pull request'),
1066 h.flash(_('Successfully deleted pull request'),
1061 category='success')
1067 category='success')
1062 raise HTTPFound(h.route_path('pullrequest_show_all',
1068 raise HTTPFound(h.route_path('pullrequest_show_all',
1063 repo_name=self.db_repo_name))
1069 repo_name=self.db_repo_name))
1064
1070
1065 log.warning('user %s tried to delete pull request without access',
1071 log.warning('user %s tried to delete pull request without access',
1066 self._rhodecode_user)
1072 self._rhodecode_user)
1067 raise HTTPNotFound()
1073 raise HTTPNotFound()
1068
1074
1069 @LoginRequired()
1075 @LoginRequired()
1070 @NotAnonymous()
1076 @NotAnonymous()
1071 @HasRepoPermissionAnyDecorator(
1077 @HasRepoPermissionAnyDecorator(
1072 'repository.read', 'repository.write', 'repository.admin')
1078 'repository.read', 'repository.write', 'repository.admin')
1073 @CSRFRequired()
1079 @CSRFRequired()
1074 @view_config(
1080 @view_config(
1075 route_name='pullrequest_comment_create', request_method='POST',
1081 route_name='pullrequest_comment_create', request_method='POST',
1076 renderer='json_ext')
1082 renderer='json_ext')
1077 def pull_request_comment_create(self):
1083 def pull_request_comment_create(self):
1078 _ = self.request.translate
1084 _ = self.request.translate
1079
1085
1080 pull_request = PullRequest.get_or_404(
1086 pull_request = PullRequest.get_or_404(
1081 self.request.matchdict['pull_request_id'])
1087 self.request.matchdict['pull_request_id'])
1082 pull_request_id = pull_request.pull_request_id
1088 pull_request_id = pull_request.pull_request_id
1083
1089
1084 if pull_request.is_closed():
1090 if pull_request.is_closed():
1085 log.debug('comment: forbidden because pull request is closed')
1091 log.debug('comment: forbidden because pull request is closed')
1086 raise HTTPForbidden()
1092 raise HTTPForbidden()
1087
1093
1088 allowed_to_comment = PullRequestModel().check_user_comment(
1094 allowed_to_comment = PullRequestModel().check_user_comment(
1089 pull_request, self._rhodecode_user)
1095 pull_request, self._rhodecode_user)
1090 if not allowed_to_comment:
1096 if not allowed_to_comment:
1091 log.debug(
1097 log.debug(
1092 'comment: forbidden because pull request is from forbidden repo')
1098 'comment: forbidden because pull request is from forbidden repo')
1093 raise HTTPForbidden()
1099 raise HTTPForbidden()
1094
1100
1095 c = self.load_default_context()
1101 c = self.load_default_context()
1096
1102
1097 status = self.request.POST.get('changeset_status', None)
1103 status = self.request.POST.get('changeset_status', None)
1098 text = self.request.POST.get('text')
1104 text = self.request.POST.get('text')
1099 comment_type = self.request.POST.get('comment_type')
1105 comment_type = self.request.POST.get('comment_type')
1100 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1106 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1101 close_pull_request = self.request.POST.get('close_pull_request')
1107 close_pull_request = self.request.POST.get('close_pull_request')
1102
1108
1103 # the logic here should work like following, if we submit close
1109 # the logic here should work like following, if we submit close
1104 # pr comment, use `close_pull_request_with_comment` function
1110 # pr comment, use `close_pull_request_with_comment` function
1105 # else handle regular comment logic
1111 # else handle regular comment logic
1106
1112
1107 if close_pull_request:
1113 if close_pull_request:
1108 # only owner or admin or person with write permissions
1114 # only owner or admin or person with write permissions
1109 allowed_to_close = PullRequestModel().check_user_update(
1115 allowed_to_close = PullRequestModel().check_user_update(
1110 pull_request, self._rhodecode_user)
1116 pull_request, self._rhodecode_user)
1111 if not allowed_to_close:
1117 if not allowed_to_close:
1112 log.debug('comment: forbidden because not allowed to close '
1118 log.debug('comment: forbidden because not allowed to close '
1113 'pull request %s', pull_request_id)
1119 'pull request %s', pull_request_id)
1114 raise HTTPForbidden()
1120 raise HTTPForbidden()
1115 comment, status = PullRequestModel().close_pull_request_with_comment(
1121 comment, status = PullRequestModel().close_pull_request_with_comment(
1116 pull_request, self._rhodecode_user, self.db_repo, message=text)
1122 pull_request, self._rhodecode_user, self.db_repo, message=text)
1117 Session().flush()
1123 Session().flush()
1118 events.trigger(
1124 events.trigger(
1119 events.PullRequestCommentEvent(pull_request, comment))
1125 events.PullRequestCommentEvent(pull_request, comment))
1120
1126
1121 else:
1127 else:
1122 # regular comment case, could be inline, or one with status.
1128 # regular comment case, could be inline, or one with status.
1123 # for that one we check also permissions
1129 # for that one we check also permissions
1124
1130
1125 allowed_to_change_status = PullRequestModel().check_user_change_status(
1131 allowed_to_change_status = PullRequestModel().check_user_change_status(
1126 pull_request, self._rhodecode_user)
1132 pull_request, self._rhodecode_user)
1127
1133
1128 if status and allowed_to_change_status:
1134 if status and allowed_to_change_status:
1129 message = (_('Status change %(transition_icon)s %(status)s')
1135 message = (_('Status change %(transition_icon)s %(status)s')
1130 % {'transition_icon': '>',
1136 % {'transition_icon': '>',
1131 'status': ChangesetStatus.get_status_lbl(status)})
1137 'status': ChangesetStatus.get_status_lbl(status)})
1132 text = text or message
1138 text = text or message
1133
1139
1134 comment = CommentsModel().create(
1140 comment = CommentsModel().create(
1135 text=text,
1141 text=text,
1136 repo=self.db_repo.repo_id,
1142 repo=self.db_repo.repo_id,
1137 user=self._rhodecode_user.user_id,
1143 user=self._rhodecode_user.user_id,
1138 pull_request=pull_request,
1144 pull_request=pull_request,
1139 f_path=self.request.POST.get('f_path'),
1145 f_path=self.request.POST.get('f_path'),
1140 line_no=self.request.POST.get('line'),
1146 line_no=self.request.POST.get('line'),
1141 status_change=(ChangesetStatus.get_status_lbl(status)
1147 status_change=(ChangesetStatus.get_status_lbl(status)
1142 if status and allowed_to_change_status else None),
1148 if status and allowed_to_change_status else None),
1143 status_change_type=(status
1149 status_change_type=(status
1144 if status and allowed_to_change_status else None),
1150 if status and allowed_to_change_status else None),
1145 comment_type=comment_type,
1151 comment_type=comment_type,
1146 resolves_comment_id=resolves_comment_id
1152 resolves_comment_id=resolves_comment_id
1147 )
1153 )
1148
1154
1149 if allowed_to_change_status:
1155 if allowed_to_change_status:
1150 # calculate old status before we change it
1156 # calculate old status before we change it
1151 old_calculated_status = pull_request.calculated_review_status()
1157 old_calculated_status = pull_request.calculated_review_status()
1152
1158
1153 # get status if set !
1159 # get status if set !
1154 if status:
1160 if status:
1155 ChangesetStatusModel().set_status(
1161 ChangesetStatusModel().set_status(
1156 self.db_repo.repo_id,
1162 self.db_repo.repo_id,
1157 status,
1163 status,
1158 self._rhodecode_user.user_id,
1164 self._rhodecode_user.user_id,
1159 comment,
1165 comment,
1160 pull_request=pull_request
1166 pull_request=pull_request
1161 )
1167 )
1162
1168
1163 Session().flush()
1169 Session().flush()
1164 events.trigger(
1170 events.trigger(
1165 events.PullRequestCommentEvent(pull_request, comment))
1171 events.PullRequestCommentEvent(pull_request, comment))
1166
1172
1167 # we now calculate the status of pull request, and based on that
1173 # we now calculate the status of pull request, and based on that
1168 # calculation we set the commits status
1174 # calculation we set the commits status
1169 calculated_status = pull_request.calculated_review_status()
1175 calculated_status = pull_request.calculated_review_status()
1170 if old_calculated_status != calculated_status:
1176 if old_calculated_status != calculated_status:
1171 PullRequestModel()._trigger_pull_request_hook(
1177 PullRequestModel()._trigger_pull_request_hook(
1172 pull_request, self._rhodecode_user, 'review_status_change')
1178 pull_request, self._rhodecode_user, 'review_status_change')
1173
1179
1174 Session().commit()
1180 Session().commit()
1175
1181
1176 data = {
1182 data = {
1177 'target_id': h.safeid(h.safe_unicode(
1183 'target_id': h.safeid(h.safe_unicode(
1178 self.request.POST.get('f_path'))),
1184 self.request.POST.get('f_path'))),
1179 }
1185 }
1180 if comment:
1186 if comment:
1181 c.co = comment
1187 c.co = comment
1182 rendered_comment = render(
1188 rendered_comment = render(
1183 'rhodecode:templates/changeset/changeset_comment_block.mako',
1189 'rhodecode:templates/changeset/changeset_comment_block.mako',
1184 self._get_template_context(c), self.request)
1190 self._get_template_context(c), self.request)
1185
1191
1186 data.update(comment.get_dict())
1192 data.update(comment.get_dict())
1187 data.update({'rendered_text': rendered_comment})
1193 data.update({'rendered_text': rendered_comment})
1188
1194
1189 return data
1195 return data
1190
1196
1191 @LoginRequired()
1197 @LoginRequired()
1192 @NotAnonymous()
1198 @NotAnonymous()
1193 @HasRepoPermissionAnyDecorator(
1199 @HasRepoPermissionAnyDecorator(
1194 'repository.read', 'repository.write', 'repository.admin')
1200 'repository.read', 'repository.write', 'repository.admin')
1195 @CSRFRequired()
1201 @CSRFRequired()
1196 @view_config(
1202 @view_config(
1197 route_name='pullrequest_comment_delete', request_method='POST',
1203 route_name='pullrequest_comment_delete', request_method='POST',
1198 renderer='json_ext')
1204 renderer='json_ext')
1199 def pull_request_comment_delete(self):
1205 def pull_request_comment_delete(self):
1200 pull_request = PullRequest.get_or_404(
1206 pull_request = PullRequest.get_or_404(
1201 self.request.matchdict['pull_request_id'])
1207 self.request.matchdict['pull_request_id'])
1202
1208
1203 comment = ChangesetComment.get_or_404(
1209 comment = ChangesetComment.get_or_404(
1204 self.request.matchdict['comment_id'])
1210 self.request.matchdict['comment_id'])
1205 comment_id = comment.comment_id
1211 comment_id = comment.comment_id
1206
1212
1207 if pull_request.is_closed():
1213 if pull_request.is_closed():
1208 log.debug('comment: forbidden because pull request is closed')
1214 log.debug('comment: forbidden because pull request is closed')
1209 raise HTTPForbidden()
1215 raise HTTPForbidden()
1210
1216
1211 if not comment:
1217 if not comment:
1212 log.debug('Comment with id:%s not found, skipping', comment_id)
1218 log.debug('Comment with id:%s not found, skipping', comment_id)
1213 # comment already deleted in another call probably
1219 # comment already deleted in another call probably
1214 return True
1220 return True
1215
1221
1216 if comment.pull_request.is_closed():
1222 if comment.pull_request.is_closed():
1217 # don't allow deleting comments on closed pull request
1223 # don't allow deleting comments on closed pull request
1218 raise HTTPForbidden()
1224 raise HTTPForbidden()
1219
1225
1220 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1226 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1221 super_admin = h.HasPermissionAny('hg.admin')()
1227 super_admin = h.HasPermissionAny('hg.admin')()
1222 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1228 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1223 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1229 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1224 comment_repo_admin = is_repo_admin and is_repo_comment
1230 comment_repo_admin = is_repo_admin and is_repo_comment
1225
1231
1226 if super_admin or comment_owner or comment_repo_admin:
1232 if super_admin or comment_owner or comment_repo_admin:
1227 old_calculated_status = comment.pull_request.calculated_review_status()
1233 old_calculated_status = comment.pull_request.calculated_review_status()
1228 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1234 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1229 Session().commit()
1235 Session().commit()
1230 calculated_status = comment.pull_request.calculated_review_status()
1236 calculated_status = comment.pull_request.calculated_review_status()
1231 if old_calculated_status != calculated_status:
1237 if old_calculated_status != calculated_status:
1232 PullRequestModel()._trigger_pull_request_hook(
1238 PullRequestModel()._trigger_pull_request_hook(
1233 comment.pull_request, self._rhodecode_user, 'review_status_change')
1239 comment.pull_request, self._rhodecode_user, 'review_status_change')
1234 return True
1240 return True
1235 else:
1241 else:
1236 log.warning('No permissions for user %s to delete comment_id: %s',
1242 log.warning('No permissions for user %s to delete comment_id: %s',
1237 self._rhodecode_db_user, comment_id)
1243 self._rhodecode_db_user, comment_id)
1238 raise HTTPNotFound()
1244 raise HTTPNotFound()
@@ -1,251 +1,252 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 import deform
23 import deform
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26
26
27 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
28 from rhodecode.forms import RcForm
28 from rhodecode.forms import RcForm
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository
33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35 from rhodecode.model.repo import RepoModel
35 from rhodecode.model.repo import RepoModel
36 from rhodecode.model.scm import RepoGroupList, ScmModel
36 from rhodecode.model.scm import RepoGroupList, ScmModel
37 from rhodecode.model.validation_schema.schemas import repo_schema
37 from rhodecode.model.validation_schema.schemas import repo_schema
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class RepoSettingsView(RepoAppView):
42 class RepoSettingsView(RepoAppView):
43
43
44 def load_default_context(self):
44 def load_default_context(self):
45 c = self._get_local_tmpl_context()
45 c = self._get_local_tmpl_context()
46
46
47 acl_groups = RepoGroupList(
47 acl_groups = RepoGroupList(
48 RepoGroup.query().all(),
48 RepoGroup.query().all(),
49 perm_set=['group.write', 'group.admin'])
49 perm_set=['group.write', 'group.admin'])
50 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
50 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
51 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
51 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
52
52
53 # in case someone no longer have a group.write access to a repository
53 # in case someone no longer have a group.write access to a repository
54 # pre fill the list with this entry, we don't care if this is the same
54 # pre fill the list with this entry, we don't care if this is the same
55 # but it will allow saving repo data properly.
55 # but it will allow saving repo data properly.
56 repo_group = self.db_repo.group
56 repo_group = self.db_repo.group
57 if repo_group and repo_group.group_id not in c.repo_groups_choices:
57 if repo_group and repo_group.group_id not in c.repo_groups_choices:
58 c.repo_groups_choices.append(repo_group.group_id)
58 c.repo_groups_choices.append(repo_group.group_id)
59 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
59 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
60
60
61 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
61 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
62 # we might be in missing requirement state, so we load things
62 # we might be in missing requirement state, so we load things
63 # without touching scm_instance()
63 # without touching scm_instance()
64 c.landing_revs_choices, c.landing_revs = \
64 c.landing_revs_choices, c.landing_revs = \
65 ScmModel().get_repo_landing_revs()
65 ScmModel().get_repo_landing_revs(self.request.translate)
66 else:
66 else:
67 c.landing_revs_choices, c.landing_revs = \
67 c.landing_revs_choices, c.landing_revs = \
68 ScmModel().get_repo_landing_revs(self.db_repo)
68 ScmModel().get_repo_landing_revs(
69 self.request.translate, self.db_repo)
69
70
70 c.personal_repo_group = c.auth_user.personal_repo_group
71 c.personal_repo_group = c.auth_user.personal_repo_group
71 c.repo_fields = RepositoryField.query()\
72 c.repo_fields = RepositoryField.query()\
72 .filter(RepositoryField.repository == self.db_repo).all()
73 .filter(RepositoryField.repository == self.db_repo).all()
73
74
74
75
75 return c
76 return c
76
77
77 def _get_schema(self, c, old_values=None):
78 def _get_schema(self, c, old_values=None):
78 return repo_schema.RepoSettingsSchema().bind(
79 return repo_schema.RepoSettingsSchema().bind(
79 repo_type=self.db_repo.repo_type,
80 repo_type=self.db_repo.repo_type,
80 repo_type_options=[self.db_repo.repo_type],
81 repo_type_options=[self.db_repo.repo_type],
81 repo_ref_options=c.landing_revs_choices,
82 repo_ref_options=c.landing_revs_choices,
82 repo_ref_items=c.landing_revs,
83 repo_ref_items=c.landing_revs,
83 repo_repo_group_options=c.repo_groups_choices,
84 repo_repo_group_options=c.repo_groups_choices,
84 repo_repo_group_items=c.repo_groups,
85 repo_repo_group_items=c.repo_groups,
85 # user caller
86 # user caller
86 user=self._rhodecode_user,
87 user=self._rhodecode_user,
87 old_values=old_values
88 old_values=old_values
88 )
89 )
89
90
90 @LoginRequired()
91 @LoginRequired()
91 @HasRepoPermissionAnyDecorator('repository.admin')
92 @HasRepoPermissionAnyDecorator('repository.admin')
92 @view_config(
93 @view_config(
93 route_name='edit_repo', request_method='GET',
94 route_name='edit_repo', request_method='GET',
94 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
95 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
95 def edit_settings(self):
96 def edit_settings(self):
96 c = self.load_default_context()
97 c = self.load_default_context()
97 c.active = 'settings'
98 c.active = 'settings'
98
99
99 defaults = RepoModel()._get_defaults(self.db_repo_name)
100 defaults = RepoModel()._get_defaults(self.db_repo_name)
100 defaults['repo_owner'] = defaults['user']
101 defaults['repo_owner'] = defaults['user']
101 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
102 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
102
103
103 schema = self._get_schema(c)
104 schema = self._get_schema(c)
104 c.form = RcForm(schema, appstruct=defaults)
105 c.form = RcForm(schema, appstruct=defaults)
105 return self._get_template_context(c)
106 return self._get_template_context(c)
106
107
107 @LoginRequired()
108 @LoginRequired()
108 @HasRepoPermissionAnyDecorator('repository.admin')
109 @HasRepoPermissionAnyDecorator('repository.admin')
109 @CSRFRequired()
110 @CSRFRequired()
110 @view_config(
111 @view_config(
111 route_name='edit_repo', request_method='POST',
112 route_name='edit_repo', request_method='POST',
112 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
113 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
113 def edit_settings_update(self):
114 def edit_settings_update(self):
114 _ = self.request.translate
115 _ = self.request.translate
115 c = self.load_default_context()
116 c = self.load_default_context()
116 c.active = 'settings'
117 c.active = 'settings'
117 old_repo_name = self.db_repo_name
118 old_repo_name = self.db_repo_name
118
119
119 old_values = self.db_repo.get_api_data()
120 old_values = self.db_repo.get_api_data()
120 schema = self._get_schema(c, old_values=old_values)
121 schema = self._get_schema(c, old_values=old_values)
121
122
122 c.form = RcForm(schema)
123 c.form = RcForm(schema)
123 pstruct = self.request.POST.items()
124 pstruct = self.request.POST.items()
124 pstruct.append(('repo_type', self.db_repo.repo_type))
125 pstruct.append(('repo_type', self.db_repo.repo_type))
125 try:
126 try:
126 schema_data = c.form.validate(pstruct)
127 schema_data = c.form.validate(pstruct)
127 except deform.ValidationFailure as err_form:
128 except deform.ValidationFailure as err_form:
128 return self._get_template_context(c)
129 return self._get_template_context(c)
129
130
130 # data is now VALID, proceed with updates
131 # data is now VALID, proceed with updates
131 # save validated data back into the updates dict
132 # save validated data back into the updates dict
132 validated_updates = dict(
133 validated_updates = dict(
133 repo_name=schema_data['repo_group']['repo_name_without_group'],
134 repo_name=schema_data['repo_group']['repo_name_without_group'],
134 repo_group=schema_data['repo_group']['repo_group_id'],
135 repo_group=schema_data['repo_group']['repo_group_id'],
135
136
136 user=schema_data['repo_owner'],
137 user=schema_data['repo_owner'],
137 repo_description=schema_data['repo_description'],
138 repo_description=schema_data['repo_description'],
138 repo_private=schema_data['repo_private'],
139 repo_private=schema_data['repo_private'],
139 clone_uri=schema_data['repo_clone_uri'],
140 clone_uri=schema_data['repo_clone_uri'],
140 repo_landing_rev=schema_data['repo_landing_commit_ref'],
141 repo_landing_rev=schema_data['repo_landing_commit_ref'],
141 repo_enable_statistics=schema_data['repo_enable_statistics'],
142 repo_enable_statistics=schema_data['repo_enable_statistics'],
142 repo_enable_locking=schema_data['repo_enable_locking'],
143 repo_enable_locking=schema_data['repo_enable_locking'],
143 repo_enable_downloads=schema_data['repo_enable_downloads'],
144 repo_enable_downloads=schema_data['repo_enable_downloads'],
144 )
145 )
145 # detect if CLONE URI changed, if we get OLD means we keep old values
146 # detect if CLONE URI changed, if we get OLD means we keep old values
146 if schema_data['repo_clone_uri_change'] == 'OLD':
147 if schema_data['repo_clone_uri_change'] == 'OLD':
147 validated_updates['clone_uri'] = self.db_repo.clone_uri
148 validated_updates['clone_uri'] = self.db_repo.clone_uri
148
149
149 # use the new full name for redirect
150 # use the new full name for redirect
150 new_repo_name = schema_data['repo_group']['repo_name_with_group']
151 new_repo_name = schema_data['repo_group']['repo_name_with_group']
151
152
152 # save extra fields into our validated data
153 # save extra fields into our validated data
153 for key, value in pstruct:
154 for key, value in pstruct:
154 if key.startswith(RepositoryField.PREFIX):
155 if key.startswith(RepositoryField.PREFIX):
155 validated_updates[key] = value
156 validated_updates[key] = value
156
157
157 try:
158 try:
158 RepoModel().update(self.db_repo, **validated_updates)
159 RepoModel().update(self.db_repo, **validated_updates)
159 ScmModel().mark_for_invalidation(new_repo_name)
160 ScmModel().mark_for_invalidation(new_repo_name)
160
161
161 audit_logger.store_web(
162 audit_logger.store_web(
162 'repo.edit', action_data={'old_data': old_values},
163 'repo.edit', action_data={'old_data': old_values},
163 user=self._rhodecode_user, repo=self.db_repo)
164 user=self._rhodecode_user, repo=self.db_repo)
164
165
165 Session().commit()
166 Session().commit()
166
167
167 h.flash(_('Repository `{}` updated successfully').format(
168 h.flash(_('Repository `{}` updated successfully').format(
168 old_repo_name), category='success')
169 old_repo_name), category='success')
169 except Exception:
170 except Exception:
170 log.exception("Exception during update of repository")
171 log.exception("Exception during update of repository")
171 h.flash(_('Error occurred during update of repository {}').format(
172 h.flash(_('Error occurred during update of repository {}').format(
172 old_repo_name), category='error')
173 old_repo_name), category='error')
173
174
174 raise HTTPFound(
175 raise HTTPFound(
175 h.route_path('edit_repo', repo_name=new_repo_name))
176 h.route_path('edit_repo', repo_name=new_repo_name))
176
177
177 @LoginRequired()
178 @LoginRequired()
178 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
179 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
179 @view_config(
180 @view_config(
180 route_name='repo_edit_toggle_locking', request_method='GET',
181 route_name='repo_edit_toggle_locking', request_method='GET',
181 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
182 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
182 def toggle_locking(self):
183 def toggle_locking(self):
183 """
184 """
184 Toggle locking of repository by simple GET call to url
185 Toggle locking of repository by simple GET call to url
185 """
186 """
186 _ = self.request.translate
187 _ = self.request.translate
187 repo = self.db_repo
188 repo = self.db_repo
188
189
189 try:
190 try:
190 if repo.enable_locking:
191 if repo.enable_locking:
191 if repo.locked[0]:
192 if repo.locked[0]:
192 Repository.unlock(repo)
193 Repository.unlock(repo)
193 action = _('Unlocked')
194 action = _('Unlocked')
194 else:
195 else:
195 Repository.lock(
196 Repository.lock(
196 repo, self._rhodecode_user.user_id,
197 repo, self._rhodecode_user.user_id,
197 lock_reason=Repository.LOCK_WEB)
198 lock_reason=Repository.LOCK_WEB)
198 action = _('Locked')
199 action = _('Locked')
199
200
200 h.flash(_('Repository has been %s') % action,
201 h.flash(_('Repository has been %s') % action,
201 category='success')
202 category='success')
202 except Exception:
203 except Exception:
203 log.exception("Exception during unlocking")
204 log.exception("Exception during unlocking")
204 h.flash(_('An error occurred during unlocking'),
205 h.flash(_('An error occurred during unlocking'),
205 category='error')
206 category='error')
206 raise HTTPFound(
207 raise HTTPFound(
207 h.route_path('repo_summary', repo_name=self.db_repo_name))
208 h.route_path('repo_summary', repo_name=self.db_repo_name))
208
209
209 @LoginRequired()
210 @LoginRequired()
210 @HasRepoPermissionAnyDecorator('repository.admin')
211 @HasRepoPermissionAnyDecorator('repository.admin')
211 @view_config(
212 @view_config(
212 route_name='edit_repo_statistics', request_method='GET',
213 route_name='edit_repo_statistics', request_method='GET',
213 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
214 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
214 def edit_statistics_form(self):
215 def edit_statistics_form(self):
215 c = self.load_default_context()
216 c = self.load_default_context()
216
217
217 if self.db_repo.stats:
218 if self.db_repo.stats:
218 # this is on what revision we ended up so we add +1 for count
219 # this is on what revision we ended up so we add +1 for count
219 last_rev = self.db_repo.stats.stat_on_revision + 1
220 last_rev = self.db_repo.stats.stat_on_revision + 1
220 else:
221 else:
221 last_rev = 0
222 last_rev = 0
222
223
223 c.active = 'statistics'
224 c.active = 'statistics'
224 c.stats_revision = last_rev
225 c.stats_revision = last_rev
225 c.repo_last_rev = self.rhodecode_vcs_repo.count()
226 c.repo_last_rev = self.rhodecode_vcs_repo.count()
226
227
227 if last_rev == 0 or c.repo_last_rev == 0:
228 if last_rev == 0 or c.repo_last_rev == 0:
228 c.stats_percentage = 0
229 c.stats_percentage = 0
229 else:
230 else:
230 c.stats_percentage = '%.2f' % (
231 c.stats_percentage = '%.2f' % (
231 (float((last_rev)) / c.repo_last_rev) * 100)
232 (float((last_rev)) / c.repo_last_rev) * 100)
232 return self._get_template_context(c)
233 return self._get_template_context(c)
233
234
234 @LoginRequired()
235 @LoginRequired()
235 @HasRepoPermissionAnyDecorator('repository.admin')
236 @HasRepoPermissionAnyDecorator('repository.admin')
236 @CSRFRequired()
237 @CSRFRequired()
237 @view_config(
238 @view_config(
238 route_name='edit_repo_statistics_reset', request_method='POST',
239 route_name='edit_repo_statistics_reset', request_method='POST',
239 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
240 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
240 def repo_statistics_reset(self):
241 def repo_statistics_reset(self):
241 _ = self.request.translate
242 _ = self.request.translate
242
243
243 try:
244 try:
244 RepoModel().delete_stats(self.db_repo_name)
245 RepoModel().delete_stats(self.db_repo_name)
245 Session().commit()
246 Session().commit()
246 except Exception:
247 except Exception:
247 log.exception('Edit statistics failure')
248 log.exception('Edit statistics failure')
248 h.flash(_('An error occurred during deletion of repository stats'),
249 h.flash(_('An error occurred during deletion of repository stats'),
249 category='error')
250 category='error')
250 raise HTTPFound(
251 raise HTTPFound(
251 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
252 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
@@ -1,546 +1,546 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 import peppercorn
23 import peppercorn
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.response import Response
28 from pyramid.response import Response
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30
30
31 from rhodecode.lib.exceptions import (
31 from rhodecode.lib.exceptions import (
32 RepoGroupAssignmentError, UserGroupAssignedException)
32 RepoGroupAssignmentError, UserGroupAssignedException)
33 from rhodecode.model.forms import (
33 from rhodecode.model.forms import (
34 UserGroupPermsForm, UserGroupForm, UserIndividualPermissionsForm,
34 UserGroupPermsForm, UserGroupForm, UserIndividualPermissionsForm,
35 UserPermissionsForm)
35 UserPermissionsForm)
36 from rhodecode.model.permission import PermissionModel
36 from rhodecode.model.permission import PermissionModel
37
37
38 from rhodecode.apps._base import UserGroupAppView
38 from rhodecode.apps._base import UserGroupAppView
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasUserGroupPermissionAnyDecorator, CSRFRequired)
40 LoginRequired, HasUserGroupPermissionAnyDecorator, CSRFRequired)
41 from rhodecode.lib import helpers as h, audit_logger
41 from rhodecode.lib import helpers as h, audit_logger
42 from rhodecode.lib.utils2 import str2bool
42 from rhodecode.lib.utils2 import str2bool
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 joinedload, User, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
44 joinedload, User, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
45 from rhodecode.model.meta import Session
45 from rhodecode.model.meta import Session
46 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class UserGroupsView(UserGroupAppView):
51 class UserGroupsView(UserGroupAppView):
52
52
53 def load_default_context(self):
53 def load_default_context(self):
54 c = self._get_local_tmpl_context()
54 c = self._get_local_tmpl_context()
55
55
56 PermissionModel().set_global_permission_choices(
56 PermissionModel().set_global_permission_choices(
57 c, gettext_translator=self.request.translate)
57 c, gettext_translator=self.request.translate)
58
58
59
59
60 return c
60 return c
61
61
62 def _get_perms_summary(self, user_group_id):
62 def _get_perms_summary(self, user_group_id):
63 permissions = {
63 permissions = {
64 'repositories': {},
64 'repositories': {},
65 'repositories_groups': {},
65 'repositories_groups': {},
66 }
66 }
67 ugroup_repo_perms = UserGroupRepoToPerm.query()\
67 ugroup_repo_perms = UserGroupRepoToPerm.query()\
68 .options(joinedload(UserGroupRepoToPerm.permission))\
68 .options(joinedload(UserGroupRepoToPerm.permission))\
69 .options(joinedload(UserGroupRepoToPerm.repository))\
69 .options(joinedload(UserGroupRepoToPerm.repository))\
70 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
70 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
71 .all()
71 .all()
72
72
73 for gr in ugroup_repo_perms:
73 for gr in ugroup_repo_perms:
74 permissions['repositories'][gr.repository.repo_name] \
74 permissions['repositories'][gr.repository.repo_name] \
75 = gr.permission.permission_name
75 = gr.permission.permission_name
76
76
77 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
77 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
78 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
78 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
79 .options(joinedload(UserGroupRepoGroupToPerm.group))\
79 .options(joinedload(UserGroupRepoGroupToPerm.group))\
80 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
80 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
81 .all()
81 .all()
82
82
83 for gr in ugroup_group_perms:
83 for gr in ugroup_group_perms:
84 permissions['repositories_groups'][gr.group.group_name] \
84 permissions['repositories_groups'][gr.group.group_name] \
85 = gr.permission.permission_name
85 = gr.permission.permission_name
86 return permissions
86 return permissions
87
87
88 @LoginRequired()
88 @LoginRequired()
89 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
89 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
90 @view_config(
90 @view_config(
91 route_name='user_group_members_data', request_method='GET',
91 route_name='user_group_members_data', request_method='GET',
92 renderer='json_ext', xhr=True)
92 renderer='json_ext', xhr=True)
93 def user_group_members(self):
93 def user_group_members(self):
94 """
94 """
95 Return members of given user group
95 Return members of given user group
96 """
96 """
97 self.load_default_context()
97 self.load_default_context()
98 user_group = self.db_user_group
98 user_group = self.db_user_group
99 group_members_obj = sorted((x.user for x in user_group.members),
99 group_members_obj = sorted((x.user for x in user_group.members),
100 key=lambda u: u.username.lower())
100 key=lambda u: u.username.lower())
101
101
102 group_members = [
102 group_members = [
103 {
103 {
104 'id': user.user_id,
104 'id': user.user_id,
105 'first_name': user.first_name,
105 'first_name': user.first_name,
106 'last_name': user.last_name,
106 'last_name': user.last_name,
107 'username': user.username,
107 'username': user.username,
108 'icon_link': h.gravatar_url(user.email, 30),
108 'icon_link': h.gravatar_url(user.email, 30),
109 'value_display': h.person(user.email),
109 'value_display': h.person(user.email),
110 'value': user.username,
110 'value': user.username,
111 'value_type': 'user',
111 'value_type': 'user',
112 'active': user.active,
112 'active': user.active,
113 }
113 }
114 for user in group_members_obj
114 for user in group_members_obj
115 ]
115 ]
116
116
117 return {
117 return {
118 'members': group_members
118 'members': group_members
119 }
119 }
120
120
121 @LoginRequired()
121 @LoginRequired()
122 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
122 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
123 @view_config(
123 @view_config(
124 route_name='edit_user_group_perms_summary', request_method='GET',
124 route_name='edit_user_group_perms_summary', request_method='GET',
125 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
125 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
126 def user_group_perms_summary(self):
126 def user_group_perms_summary(self):
127 c = self.load_default_context()
127 c = self.load_default_context()
128 c.user_group = self.db_user_group
128 c.user_group = self.db_user_group
129 c.active = 'perms_summary'
129 c.active = 'perms_summary'
130 c.permissions = self._get_perms_summary(c.user_group.users_group_id)
130 c.permissions = self._get_perms_summary(c.user_group.users_group_id)
131 return self._get_template_context(c)
131 return self._get_template_context(c)
132
132
133 @LoginRequired()
133 @LoginRequired()
134 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
134 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
135 @view_config(
135 @view_config(
136 route_name='edit_user_group_perms_summary_json', request_method='GET',
136 route_name='edit_user_group_perms_summary_json', request_method='GET',
137 renderer='json_ext')
137 renderer='json_ext')
138 def user_group_perms_summary_json(self):
138 def user_group_perms_summary_json(self):
139 self.load_default_context()
139 self.load_default_context()
140 user_group = self.db_user_group
140 user_group = self.db_user_group
141 return self._get_perms_summary(user_group.users_group_id)
141 return self._get_perms_summary(user_group.users_group_id)
142
142
143 def _revoke_perms_on_yourself(self, form_result):
143 def _revoke_perms_on_yourself(self, form_result):
144 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
144 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
145 form_result['perm_updates'])
145 form_result['perm_updates'])
146 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
146 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
147 form_result['perm_additions'])
147 form_result['perm_additions'])
148 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
148 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
149 form_result['perm_deletions'])
149 form_result['perm_deletions'])
150 admin_perm = 'usergroup.admin'
150 admin_perm = 'usergroup.admin'
151 if _updates and _updates[0][1] != admin_perm or \
151 if _updates and _updates[0][1] != admin_perm or \
152 _additions and _additions[0][1] != admin_perm or \
152 _additions and _additions[0][1] != admin_perm or \
153 _deletions and _deletions[0][1] != admin_perm:
153 _deletions and _deletions[0][1] != admin_perm:
154 return True
154 return True
155 return False
155 return False
156
156
157 @LoginRequired()
157 @LoginRequired()
158 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
158 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
159 @CSRFRequired()
159 @CSRFRequired()
160 @view_config(
160 @view_config(
161 route_name='user_groups_update', request_method='POST',
161 route_name='user_groups_update', request_method='POST',
162 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
162 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
163 def user_group_update(self):
163 def user_group_update(self):
164 _ = self.request.translate
164 _ = self.request.translate
165
165
166 user_group = self.db_user_group
166 user_group = self.db_user_group
167 user_group_id = user_group.users_group_id
167 user_group_id = user_group.users_group_id
168
168
169 c = self.load_default_context()
169 c = self.load_default_context()
170 c.user_group = user_group
170 c.user_group = user_group
171 c.group_members_obj = [x.user for x in c.user_group.members]
171 c.group_members_obj = [x.user for x in c.user_group.members]
172 c.group_members_obj.sort(key=lambda u: u.username.lower())
172 c.group_members_obj.sort(key=lambda u: u.username.lower())
173 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
173 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
174 c.active = 'settings'
174 c.active = 'settings'
175
175
176 users_group_form = UserGroupForm(
176 users_group_form = UserGroupForm(
177 self.request.translate, edit=True,
177 self.request.translate, edit=True,
178 old_data=c.user_group.get_dict(), allow_disabled=True)()
178 old_data=c.user_group.get_dict(), allow_disabled=True)()
179
179
180 old_values = c.user_group.get_api_data()
180 old_values = c.user_group.get_api_data()
181 user_group_name = self.request.POST.get('users_group_name')
181 user_group_name = self.request.POST.get('users_group_name')
182 try:
182 try:
183 form_result = users_group_form.to_python(self.request.POST)
183 form_result = users_group_form.to_python(self.request.POST)
184 pstruct = peppercorn.parse(self.request.POST.items())
184 pstruct = peppercorn.parse(self.request.POST.items())
185 form_result['users_group_members'] = pstruct['user_group_members']
185 form_result['users_group_members'] = pstruct['user_group_members']
186
186
187 user_group, added_members, removed_members = \
187 user_group, added_members, removed_members = \
188 UserGroupModel().update(c.user_group, form_result)
188 UserGroupModel().update(c.user_group, form_result)
189 updated_user_group = form_result['users_group_name']
189 updated_user_group = form_result['users_group_name']
190
190
191 for user_id in added_members:
191 for user_id in added_members:
192 user = User.get(user_id)
192 user = User.get(user_id)
193 user_data = user.get_api_data()
193 user_data = user.get_api_data()
194 audit_logger.store_web(
194 audit_logger.store_web(
195 'user_group.edit.member.add',
195 'user_group.edit.member.add',
196 action_data={'user': user_data, 'old_data': old_values},
196 action_data={'user': user_data, 'old_data': old_values},
197 user=self._rhodecode_user)
197 user=self._rhodecode_user)
198
198
199 for user_id in removed_members:
199 for user_id in removed_members:
200 user = User.get(user_id)
200 user = User.get(user_id)
201 user_data = user.get_api_data()
201 user_data = user.get_api_data()
202 audit_logger.store_web(
202 audit_logger.store_web(
203 'user_group.edit.member.delete',
203 'user_group.edit.member.delete',
204 action_data={'user': user_data, 'old_data': old_values},
204 action_data={'user': user_data, 'old_data': old_values},
205 user=self._rhodecode_user)
205 user=self._rhodecode_user)
206
206
207 audit_logger.store_web(
207 audit_logger.store_web(
208 'user_group.edit', action_data={'old_data': old_values},
208 'user_group.edit', action_data={'old_data': old_values},
209 user=self._rhodecode_user)
209 user=self._rhodecode_user)
210
210
211 h.flash(_('Updated user group %s') % updated_user_group,
211 h.flash(_('Updated user group %s') % updated_user_group,
212 category='success')
212 category='success')
213 Session().commit()
213 Session().commit()
214 except formencode.Invalid as errors:
214 except formencode.Invalid as errors:
215 defaults = errors.value
215 defaults = errors.value
216 e = errors.error_dict or {}
216 e = errors.error_dict or {}
217
217
218 data = render(
218 data = render(
219 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
219 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
220 self._get_template_context(c), self.request)
220 self._get_template_context(c), self.request)
221 html = formencode.htmlfill.render(
221 html = formencode.htmlfill.render(
222 data,
222 data,
223 defaults=defaults,
223 defaults=defaults,
224 errors=e,
224 errors=e,
225 prefix_error=False,
225 prefix_error=False,
226 encoding="UTF-8",
226 encoding="UTF-8",
227 force_defaults=False
227 force_defaults=False
228 )
228 )
229 return Response(html)
229 return Response(html)
230
230
231 except Exception:
231 except Exception:
232 log.exception("Exception during update of user group")
232 log.exception("Exception during update of user group")
233 h.flash(_('Error occurred during update of user group %s')
233 h.flash(_('Error occurred during update of user group %s')
234 % user_group_name, category='error')
234 % user_group_name, category='error')
235
235
236 raise HTTPFound(
236 raise HTTPFound(
237 h.route_path('edit_user_group', user_group_id=user_group_id))
237 h.route_path('edit_user_group', user_group_id=user_group_id))
238
238
239 @LoginRequired()
239 @LoginRequired()
240 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
240 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
241 @CSRFRequired()
241 @CSRFRequired()
242 @view_config(
242 @view_config(
243 route_name='user_groups_delete', request_method='POST',
243 route_name='user_groups_delete', request_method='POST',
244 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
244 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
245 def user_group_delete(self):
245 def user_group_delete(self):
246 _ = self.request.translate
246 _ = self.request.translate
247 user_group = self.db_user_group
247 user_group = self.db_user_group
248
248
249 self.load_default_context()
249 self.load_default_context()
250 force = str2bool(self.request.POST.get('force'))
250 force = str2bool(self.request.POST.get('force'))
251
251
252 old_values = user_group.get_api_data()
252 old_values = user_group.get_api_data()
253 try:
253 try:
254 UserGroupModel().delete(user_group, force=force)
254 UserGroupModel().delete(user_group, force=force)
255 audit_logger.store_web(
255 audit_logger.store_web(
256 'user.delete', action_data={'old_data': old_values},
256 'user.delete', action_data={'old_data': old_values},
257 user=self._rhodecode_user)
257 user=self._rhodecode_user)
258 Session().commit()
258 Session().commit()
259 h.flash(_('Successfully deleted user group'), category='success')
259 h.flash(_('Successfully deleted user group'), category='success')
260 except UserGroupAssignedException as e:
260 except UserGroupAssignedException as e:
261 h.flash(str(e), category='error')
261 h.flash(str(e), category='error')
262 except Exception:
262 except Exception:
263 log.exception("Exception during deletion of user group")
263 log.exception("Exception during deletion of user group")
264 h.flash(_('An error occurred during deletion of user group'),
264 h.flash(_('An error occurred during deletion of user group'),
265 category='error')
265 category='error')
266 raise HTTPFound(h.route_path('user_groups'))
266 raise HTTPFound(h.route_path('user_groups'))
267
267
268 @LoginRequired()
268 @LoginRequired()
269 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
269 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
270 @view_config(
270 @view_config(
271 route_name='edit_user_group', request_method='GET',
271 route_name='edit_user_group', request_method='GET',
272 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
272 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
273 def user_group_edit(self):
273 def user_group_edit(self):
274 user_group = self.db_user_group
274 user_group = self.db_user_group
275
275
276 c = self.load_default_context()
276 c = self.load_default_context()
277 c.user_group = user_group
277 c.user_group = user_group
278 c.group_members_obj = [x.user for x in c.user_group.members]
278 c.group_members_obj = [x.user for x in c.user_group.members]
279 c.group_members_obj.sort(key=lambda u: u.username.lower())
279 c.group_members_obj.sort(key=lambda u: u.username.lower())
280 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
280 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
281
281
282 c.active = 'settings'
282 c.active = 'settings'
283
283
284 defaults = user_group.get_dict()
284 defaults = user_group.get_dict()
285 # fill owner
285 # fill owner
286 if user_group.user:
286 if user_group.user:
287 defaults.update({'user': user_group.user.username})
287 defaults.update({'user': user_group.user.username})
288 else:
288 else:
289 replacement_user = User.get_first_super_admin().username
289 replacement_user = User.get_first_super_admin().username
290 defaults.update({'user': replacement_user})
290 defaults.update({'user': replacement_user})
291
291
292 data = render(
292 data = render(
293 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
293 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
294 self._get_template_context(c), self.request)
294 self._get_template_context(c), self.request)
295 html = formencode.htmlfill.render(
295 html = formencode.htmlfill.render(
296 data,
296 data,
297 defaults=defaults,
297 defaults=defaults,
298 encoding="UTF-8",
298 encoding="UTF-8",
299 force_defaults=False
299 force_defaults=False
300 )
300 )
301 return Response(html)
301 return Response(html)
302
302
303 @LoginRequired()
303 @LoginRequired()
304 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
304 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
305 @view_config(
305 @view_config(
306 route_name='edit_user_group_perms', request_method='GET',
306 route_name='edit_user_group_perms', request_method='GET',
307 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
307 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
308 def user_group_edit_perms(self):
308 def user_group_edit_perms(self):
309 user_group = self.db_user_group
309 user_group = self.db_user_group
310 c = self.load_default_context()
310 c = self.load_default_context()
311 c.user_group = user_group
311 c.user_group = user_group
312 c.active = 'perms'
312 c.active = 'perms'
313
313
314 defaults = {}
314 defaults = {}
315 # fill user group users
315 # fill user group users
316 for p in c.user_group.user_user_group_to_perm:
316 for p in c.user_group.user_user_group_to_perm:
317 defaults.update({'u_perm_%s' % p.user.user_id:
317 defaults.update({'u_perm_%s' % p.user.user_id:
318 p.permission.permission_name})
318 p.permission.permission_name})
319
319
320 for p in c.user_group.user_group_user_group_to_perm:
320 for p in c.user_group.user_group_user_group_to_perm:
321 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
321 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
322 p.permission.permission_name})
322 p.permission.permission_name})
323
323
324 data = render(
324 data = render(
325 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
325 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
326 self._get_template_context(c), self.request)
326 self._get_template_context(c), self.request)
327 html = formencode.htmlfill.render(
327 html = formencode.htmlfill.render(
328 data,
328 data,
329 defaults=defaults,
329 defaults=defaults,
330 encoding="UTF-8",
330 encoding="UTF-8",
331 force_defaults=False
331 force_defaults=False
332 )
332 )
333 return Response(html)
333 return Response(html)
334
334
335 @LoginRequired()
335 @LoginRequired()
336 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
336 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
337 @CSRFRequired()
337 @CSRFRequired()
338 @view_config(
338 @view_config(
339 route_name='edit_user_group_perms_update', request_method='POST',
339 route_name='edit_user_group_perms_update', request_method='POST',
340 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
340 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
341 def user_group_update_perms(self):
341 def user_group_update_perms(self):
342 """
342 """
343 grant permission for given user group
343 grant permission for given user group
344 """
344 """
345 _ = self.request.translate
345 _ = self.request.translate
346
346
347 user_group = self.db_user_group
347 user_group = self.db_user_group
348 user_group_id = user_group.users_group_id
348 user_group_id = user_group.users_group_id
349 c = self.load_default_context()
349 c = self.load_default_context()
350 c.user_group = user_group
350 c.user_group = user_group
351 form = UserGroupPermsForm(self.request.translate)().to_python(self.request.POST)
351 form = UserGroupPermsForm(self.request.translate)().to_python(self.request.POST)
352
352
353 if not self._rhodecode_user.is_admin:
353 if not self._rhodecode_user.is_admin:
354 if self._revoke_perms_on_yourself(form):
354 if self._revoke_perms_on_yourself(form):
355 msg = _('Cannot change permission for yourself as admin')
355 msg = _('Cannot change permission for yourself as admin')
356 h.flash(msg, category='warning')
356 h.flash(msg, category='warning')
357 raise HTTPFound(
357 raise HTTPFound(
358 h.route_path('edit_user_group_perms',
358 h.route_path('edit_user_group_perms',
359 user_group_id=user_group_id))
359 user_group_id=user_group_id))
360
360
361 try:
361 try:
362 changes = UserGroupModel().update_permissions(
362 changes = UserGroupModel().update_permissions(
363 user_group_id,
363 user_group_id,
364 form['perm_additions'], form['perm_updates'],
364 form['perm_additions'], form['perm_updates'],
365 form['perm_deletions'])
365 form['perm_deletions'])
366
366
367 except RepoGroupAssignmentError:
367 except RepoGroupAssignmentError:
368 h.flash(_('Target group cannot be the same'), category='error')
368 h.flash(_('Target group cannot be the same'), category='error')
369 raise HTTPFound(
369 raise HTTPFound(
370 h.route_path('edit_user_group_perms',
370 h.route_path('edit_user_group_perms',
371 user_group_id=user_group_id))
371 user_group_id=user_group_id))
372
372
373 action_data = {
373 action_data = {
374 'added': changes['added'],
374 'added': changes['added'],
375 'updated': changes['updated'],
375 'updated': changes['updated'],
376 'deleted': changes['deleted'],
376 'deleted': changes['deleted'],
377 }
377 }
378 audit_logger.store_web(
378 audit_logger.store_web(
379 'user_group.edit.permissions', action_data=action_data,
379 'user_group.edit.permissions', action_data=action_data,
380 user=self._rhodecode_user)
380 user=self._rhodecode_user)
381
381
382 Session().commit()
382 Session().commit()
383 h.flash(_('User Group permissions updated'), category='success')
383 h.flash(_('User Group permissions updated'), category='success')
384 raise HTTPFound(
384 raise HTTPFound(
385 h.route_path('edit_user_group_perms', user_group_id=user_group_id))
385 h.route_path('edit_user_group_perms', user_group_id=user_group_id))
386
386
387 @LoginRequired()
387 @LoginRequired()
388 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
388 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
389 @view_config(
389 @view_config(
390 route_name='edit_user_group_global_perms', request_method='GET',
390 route_name='edit_user_group_global_perms', request_method='GET',
391 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
391 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
392 def user_group_global_perms_edit(self):
392 def user_group_global_perms_edit(self):
393 user_group = self.db_user_group
393 user_group = self.db_user_group
394 c = self.load_default_context()
394 c = self.load_default_context()
395 c.user_group = user_group
395 c.user_group = user_group
396 c.active = 'global_perms'
396 c.active = 'global_perms'
397
397
398 c.default_user = User.get_default_user()
398 c.default_user = User.get_default_user()
399 defaults = c.user_group.get_dict()
399 defaults = c.user_group.get_dict()
400 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
400 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
401 defaults.update(c.user_group.get_default_perms())
401 defaults.update(c.user_group.get_default_perms())
402
402
403 data = render(
403 data = render(
404 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
404 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
405 self._get_template_context(c), self.request)
405 self._get_template_context(c), self.request)
406 html = formencode.htmlfill.render(
406 html = formencode.htmlfill.render(
407 data,
407 data,
408 defaults=defaults,
408 defaults=defaults,
409 encoding="UTF-8",
409 encoding="UTF-8",
410 force_defaults=False
410 force_defaults=False
411 )
411 )
412 return Response(html)
412 return Response(html)
413
413
414 @LoginRequired()
414 @LoginRequired()
415 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
415 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
416 @CSRFRequired()
416 @CSRFRequired()
417 @view_config(
417 @view_config(
418 route_name='edit_user_group_global_perms_update', request_method='POST',
418 route_name='edit_user_group_global_perms_update', request_method='POST',
419 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
419 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
420 def user_group_global_perms_update(self):
420 def user_group_global_perms_update(self):
421 _ = self.request.translate
421 _ = self.request.translate
422 user_group = self.db_user_group
422 user_group = self.db_user_group
423 user_group_id = self.db_user_group.users_group_id
423 user_group_id = self.db_user_group.users_group_id
424
424
425 c = self.load_default_context()
425 c = self.load_default_context()
426 c.user_group = user_group
426 c.user_group = user_group
427 c.active = 'global_perms'
427 c.active = 'global_perms'
428
428
429 try:
429 try:
430 # first stage that verifies the checkbox
430 # first stage that verifies the checkbox
431 _form = UserIndividualPermissionsForm()
431 _form = UserIndividualPermissionsForm(self.request.translate)
432 form_result = _form.to_python(dict(self.request.POST))
432 form_result = _form.to_python(dict(self.request.POST))
433 inherit_perms = form_result['inherit_default_permissions']
433 inherit_perms = form_result['inherit_default_permissions']
434 user_group.inherit_default_permissions = inherit_perms
434 user_group.inherit_default_permissions = inherit_perms
435 Session().add(user_group)
435 Session().add(user_group)
436
436
437 if not inherit_perms:
437 if not inherit_perms:
438 # only update the individual ones if we un check the flag
438 # only update the individual ones if we un check the flag
439 _form = UserPermissionsForm(
439 _form = UserPermissionsForm(
440 self.request.translate,
440 self.request.translate,
441 [x[0] for x in c.repo_create_choices],
441 [x[0] for x in c.repo_create_choices],
442 [x[0] for x in c.repo_create_on_write_choices],
442 [x[0] for x in c.repo_create_on_write_choices],
443 [x[0] for x in c.repo_group_create_choices],
443 [x[0] for x in c.repo_group_create_choices],
444 [x[0] for x in c.user_group_create_choices],
444 [x[0] for x in c.user_group_create_choices],
445 [x[0] for x in c.fork_choices],
445 [x[0] for x in c.fork_choices],
446 [x[0] for x in c.inherit_default_permission_choices])()
446 [x[0] for x in c.inherit_default_permission_choices])()
447
447
448 form_result = _form.to_python(dict(self.request.POST))
448 form_result = _form.to_python(dict(self.request.POST))
449 form_result.update(
449 form_result.update(
450 {'perm_user_group_id': user_group.users_group_id})
450 {'perm_user_group_id': user_group.users_group_id})
451
451
452 PermissionModel().update_user_group_permissions(form_result)
452 PermissionModel().update_user_group_permissions(form_result)
453
453
454 Session().commit()
454 Session().commit()
455 h.flash(_('User Group global permissions updated successfully'),
455 h.flash(_('User Group global permissions updated successfully'),
456 category='success')
456 category='success')
457
457
458 except formencode.Invalid as errors:
458 except formencode.Invalid as errors:
459 defaults = errors.value
459 defaults = errors.value
460
460
461 data = render(
461 data = render(
462 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
462 'rhodecode:templates/admin/user_groups/user_group_edit.mako',
463 self._get_template_context(c), self.request)
463 self._get_template_context(c), self.request)
464 html = formencode.htmlfill.render(
464 html = formencode.htmlfill.render(
465 data,
465 data,
466 defaults=defaults,
466 defaults=defaults,
467 errors=errors.error_dict or {},
467 errors=errors.error_dict or {},
468 prefix_error=False,
468 prefix_error=False,
469 encoding="UTF-8",
469 encoding="UTF-8",
470 force_defaults=False
470 force_defaults=False
471 )
471 )
472 return Response(html)
472 return Response(html)
473 except Exception:
473 except Exception:
474 log.exception("Exception during permissions saving")
474 log.exception("Exception during permissions saving")
475 h.flash(_('An error occurred during permissions saving'),
475 h.flash(_('An error occurred during permissions saving'),
476 category='error')
476 category='error')
477
477
478 raise HTTPFound(
478 raise HTTPFound(
479 h.route_path('edit_user_group_global_perms',
479 h.route_path('edit_user_group_global_perms',
480 user_group_id=user_group_id))
480 user_group_id=user_group_id))
481
481
482 @LoginRequired()
482 @LoginRequired()
483 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
483 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
484 @view_config(
484 @view_config(
485 route_name='edit_user_group_advanced', request_method='GET',
485 route_name='edit_user_group_advanced', request_method='GET',
486 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
486 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
487 def user_group_edit_advanced(self):
487 def user_group_edit_advanced(self):
488 user_group = self.db_user_group
488 user_group = self.db_user_group
489
489
490 c = self.load_default_context()
490 c = self.load_default_context()
491 c.user_group = user_group
491 c.user_group = user_group
492 c.active = 'advanced'
492 c.active = 'advanced'
493 c.group_members_obj = sorted(
493 c.group_members_obj = sorted(
494 (x.user for x in c.user_group.members),
494 (x.user for x in c.user_group.members),
495 key=lambda u: u.username.lower())
495 key=lambda u: u.username.lower())
496
496
497 c.group_to_repos = sorted(
497 c.group_to_repos = sorted(
498 (x.repository for x in c.user_group.users_group_repo_to_perm),
498 (x.repository for x in c.user_group.users_group_repo_to_perm),
499 key=lambda u: u.repo_name.lower())
499 key=lambda u: u.repo_name.lower())
500
500
501 c.group_to_repo_groups = sorted(
501 c.group_to_repo_groups = sorted(
502 (x.group for x in c.user_group.users_group_repo_group_to_perm),
502 (x.group for x in c.user_group.users_group_repo_group_to_perm),
503 key=lambda u: u.group_name.lower())
503 key=lambda u: u.group_name.lower())
504
504
505 c.group_to_review_rules = sorted(
505 c.group_to_review_rules = sorted(
506 (x.users_group for x in c.user_group.user_group_review_rules),
506 (x.users_group for x in c.user_group.user_group_review_rules),
507 key=lambda u: u.users_group_name.lower())
507 key=lambda u: u.users_group_name.lower())
508
508
509 return self._get_template_context(c)
509 return self._get_template_context(c)
510
510
511 @LoginRequired()
511 @LoginRequired()
512 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
512 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
513 @CSRFRequired()
513 @CSRFRequired()
514 @view_config(
514 @view_config(
515 route_name='edit_user_group_advanced_sync', request_method='POST',
515 route_name='edit_user_group_advanced_sync', request_method='POST',
516 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
516 renderer='rhodecode:templates/admin/user_groups/user_group_edit.mako')
517 def user_group_edit_advanced_set_synchronization(self):
517 def user_group_edit_advanced_set_synchronization(self):
518 _ = self.request.translate
518 _ = self.request.translate
519 user_group = self.db_user_group
519 user_group = self.db_user_group
520 user_group_id = user_group.users_group_id
520 user_group_id = user_group.users_group_id
521
521
522 existing = user_group.group_data.get('extern_type')
522 existing = user_group.group_data.get('extern_type')
523
523
524 if existing:
524 if existing:
525 new_state = user_group.group_data
525 new_state = user_group.group_data
526 new_state['extern_type'] = None
526 new_state['extern_type'] = None
527 else:
527 else:
528 new_state = user_group.group_data
528 new_state = user_group.group_data
529 new_state['extern_type'] = 'manual'
529 new_state['extern_type'] = 'manual'
530 new_state['extern_type_set_by'] = self._rhodecode_user.username
530 new_state['extern_type_set_by'] = self._rhodecode_user.username
531
531
532 try:
532 try:
533 user_group.group_data = new_state
533 user_group.group_data = new_state
534 Session().add(user_group)
534 Session().add(user_group)
535 Session().commit()
535 Session().commit()
536
536
537 h.flash(_('User Group synchronization updated successfully'),
537 h.flash(_('User Group synchronization updated successfully'),
538 category='success')
538 category='success')
539 except Exception:
539 except Exception:
540 log.exception("Exception during sync settings saving")
540 log.exception("Exception during sync settings saving")
541 h.flash(_('An error occurred during synchronization update'),
541 h.flash(_('An error occurred during synchronization update'),
542 category='error')
542 category='error')
543
543
544 raise HTTPFound(
544 raise HTTPFound(
545 h.route_path('edit_user_group_advanced',
545 h.route_path('edit_user_group_advanced',
546 user_group_id=user_group_id))
546 user_group_id=user_group_id))
@@ -1,109 +1,106 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import os
22 import os
23 import logging
23 import logging
24 import rhodecode
24 import rhodecode
25
25
26 # ------------------------------------------------------------------------------
26 # ------------------------------------------------------------------------------
27 # CELERY magic until refactor - issue #4163 - import order matters here:
27 # CELERY magic until refactor - issue #4163 - import order matters here:
28 #from rhodecode.lib import celerypylons # this must be first, celerypylons
28 #from rhodecode.lib import celerypylons # this must be first, celerypylons
29 # sets config settings upon import
29 # sets config settings upon import
30
30
31 import rhodecode.integrations # any modules using celery task
31 import rhodecode.integrations # any modules using celery task
32 # decorators should be added afterwards:
32 # decorators should be added afterwards:
33 # ------------------------------------------------------------------------------
33 # ------------------------------------------------------------------------------
34
34
35 from rhodecode.config import utils
35 from rhodecode.config import utils
36
36
37 from rhodecode.lib.utils import load_rcextensions
37 from rhodecode.lib.utils import load_rcextensions
38 from rhodecode.lib.utils2 import str2bool
38 from rhodecode.lib.utils2 import str2bool
39 from rhodecode.lib.vcs import connect_vcs, start_vcs_server
39 from rhodecode.lib.vcs import connect_vcs, start_vcs_server
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 def load_pyramid_environment(global_config, settings):
44 def load_pyramid_environment(global_config, settings):
45 # Some parts of the code expect a merge of global and app settings.
45 # Some parts of the code expect a merge of global and app settings.
46 settings_merged = global_config.copy()
46 settings_merged = global_config.copy()
47 settings_merged.update(settings)
47 settings_merged.update(settings)
48
48
49 # TODO(marcink): probably not required anymore
49 # TODO(marcink): probably not required anymore
50 # configure channelstream,
50 # configure channelstream,
51 settings_merged['channelstream_config'] = {
51 settings_merged['channelstream_config'] = {
52 'enabled': str2bool(settings_merged.get('channelstream.enabled', False)),
52 'enabled': str2bool(settings_merged.get('channelstream.enabled', False)),
53 'server': settings_merged.get('channelstream.server'),
53 'server': settings_merged.get('channelstream.server'),
54 'secret': settings_merged.get('channelstream.secret')
54 'secret': settings_merged.get('channelstream.secret')
55 }
55 }
56
56
57
57
58 # TODO(marcink): celery
58 # TODO(marcink): celery
59 # # store some globals into rhodecode
59 # # store some globals into rhodecode
60 # rhodecode.CELERY_ENABLED = str2bool(config['app_conf'].get('use_celery'))
60 # rhodecode.CELERY_ENABLED = str2bool(config['app_conf'].get('use_celery'))
61 # rhodecode.CELERY_EAGER = str2bool(
61 # rhodecode.CELERY_EAGER = str2bool(
62 # config['app_conf'].get('celery.always.eager'))
62 # config['app_conf'].get('celery.always.eager'))
63
63
64
64
65 # If this is a test run we prepare the test environment like
65 # If this is a test run we prepare the test environment like
66 # creating a test database, test search index and test repositories.
66 # creating a test database, test search index and test repositories.
67 # This has to be done before the database connection is initialized.
67 # This has to be done before the database connection is initialized.
68 if settings['is_test']:
68 if settings['is_test']:
69 rhodecode.is_test = True
69 rhodecode.is_test = True
70 rhodecode.disable_error_handler = True
70 rhodecode.disable_error_handler = True
71
71
72 utils.initialize_test_environment(settings_merged)
72 utils.initialize_test_environment(settings_merged)
73
73
74 # Initialize the database connection.
74 # Initialize the database connection.
75 utils.initialize_database(settings_merged)
75 utils.initialize_database(settings_merged)
76
76
77 # TODO(marcink): base_path handling ?
78 # repos_path = list(db_cfg.items('paths'))[0][1]
79
80 load_rcextensions(root_path=settings_merged['here'])
77 load_rcextensions(root_path=settings_merged['here'])
81
78
82 # Limit backends to `vcs.backends` from configuration
79 # Limit backends to `vcs.backends` from configuration
83 for alias in rhodecode.BACKENDS.keys():
80 for alias in rhodecode.BACKENDS.keys():
84 if alias not in settings['vcs.backends']:
81 if alias not in settings['vcs.backends']:
85 del rhodecode.BACKENDS[alias]
82 del rhodecode.BACKENDS[alias]
86 log.info('Enabled VCS backends: %s', rhodecode.BACKENDS.keys())
83 log.info('Enabled VCS backends: %s', rhodecode.BACKENDS.keys())
87
84
88 # initialize vcs client and optionally run the server if enabled
85 # initialize vcs client and optionally run the server if enabled
89 vcs_server_uri = settings['vcs.server']
86 vcs_server_uri = settings['vcs.server']
90 vcs_server_enabled = settings['vcs.server.enable']
87 vcs_server_enabled = settings['vcs.server.enable']
91 start_server = (
88 start_server = (
92 settings['vcs.start_server'] and
89 settings['vcs.start_server'] and
93 not int(os.environ.get('RC_VCSSERVER_TEST_DISABLE', '0')))
90 not int(os.environ.get('RC_VCSSERVER_TEST_DISABLE', '0')))
94
91
95 if vcs_server_enabled and start_server:
92 if vcs_server_enabled and start_server:
96 log.info("Starting vcsserver")
93 log.info("Starting vcsserver")
97 start_vcs_server(server_and_port=vcs_server_uri,
94 start_vcs_server(server_and_port=vcs_server_uri,
98 protocol=utils.get_vcs_server_protocol(settings),
95 protocol=utils.get_vcs_server_protocol(settings),
99 log_level=settings['vcs.server.log_level'])
96 log_level=settings['vcs.server.log_level'])
100
97
101 utils.configure_vcs(settings)
98 utils.configure_vcs(settings)
102
99
103 # Store the settings to make them available to other modules.
100 # Store the settings to make them available to other modules.
104
101
105 rhodecode.PYRAMID_SETTINGS = settings_merged
102 rhodecode.PYRAMID_SETTINGS = settings_merged
106 rhodecode.CONFIG = settings_merged
103 rhodecode.CONFIG = settings_merged
107
104
108 if vcs_server_enabled:
105 if vcs_server_enabled:
109 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
106 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
@@ -1,543 +1,540 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32 import pyramid.threadlocal
33
32
34 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37
36
38 import rhodecode
37 import rhodecode
39 from rhodecode.authentication.base import VCS_TYPE
38 from rhodecode.authentication.base import VCS_TYPE
40 from rhodecode.lib import auth, utils2
39 from rhodecode.lib import auth, utils2
41 from rhodecode.lib import helpers as h
40 from rhodecode.lib import helpers as h
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 from rhodecode.lib.exceptions import UserCreationError
42 from rhodecode.lib.exceptions import UserCreationError
44 from rhodecode.lib.utils import (
43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
45 get_repo_slug, set_rhodecode_config, password_changed,
46 get_enabled_hook_classes)
47 from rhodecode.lib.utils2 import (
44 from rhodecode.lib.utils2 import (
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
45 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
49 from rhodecode.model import meta
50 from rhodecode.model.db import Repository, User, ChangesetComment
46 from rhodecode.model.db import Repository, User, ChangesetComment
51 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54
49
55 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
56
51
57
52
58 def _filter_proxy(ip):
53 def _filter_proxy(ip):
59 """
54 """
60 Passed in IP addresses in HEADERS can be in a special format of multiple
55 Passed in IP addresses in HEADERS can be in a special format of multiple
61 ips. Those comma separated IPs are passed from various proxies in the
56 ips. Those comma separated IPs are passed from various proxies in the
62 chain of request processing. The left-most being the original client.
57 chain of request processing. The left-most being the original client.
63 We only care about the first IP which came from the org. client.
58 We only care about the first IP which came from the org. client.
64
59
65 :param ip: ip string from headers
60 :param ip: ip string from headers
66 """
61 """
67 if ',' in ip:
62 if ',' in ip:
68 _ips = ip.split(',')
63 _ips = ip.split(',')
69 _first_ip = _ips[0].strip()
64 _first_ip = _ips[0].strip()
70 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
71 return _first_ip
66 return _first_ip
72 return ip
67 return ip
73
68
74
69
75 def _filter_port(ip):
70 def _filter_port(ip):
76 """
71 """
77 Removes a port from ip, there are 4 main cases to handle here.
72 Removes a port from ip, there are 4 main cases to handle here.
78 - ipv4 eg. 127.0.0.1
73 - ipv4 eg. 127.0.0.1
79 - ipv6 eg. ::1
74 - ipv6 eg. ::1
80 - ipv4+port eg. 127.0.0.1:8080
75 - ipv4+port eg. 127.0.0.1:8080
81 - ipv6+port eg. [::1]:8080
76 - ipv6+port eg. [::1]:8080
82
77
83 :param ip:
78 :param ip:
84 """
79 """
85 def is_ipv6(ip_addr):
80 def is_ipv6(ip_addr):
86 if hasattr(socket, 'inet_pton'):
81 if hasattr(socket, 'inet_pton'):
87 try:
82 try:
88 socket.inet_pton(socket.AF_INET6, ip_addr)
83 socket.inet_pton(socket.AF_INET6, ip_addr)
89 except socket.error:
84 except socket.error:
90 return False
85 return False
91 else:
86 else:
92 # fallback to ipaddress
87 # fallback to ipaddress
93 try:
88 try:
94 ipaddress.IPv6Address(safe_unicode(ip_addr))
89 ipaddress.IPv6Address(safe_unicode(ip_addr))
95 except Exception:
90 except Exception:
96 return False
91 return False
97 return True
92 return True
98
93
99 if ':' not in ip: # must be ipv4 pure ip
94 if ':' not in ip: # must be ipv4 pure ip
100 return ip
95 return ip
101
96
102 if '[' in ip and ']' in ip: # ipv6 with port
97 if '[' in ip and ']' in ip: # ipv6 with port
103 return ip.split(']')[0][1:].lower()
98 return ip.split(']')[0][1:].lower()
104
99
105 # must be ipv6 or ipv4 with port
100 # must be ipv6 or ipv4 with port
106 if is_ipv6(ip):
101 if is_ipv6(ip):
107 return ip
102 return ip
108 else:
103 else:
109 ip, _port = ip.split(':')[:2] # means ipv4+port
104 ip, _port = ip.split(':')[:2] # means ipv4+port
110 return ip
105 return ip
111
106
112
107
113 def get_ip_addr(environ):
108 def get_ip_addr(environ):
114 proxy_key = 'HTTP_X_REAL_IP'
109 proxy_key = 'HTTP_X_REAL_IP'
115 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
116 def_key = 'REMOTE_ADDR'
111 def_key = 'REMOTE_ADDR'
117 _filters = lambda x: _filter_port(_filter_proxy(x))
112 _filters = lambda x: _filter_port(_filter_proxy(x))
118
113
119 ip = environ.get(proxy_key)
114 ip = environ.get(proxy_key)
120 if ip:
115 if ip:
121 return _filters(ip)
116 return _filters(ip)
122
117
123 ip = environ.get(proxy_key2)
118 ip = environ.get(proxy_key2)
124 if ip:
119 if ip:
125 return _filters(ip)
120 return _filters(ip)
126
121
127 ip = environ.get(def_key, '0.0.0.0')
122 ip = environ.get(def_key, '0.0.0.0')
128 return _filters(ip)
123 return _filters(ip)
129
124
130
125
131 def get_server_ip_addr(environ, log_errors=True):
126 def get_server_ip_addr(environ, log_errors=True):
132 hostname = environ.get('SERVER_NAME')
127 hostname = environ.get('SERVER_NAME')
133 try:
128 try:
134 return socket.gethostbyname(hostname)
129 return socket.gethostbyname(hostname)
135 except Exception as e:
130 except Exception as e:
136 if log_errors:
131 if log_errors:
137 # in some cases this lookup is not possible, and we don't want to
132 # in some cases this lookup is not possible, and we don't want to
138 # make it an exception in logs
133 # make it an exception in logs
139 log.exception('Could not retrieve server ip address: %s', e)
134 log.exception('Could not retrieve server ip address: %s', e)
140 return hostname
135 return hostname
141
136
142
137
143 def get_server_port(environ):
138 def get_server_port(environ):
144 return environ.get('SERVER_PORT')
139 return environ.get('SERVER_PORT')
145
140
146
141
147 def get_access_path(environ):
142 def get_access_path(environ):
148 path = environ.get('PATH_INFO')
143 path = environ.get('PATH_INFO')
149 org_req = environ.get('pylons.original_request')
144 org_req = environ.get('pylons.original_request')
150 if org_req:
145 if org_req:
151 path = org_req.environ.get('PATH_INFO')
146 path = org_req.environ.get('PATH_INFO')
152 return path
147 return path
153
148
154
149
155 def get_user_agent(environ):
150 def get_user_agent(environ):
156 return environ.get('HTTP_USER_AGENT')
151 return environ.get('HTTP_USER_AGENT')
157
152
158
153
159 def vcs_operation_context(
154 def vcs_operation_context(
160 environ, repo_name, username, action, scm, check_locking=True,
155 environ, repo_name, username, action, scm, check_locking=True,
161 is_shadow_repo=False):
156 is_shadow_repo=False):
162 """
157 """
163 Generate the context for a vcs operation, e.g. push or pull.
158 Generate the context for a vcs operation, e.g. push or pull.
164
159
165 This context is passed over the layers so that hooks triggered by the
160 This context is passed over the layers so that hooks triggered by the
166 vcs operation know details like the user, the user's IP address etc.
161 vcs operation know details like the user, the user's IP address etc.
167
162
168 :param check_locking: Allows to switch of the computation of the locking
163 :param check_locking: Allows to switch of the computation of the locking
169 data. This serves mainly the need of the simplevcs middleware to be
164 data. This serves mainly the need of the simplevcs middleware to be
170 able to disable this for certain operations.
165 able to disable this for certain operations.
171
166
172 """
167 """
173 # Tri-state value: False: unlock, None: nothing, True: lock
168 # Tri-state value: False: unlock, None: nothing, True: lock
174 make_lock = None
169 make_lock = None
175 locked_by = [None, None, None]
170 locked_by = [None, None, None]
176 is_anonymous = username == User.DEFAULT_USER
171 is_anonymous = username == User.DEFAULT_USER
177 if not is_anonymous and check_locking:
172 if not is_anonymous and check_locking:
178 log.debug('Checking locking on repository "%s"', repo_name)
173 log.debug('Checking locking on repository "%s"', repo_name)
179 user = User.get_by_username(username)
174 user = User.get_by_username(username)
180 repo = Repository.get_by_repo_name(repo_name)
175 repo = Repository.get_by_repo_name(repo_name)
181 make_lock, __, locked_by = repo.get_locking_state(
176 make_lock, __, locked_by = repo.get_locking_state(
182 action, user.user_id)
177 action, user.user_id)
183
178
184 settings_model = VcsSettingsModel(repo=repo_name)
179 settings_model = VcsSettingsModel(repo=repo_name)
185 ui_settings = settings_model.get_ui_settings()
180 ui_settings = settings_model.get_ui_settings()
186
181
187 extras = {
182 extras = {
188 'ip': get_ip_addr(environ),
183 'ip': get_ip_addr(environ),
189 'username': username,
184 'username': username,
190 'action': action,
185 'action': action,
191 'repository': repo_name,
186 'repository': repo_name,
192 'scm': scm,
187 'scm': scm,
193 'config': rhodecode.CONFIG['__file__'],
188 'config': rhodecode.CONFIG['__file__'],
194 'make_lock': make_lock,
189 'make_lock': make_lock,
195 'locked_by': locked_by,
190 'locked_by': locked_by,
196 'server_url': utils2.get_server_url(environ),
191 'server_url': utils2.get_server_url(environ),
197 'user_agent': get_user_agent(environ),
192 'user_agent': get_user_agent(environ),
198 'hooks': get_enabled_hook_classes(ui_settings),
193 'hooks': get_enabled_hook_classes(ui_settings),
199 'is_shadow_repo': is_shadow_repo,
194 'is_shadow_repo': is_shadow_repo,
200 }
195 }
201 return extras
196 return extras
202
197
203
198
204 class BasicAuth(AuthBasicAuthenticator):
199 class BasicAuth(AuthBasicAuthenticator):
205
200
206 def __init__(self, realm, authfunc, registry, auth_http_code=None,
201 def __init__(self, realm, authfunc, registry, auth_http_code=None,
207 initial_call_detection=False, acl_repo_name=None):
202 initial_call_detection=False, acl_repo_name=None):
208 self.realm = realm
203 self.realm = realm
209 self.initial_call = initial_call_detection
204 self.initial_call = initial_call_detection
210 self.authfunc = authfunc
205 self.authfunc = authfunc
211 self.registry = registry
206 self.registry = registry
212 self.acl_repo_name = acl_repo_name
207 self.acl_repo_name = acl_repo_name
213 self._rc_auth_http_code = auth_http_code
208 self._rc_auth_http_code = auth_http_code
214
209
215 def _get_response_from_code(self, http_code):
210 def _get_response_from_code(self, http_code):
216 try:
211 try:
217 return get_exception(safe_int(http_code))
212 return get_exception(safe_int(http_code))
218 except Exception:
213 except Exception:
219 log.exception('Failed to fetch response for code %s' % http_code)
214 log.exception('Failed to fetch response for code %s' % http_code)
220 return HTTPForbidden
215 return HTTPForbidden
221
216
222 def get_rc_realm(self):
217 def get_rc_realm(self):
223 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
218 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
224
219
225 def build_authentication(self):
220 def build_authentication(self):
226 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
221 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
227 if self._rc_auth_http_code and not self.initial_call:
222 if self._rc_auth_http_code and not self.initial_call:
228 # return alternative HTTP code if alternative http return code
223 # return alternative HTTP code if alternative http return code
229 # is specified in RhodeCode config, but ONLY if it's not the
224 # is specified in RhodeCode config, but ONLY if it's not the
230 # FIRST call
225 # FIRST call
231 custom_response_klass = self._get_response_from_code(
226 custom_response_klass = self._get_response_from_code(
232 self._rc_auth_http_code)
227 self._rc_auth_http_code)
233 return custom_response_klass(headers=head)
228 return custom_response_klass(headers=head)
234 return HTTPUnauthorized(headers=head)
229 return HTTPUnauthorized(headers=head)
235
230
236 def authenticate(self, environ):
231 def authenticate(self, environ):
237 authorization = AUTHORIZATION(environ)
232 authorization = AUTHORIZATION(environ)
238 if not authorization:
233 if not authorization:
239 return self.build_authentication()
234 return self.build_authentication()
240 (authmeth, auth) = authorization.split(' ', 1)
235 (authmeth, auth) = authorization.split(' ', 1)
241 if 'basic' != authmeth.lower():
236 if 'basic' != authmeth.lower():
242 return self.build_authentication()
237 return self.build_authentication()
243 auth = auth.strip().decode('base64')
238 auth = auth.strip().decode('base64')
244 _parts = auth.split(':', 1)
239 _parts = auth.split(':', 1)
245 if len(_parts) == 2:
240 if len(_parts) == 2:
246 username, password = _parts
241 username, password = _parts
247 auth_data = self.authfunc(
242 auth_data = self.authfunc(
248 username, password, environ, VCS_TYPE,
243 username, password, environ, VCS_TYPE,
249 registry=self.registry, acl_repo_name=self.acl_repo_name)
244 registry=self.registry, acl_repo_name=self.acl_repo_name)
250 if auth_data:
245 if auth_data:
251 return {'username': username, 'auth_data': auth_data}
246 return {'username': username, 'auth_data': auth_data}
252 if username and password:
247 if username and password:
253 # we mark that we actually executed authentication once, at
248 # we mark that we actually executed authentication once, at
254 # that point we can use the alternative auth code
249 # that point we can use the alternative auth code
255 self.initial_call = False
250 self.initial_call = False
256
251
257 return self.build_authentication()
252 return self.build_authentication()
258
253
259 __call__ = authenticate
254 __call__ = authenticate
260
255
261
256
262 def calculate_version_hash(config):
257 def calculate_version_hash(config):
263 return md5(
258 return md5(
264 config.get('beaker.session.secret', '') +
259 config.get('beaker.session.secret', '') +
265 rhodecode.__version__)[:8]
260 rhodecode.__version__)[:8]
266
261
267
262
268 def get_current_lang(request):
263 def get_current_lang(request):
269 # NOTE(marcink): remove after pyramid move
264 # NOTE(marcink): remove after pyramid move
270 try:
265 try:
271 return translation.get_lang()[0]
266 return translation.get_lang()[0]
272 except:
267 except:
273 pass
268 pass
274
269
275 return getattr(request, '_LOCALE_', request.locale_name)
270 return getattr(request, '_LOCALE_', request.locale_name)
276
271
277
272
278 def attach_context_attributes(context, request, user_id):
273 def attach_context_attributes(context, request, user_id):
279 """
274 """
280 Attach variables into template context called `c`.
275 Attach variables into template context called `c`.
281 """
276 """
282 config = request.registry.settings
277 config = request.registry.settings
283
278
284
279
285 rc_config = SettingsModel().get_all_settings(cache=True)
280 rc_config = SettingsModel().get_all_settings(cache=True)
286
281
287 context.rhodecode_version = rhodecode.__version__
282 context.rhodecode_version = rhodecode.__version__
288 context.rhodecode_edition = config.get('rhodecode.edition')
283 context.rhodecode_edition = config.get('rhodecode.edition')
289 # unique secret + version does not leak the version but keep consistency
284 # unique secret + version does not leak the version but keep consistency
290 context.rhodecode_version_hash = calculate_version_hash(config)
285 context.rhodecode_version_hash = calculate_version_hash(config)
291
286
292 # Default language set for the incoming request
287 # Default language set for the incoming request
293 context.language = get_current_lang(request)
288 context.language = get_current_lang(request)
294
289
295 # Visual options
290 # Visual options
296 context.visual = AttributeDict({})
291 context.visual = AttributeDict({})
297
292
298 # DB stored Visual Items
293 # DB stored Visual Items
299 context.visual.show_public_icon = str2bool(
294 context.visual.show_public_icon = str2bool(
300 rc_config.get('rhodecode_show_public_icon'))
295 rc_config.get('rhodecode_show_public_icon'))
301 context.visual.show_private_icon = str2bool(
296 context.visual.show_private_icon = str2bool(
302 rc_config.get('rhodecode_show_private_icon'))
297 rc_config.get('rhodecode_show_private_icon'))
303 context.visual.stylify_metatags = str2bool(
298 context.visual.stylify_metatags = str2bool(
304 rc_config.get('rhodecode_stylify_metatags'))
299 rc_config.get('rhodecode_stylify_metatags'))
305 context.visual.dashboard_items = safe_int(
300 context.visual.dashboard_items = safe_int(
306 rc_config.get('rhodecode_dashboard_items', 100))
301 rc_config.get('rhodecode_dashboard_items', 100))
307 context.visual.admin_grid_items = safe_int(
302 context.visual.admin_grid_items = safe_int(
308 rc_config.get('rhodecode_admin_grid_items', 100))
303 rc_config.get('rhodecode_admin_grid_items', 100))
309 context.visual.repository_fields = str2bool(
304 context.visual.repository_fields = str2bool(
310 rc_config.get('rhodecode_repository_fields'))
305 rc_config.get('rhodecode_repository_fields'))
311 context.visual.show_version = str2bool(
306 context.visual.show_version = str2bool(
312 rc_config.get('rhodecode_show_version'))
307 rc_config.get('rhodecode_show_version'))
313 context.visual.use_gravatar = str2bool(
308 context.visual.use_gravatar = str2bool(
314 rc_config.get('rhodecode_use_gravatar'))
309 rc_config.get('rhodecode_use_gravatar'))
315 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
310 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
316 context.visual.default_renderer = rc_config.get(
311 context.visual.default_renderer = rc_config.get(
317 'rhodecode_markup_renderer', 'rst')
312 'rhodecode_markup_renderer', 'rst')
318 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
313 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
319 context.visual.rhodecode_support_url = \
314 context.visual.rhodecode_support_url = \
320 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
315 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
321
316
322 context.visual.affected_files_cut_off = 60
317 context.visual.affected_files_cut_off = 60
323
318
324 context.pre_code = rc_config.get('rhodecode_pre_code')
319 context.pre_code = rc_config.get('rhodecode_pre_code')
325 context.post_code = rc_config.get('rhodecode_post_code')
320 context.post_code = rc_config.get('rhodecode_post_code')
326 context.rhodecode_name = rc_config.get('rhodecode_title')
321 context.rhodecode_name = rc_config.get('rhodecode_title')
327 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
322 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
328 # if we have specified default_encoding in the request, it has more
323 # if we have specified default_encoding in the request, it has more
329 # priority
324 # priority
330 if request.GET.get('default_encoding'):
325 if request.GET.get('default_encoding'):
331 context.default_encodings.insert(0, request.GET.get('default_encoding'))
326 context.default_encodings.insert(0, request.GET.get('default_encoding'))
332 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
327 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
333
328
334 # INI stored
329 # INI stored
335 context.labs_active = str2bool(
330 context.labs_active = str2bool(
336 config.get('labs_settings_active', 'false'))
331 config.get('labs_settings_active', 'false'))
337 context.visual.allow_repo_location_change = str2bool(
332 context.visual.allow_repo_location_change = str2bool(
338 config.get('allow_repo_location_change', True))
333 config.get('allow_repo_location_change', True))
339 context.visual.allow_custom_hooks_settings = str2bool(
334 context.visual.allow_custom_hooks_settings = str2bool(
340 config.get('allow_custom_hooks_settings', True))
335 config.get('allow_custom_hooks_settings', True))
341 context.debug_style = str2bool(config.get('debug_style', False))
336 context.debug_style = str2bool(config.get('debug_style', False))
342
337
343 context.rhodecode_instanceid = config.get('instance_id')
338 context.rhodecode_instanceid = config.get('instance_id')
344
339
345 context.visual.cut_off_limit_diff = safe_int(
340 context.visual.cut_off_limit_diff = safe_int(
346 config.get('cut_off_limit_diff'))
341 config.get('cut_off_limit_diff'))
347 context.visual.cut_off_limit_file = safe_int(
342 context.visual.cut_off_limit_file = safe_int(
348 config.get('cut_off_limit_file'))
343 config.get('cut_off_limit_file'))
349
344
350 # AppEnlight
345 # AppEnlight
351 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
346 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
352 context.appenlight_api_public_key = config.get(
347 context.appenlight_api_public_key = config.get(
353 'appenlight.api_public_key', '')
348 'appenlight.api_public_key', '')
354 context.appenlight_server_url = config.get('appenlight.server_url', '')
349 context.appenlight_server_url = config.get('appenlight.server_url', '')
355
350
356 # JS template context
351 # JS template context
357 context.template_context = {
352 context.template_context = {
358 'repo_name': None,
353 'repo_name': None,
359 'repo_type': None,
354 'repo_type': None,
360 'repo_landing_commit': None,
355 'repo_landing_commit': None,
361 'rhodecode_user': {
356 'rhodecode_user': {
362 'username': None,
357 'username': None,
363 'email': None,
358 'email': None,
364 'notification_status': False
359 'notification_status': False
365 },
360 },
366 'visual': {
361 'visual': {
367 'default_renderer': None
362 'default_renderer': None
368 },
363 },
369 'commit_data': {
364 'commit_data': {
370 'commit_id': None
365 'commit_id': None
371 },
366 },
372 'pull_request_data': {'pull_request_id': None},
367 'pull_request_data': {'pull_request_id': None},
373 'timeago': {
368 'timeago': {
374 'refresh_time': 120 * 1000,
369 'refresh_time': 120 * 1000,
375 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
370 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
376 },
371 },
377 'pyramid_dispatch': {
372 'pyramid_dispatch': {
378
373
379 },
374 },
380 'extra': {'plugins': {}}
375 'extra': {'plugins': {}}
381 }
376 }
382 # END CONFIG VARS
377 # END CONFIG VARS
383
378
384 diffmode = 'sideside'
379 diffmode = 'sideside'
385 if request.GET.get('diffmode'):
380 if request.GET.get('diffmode'):
386 if request.GET['diffmode'] == 'unified':
381 if request.GET['diffmode'] == 'unified':
387 diffmode = 'unified'
382 diffmode = 'unified'
388 elif request.session.get('diffmode'):
383 elif request.session.get('diffmode'):
389 diffmode = request.session['diffmode']
384 diffmode = request.session['diffmode']
390
385
391 context.diffmode = diffmode
386 context.diffmode = diffmode
392
387
393 if request.session.get('diffmode') != diffmode:
388 if request.session.get('diffmode') != diffmode:
394 request.session['diffmode'] = diffmode
389 request.session['diffmode'] = diffmode
395
390
396 context.csrf_token = auth.get_csrf_token(session=request.session)
391 context.csrf_token = auth.get_csrf_token(session=request.session)
397 context.backends = rhodecode.BACKENDS.keys()
392 context.backends = rhodecode.BACKENDS.keys()
398 context.backends.sort()
393 context.backends.sort()
399 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
394 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
400
395
401 # web case
396 # web case
402 if hasattr(request, 'user'):
397 if hasattr(request, 'user'):
403 context.auth_user = request.user
398 context.auth_user = request.user
404 context.rhodecode_user = request.user
399 context.rhodecode_user = request.user
405
400
406 # api case
401 # api case
407 if hasattr(request, 'rpc_user'):
402 if hasattr(request, 'rpc_user'):
408 context.auth_user = request.rpc_user
403 context.auth_user = request.rpc_user
409 context.rhodecode_user = request.rpc_user
404 context.rhodecode_user = request.rpc_user
410
405
411 # attach the whole call context to the request
406 # attach the whole call context to the request
412 request.call_context = context
407 request.call_context = context
413
408
414
409
415 def get_auth_user(request):
410 def get_auth_user(request):
416 environ = request.environ
411 environ = request.environ
417 session = request.session
412 session = request.session
418
413
419 ip_addr = get_ip_addr(environ)
414 ip_addr = get_ip_addr(environ)
420 # make sure that we update permissions each time we call controller
415 # make sure that we update permissions each time we call controller
421 _auth_token = (request.GET.get('auth_token', '') or
416 _auth_token = (request.GET.get('auth_token', '') or
422 request.GET.get('api_key', ''))
417 request.GET.get('api_key', ''))
423
418
424 if _auth_token:
419 if _auth_token:
425 # when using API_KEY we assume user exists, and
420 # when using API_KEY we assume user exists, and
426 # doesn't need auth based on cookies.
421 # doesn't need auth based on cookies.
427 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
422 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
428 authenticated = False
423 authenticated = False
429 else:
424 else:
430 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
425 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
431 try:
426 try:
432 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
427 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
433 ip_addr=ip_addr)
428 ip_addr=ip_addr)
434 except UserCreationError as e:
429 except UserCreationError as e:
435 h.flash(e, 'error')
430 h.flash(e, 'error')
436 # container auth or other auth functions that create users
431 # container auth or other auth functions that create users
437 # on the fly can throw this exception signaling that there's
432 # on the fly can throw this exception signaling that there's
438 # issue with user creation, explanation should be provided
433 # issue with user creation, explanation should be provided
439 # in Exception itself. We then create a simple blank
434 # in Exception itself. We then create a simple blank
440 # AuthUser
435 # AuthUser
441 auth_user = AuthUser(ip_addr=ip_addr)
436 auth_user = AuthUser(ip_addr=ip_addr)
442
437
438 # in case someone changes a password for user it triggers session
439 # flush and forces a re-login
443 if password_changed(auth_user, session):
440 if password_changed(auth_user, session):
444 session.invalidate()
441 session.invalidate()
445 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
442 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
446 auth_user = AuthUser(ip_addr=ip_addr)
443 auth_user = AuthUser(ip_addr=ip_addr)
447
444
448 authenticated = cookie_store.get('is_authenticated')
445 authenticated = cookie_store.get('is_authenticated')
449
446
450 if not auth_user.is_authenticated and auth_user.is_user_object:
447 if not auth_user.is_authenticated and auth_user.is_user_object:
451 # user is not authenticated and not empty
448 # user is not authenticated and not empty
452 auth_user.set_authenticated(authenticated)
449 auth_user.set_authenticated(authenticated)
453
450
454 return auth_user
451 return auth_user
455
452
456
453
457 def h_filter(s):
454 def h_filter(s):
458 """
455 """
459 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
456 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
460 we wrap this with additional functionality that converts None to empty
457 we wrap this with additional functionality that converts None to empty
461 strings
458 strings
462 """
459 """
463 if s is None:
460 if s is None:
464 return markupsafe.Markup()
461 return markupsafe.Markup()
465 return markupsafe.escape(s)
462 return markupsafe.escape(s)
466
463
467
464
468 def add_events_routes(config):
465 def add_events_routes(config):
469 """
466 """
470 Adds routing that can be used in events. Because some events are triggered
467 Adds routing that can be used in events. Because some events are triggered
471 outside of pyramid context, we need to bootstrap request with some
468 outside of pyramid context, we need to bootstrap request with some
472 routing registered
469 routing registered
473 """
470 """
474
471
475 from rhodecode.apps._base import ADMIN_PREFIX
472 from rhodecode.apps._base import ADMIN_PREFIX
476
473
477 config.add_route(name='home', pattern='/')
474 config.add_route(name='home', pattern='/')
478
475
479 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
476 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
480 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
477 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
481 config.add_route(name='repo_summary', pattern='/{repo_name}')
478 config.add_route(name='repo_summary', pattern='/{repo_name}')
482 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
479 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
483 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
480 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
484
481
485 config.add_route(name='pullrequest_show',
482 config.add_route(name='pullrequest_show',
486 pattern='/{repo_name}/pull-request/{pull_request_id}')
483 pattern='/{repo_name}/pull-request/{pull_request_id}')
487 config.add_route(name='pull_requests_global',
484 config.add_route(name='pull_requests_global',
488 pattern='/pull-request/{pull_request_id}')
485 pattern='/pull-request/{pull_request_id}')
489 config.add_route(name='repo_commit',
486 config.add_route(name='repo_commit',
490 pattern='/{repo_name}/changeset/{commit_id}')
487 pattern='/{repo_name}/changeset/{commit_id}')
491
488
492 config.add_route(name='repo_files',
489 config.add_route(name='repo_files',
493 pattern='/{repo_name}/files/{commit_id}/{f_path}')
490 pattern='/{repo_name}/files/{commit_id}/{f_path}')
494
491
495
492
496 def bootstrap_config(request):
493 def bootstrap_config(request):
497 import pyramid.testing
494 import pyramid.testing
498 registry = pyramid.testing.Registry('RcTestRegistry')
495 registry = pyramid.testing.Registry('RcTestRegistry')
499
496
500 config = pyramid.testing.setUp(registry=registry, request=request)
497 config = pyramid.testing.setUp(registry=registry, request=request)
501
498
502 # allow pyramid lookup in testing
499 # allow pyramid lookup in testing
503 config.include('pyramid_mako')
500 config.include('pyramid_mako')
504 config.include('pyramid_beaker')
501 config.include('pyramid_beaker')
505
502
506 add_events_routes(config)
503 add_events_routes(config)
507
504
508 return config
505 return config
509
506
510
507
511 def bootstrap_request(**kwargs):
508 def bootstrap_request(**kwargs):
512 import pyramid.testing
509 import pyramid.testing
513
510
514 class TestRequest(pyramid.testing.DummyRequest):
511 class TestRequest(pyramid.testing.DummyRequest):
515 application_url = kwargs.pop('application_url', 'http://example.com')
512 application_url = kwargs.pop('application_url', 'http://example.com')
516 host = kwargs.pop('host', 'example.com:80')
513 host = kwargs.pop('host', 'example.com:80')
517 domain = kwargs.pop('domain', 'example.com')
514 domain = kwargs.pop('domain', 'example.com')
518
515
519 def translate(self, msg):
516 def translate(self, msg):
520 return msg
517 return msg
521
518
522 def plularize(self, singular, plural, n):
519 def plularize(self, singular, plural, n):
523 return singular
520 return singular
524
521
525 def get_partial_renderer(self, tmpl_name):
522 def get_partial_renderer(self, tmpl_name):
526
523
527 from rhodecode.lib.partial_renderer import get_partial_renderer
524 from rhodecode.lib.partial_renderer import get_partial_renderer
528 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
525 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
529
526
530 _call_context = {}
527 _call_context = {}
531 @property
528 @property
532 def call_context(self):
529 def call_context(self):
533 return self._call_context
530 return self._call_context
534
531
535 class TestDummySession(pyramid.testing.DummySession):
532 class TestDummySession(pyramid.testing.DummySession):
536 def save(*arg, **kw):
533 def save(*arg, **kw):
537 pass
534 pass
538
535
539 request = TestRequest(**kwargs)
536 request = TestRequest(**kwargs)
540 request.session = TestDummySession()
537 request.session = TestDummySession()
541
538
542 return request
539 return request
543
540
@@ -1,1170 +1,1107 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Set of diffing helpers, previously part of vcs
23 Set of diffing helpers, previously part of vcs
24 """
24 """
25
25
26 import re
26 import collections
27 import collections
27 import re
28 import difflib
28 import difflib
29 import logging
29 import logging
30
30
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from rhodecode.translation import temp_translation_factory as _
34
35 from rhodecode.lib.vcs.exceptions import VCSError
33 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
34 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 from rhodecode.lib.helpers import escape
39 from rhodecode.lib.utils2 import safe_unicode
35 from rhodecode.lib.utils2 import safe_unicode
40
36
41 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
42
38
43 # define max context, a file with more than this numbers of lines is unusable
39 # define max context, a file with more than this numbers of lines is unusable
44 # in browser anyway
40 # in browser anyway
45 MAX_CONTEXT = 1024 * 1014
41 MAX_CONTEXT = 1024 * 1014
46
42
47
43
48 class OPS(object):
44 class OPS(object):
49 ADD = 'A'
45 ADD = 'A'
50 MOD = 'M'
46 MOD = 'M'
51 DEL = 'D'
47 DEL = 'D'
52
48
53
49
54 def wrap_to_table(str_):
55 return '''<table class="code-difftable">
56 <tr class="line no-comment">
57 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
58 <td></td>
59 <td class="lineno new"></td>
60 <td class="code no-comment"><pre>%s</pre></td>
61 </tr>
62 </table>''' % (_('Click to comment'), str_)
63
64
65 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
66 show_full_diff=False, ignore_whitespace=True, line_context=3,
67 enable_comments=False):
68 """
69 returns a wrapped diff into a table, checks for cut_off_limit for file and
70 whole diff and presents proper message
71 """
72
73 if filenode_old is None:
74 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
75
76 if filenode_old.is_binary or filenode_new.is_binary:
77 diff = wrap_to_table(_('Binary file'))
78 stats = None
79 size = 0
80 data = None
81
82 elif diff_limit != -1 and (diff_limit is None or
83 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
84
85 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
86 ignore_whitespace=ignore_whitespace,
87 context=line_context)
88 diff_processor = DiffProcessor(
89 f_gitdiff, format='gitdiff', diff_limit=diff_limit,
90 file_limit=file_limit, show_full_diff=show_full_diff)
91 _parsed = diff_processor.prepare()
92
93 diff = diff_processor.as_html(enable_comments=enable_comments)
94 stats = _parsed[0]['stats'] if _parsed else None
95 size = len(diff or '')
96 data = _parsed[0] if _parsed else None
97 else:
98 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
99 'diff menu to display this diff'))
100 stats = None
101 size = 0
102 data = None
103 if not diff:
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 [filenode_new, filenode_old])
106 if submodules:
107 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
108 else:
109 diff = wrap_to_table(_('No changes detected'))
110
111 cs1 = filenode_old.commit.raw_id
112 cs2 = filenode_new.commit.raw_id
113
114 return size, cs1, cs2, diff, stats, data
115
116
117 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
50 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
118 """
51 """
119 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
52 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
120
53
121 :param ignore_whitespace: ignore whitespaces in diff
54 :param ignore_whitespace: ignore whitespaces in diff
122 """
55 """
123 # make sure we pass in default context
56 # make sure we pass in default context
124 context = context or 3
57 context = context or 3
125 # protect against IntOverflow when passing HUGE context
58 # protect against IntOverflow when passing HUGE context
126 if context > MAX_CONTEXT:
59 if context > MAX_CONTEXT:
127 context = MAX_CONTEXT
60 context = MAX_CONTEXT
128
61
129 submodules = filter(lambda o: isinstance(o, SubModuleNode),
62 submodules = filter(lambda o: isinstance(o, SubModuleNode),
130 [filenode_new, filenode_old])
63 [filenode_new, filenode_old])
131 if submodules:
64 if submodules:
132 return ''
65 return ''
133
66
134 for filenode in (filenode_old, filenode_new):
67 for filenode in (filenode_old, filenode_new):
135 if not isinstance(filenode, FileNode):
68 if not isinstance(filenode, FileNode):
136 raise VCSError(
69 raise VCSError(
137 "Given object should be FileNode object, not %s"
70 "Given object should be FileNode object, not %s"
138 % filenode.__class__)
71 % filenode.__class__)
139
72
140 repo = filenode_new.commit.repository
73 repo = filenode_new.commit.repository
141 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
74 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
142 new_commit = filenode_new.commit
75 new_commit = filenode_new.commit
143
76
144 vcs_gitdiff = repo.get_diff(
77 vcs_gitdiff = repo.get_diff(
145 old_commit, new_commit, filenode_new.path,
78 old_commit, new_commit, filenode_new.path,
146 ignore_whitespace, context, path1=filenode_old.path)
79 ignore_whitespace, context, path1=filenode_old.path)
147 return vcs_gitdiff
80 return vcs_gitdiff
148
81
149 NEW_FILENODE = 1
82 NEW_FILENODE = 1
150 DEL_FILENODE = 2
83 DEL_FILENODE = 2
151 MOD_FILENODE = 3
84 MOD_FILENODE = 3
152 RENAMED_FILENODE = 4
85 RENAMED_FILENODE = 4
153 COPIED_FILENODE = 5
86 COPIED_FILENODE = 5
154 CHMOD_FILENODE = 6
87 CHMOD_FILENODE = 6
155 BIN_FILENODE = 7
88 BIN_FILENODE = 7
156
89
157
90
158 class LimitedDiffContainer(object):
91 class LimitedDiffContainer(object):
159
92
160 def __init__(self, diff_limit, cur_diff_size, diff):
93 def __init__(self, diff_limit, cur_diff_size, diff):
161 self.diff = diff
94 self.diff = diff
162 self.diff_limit = diff_limit
95 self.diff_limit = diff_limit
163 self.cur_diff_size = cur_diff_size
96 self.cur_diff_size = cur_diff_size
164
97
165 def __getitem__(self, key):
98 def __getitem__(self, key):
166 return self.diff.__getitem__(key)
99 return self.diff.__getitem__(key)
167
100
168 def __iter__(self):
101 def __iter__(self):
169 for l in self.diff:
102 for l in self.diff:
170 yield l
103 yield l
171
104
172
105
173 class Action(object):
106 class Action(object):
174 """
107 """
175 Contains constants for the action value of the lines in a parsed diff.
108 Contains constants for the action value of the lines in a parsed diff.
176 """
109 """
177
110
178 ADD = 'add'
111 ADD = 'add'
179 DELETE = 'del'
112 DELETE = 'del'
180 UNMODIFIED = 'unmod'
113 UNMODIFIED = 'unmod'
181
114
182 CONTEXT = 'context'
115 CONTEXT = 'context'
183 OLD_NO_NL = 'old-no-nl'
116 OLD_NO_NL = 'old-no-nl'
184 NEW_NO_NL = 'new-no-nl'
117 NEW_NO_NL = 'new-no-nl'
185
118
186
119
187 class DiffProcessor(object):
120 class DiffProcessor(object):
188 """
121 """
189 Give it a unified or git diff and it returns a list of the files that were
122 Give it a unified or git diff and it returns a list of the files that were
190 mentioned in the diff together with a dict of meta information that
123 mentioned in the diff together with a dict of meta information that
191 can be used to render it in a HTML template.
124 can be used to render it in a HTML template.
192
125
193 .. note:: Unicode handling
126 .. note:: Unicode handling
194
127
195 The original diffs are a byte sequence and can contain filenames
128 The original diffs are a byte sequence and can contain filenames
196 in mixed encodings. This class generally returns `unicode` objects
129 in mixed encodings. This class generally returns `unicode` objects
197 since the result is intended for presentation to the user.
130 since the result is intended for presentation to the user.
198
131
199 """
132 """
200 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
133 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
201 _newline_marker = re.compile(r'^\\ No newline at end of file')
134 _newline_marker = re.compile(r'^\\ No newline at end of file')
202
135
203 # used for inline highlighter word split
136 # used for inline highlighter word split
204 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
137 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
205
138
206 # collapse ranges of commits over given number
139 # collapse ranges of commits over given number
207 _collapse_commits_over = 5
140 _collapse_commits_over = 5
208
141
209 def __init__(self, diff, format='gitdiff', diff_limit=None,
142 def __init__(self, diff, format='gitdiff', diff_limit=None,
210 file_limit=None, show_full_diff=True):
143 file_limit=None, show_full_diff=True):
211 """
144 """
212 :param diff: A `Diff` object representing a diff from a vcs backend
145 :param diff: A `Diff` object representing a diff from a vcs backend
213 :param format: format of diff passed, `udiff` or `gitdiff`
146 :param format: format of diff passed, `udiff` or `gitdiff`
214 :param diff_limit: define the size of diff that is considered "big"
147 :param diff_limit: define the size of diff that is considered "big"
215 based on that parameter cut off will be triggered, set to None
148 based on that parameter cut off will be triggered, set to None
216 to show full diff
149 to show full diff
217 """
150 """
218 self._diff = diff
151 self._diff = diff
219 self._format = format
152 self._format = format
220 self.adds = 0
153 self.adds = 0
221 self.removes = 0
154 self.removes = 0
222 # calculate diff size
155 # calculate diff size
223 self.diff_limit = diff_limit
156 self.diff_limit = diff_limit
224 self.file_limit = file_limit
157 self.file_limit = file_limit
225 self.show_full_diff = show_full_diff
158 self.show_full_diff = show_full_diff
226 self.cur_diff_size = 0
159 self.cur_diff_size = 0
227 self.parsed = False
160 self.parsed = False
228 self.parsed_diff = []
161 self.parsed_diff = []
229
162
230 log.debug('Initialized DiffProcessor with %s mode', format)
163 log.debug('Initialized DiffProcessor with %s mode', format)
231 if format == 'gitdiff':
164 if format == 'gitdiff':
232 self.differ = self._highlight_line_difflib
165 self.differ = self._highlight_line_difflib
233 self._parser = self._parse_gitdiff
166 self._parser = self._parse_gitdiff
234 else:
167 else:
235 self.differ = self._highlight_line_udiff
168 self.differ = self._highlight_line_udiff
236 self._parser = self._new_parse_gitdiff
169 self._parser = self._new_parse_gitdiff
237
170
238 def _copy_iterator(self):
171 def _copy_iterator(self):
239 """
172 """
240 make a fresh copy of generator, we should not iterate thru
173 make a fresh copy of generator, we should not iterate thru
241 an original as it's needed for repeating operations on
174 an original as it's needed for repeating operations on
242 this instance of DiffProcessor
175 this instance of DiffProcessor
243 """
176 """
244 self.__udiff, iterator_copy = tee(self.__udiff)
177 self.__udiff, iterator_copy = tee(self.__udiff)
245 return iterator_copy
178 return iterator_copy
246
179
247 def _escaper(self, string):
180 def _escaper(self, string):
248 """
181 """
249 Escaper for diff escapes special chars and checks the diff limit
182 Escaper for diff escapes special chars and checks the diff limit
250
183
251 :param string:
184 :param string:
252 """
185 """
253
186
254 self.cur_diff_size += len(string)
187 self.cur_diff_size += len(string)
255
188
256 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
189 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
257 raise DiffLimitExceeded('Diff Limit Exceeded')
190 raise DiffLimitExceeded('Diff Limit Exceeded')
258
191
259 return safe_unicode(string)\
192 return safe_unicode(string)\
260 .replace('&', '&amp;')\
193 .replace('&', '&amp;')\
261 .replace('<', '&lt;')\
194 .replace('<', '&lt;')\
262 .replace('>', '&gt;')
195 .replace('>', '&gt;')
263
196
264 def _line_counter(self, l):
197 def _line_counter(self, l):
265 """
198 """
266 Checks each line and bumps total adds/removes for this diff
199 Checks each line and bumps total adds/removes for this diff
267
200
268 :param l:
201 :param l:
269 """
202 """
270 if l.startswith('+') and not l.startswith('+++'):
203 if l.startswith('+') and not l.startswith('+++'):
271 self.adds += 1
204 self.adds += 1
272 elif l.startswith('-') and not l.startswith('---'):
205 elif l.startswith('-') and not l.startswith('---'):
273 self.removes += 1
206 self.removes += 1
274 return safe_unicode(l)
207 return safe_unicode(l)
275
208
276 def _highlight_line_difflib(self, line, next_):
209 def _highlight_line_difflib(self, line, next_):
277 """
210 """
278 Highlight inline changes in both lines.
211 Highlight inline changes in both lines.
279 """
212 """
280
213
281 if line['action'] == Action.DELETE:
214 if line['action'] == Action.DELETE:
282 old, new = line, next_
215 old, new = line, next_
283 else:
216 else:
284 old, new = next_, line
217 old, new = next_, line
285
218
286 oldwords = self._token_re.split(old['line'])
219 oldwords = self._token_re.split(old['line'])
287 newwords = self._token_re.split(new['line'])
220 newwords = self._token_re.split(new['line'])
288 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
221 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
289
222
290 oldfragments, newfragments = [], []
223 oldfragments, newfragments = [], []
291 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
224 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
292 oldfrag = ''.join(oldwords[i1:i2])
225 oldfrag = ''.join(oldwords[i1:i2])
293 newfrag = ''.join(newwords[j1:j2])
226 newfrag = ''.join(newwords[j1:j2])
294 if tag != 'equal':
227 if tag != 'equal':
295 if oldfrag:
228 if oldfrag:
296 oldfrag = '<del>%s</del>' % oldfrag
229 oldfrag = '<del>%s</del>' % oldfrag
297 if newfrag:
230 if newfrag:
298 newfrag = '<ins>%s</ins>' % newfrag
231 newfrag = '<ins>%s</ins>' % newfrag
299 oldfragments.append(oldfrag)
232 oldfragments.append(oldfrag)
300 newfragments.append(newfrag)
233 newfragments.append(newfrag)
301
234
302 old['line'] = "".join(oldfragments)
235 old['line'] = "".join(oldfragments)
303 new['line'] = "".join(newfragments)
236 new['line'] = "".join(newfragments)
304
237
305 def _highlight_line_udiff(self, line, next_):
238 def _highlight_line_udiff(self, line, next_):
306 """
239 """
307 Highlight inline changes in both lines.
240 Highlight inline changes in both lines.
308 """
241 """
309 start = 0
242 start = 0
310 limit = min(len(line['line']), len(next_['line']))
243 limit = min(len(line['line']), len(next_['line']))
311 while start < limit and line['line'][start] == next_['line'][start]:
244 while start < limit and line['line'][start] == next_['line'][start]:
312 start += 1
245 start += 1
313 end = -1
246 end = -1
314 limit -= start
247 limit -= start
315 while -end <= limit and line['line'][end] == next_['line'][end]:
248 while -end <= limit and line['line'][end] == next_['line'][end]:
316 end -= 1
249 end -= 1
317 end += 1
250 end += 1
318 if start or end:
251 if start or end:
319 def do(l):
252 def do(l):
320 last = end + len(l['line'])
253 last = end + len(l['line'])
321 if l['action'] == Action.ADD:
254 if l['action'] == Action.ADD:
322 tag = 'ins'
255 tag = 'ins'
323 else:
256 else:
324 tag = 'del'
257 tag = 'del'
325 l['line'] = '%s<%s>%s</%s>%s' % (
258 l['line'] = '%s<%s>%s</%s>%s' % (
326 l['line'][:start],
259 l['line'][:start],
327 tag,
260 tag,
328 l['line'][start:last],
261 l['line'][start:last],
329 tag,
262 tag,
330 l['line'][last:]
263 l['line'][last:]
331 )
264 )
332 do(line)
265 do(line)
333 do(next_)
266 do(next_)
334
267
335 def _clean_line(self, line, command):
268 def _clean_line(self, line, command):
336 if command in ['+', '-', ' ']:
269 if command in ['+', '-', ' ']:
337 # only modify the line if it's actually a diff thing
270 # only modify the line if it's actually a diff thing
338 line = line[1:]
271 line = line[1:]
339 return line
272 return line
340
273
341 def _parse_gitdiff(self, inline_diff=True):
274 def _parse_gitdiff(self, inline_diff=True):
342 _files = []
275 _files = []
343 diff_container = lambda arg: arg
276 diff_container = lambda arg: arg
344
277
345 for chunk in self._diff.chunks():
278 for chunk in self._diff.chunks():
346 head = chunk.header
279 head = chunk.header
347
280
348 diff = imap(self._escaper, chunk.diff.splitlines(1))
281 diff = imap(self._escaper, chunk.diff.splitlines(1))
349 raw_diff = chunk.raw
282 raw_diff = chunk.raw
350 limited_diff = False
283 limited_diff = False
351 exceeds_limit = False
284 exceeds_limit = False
352
285
353 op = None
286 op = None
354 stats = {
287 stats = {
355 'added': 0,
288 'added': 0,
356 'deleted': 0,
289 'deleted': 0,
357 'binary': False,
290 'binary': False,
358 'ops': {},
291 'ops': {},
359 }
292 }
360
293
361 if head['deleted_file_mode']:
294 if head['deleted_file_mode']:
362 op = OPS.DEL
295 op = OPS.DEL
363 stats['binary'] = True
296 stats['binary'] = True
364 stats['ops'][DEL_FILENODE] = 'deleted file'
297 stats['ops'][DEL_FILENODE] = 'deleted file'
365
298
366 elif head['new_file_mode']:
299 elif head['new_file_mode']:
367 op = OPS.ADD
300 op = OPS.ADD
368 stats['binary'] = True
301 stats['binary'] = True
369 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
302 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
370 else: # modify operation, can be copy, rename or chmod
303 else: # modify operation, can be copy, rename or chmod
371
304
372 # CHMOD
305 # CHMOD
373 if head['new_mode'] and head['old_mode']:
306 if head['new_mode'] and head['old_mode']:
374 op = OPS.MOD
307 op = OPS.MOD
375 stats['binary'] = True
308 stats['binary'] = True
376 stats['ops'][CHMOD_FILENODE] = (
309 stats['ops'][CHMOD_FILENODE] = (
377 'modified file chmod %s => %s' % (
310 'modified file chmod %s => %s' % (
378 head['old_mode'], head['new_mode']))
311 head['old_mode'], head['new_mode']))
379 # RENAME
312 # RENAME
380 if head['rename_from'] != head['rename_to']:
313 if head['rename_from'] != head['rename_to']:
381 op = OPS.MOD
314 op = OPS.MOD
382 stats['binary'] = True
315 stats['binary'] = True
383 stats['ops'][RENAMED_FILENODE] = (
316 stats['ops'][RENAMED_FILENODE] = (
384 'file renamed from %s to %s' % (
317 'file renamed from %s to %s' % (
385 head['rename_from'], head['rename_to']))
318 head['rename_from'], head['rename_to']))
386 # COPY
319 # COPY
387 if head.get('copy_from') and head.get('copy_to'):
320 if head.get('copy_from') and head.get('copy_to'):
388 op = OPS.MOD
321 op = OPS.MOD
389 stats['binary'] = True
322 stats['binary'] = True
390 stats['ops'][COPIED_FILENODE] = (
323 stats['ops'][COPIED_FILENODE] = (
391 'file copied from %s to %s' % (
324 'file copied from %s to %s' % (
392 head['copy_from'], head['copy_to']))
325 head['copy_from'], head['copy_to']))
393
326
394 # If our new parsed headers didn't match anything fallback to
327 # If our new parsed headers didn't match anything fallback to
395 # old style detection
328 # old style detection
396 if op is None:
329 if op is None:
397 if not head['a_file'] and head['b_file']:
330 if not head['a_file'] and head['b_file']:
398 op = OPS.ADD
331 op = OPS.ADD
399 stats['binary'] = True
332 stats['binary'] = True
400 stats['ops'][NEW_FILENODE] = 'new file'
333 stats['ops'][NEW_FILENODE] = 'new file'
401
334
402 elif head['a_file'] and not head['b_file']:
335 elif head['a_file'] and not head['b_file']:
403 op = OPS.DEL
336 op = OPS.DEL
404 stats['binary'] = True
337 stats['binary'] = True
405 stats['ops'][DEL_FILENODE] = 'deleted file'
338 stats['ops'][DEL_FILENODE] = 'deleted file'
406
339
407 # it's not ADD not DELETE
340 # it's not ADD not DELETE
408 if op is None:
341 if op is None:
409 op = OPS.MOD
342 op = OPS.MOD
410 stats['binary'] = True
343 stats['binary'] = True
411 stats['ops'][MOD_FILENODE] = 'modified file'
344 stats['ops'][MOD_FILENODE] = 'modified file'
412
345
413 # a real non-binary diff
346 # a real non-binary diff
414 if head['a_file'] or head['b_file']:
347 if head['a_file'] or head['b_file']:
415 try:
348 try:
416 raw_diff, chunks, _stats = self._parse_lines(diff)
349 raw_diff, chunks, _stats = self._parse_lines(diff)
417 stats['binary'] = False
350 stats['binary'] = False
418 stats['added'] = _stats[0]
351 stats['added'] = _stats[0]
419 stats['deleted'] = _stats[1]
352 stats['deleted'] = _stats[1]
420 # explicit mark that it's a modified file
353 # explicit mark that it's a modified file
421 if op == OPS.MOD:
354 if op == OPS.MOD:
422 stats['ops'][MOD_FILENODE] = 'modified file'
355 stats['ops'][MOD_FILENODE] = 'modified file'
423 exceeds_limit = len(raw_diff) > self.file_limit
356 exceeds_limit = len(raw_diff) > self.file_limit
424
357
425 # changed from _escaper function so we validate size of
358 # changed from _escaper function so we validate size of
426 # each file instead of the whole diff
359 # each file instead of the whole diff
427 # diff will hide big files but still show small ones
360 # diff will hide big files but still show small ones
428 # from my tests, big files are fairly safe to be parsed
361 # from my tests, big files are fairly safe to be parsed
429 # but the browser is the bottleneck
362 # but the browser is the bottleneck
430 if not self.show_full_diff and exceeds_limit:
363 if not self.show_full_diff and exceeds_limit:
431 raise DiffLimitExceeded('File Limit Exceeded')
364 raise DiffLimitExceeded('File Limit Exceeded')
432
365
433 except DiffLimitExceeded:
366 except DiffLimitExceeded:
434 diff_container = lambda _diff: \
367 diff_container = lambda _diff: \
435 LimitedDiffContainer(
368 LimitedDiffContainer(
436 self.diff_limit, self.cur_diff_size, _diff)
369 self.diff_limit, self.cur_diff_size, _diff)
437
370
438 exceeds_limit = len(raw_diff) > self.file_limit
371 exceeds_limit = len(raw_diff) > self.file_limit
439 limited_diff = True
372 limited_diff = True
440 chunks = []
373 chunks = []
441
374
442 else: # GIT format binary patch, or possibly empty diff
375 else: # GIT format binary patch, or possibly empty diff
443 if head['bin_patch']:
376 if head['bin_patch']:
444 # we have operation already extracted, but we mark simply
377 # we have operation already extracted, but we mark simply
445 # it's a diff we wont show for binary files
378 # it's a diff we wont show for binary files
446 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
379 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
447 chunks = []
380 chunks = []
448
381
449 if chunks and not self.show_full_diff and op == OPS.DEL:
382 if chunks and not self.show_full_diff and op == OPS.DEL:
450 # if not full diff mode show deleted file contents
383 # if not full diff mode show deleted file contents
451 # TODO: anderson: if the view is not too big, there is no way
384 # TODO: anderson: if the view is not too big, there is no way
452 # to see the content of the file
385 # to see the content of the file
453 chunks = []
386 chunks = []
454
387
455 chunks.insert(0, [{
388 chunks.insert(0, [{
456 'old_lineno': '',
389 'old_lineno': '',
457 'new_lineno': '',
390 'new_lineno': '',
458 'action': Action.CONTEXT,
391 'action': Action.CONTEXT,
459 'line': msg,
392 'line': msg,
460 } for _op, msg in stats['ops'].iteritems()
393 } for _op, msg in stats['ops'].iteritems()
461 if _op not in [MOD_FILENODE]])
394 if _op not in [MOD_FILENODE]])
462
395
463 _files.append({
396 _files.append({
464 'filename': safe_unicode(head['b_path']),
397 'filename': safe_unicode(head['b_path']),
465 'old_revision': head['a_blob_id'],
398 'old_revision': head['a_blob_id'],
466 'new_revision': head['b_blob_id'],
399 'new_revision': head['b_blob_id'],
467 'chunks': chunks,
400 'chunks': chunks,
468 'raw_diff': safe_unicode(raw_diff),
401 'raw_diff': safe_unicode(raw_diff),
469 'operation': op,
402 'operation': op,
470 'stats': stats,
403 'stats': stats,
471 'exceeds_limit': exceeds_limit,
404 'exceeds_limit': exceeds_limit,
472 'is_limited_diff': limited_diff,
405 'is_limited_diff': limited_diff,
473 })
406 })
474
407
475 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
408 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
476 OPS.DEL: 2}.get(info['operation'])
409 OPS.DEL: 2}.get(info['operation'])
477
410
478 if not inline_diff:
411 if not inline_diff:
479 return diff_container(sorted(_files, key=sorter))
412 return diff_container(sorted(_files, key=sorter))
480
413
481 # highlight inline changes
414 # highlight inline changes
482 for diff_data in _files:
415 for diff_data in _files:
483 for chunk in diff_data['chunks']:
416 for chunk in diff_data['chunks']:
484 lineiter = iter(chunk)
417 lineiter = iter(chunk)
485 try:
418 try:
486 while 1:
419 while 1:
487 line = lineiter.next()
420 line = lineiter.next()
488 if line['action'] not in (
421 if line['action'] not in (
489 Action.UNMODIFIED, Action.CONTEXT):
422 Action.UNMODIFIED, Action.CONTEXT):
490 nextline = lineiter.next()
423 nextline = lineiter.next()
491 if nextline['action'] in ['unmod', 'context'] or \
424 if nextline['action'] in ['unmod', 'context'] or \
492 nextline['action'] == line['action']:
425 nextline['action'] == line['action']:
493 continue
426 continue
494 self.differ(line, nextline)
427 self.differ(line, nextline)
495 except StopIteration:
428 except StopIteration:
496 pass
429 pass
497
430
498 return diff_container(sorted(_files, key=sorter))
431 return diff_container(sorted(_files, key=sorter))
499
432
500 def _check_large_diff(self):
433 def _check_large_diff(self):
501 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
434 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
502 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
435 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
503 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
436 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
504
437
505 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
438 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
506 def _new_parse_gitdiff(self, inline_diff=True):
439 def _new_parse_gitdiff(self, inline_diff=True):
507 _files = []
440 _files = []
508
441
509 # this can be overriden later to a LimitedDiffContainer type
442 # this can be overriden later to a LimitedDiffContainer type
510 diff_container = lambda arg: arg
443 diff_container = lambda arg: arg
511
444
512 for chunk in self._diff.chunks():
445 for chunk in self._diff.chunks():
513 head = chunk.header
446 head = chunk.header
514 log.debug('parsing diff %r' % head)
447 log.debug('parsing diff %r' % head)
515
448
516 raw_diff = chunk.raw
449 raw_diff = chunk.raw
517 limited_diff = False
450 limited_diff = False
518 exceeds_limit = False
451 exceeds_limit = False
519
452
520 op = None
453 op = None
521 stats = {
454 stats = {
522 'added': 0,
455 'added': 0,
523 'deleted': 0,
456 'deleted': 0,
524 'binary': False,
457 'binary': False,
525 'old_mode': None,
458 'old_mode': None,
526 'new_mode': None,
459 'new_mode': None,
527 'ops': {},
460 'ops': {},
528 }
461 }
529 if head['old_mode']:
462 if head['old_mode']:
530 stats['old_mode'] = head['old_mode']
463 stats['old_mode'] = head['old_mode']
531 if head['new_mode']:
464 if head['new_mode']:
532 stats['new_mode'] = head['new_mode']
465 stats['new_mode'] = head['new_mode']
533 if head['b_mode']:
466 if head['b_mode']:
534 stats['new_mode'] = head['b_mode']
467 stats['new_mode'] = head['b_mode']
535
468
536 # delete file
469 # delete file
537 if head['deleted_file_mode']:
470 if head['deleted_file_mode']:
538 op = OPS.DEL
471 op = OPS.DEL
539 stats['binary'] = True
472 stats['binary'] = True
540 stats['ops'][DEL_FILENODE] = 'deleted file'
473 stats['ops'][DEL_FILENODE] = 'deleted file'
541
474
542 # new file
475 # new file
543 elif head['new_file_mode']:
476 elif head['new_file_mode']:
544 op = OPS.ADD
477 op = OPS.ADD
545 stats['binary'] = True
478 stats['binary'] = True
546 stats['old_mode'] = None
479 stats['old_mode'] = None
547 stats['new_mode'] = head['new_file_mode']
480 stats['new_mode'] = head['new_file_mode']
548 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
481 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
549
482
550 # modify operation, can be copy, rename or chmod
483 # modify operation, can be copy, rename or chmod
551 else:
484 else:
552 # CHMOD
485 # CHMOD
553 if head['new_mode'] and head['old_mode']:
486 if head['new_mode'] and head['old_mode']:
554 op = OPS.MOD
487 op = OPS.MOD
555 stats['binary'] = True
488 stats['binary'] = True
556 stats['ops'][CHMOD_FILENODE] = (
489 stats['ops'][CHMOD_FILENODE] = (
557 'modified file chmod %s => %s' % (
490 'modified file chmod %s => %s' % (
558 head['old_mode'], head['new_mode']))
491 head['old_mode'], head['new_mode']))
559
492
560 # RENAME
493 # RENAME
561 if head['rename_from'] != head['rename_to']:
494 if head['rename_from'] != head['rename_to']:
562 op = OPS.MOD
495 op = OPS.MOD
563 stats['binary'] = True
496 stats['binary'] = True
564 stats['renamed'] = (head['rename_from'], head['rename_to'])
497 stats['renamed'] = (head['rename_from'], head['rename_to'])
565 stats['ops'][RENAMED_FILENODE] = (
498 stats['ops'][RENAMED_FILENODE] = (
566 'file renamed from %s to %s' % (
499 'file renamed from %s to %s' % (
567 head['rename_from'], head['rename_to']))
500 head['rename_from'], head['rename_to']))
568 # COPY
501 # COPY
569 if head.get('copy_from') and head.get('copy_to'):
502 if head.get('copy_from') and head.get('copy_to'):
570 op = OPS.MOD
503 op = OPS.MOD
571 stats['binary'] = True
504 stats['binary'] = True
572 stats['copied'] = (head['copy_from'], head['copy_to'])
505 stats['copied'] = (head['copy_from'], head['copy_to'])
573 stats['ops'][COPIED_FILENODE] = (
506 stats['ops'][COPIED_FILENODE] = (
574 'file copied from %s to %s' % (
507 'file copied from %s to %s' % (
575 head['copy_from'], head['copy_to']))
508 head['copy_from'], head['copy_to']))
576
509
577 # If our new parsed headers didn't match anything fallback to
510 # If our new parsed headers didn't match anything fallback to
578 # old style detection
511 # old style detection
579 if op is None:
512 if op is None:
580 if not head['a_file'] and head['b_file']:
513 if not head['a_file'] and head['b_file']:
581 op = OPS.ADD
514 op = OPS.ADD
582 stats['binary'] = True
515 stats['binary'] = True
583 stats['new_file'] = True
516 stats['new_file'] = True
584 stats['ops'][NEW_FILENODE] = 'new file'
517 stats['ops'][NEW_FILENODE] = 'new file'
585
518
586 elif head['a_file'] and not head['b_file']:
519 elif head['a_file'] and not head['b_file']:
587 op = OPS.DEL
520 op = OPS.DEL
588 stats['binary'] = True
521 stats['binary'] = True
589 stats['ops'][DEL_FILENODE] = 'deleted file'
522 stats['ops'][DEL_FILENODE] = 'deleted file'
590
523
591 # it's not ADD not DELETE
524 # it's not ADD not DELETE
592 if op is None:
525 if op is None:
593 op = OPS.MOD
526 op = OPS.MOD
594 stats['binary'] = True
527 stats['binary'] = True
595 stats['ops'][MOD_FILENODE] = 'modified file'
528 stats['ops'][MOD_FILENODE] = 'modified file'
596
529
597 # a real non-binary diff
530 # a real non-binary diff
598 if head['a_file'] or head['b_file']:
531 if head['a_file'] or head['b_file']:
599 diff = iter(chunk.diff.splitlines(1))
532 diff = iter(chunk.diff.splitlines(1))
600
533
601 # append each file to the diff size
534 # append each file to the diff size
602 raw_chunk_size = len(raw_diff)
535 raw_chunk_size = len(raw_diff)
603
536
604 exceeds_limit = raw_chunk_size > self.file_limit
537 exceeds_limit = raw_chunk_size > self.file_limit
605 self.cur_diff_size += raw_chunk_size
538 self.cur_diff_size += raw_chunk_size
606
539
607 try:
540 try:
608 # Check each file instead of the whole diff.
541 # Check each file instead of the whole diff.
609 # Diff will hide big files but still show small ones.
542 # Diff will hide big files but still show small ones.
610 # From the tests big files are fairly safe to be parsed
543 # From the tests big files are fairly safe to be parsed
611 # but the browser is the bottleneck.
544 # but the browser is the bottleneck.
612 if not self.show_full_diff and exceeds_limit:
545 if not self.show_full_diff and exceeds_limit:
613 log.debug('File `%s` exceeds current file_limit of %s',
546 log.debug('File `%s` exceeds current file_limit of %s',
614 safe_unicode(head['b_path']), self.file_limit)
547 safe_unicode(head['b_path']), self.file_limit)
615 raise DiffLimitExceeded(
548 raise DiffLimitExceeded(
616 'File Limit %s Exceeded', self.file_limit)
549 'File Limit %s Exceeded', self.file_limit)
617
550
618 self._check_large_diff()
551 self._check_large_diff()
619
552
620 raw_diff, chunks, _stats = self._new_parse_lines(diff)
553 raw_diff, chunks, _stats = self._new_parse_lines(diff)
621 stats['binary'] = False
554 stats['binary'] = False
622 stats['added'] = _stats[0]
555 stats['added'] = _stats[0]
623 stats['deleted'] = _stats[1]
556 stats['deleted'] = _stats[1]
624 # explicit mark that it's a modified file
557 # explicit mark that it's a modified file
625 if op == OPS.MOD:
558 if op == OPS.MOD:
626 stats['ops'][MOD_FILENODE] = 'modified file'
559 stats['ops'][MOD_FILENODE] = 'modified file'
627
560
628 except DiffLimitExceeded:
561 except DiffLimitExceeded:
629 diff_container = lambda _diff: \
562 diff_container = lambda _diff: \
630 LimitedDiffContainer(
563 LimitedDiffContainer(
631 self.diff_limit, self.cur_diff_size, _diff)
564 self.diff_limit, self.cur_diff_size, _diff)
632
565
633 limited_diff = True
566 limited_diff = True
634 chunks = []
567 chunks = []
635
568
636 else: # GIT format binary patch, or possibly empty diff
569 else: # GIT format binary patch, or possibly empty diff
637 if head['bin_patch']:
570 if head['bin_patch']:
638 # we have operation already extracted, but we mark simply
571 # we have operation already extracted, but we mark simply
639 # it's a diff we wont show for binary files
572 # it's a diff we wont show for binary files
640 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
573 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
641 chunks = []
574 chunks = []
642
575
643 # Hide content of deleted node by setting empty chunks
576 # Hide content of deleted node by setting empty chunks
644 if chunks and not self.show_full_diff and op == OPS.DEL:
577 if chunks and not self.show_full_diff and op == OPS.DEL:
645 # if not full diff mode show deleted file contents
578 # if not full diff mode show deleted file contents
646 # TODO: anderson: if the view is not too big, there is no way
579 # TODO: anderson: if the view is not too big, there is no way
647 # to see the content of the file
580 # to see the content of the file
648 chunks = []
581 chunks = []
649
582
650 chunks.insert(
583 chunks.insert(
651 0, [{'old_lineno': '',
584 0, [{'old_lineno': '',
652 'new_lineno': '',
585 'new_lineno': '',
653 'action': Action.CONTEXT,
586 'action': Action.CONTEXT,
654 'line': msg,
587 'line': msg,
655 } for _op, msg in stats['ops'].iteritems()
588 } for _op, msg in stats['ops'].iteritems()
656 if _op not in [MOD_FILENODE]])
589 if _op not in [MOD_FILENODE]])
657
590
658 original_filename = safe_unicode(head['a_path'])
591 original_filename = safe_unicode(head['a_path'])
659 _files.append({
592 _files.append({
660 'original_filename': original_filename,
593 'original_filename': original_filename,
661 'filename': safe_unicode(head['b_path']),
594 'filename': safe_unicode(head['b_path']),
662 'old_revision': head['a_blob_id'],
595 'old_revision': head['a_blob_id'],
663 'new_revision': head['b_blob_id'],
596 'new_revision': head['b_blob_id'],
664 'chunks': chunks,
597 'chunks': chunks,
665 'raw_diff': safe_unicode(raw_diff),
598 'raw_diff': safe_unicode(raw_diff),
666 'operation': op,
599 'operation': op,
667 'stats': stats,
600 'stats': stats,
668 'exceeds_limit': exceeds_limit,
601 'exceeds_limit': exceeds_limit,
669 'is_limited_diff': limited_diff,
602 'is_limited_diff': limited_diff,
670 })
603 })
671
604
672 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
605 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
673 OPS.DEL: 2}.get(info['operation'])
606 OPS.DEL: 2}.get(info['operation'])
674
607
675 return diff_container(sorted(_files, key=sorter))
608 return diff_container(sorted(_files, key=sorter))
676
609
677 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
610 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
678 def _parse_lines(self, diff):
611 def _parse_lines(self, diff):
679 """
612 """
680 Parse the diff an return data for the template.
613 Parse the diff an return data for the template.
681 """
614 """
682
615
683 lineiter = iter(diff)
616 lineiter = iter(diff)
684 stats = [0, 0]
617 stats = [0, 0]
685 chunks = []
618 chunks = []
686 raw_diff = []
619 raw_diff = []
687
620
688 try:
621 try:
689 line = lineiter.next()
622 line = lineiter.next()
690
623
691 while line:
624 while line:
692 raw_diff.append(line)
625 raw_diff.append(line)
693 lines = []
626 lines = []
694 chunks.append(lines)
627 chunks.append(lines)
695
628
696 match = self._chunk_re.match(line)
629 match = self._chunk_re.match(line)
697
630
698 if not match:
631 if not match:
699 break
632 break
700
633
701 gr = match.groups()
634 gr = match.groups()
702 (old_line, old_end,
635 (old_line, old_end,
703 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
636 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
704 old_line -= 1
637 old_line -= 1
705 new_line -= 1
638 new_line -= 1
706
639
707 context = len(gr) == 5
640 context = len(gr) == 5
708 old_end += old_line
641 old_end += old_line
709 new_end += new_line
642 new_end += new_line
710
643
711 if context:
644 if context:
712 # skip context only if it's first line
645 # skip context only if it's first line
713 if int(gr[0]) > 1:
646 if int(gr[0]) > 1:
714 lines.append({
647 lines.append({
715 'old_lineno': '...',
648 'old_lineno': '...',
716 'new_lineno': '...',
649 'new_lineno': '...',
717 'action': Action.CONTEXT,
650 'action': Action.CONTEXT,
718 'line': line,
651 'line': line,
719 })
652 })
720
653
721 line = lineiter.next()
654 line = lineiter.next()
722
655
723 while old_line < old_end or new_line < new_end:
656 while old_line < old_end or new_line < new_end:
724 command = ' '
657 command = ' '
725 if line:
658 if line:
726 command = line[0]
659 command = line[0]
727
660
728 affects_old = affects_new = False
661 affects_old = affects_new = False
729
662
730 # ignore those if we don't expect them
663 # ignore those if we don't expect them
731 if command in '#@':
664 if command in '#@':
732 continue
665 continue
733 elif command == '+':
666 elif command == '+':
734 affects_new = True
667 affects_new = True
735 action = Action.ADD
668 action = Action.ADD
736 stats[0] += 1
669 stats[0] += 1
737 elif command == '-':
670 elif command == '-':
738 affects_old = True
671 affects_old = True
739 action = Action.DELETE
672 action = Action.DELETE
740 stats[1] += 1
673 stats[1] += 1
741 else:
674 else:
742 affects_old = affects_new = True
675 affects_old = affects_new = True
743 action = Action.UNMODIFIED
676 action = Action.UNMODIFIED
744
677
745 if not self._newline_marker.match(line):
678 if not self._newline_marker.match(line):
746 old_line += affects_old
679 old_line += affects_old
747 new_line += affects_new
680 new_line += affects_new
748 lines.append({
681 lines.append({
749 'old_lineno': affects_old and old_line or '',
682 'old_lineno': affects_old and old_line or '',
750 'new_lineno': affects_new and new_line or '',
683 'new_lineno': affects_new and new_line or '',
751 'action': action,
684 'action': action,
752 'line': self._clean_line(line, command)
685 'line': self._clean_line(line, command)
753 })
686 })
754 raw_diff.append(line)
687 raw_diff.append(line)
755
688
756 line = lineiter.next()
689 line = lineiter.next()
757
690
758 if self._newline_marker.match(line):
691 if self._newline_marker.match(line):
759 # we need to append to lines, since this is not
692 # we need to append to lines, since this is not
760 # counted in the line specs of diff
693 # counted in the line specs of diff
761 lines.append({
694 lines.append({
762 'old_lineno': '...',
695 'old_lineno': '...',
763 'new_lineno': '...',
696 'new_lineno': '...',
764 'action': Action.CONTEXT,
697 'action': Action.CONTEXT,
765 'line': self._clean_line(line, command)
698 'line': self._clean_line(line, command)
766 })
699 })
767
700
768 except StopIteration:
701 except StopIteration:
769 pass
702 pass
770 return ''.join(raw_diff), chunks, stats
703 return ''.join(raw_diff), chunks, stats
771
704
772 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
705 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
773 def _new_parse_lines(self, diff_iter):
706 def _new_parse_lines(self, diff_iter):
774 """
707 """
775 Parse the diff an return data for the template.
708 Parse the diff an return data for the template.
776 """
709 """
777
710
778 stats = [0, 0]
711 stats = [0, 0]
779 chunks = []
712 chunks = []
780 raw_diff = []
713 raw_diff = []
781
714
782 diff_iter = imap(lambda s: safe_unicode(s), diff_iter)
715 diff_iter = imap(lambda s: safe_unicode(s), diff_iter)
783
716
784 try:
717 try:
785 line = diff_iter.next()
718 line = diff_iter.next()
786
719
787 while line:
720 while line:
788 raw_diff.append(line)
721 raw_diff.append(line)
789 match = self._chunk_re.match(line)
722 match = self._chunk_re.match(line)
790
723
791 if not match:
724 if not match:
792 break
725 break
793
726
794 gr = match.groups()
727 gr = match.groups()
795 (old_line, old_end,
728 (old_line, old_end,
796 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
729 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
797
730
798 lines = []
731 lines = []
799 hunk = {
732 hunk = {
800 'section_header': gr[-1],
733 'section_header': gr[-1],
801 'source_start': old_line,
734 'source_start': old_line,
802 'source_length': old_end,
735 'source_length': old_end,
803 'target_start': new_line,
736 'target_start': new_line,
804 'target_length': new_end,
737 'target_length': new_end,
805 'lines': lines,
738 'lines': lines,
806 }
739 }
807 chunks.append(hunk)
740 chunks.append(hunk)
808
741
809 old_line -= 1
742 old_line -= 1
810 new_line -= 1
743 new_line -= 1
811
744
812 context = len(gr) == 5
745 context = len(gr) == 5
813 old_end += old_line
746 old_end += old_line
814 new_end += new_line
747 new_end += new_line
815
748
816 line = diff_iter.next()
749 line = diff_iter.next()
817
750
818 while old_line < old_end or new_line < new_end:
751 while old_line < old_end or new_line < new_end:
819 command = ' '
752 command = ' '
820 if line:
753 if line:
821 command = line[0]
754 command = line[0]
822
755
823 affects_old = affects_new = False
756 affects_old = affects_new = False
824
757
825 # ignore those if we don't expect them
758 # ignore those if we don't expect them
826 if command in '#@':
759 if command in '#@':
827 continue
760 continue
828 elif command == '+':
761 elif command == '+':
829 affects_new = True
762 affects_new = True
830 action = Action.ADD
763 action = Action.ADD
831 stats[0] += 1
764 stats[0] += 1
832 elif command == '-':
765 elif command == '-':
833 affects_old = True
766 affects_old = True
834 action = Action.DELETE
767 action = Action.DELETE
835 stats[1] += 1
768 stats[1] += 1
836 else:
769 else:
837 affects_old = affects_new = True
770 affects_old = affects_new = True
838 action = Action.UNMODIFIED
771 action = Action.UNMODIFIED
839
772
840 if not self._newline_marker.match(line):
773 if not self._newline_marker.match(line):
841 old_line += affects_old
774 old_line += affects_old
842 new_line += affects_new
775 new_line += affects_new
843 lines.append({
776 lines.append({
844 'old_lineno': affects_old and old_line or '',
777 'old_lineno': affects_old and old_line or '',
845 'new_lineno': affects_new and new_line or '',
778 'new_lineno': affects_new and new_line or '',
846 'action': action,
779 'action': action,
847 'line': self._clean_line(line, command)
780 'line': self._clean_line(line, command)
848 })
781 })
849 raw_diff.append(line)
782 raw_diff.append(line)
850
783
851 line = diff_iter.next()
784 line = diff_iter.next()
852
785
853 if self._newline_marker.match(line):
786 if self._newline_marker.match(line):
854 # we need to append to lines, since this is not
787 # we need to append to lines, since this is not
855 # counted in the line specs of diff
788 # counted in the line specs of diff
856 if affects_old:
789 if affects_old:
857 action = Action.OLD_NO_NL
790 action = Action.OLD_NO_NL
858 elif affects_new:
791 elif affects_new:
859 action = Action.NEW_NO_NL
792 action = Action.NEW_NO_NL
860 else:
793 else:
861 raise Exception('invalid context for no newline')
794 raise Exception('invalid context for no newline')
862
795
863 lines.append({
796 lines.append({
864 'old_lineno': None,
797 'old_lineno': None,
865 'new_lineno': None,
798 'new_lineno': None,
866 'action': action,
799 'action': action,
867 'line': self._clean_line(line, command)
800 'line': self._clean_line(line, command)
868 })
801 })
869
802
870 except StopIteration:
803 except StopIteration:
871 pass
804 pass
872
805
873 return ''.join(raw_diff), chunks, stats
806 return ''.join(raw_diff), chunks, stats
874
807
875 def _safe_id(self, idstring):
808 def _safe_id(self, idstring):
876 """Make a string safe for including in an id attribute.
809 """Make a string safe for including in an id attribute.
877
810
878 The HTML spec says that id attributes 'must begin with
811 The HTML spec says that id attributes 'must begin with
879 a letter ([A-Za-z]) and may be followed by any number
812 a letter ([A-Za-z]) and may be followed by any number
880 of letters, digits ([0-9]), hyphens ("-"), underscores
813 of letters, digits ([0-9]), hyphens ("-"), underscores
881 ("_"), colons (":"), and periods (".")'. These regexps
814 ("_"), colons (":"), and periods (".")'. These regexps
882 are slightly over-zealous, in that they remove colons
815 are slightly over-zealous, in that they remove colons
883 and periods unnecessarily.
816 and periods unnecessarily.
884
817
885 Whitespace is transformed into underscores, and then
818 Whitespace is transformed into underscores, and then
886 anything which is not a hyphen or a character that
819 anything which is not a hyphen or a character that
887 matches \w (alphanumerics and underscore) is removed.
820 matches \w (alphanumerics and underscore) is removed.
888
821
889 """
822 """
890 # Transform all whitespace to underscore
823 # Transform all whitespace to underscore
891 idstring = re.sub(r'\s', "_", '%s' % idstring)
824 idstring = re.sub(r'\s', "_", '%s' % idstring)
892 # Remove everything that is not a hyphen or a member of \w
825 # Remove everything that is not a hyphen or a member of \w
893 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
826 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
894 return idstring
827 return idstring
895
828
896 def prepare(self, inline_diff=True):
829 def prepare(self, inline_diff=True):
897 """
830 """
898 Prepare the passed udiff for HTML rendering.
831 Prepare the passed udiff for HTML rendering.
899
832
900 :return: A list of dicts with diff information.
833 :return: A list of dicts with diff information.
901 """
834 """
902 parsed = self._parser(inline_diff=inline_diff)
835 parsed = self._parser(inline_diff=inline_diff)
903 self.parsed = True
836 self.parsed = True
904 self.parsed_diff = parsed
837 self.parsed_diff = parsed
905 return parsed
838 return parsed
906
839
907 def as_raw(self, diff_lines=None):
840 def as_raw(self, diff_lines=None):
908 """
841 """
909 Returns raw diff as a byte string
842 Returns raw diff as a byte string
910 """
843 """
911 return self._diff.raw
844 return self._diff.raw
912
845
913 def as_html(self, table_class='code-difftable', line_class='line',
846 def as_html(self, table_class='code-difftable', line_class='line',
914 old_lineno_class='lineno old', new_lineno_class='lineno new',
847 old_lineno_class='lineno old', new_lineno_class='lineno new',
915 code_class='code', enable_comments=False, parsed_lines=None):
848 code_class='code', enable_comments=False, parsed_lines=None):
916 """
849 """
917 Return given diff as html table with customized css classes
850 Return given diff as html table with customized css classes
918 """
851 """
852 # TODO(marcink): not sure how to pass in translator
853 # here in an efficient way, leave the _ for proper gettext extraction
854 _ = lambda s: s
855
919 def _link_to_if(condition, label, url):
856 def _link_to_if(condition, label, url):
920 """
857 """
921 Generates a link if condition is meet or just the label if not.
858 Generates a link if condition is meet or just the label if not.
922 """
859 """
923
860
924 if condition:
861 if condition:
925 return '''<a href="%(url)s" class="tooltip"
862 return '''<a href="%(url)s" class="tooltip"
926 title="%(title)s">%(label)s</a>''' % {
863 title="%(title)s">%(label)s</a>''' % {
927 'title': _('Click to select line'),
864 'title': _('Click to select line'),
928 'url': url,
865 'url': url,
929 'label': label
866 'label': label
930 }
867 }
931 else:
868 else:
932 return label
869 return label
933 if not self.parsed:
870 if not self.parsed:
934 self.prepare()
871 self.prepare()
935
872
936 diff_lines = self.parsed_diff
873 diff_lines = self.parsed_diff
937 if parsed_lines:
874 if parsed_lines:
938 diff_lines = parsed_lines
875 diff_lines = parsed_lines
939
876
940 _html_empty = True
877 _html_empty = True
941 _html = []
878 _html = []
942 _html.append('''<table class="%(table_class)s">\n''' % {
879 _html.append('''<table class="%(table_class)s">\n''' % {
943 'table_class': table_class
880 'table_class': table_class
944 })
881 })
945
882
946 for diff in diff_lines:
883 for diff in diff_lines:
947 for line in diff['chunks']:
884 for line in diff['chunks']:
948 _html_empty = False
885 _html_empty = False
949 for change in line:
886 for change in line:
950 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
887 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
951 'lc': line_class,
888 'lc': line_class,
952 'action': change['action']
889 'action': change['action']
953 })
890 })
954 anchor_old_id = ''
891 anchor_old_id = ''
955 anchor_new_id = ''
892 anchor_new_id = ''
956 anchor_old = "%(filename)s_o%(oldline_no)s" % {
893 anchor_old = "%(filename)s_o%(oldline_no)s" % {
957 'filename': self._safe_id(diff['filename']),
894 'filename': self._safe_id(diff['filename']),
958 'oldline_no': change['old_lineno']
895 'oldline_no': change['old_lineno']
959 }
896 }
960 anchor_new = "%(filename)s_n%(oldline_no)s" % {
897 anchor_new = "%(filename)s_n%(oldline_no)s" % {
961 'filename': self._safe_id(diff['filename']),
898 'filename': self._safe_id(diff['filename']),
962 'oldline_no': change['new_lineno']
899 'oldline_no': change['new_lineno']
963 }
900 }
964 cond_old = (change['old_lineno'] != '...' and
901 cond_old = (change['old_lineno'] != '...' and
965 change['old_lineno'])
902 change['old_lineno'])
966 cond_new = (change['new_lineno'] != '...' and
903 cond_new = (change['new_lineno'] != '...' and
967 change['new_lineno'])
904 change['new_lineno'])
968 if cond_old:
905 if cond_old:
969 anchor_old_id = 'id="%s"' % anchor_old
906 anchor_old_id = 'id="%s"' % anchor_old
970 if cond_new:
907 if cond_new:
971 anchor_new_id = 'id="%s"' % anchor_new
908 anchor_new_id = 'id="%s"' % anchor_new
972
909
973 if change['action'] != Action.CONTEXT:
910 if change['action'] != Action.CONTEXT:
974 anchor_link = True
911 anchor_link = True
975 else:
912 else:
976 anchor_link = False
913 anchor_link = False
977
914
978 ###########################################################
915 ###########################################################
979 # COMMENT ICONS
916 # COMMENT ICONS
980 ###########################################################
917 ###########################################################
981 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
918 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
982
919
983 if enable_comments and change['action'] != Action.CONTEXT:
920 if enable_comments and change['action'] != Action.CONTEXT:
984 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
921 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
985
922
986 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
923 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
987
924
988 ###########################################################
925 ###########################################################
989 # OLD LINE NUMBER
926 # OLD LINE NUMBER
990 ###########################################################
927 ###########################################################
991 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
928 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
992 'a_id': anchor_old_id,
929 'a_id': anchor_old_id,
993 'olc': old_lineno_class
930 'olc': old_lineno_class
994 })
931 })
995
932
996 _html.append('''%(link)s''' % {
933 _html.append('''%(link)s''' % {
997 'link': _link_to_if(anchor_link, change['old_lineno'],
934 'link': _link_to_if(anchor_link, change['old_lineno'],
998 '#%s' % anchor_old)
935 '#%s' % anchor_old)
999 })
936 })
1000 _html.append('''</td>\n''')
937 _html.append('''</td>\n''')
1001 ###########################################################
938 ###########################################################
1002 # NEW LINE NUMBER
939 # NEW LINE NUMBER
1003 ###########################################################
940 ###########################################################
1004
941
1005 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
942 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
1006 'a_id': anchor_new_id,
943 'a_id': anchor_new_id,
1007 'nlc': new_lineno_class
944 'nlc': new_lineno_class
1008 })
945 })
1009
946
1010 _html.append('''%(link)s''' % {
947 _html.append('''%(link)s''' % {
1011 'link': _link_to_if(anchor_link, change['new_lineno'],
948 'link': _link_to_if(anchor_link, change['new_lineno'],
1012 '#%s' % anchor_new)
949 '#%s' % anchor_new)
1013 })
950 })
1014 _html.append('''</td>\n''')
951 _html.append('''</td>\n''')
1015 ###########################################################
952 ###########################################################
1016 # CODE
953 # CODE
1017 ###########################################################
954 ###########################################################
1018 code_classes = [code_class]
955 code_classes = [code_class]
1019 if (not enable_comments or
956 if (not enable_comments or
1020 change['action'] == Action.CONTEXT):
957 change['action'] == Action.CONTEXT):
1021 code_classes.append('no-comment')
958 code_classes.append('no-comment')
1022 _html.append('\t<td class="%s">' % ' '.join(code_classes))
959 _html.append('\t<td class="%s">' % ' '.join(code_classes))
1023 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
960 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
1024 'code': change['line']
961 'code': change['line']
1025 })
962 })
1026
963
1027 _html.append('''\t</td>''')
964 _html.append('''\t</td>''')
1028 _html.append('''\n</tr>\n''')
965 _html.append('''\n</tr>\n''')
1029 _html.append('''</table>''')
966 _html.append('''</table>''')
1030 if _html_empty:
967 if _html_empty:
1031 return None
968 return None
1032 return ''.join(_html)
969 return ''.join(_html)
1033
970
1034 def stat(self):
971 def stat(self):
1035 """
972 """
1036 Returns tuple of added, and removed lines for this instance
973 Returns tuple of added, and removed lines for this instance
1037 """
974 """
1038 return self.adds, self.removes
975 return self.adds, self.removes
1039
976
1040 def get_context_of_line(
977 def get_context_of_line(
1041 self, path, diff_line=None, context_before=3, context_after=3):
978 self, path, diff_line=None, context_before=3, context_after=3):
1042 """
979 """
1043 Returns the context lines for the specified diff line.
980 Returns the context lines for the specified diff line.
1044
981
1045 :type diff_line: :class:`DiffLineNumber`
982 :type diff_line: :class:`DiffLineNumber`
1046 """
983 """
1047 assert self.parsed, "DiffProcessor is not initialized."
984 assert self.parsed, "DiffProcessor is not initialized."
1048
985
1049 if None not in diff_line:
986 if None not in diff_line:
1050 raise ValueError(
987 raise ValueError(
1051 "Cannot specify both line numbers: {}".format(diff_line))
988 "Cannot specify both line numbers: {}".format(diff_line))
1052
989
1053 file_diff = self._get_file_diff(path)
990 file_diff = self._get_file_diff(path)
1054 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
991 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1055
992
1056 first_line_to_include = max(idx - context_before, 0)
993 first_line_to_include = max(idx - context_before, 0)
1057 first_line_after_context = idx + context_after + 1
994 first_line_after_context = idx + context_after + 1
1058 context_lines = chunk[first_line_to_include:first_line_after_context]
995 context_lines = chunk[first_line_to_include:first_line_after_context]
1059
996
1060 line_contents = [
997 line_contents = [
1061 _context_line(line) for line in context_lines
998 _context_line(line) for line in context_lines
1062 if _is_diff_content(line)]
999 if _is_diff_content(line)]
1063 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1000 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1064 # Once they are fixed, we can drop this line here.
1001 # Once they are fixed, we can drop this line here.
1065 if line_contents:
1002 if line_contents:
1066 line_contents[-1] = (
1003 line_contents[-1] = (
1067 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1004 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1068 return line_contents
1005 return line_contents
1069
1006
1070 def find_context(self, path, context, offset=0):
1007 def find_context(self, path, context, offset=0):
1071 """
1008 """
1072 Finds the given `context` inside of the diff.
1009 Finds the given `context` inside of the diff.
1073
1010
1074 Use the parameter `offset` to specify which offset the target line has
1011 Use the parameter `offset` to specify which offset the target line has
1075 inside of the given `context`. This way the correct diff line will be
1012 inside of the given `context`. This way the correct diff line will be
1076 returned.
1013 returned.
1077
1014
1078 :param offset: Shall be used to specify the offset of the main line
1015 :param offset: Shall be used to specify the offset of the main line
1079 within the given `context`.
1016 within the given `context`.
1080 """
1017 """
1081 if offset < 0 or offset >= len(context):
1018 if offset < 0 or offset >= len(context):
1082 raise ValueError(
1019 raise ValueError(
1083 "Only positive values up to the length of the context "
1020 "Only positive values up to the length of the context "
1084 "minus one are allowed.")
1021 "minus one are allowed.")
1085
1022
1086 matches = []
1023 matches = []
1087 file_diff = self._get_file_diff(path)
1024 file_diff = self._get_file_diff(path)
1088
1025
1089 for chunk in file_diff['chunks']:
1026 for chunk in file_diff['chunks']:
1090 context_iter = iter(context)
1027 context_iter = iter(context)
1091 for line_idx, line in enumerate(chunk):
1028 for line_idx, line in enumerate(chunk):
1092 try:
1029 try:
1093 if _context_line(line) == context_iter.next():
1030 if _context_line(line) == context_iter.next():
1094 continue
1031 continue
1095 except StopIteration:
1032 except StopIteration:
1096 matches.append((line_idx, chunk))
1033 matches.append((line_idx, chunk))
1097 context_iter = iter(context)
1034 context_iter = iter(context)
1098
1035
1099 # Increment position and triger StopIteration
1036 # Increment position and triger StopIteration
1100 # if we had a match at the end
1037 # if we had a match at the end
1101 line_idx += 1
1038 line_idx += 1
1102 try:
1039 try:
1103 context_iter.next()
1040 context_iter.next()
1104 except StopIteration:
1041 except StopIteration:
1105 matches.append((line_idx, chunk))
1042 matches.append((line_idx, chunk))
1106
1043
1107 effective_offset = len(context) - offset
1044 effective_offset = len(context) - offset
1108 found_at_diff_lines = [
1045 found_at_diff_lines = [
1109 _line_to_diff_line_number(chunk[idx - effective_offset])
1046 _line_to_diff_line_number(chunk[idx - effective_offset])
1110 for idx, chunk in matches]
1047 for idx, chunk in matches]
1111
1048
1112 return found_at_diff_lines
1049 return found_at_diff_lines
1113
1050
1114 def _get_file_diff(self, path):
1051 def _get_file_diff(self, path):
1115 for file_diff in self.parsed_diff:
1052 for file_diff in self.parsed_diff:
1116 if file_diff['filename'] == path:
1053 if file_diff['filename'] == path:
1117 break
1054 break
1118 else:
1055 else:
1119 raise FileNotInDiffException("File {} not in diff".format(path))
1056 raise FileNotInDiffException("File {} not in diff".format(path))
1120 return file_diff
1057 return file_diff
1121
1058
1122 def _find_chunk_line_index(self, file_diff, diff_line):
1059 def _find_chunk_line_index(self, file_diff, diff_line):
1123 for chunk in file_diff['chunks']:
1060 for chunk in file_diff['chunks']:
1124 for idx, line in enumerate(chunk):
1061 for idx, line in enumerate(chunk):
1125 if line['old_lineno'] == diff_line.old:
1062 if line['old_lineno'] == diff_line.old:
1126 return chunk, idx
1063 return chunk, idx
1127 if line['new_lineno'] == diff_line.new:
1064 if line['new_lineno'] == diff_line.new:
1128 return chunk, idx
1065 return chunk, idx
1129 raise LineNotInDiffException(
1066 raise LineNotInDiffException(
1130 "The line {} is not part of the diff.".format(diff_line))
1067 "The line {} is not part of the diff.".format(diff_line))
1131
1068
1132
1069
1133 def _is_diff_content(line):
1070 def _is_diff_content(line):
1134 return line['action'] in (
1071 return line['action'] in (
1135 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1072 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1136
1073
1137
1074
1138 def _context_line(line):
1075 def _context_line(line):
1139 return (line['action'], line['line'])
1076 return (line['action'], line['line'])
1140
1077
1141
1078
1142 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1079 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1143
1080
1144
1081
1145 def _line_to_diff_line_number(line):
1082 def _line_to_diff_line_number(line):
1146 new_line_no = line['new_lineno'] or None
1083 new_line_no = line['new_lineno'] or None
1147 old_line_no = line['old_lineno'] or None
1084 old_line_no = line['old_lineno'] or None
1148 return DiffLineNumber(old=old_line_no, new=new_line_no)
1085 return DiffLineNumber(old=old_line_no, new=new_line_no)
1149
1086
1150
1087
1151 class FileNotInDiffException(Exception):
1088 class FileNotInDiffException(Exception):
1152 """
1089 """
1153 Raised when the context for a missing file is requested.
1090 Raised when the context for a missing file is requested.
1154
1091
1155 If you request the context for a line in a file which is not part of the
1092 If you request the context for a line in a file which is not part of the
1156 given diff, then this exception is raised.
1093 given diff, then this exception is raised.
1157 """
1094 """
1158
1095
1159
1096
1160 class LineNotInDiffException(Exception):
1097 class LineNotInDiffException(Exception):
1161 """
1098 """
1162 Raised when the context for a missing line is requested.
1099 Raised when the context for a missing line is requested.
1163
1100
1164 If you request the context for a line in a file and this line is not
1101 If you request the context for a line in a file and this line is not
1165 part of the given diff, then this exception is raised.
1102 part of the given diff, then this exception is raised.
1166 """
1103 """
1167
1104
1168
1105
1169 class DiffLimitExceeded(Exception):
1106 class DiffLimitExceeded(Exception):
1170 pass
1107 pass
@@ -1,64 +1,64 b''
1 import datetime
1 import datetime
2 import decimal
2 import decimal
3 import functools
3 import functools
4
4
5 import simplejson as json
5 import simplejson as json
6
6
7 from rhodecode.lib.datelib import is_aware
7 from rhodecode.lib.datelib import is_aware
8
8
9 try:
9 try:
10 import rhodecode.translation
10 import rhodecode.translation
11 except ImportError:
11 except ImportError:
12 rhodecode = None
12 rhodecode = None
13
13
14 __all__ = ['json']
14 __all__ = ['json']
15
15
16
16
17 def _obj_dump(obj):
17 def _obj_dump(obj):
18 """
18 """
19 Custom function for dumping objects to JSON, if obj has __json__ attribute
19 Custom function for dumping objects to JSON, if obj has __json__ attribute
20 or method defined it will be used for serialization
20 or method defined it will be used for serialization
21
21
22 :param obj:
22 :param obj:
23 """
23 """
24
24
25 # See "Date Time String Format" in the ECMA-262 specification.
25 # See "Date Time String Format" in the ECMA-262 specification.
26 # some code borrowed from django 1.4
26 # some code borrowed from django 1.4
27 if isinstance(obj, set):
27 if isinstance(obj, set):
28 return list(obj)
28 return list(obj)
29 elif isinstance(obj, datetime.datetime):
29 elif isinstance(obj, datetime.datetime):
30 r = obj.isoformat()
30 r = obj.isoformat()
31 if isinstance(obj.microsecond, (int, long)):
31 if isinstance(obj.microsecond, (int, long)):
32 r = r[:23] + r[26:]
32 r = r[:23] + r[26:]
33 if r.endswith('+00:00'):
33 if r.endswith('+00:00'):
34 r = r[:-6] + 'Z'
34 r = r[:-6] + 'Z'
35 return r
35 return r
36 elif isinstance(obj, datetime.date):
36 elif isinstance(obj, datetime.date):
37 return obj.isoformat()
37 return obj.isoformat()
38 elif isinstance(obj, datetime.time):
38 elif isinstance(obj, datetime.time):
39 if is_aware(obj):
39 if is_aware(obj):
40 raise TypeError("Time-zone aware times are not JSON serializable")
40 raise TypeError("Time-zone aware times are not JSON serializable")
41 r = obj.isoformat()
41 r = obj.isoformat()
42 if isinstance(obj.microsecond, (int, long)):
42 if isinstance(obj.microsecond, (int, long)):
43 r = r[:12]
43 r = r[:12]
44 return r
44 return r
45 elif hasattr(obj, '__json__'):
45 elif hasattr(obj, '__json__'):
46 if callable(obj.__json__):
46 if callable(obj.__json__):
47 return obj.__json__()
47 return obj.__json__()
48 else:
48 else:
49 return obj.__json__
49 return obj.__json__
50 elif isinstance(obj, decimal.Decimal):
50 elif isinstance(obj, decimal.Decimal):
51 return str(obj)
51 return str(obj)
52 elif isinstance(obj, complex):
52 elif isinstance(obj, complex):
53 return [obj.real, obj.imag]
53 return [obj.real, obj.imag]
54 elif rhodecode and isinstance(obj, rhodecode.translation.LazyString):
54 elif rhodecode and isinstance(obj, rhodecode.translation._LazyString):
55 return obj.eval()
55 return obj.eval()
56 else:
56 else:
57 raise TypeError(repr(obj) + " is not JSON serializable")
57 raise TypeError(repr(obj) + " is not JSON serializable")
58
58
59
59
60 json.dumps = functools.partial(json.dumps, default=_obj_dump, use_decimal=False)
60 json.dumps = functools.partial(json.dumps, default=_obj_dump, use_decimal=False)
61 json.dump = functools.partial(json.dump, default=_obj_dump, use_decimal=False)
61 json.dump = functools.partial(json.dump, default=_obj_dump, use_decimal=False)
62
62
63 # alias for formatted json
63 # alias for formatted json
64 formatted_json = functools.partial(json.dumps, indent=4, sort_keys=True)
64 formatted_json = functools.partial(json.dumps, indent=4, sort_keys=True)
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
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
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