##// END OF EJS Templates
py3: remove use of pyramid.compat
super-admin -
r4908:04e2d7da default
parent child Browse files
Show More

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

@@ -1,421 +1,419 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import itertools
22 import itertools
23 import base64
23 import base64
24
24
25 from pyramid import compat
26
27 from rhodecode.api import (
25 from rhodecode.api import (
28 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
29
27
30 from rhodecode.api.utils import (
28 from rhodecode.api.utils import (
31 Optional, OAttr, has_superadmin_permission, get_user_or_error)
29 Optional, OAttr, has_superadmin_permission, get_user_or_error)
32 from rhodecode.lib.utils import repo2db_mapper
30 from rhodecode.lib.utils import repo2db_mapper
33 from rhodecode.lib import system_info
31 from rhodecode.lib import system_info
34 from rhodecode.lib import user_sessions
32 from rhodecode.lib import user_sessions
35 from rhodecode.lib import exc_tracking
33 from rhodecode.lib import exc_tracking
36 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.utils2 import safe_int
38 from rhodecode.model.db import UserIpMap
36 from rhodecode.model.db import UserIpMap
39 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.scm import ScmModel
40 from rhodecode.model.settings import VcsSettingsModel
38 from rhodecode.model.settings import VcsSettingsModel
41 from rhodecode.apps.file_store import utils
39 from rhodecode.apps.file_store import utils
42 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
40 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
43 FileOverSizeException
41 FileOverSizeException
44
42
45 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
46
44
47
45
48 @jsonrpc_method()
46 @jsonrpc_method()
49 def get_server_info(request, apiuser):
47 def get_server_info(request, apiuser):
50 """
48 """
51 Returns the |RCE| server information.
49 Returns the |RCE| server information.
52
50
53 This includes the running version of |RCE| and all installed
51 This includes the running version of |RCE| and all installed
54 packages. This command takes the following options:
52 packages. This command takes the following options:
55
53
56 :param apiuser: This is filled automatically from the |authtoken|.
54 :param apiuser: This is filled automatically from the |authtoken|.
57 :type apiuser: AuthUser
55 :type apiuser: AuthUser
58
56
59 Example output:
57 Example output:
60
58
61 .. code-block:: bash
59 .. code-block:: bash
62
60
63 id : <id_given_in_input>
61 id : <id_given_in_input>
64 result : {
62 result : {
65 'modules': [<module name>,...]
63 'modules': [<module name>,...]
66 'py_version': <python version>,
64 'py_version': <python version>,
67 'platform': <platform type>,
65 'platform': <platform type>,
68 'rhodecode_version': <rhodecode version>
66 'rhodecode_version': <rhodecode version>
69 }
67 }
70 error : null
68 error : null
71 """
69 """
72
70
73 if not has_superadmin_permission(apiuser):
71 if not has_superadmin_permission(apiuser):
74 raise JSONRPCForbidden()
72 raise JSONRPCForbidden()
75
73
76 server_info = ScmModel().get_server_info(request.environ)
74 server_info = ScmModel().get_server_info(request.environ)
77 # rhodecode-index requires those
75 # rhodecode-index requires those
78
76
79 server_info['index_storage'] = server_info['search']['value']['location']
77 server_info['index_storage'] = server_info['search']['value']['location']
80 server_info['storage'] = server_info['storage']['value']['path']
78 server_info['storage'] = server_info['storage']['value']['path']
81
79
82 return server_info
80 return server_info
83
81
84
82
85 @jsonrpc_method()
83 @jsonrpc_method()
86 def get_repo_store(request, apiuser):
84 def get_repo_store(request, apiuser):
87 """
85 """
88 Returns the |RCE| repository storage information.
86 Returns the |RCE| repository storage information.
89
87
90 :param apiuser: This is filled automatically from the |authtoken|.
88 :param apiuser: This is filled automatically from the |authtoken|.
91 :type apiuser: AuthUser
89 :type apiuser: AuthUser
92
90
93 Example output:
91 Example output:
94
92
95 .. code-block:: bash
93 .. code-block:: bash
96
94
97 id : <id_given_in_input>
95 id : <id_given_in_input>
98 result : {
96 result : {
99 'modules': [<module name>,...]
97 'modules': [<module name>,...]
100 'py_version': <python version>,
98 'py_version': <python version>,
101 'platform': <platform type>,
99 'platform': <platform type>,
102 'rhodecode_version': <rhodecode version>
100 'rhodecode_version': <rhodecode version>
103 }
101 }
104 error : null
102 error : null
105 """
103 """
106
104
107 if not has_superadmin_permission(apiuser):
105 if not has_superadmin_permission(apiuser):
108 raise JSONRPCForbidden()
106 raise JSONRPCForbidden()
109
107
110 path = VcsSettingsModel().get_repos_location()
108 path = VcsSettingsModel().get_repos_location()
111 return {"path": path}
109 return {"path": path}
112
110
113
111
114 @jsonrpc_method()
112 @jsonrpc_method()
115 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
113 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
116 """
114 """
117 Displays the IP Address as seen from the |RCE| server.
115 Displays the IP Address as seen from the |RCE| server.
118
116
119 * This command displays the IP Address, as well as all the defined IP
117 * This command displays the IP Address, as well as all the defined IP
120 addresses for the specified user. If the ``userid`` is not set, the
118 addresses for the specified user. If the ``userid`` is not set, the
121 data returned is for the user calling the method.
119 data returned is for the user calling the method.
122
120
123 This command can only be run using an |authtoken| with admin rights to
121 This command can only be run using an |authtoken| with admin rights to
124 the specified repository.
122 the specified repository.
125
123
126 This command takes the following options:
124 This command takes the following options:
127
125
128 :param apiuser: This is filled automatically from |authtoken|.
126 :param apiuser: This is filled automatically from |authtoken|.
129 :type apiuser: AuthUser
127 :type apiuser: AuthUser
130 :param userid: Sets the userid for which associated IP Address data
128 :param userid: Sets the userid for which associated IP Address data
131 is returned.
129 is returned.
132 :type userid: Optional(str or int)
130 :type userid: Optional(str or int)
133
131
134 Example output:
132 Example output:
135
133
136 .. code-block:: bash
134 .. code-block:: bash
137
135
138 id : <id_given_in_input>
136 id : <id_given_in_input>
139 result : {
137 result : {
140 "server_ip_addr": "<ip_from_clien>",
138 "server_ip_addr": "<ip_from_clien>",
141 "user_ips": [
139 "user_ips": [
142 {
140 {
143 "ip_addr": "<ip_with_mask>",
141 "ip_addr": "<ip_with_mask>",
144 "ip_range": ["<start_ip>", "<end_ip>"],
142 "ip_range": ["<start_ip>", "<end_ip>"],
145 },
143 },
146 ...
144 ...
147 ]
145 ]
148 }
146 }
149
147
150 """
148 """
151 if not has_superadmin_permission(apiuser):
149 if not has_superadmin_permission(apiuser):
152 raise JSONRPCForbidden()
150 raise JSONRPCForbidden()
153
151
154 userid = Optional.extract(userid, evaluate_locals=locals())
152 userid = Optional.extract(userid, evaluate_locals=locals())
155 userid = getattr(userid, 'user_id', userid)
153 userid = getattr(userid, 'user_id', userid)
156
154
157 user = get_user_or_error(userid)
155 user = get_user_or_error(userid)
158 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
156 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
159 return {
157 return {
160 'server_ip_addr': request.rpc_ip_addr,
158 'server_ip_addr': request.rpc_ip_addr,
161 'user_ips': ips
159 'user_ips': ips
162 }
160 }
163
161
164
162
165 @jsonrpc_method()
163 @jsonrpc_method()
166 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
164 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
167 """
165 """
168 Triggers a rescan of the specified repositories.
166 Triggers a rescan of the specified repositories.
169
167
170 * If the ``remove_obsolete`` option is set, it also deletes repositories
168 * If the ``remove_obsolete`` option is set, it also deletes repositories
171 that are found in the database but not on the file system, so called
169 that are found in the database but not on the file system, so called
172 "clean zombies".
170 "clean zombies".
173
171
174 This command can only be run using an |authtoken| with admin rights to
172 This command can only be run using an |authtoken| with admin rights to
175 the specified repository.
173 the specified repository.
176
174
177 This command takes the following options:
175 This command takes the following options:
178
176
179 :param apiuser: This is filled automatically from the |authtoken|.
177 :param apiuser: This is filled automatically from the |authtoken|.
180 :type apiuser: AuthUser
178 :type apiuser: AuthUser
181 :param remove_obsolete: Deletes repositories from the database that
179 :param remove_obsolete: Deletes repositories from the database that
182 are not found on the filesystem.
180 are not found on the filesystem.
183 :type remove_obsolete: Optional(``True`` | ``False``)
181 :type remove_obsolete: Optional(``True`` | ``False``)
184
182
185 Example output:
183 Example output:
186
184
187 .. code-block:: bash
185 .. code-block:: bash
188
186
189 id : <id_given_in_input>
187 id : <id_given_in_input>
190 result : {
188 result : {
191 'added': [<added repository name>,...]
189 'added': [<added repository name>,...]
192 'removed': [<removed repository name>,...]
190 'removed': [<removed repository name>,...]
193 }
191 }
194 error : null
192 error : null
195
193
196 Example error output:
194 Example error output:
197
195
198 .. code-block:: bash
196 .. code-block:: bash
199
197
200 id : <id_given_in_input>
198 id : <id_given_in_input>
201 result : null
199 result : null
202 error : {
200 error : {
203 'Error occurred during rescan repositories action'
201 'Error occurred during rescan repositories action'
204 }
202 }
205
203
206 """
204 """
207 if not has_superadmin_permission(apiuser):
205 if not has_superadmin_permission(apiuser):
208 raise JSONRPCForbidden()
206 raise JSONRPCForbidden()
209
207
210 try:
208 try:
211 rm_obsolete = Optional.extract(remove_obsolete)
209 rm_obsolete = Optional.extract(remove_obsolete)
212 added, removed = repo2db_mapper(ScmModel().repo_scan(),
210 added, removed = repo2db_mapper(ScmModel().repo_scan(),
213 remove_obsolete=rm_obsolete)
211 remove_obsolete=rm_obsolete)
214 return {'added': added, 'removed': removed}
212 return {'added': added, 'removed': removed}
215 except Exception:
213 except Exception:
216 log.exception('Failed to run repo rescann')
214 log.exception('Failed to run repo rescann')
217 raise JSONRPCError(
215 raise JSONRPCError(
218 'Error occurred during rescan repositories action'
216 'Error occurred during rescan repositories action'
219 )
217 )
220
218
221
219
222 @jsonrpc_method()
220 @jsonrpc_method()
223 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
221 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
224 """
222 """
225 Triggers a session cleanup action.
223 Triggers a session cleanup action.
226
224
227 If the ``older_then`` option is set, only sessions that hasn't been
225 If the ``older_then`` option is set, only sessions that hasn't been
228 accessed in the given number of days will be removed.
226 accessed in the given number of days will be removed.
229
227
230 This command can only be run using an |authtoken| with admin rights to
228 This command can only be run using an |authtoken| with admin rights to
231 the specified repository.
229 the specified repository.
232
230
233 This command takes the following options:
231 This command takes the following options:
234
232
235 :param apiuser: This is filled automatically from the |authtoken|.
233 :param apiuser: This is filled automatically from the |authtoken|.
236 :type apiuser: AuthUser
234 :type apiuser: AuthUser
237 :param older_then: Deletes session that hasn't been accessed
235 :param older_then: Deletes session that hasn't been accessed
238 in given number of days.
236 in given number of days.
239 :type older_then: Optional(int)
237 :type older_then: Optional(int)
240
238
241 Example output:
239 Example output:
242
240
243 .. code-block:: bash
241 .. code-block:: bash
244
242
245 id : <id_given_in_input>
243 id : <id_given_in_input>
246 result: {
244 result: {
247 "backend": "<type of backend>",
245 "backend": "<type of backend>",
248 "sessions_removed": <number_of_removed_sessions>
246 "sessions_removed": <number_of_removed_sessions>
249 }
247 }
250 error : null
248 error : null
251
249
252 Example error output:
250 Example error output:
253
251
254 .. code-block:: bash
252 .. code-block:: bash
255
253
256 id : <id_given_in_input>
254 id : <id_given_in_input>
257 result : null
255 result : null
258 error : {
256 error : {
259 'Error occurred during session cleanup'
257 'Error occurred during session cleanup'
260 }
258 }
261
259
262 """
260 """
263 if not has_superadmin_permission(apiuser):
261 if not has_superadmin_permission(apiuser):
264 raise JSONRPCForbidden()
262 raise JSONRPCForbidden()
265
263
266 older_then = safe_int(Optional.extract(older_then)) or 60
264 older_then = safe_int(Optional.extract(older_then)) or 60
267 older_than_seconds = 60 * 60 * 24 * older_then
265 older_than_seconds = 60 * 60 * 24 * older_then
268
266
269 config = system_info.rhodecode_config().get_value()['value']['config']
267 config = system_info.rhodecode_config().get_value()['value']['config']
270 session_model = user_sessions.get_session_handler(
268 session_model = user_sessions.get_session_handler(
271 config.get('beaker.session.type', 'memory'))(config)
269 config.get('beaker.session.type', 'memory'))(config)
272
270
273 backend = session_model.SESSION_TYPE
271 backend = session_model.SESSION_TYPE
274 try:
272 try:
275 cleaned = session_model.clean_sessions(
273 cleaned = session_model.clean_sessions(
276 older_than_seconds=older_than_seconds)
274 older_than_seconds=older_than_seconds)
277 return {'sessions_removed': cleaned, 'backend': backend}
275 return {'sessions_removed': cleaned, 'backend': backend}
278 except user_sessions.CleanupCommand as msg:
276 except user_sessions.CleanupCommand as msg:
279 return {'cleanup_command': msg.message, 'backend': backend}
277 return {'cleanup_command': msg.message, 'backend': backend}
280 except Exception as e:
278 except Exception as e:
281 log.exception('Failed session cleanup')
279 log.exception('Failed session cleanup')
282 raise JSONRPCError(
280 raise JSONRPCError(
283 'Error occurred during session cleanup'
281 'Error occurred during session cleanup'
284 )
282 )
285
283
286
284
287 @jsonrpc_method()
285 @jsonrpc_method()
288 def get_method(request, apiuser, pattern=Optional('*')):
286 def get_method(request, apiuser, pattern=Optional('*')):
289 """
287 """
290 Returns list of all available API methods. By default match pattern
288 Returns list of all available API methods. By default match pattern
291 os "*" but any other pattern can be specified. eg *comment* will return
289 os "*" but any other pattern can be specified. eg *comment* will return
292 all methods with comment inside them. If just single method is matched
290 all methods with comment inside them. If just single method is matched
293 returned data will also include method specification
291 returned data will also include method specification
294
292
295 This command can only be run using an |authtoken| with admin rights to
293 This command can only be run using an |authtoken| with admin rights to
296 the specified repository.
294 the specified repository.
297
295
298 This command takes the following options:
296 This command takes the following options:
299
297
300 :param apiuser: This is filled automatically from the |authtoken|.
298 :param apiuser: This is filled automatically from the |authtoken|.
301 :type apiuser: AuthUser
299 :type apiuser: AuthUser
302 :param pattern: pattern to match method names against
300 :param pattern: pattern to match method names against
303 :type pattern: Optional("*")
301 :type pattern: Optional("*")
304
302
305 Example output:
303 Example output:
306
304
307 .. code-block:: bash
305 .. code-block:: bash
308
306
309 id : <id_given_in_input>
307 id : <id_given_in_input>
310 "result": [
308 "result": [
311 "changeset_comment",
309 "changeset_comment",
312 "comment_pull_request",
310 "comment_pull_request",
313 "comment_commit"
311 "comment_commit"
314 ]
312 ]
315 error : null
313 error : null
316
314
317 .. code-block:: bash
315 .. code-block:: bash
318
316
319 id : <id_given_in_input>
317 id : <id_given_in_input>
320 "result": [
318 "result": [
321 "comment_commit",
319 "comment_commit",
322 {
320 {
323 "apiuser": "<RequiredType>",
321 "apiuser": "<RequiredType>",
324 "comment_type": "<Optional:u'note'>",
322 "comment_type": "<Optional:u'note'>",
325 "commit_id": "<RequiredType>",
323 "commit_id": "<RequiredType>",
326 "message": "<RequiredType>",
324 "message": "<RequiredType>",
327 "repoid": "<RequiredType>",
325 "repoid": "<RequiredType>",
328 "request": "<RequiredType>",
326 "request": "<RequiredType>",
329 "resolves_comment_id": "<Optional:None>",
327 "resolves_comment_id": "<Optional:None>",
330 "status": "<Optional:None>",
328 "status": "<Optional:None>",
331 "userid": "<Optional:<OptionalAttr:apiuser>>"
329 "userid": "<Optional:<OptionalAttr:apiuser>>"
332 }
330 }
333 ]
331 ]
334 error : null
332 error : null
335 """
333 """
336 from rhodecode.config.patches import inspect_getargspec
334 from rhodecode.config.patches import inspect_getargspec
337 inspect = inspect_getargspec()
335 inspect = inspect_getargspec()
338
336
339 if not has_superadmin_permission(apiuser):
337 if not has_superadmin_permission(apiuser):
340 raise JSONRPCForbidden()
338 raise JSONRPCForbidden()
341
339
342 pattern = Optional.extract(pattern)
340 pattern = Optional.extract(pattern)
343
341
344 matches = find_methods(request.registry.jsonrpc_methods, pattern)
342 matches = find_methods(request.registry.jsonrpc_methods, pattern)
345
343
346 args_desc = []
344 args_desc = []
347 if len(matches) == 1:
345 if len(matches) == 1:
348 func = matches[matches.keys()[0]]
346 func = matches[matches.keys()[0]]
349
347
350 argspec = inspect.getargspec(func)
348 argspec = inspect.getargspec(func)
351 arglist = argspec[0]
349 arglist = argspec[0]
352 defaults = map(repr, argspec[3] or [])
350 defaults = map(repr, argspec[3] or [])
353
351
354 default_empty = '<RequiredType>'
352 default_empty = '<RequiredType>'
355
353
356 # kw arguments required by this method
354 # kw arguments required by this method
357 func_kwargs = dict(itertools.izip_longest(
355 func_kwargs = dict(itertools.izip_longest(
358 reversed(arglist), reversed(defaults), fillvalue=default_empty))
356 reversed(arglist), reversed(defaults), fillvalue=default_empty))
359 args_desc.append(func_kwargs)
357 args_desc.append(func_kwargs)
360
358
361 return matches.keys() + args_desc
359 return matches.keys() + args_desc
362
360
363
361
364 @jsonrpc_method()
362 @jsonrpc_method()
365 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
363 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
366 """
364 """
367 Stores sent exception inside the built-in exception tracker in |RCE| server.
365 Stores sent exception inside the built-in exception tracker in |RCE| server.
368
366
369 This command can only be run using an |authtoken| with admin rights to
367 This command can only be run using an |authtoken| with admin rights to
370 the specified repository.
368 the specified repository.
371
369
372 This command takes the following options:
370 This command takes the following options:
373
371
374 :param apiuser: This is filled automatically from the |authtoken|.
372 :param apiuser: This is filled automatically from the |authtoken|.
375 :type apiuser: AuthUser
373 :type apiuser: AuthUser
376
374
377 :param exc_data_json: JSON data with exception e.g
375 :param exc_data_json: JSON data with exception e.g
378 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
376 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
379 :type exc_data_json: JSON data
377 :type exc_data_json: JSON data
380
378
381 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
379 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
382 :type prefix: Optional("rhodecode")
380 :type prefix: Optional("rhodecode")
383
381
384 Example output:
382 Example output:
385
383
386 .. code-block:: bash
384 .. code-block:: bash
387
385
388 id : <id_given_in_input>
386 id : <id_given_in_input>
389 "result": {
387 "result": {
390 "exc_id": 139718459226384,
388 "exc_id": 139718459226384,
391 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
389 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
392 }
390 }
393 error : null
391 error : null
394 """
392 """
395 if not has_superadmin_permission(apiuser):
393 if not has_superadmin_permission(apiuser):
396 raise JSONRPCForbidden()
394 raise JSONRPCForbidden()
397
395
398 prefix = Optional.extract(prefix)
396 prefix = Optional.extract(prefix)
399 exc_id = exc_tracking.generate_id()
397 exc_id = exc_tracking.generate_id()
400
398
401 try:
399 try:
402 exc_data = json.loads(exc_data_json)
400 exc_data = json.loads(exc_data_json)
403 except Exception:
401 except Exception:
404 log.error('Failed to parse JSON: %r', exc_data_json)
402 log.error('Failed to parse JSON: %r', exc_data_json)
405 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
403 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
406 'Please make sure it contains a valid JSON.')
404 'Please make sure it contains a valid JSON.')
407
405
408 try:
406 try:
409 exc_traceback = exc_data['exc_traceback']
407 exc_traceback = exc_data['exc_traceback']
410 exc_type_name = exc_data['exc_type_name']
408 exc_type_name = exc_data['exc_type_name']
411 except KeyError as err:
409 except KeyError as err:
412 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
410 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
413 'in exc_data_json field. Missing: {}'.format(err))
411 'in exc_data_json field. Missing: {}'.format(err))
414
412
415 exc_tracking._store_exception(
413 exc_tracking._store_exception(
416 exc_id=exc_id, exc_traceback=exc_traceback,
414 exc_id=exc_id, exc_traceback=exc_traceback,
417 exc_type_name=exc_type_name, prefix=prefix)
415 exc_type_name=exc_type_name, prefix=prefix)
418
416
419 exc_url = request.route_url(
417 exc_url = request.route_url(
420 'admin_settings_exception_tracker_show', exception_id=exc_id)
418 'admin_settings_exception_tracker_show', exception_id=exc_id)
421 return {'exc_id': exc_id, 'exc_url': exc_url}
419 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,573 +1,572 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 from pyramid import compat
23
22
24 from rhodecode.api import (
23 from rhodecode.api import (
25 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
27 Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
26 Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
28 from rhodecode.lib import audit_logger
27 from rhodecode.lib import audit_logger
29 from rhodecode.lib.auth import AuthUser, PasswordGenerator
28 from rhodecode.lib.auth import AuthUser, PasswordGenerator
30 from rhodecode.lib.exceptions import DefaultUserException
29 from rhodecode.lib.exceptions import DefaultUserException
31 from rhodecode.lib.utils2 import safe_int, str2bool
30 from rhodecode.lib.utils2 import safe_int, str2bool
32 from rhodecode.model.db import Session, User, Repository
31 from rhodecode.model.db import Session, User, Repository
33 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
34 from rhodecode.model import validation_schema
33 from rhodecode.model import validation_schema
35 from rhodecode.model.validation_schema.schemas import user_schema
34 from rhodecode.model.validation_schema.schemas import user_schema
36
35
37 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
38
37
39
38
40 @jsonrpc_method()
39 @jsonrpc_method()
41 def get_user(request, apiuser, userid=Optional(OAttr('apiuser'))):
40 def get_user(request, apiuser, userid=Optional(OAttr('apiuser'))):
42 """
41 """
43 Returns the information associated with a username or userid.
42 Returns the information associated with a username or userid.
44
43
45 * If the ``userid`` is not set, this command returns the information
44 * If the ``userid`` is not set, this command returns the information
46 for the ``userid`` calling the method.
45 for the ``userid`` calling the method.
47
46
48 .. note::
47 .. note::
49
48
50 Normal users may only run this command against their ``userid``. For
49 Normal users may only run this command against their ``userid``. For
51 full privileges you must run this command using an |authtoken| with
50 full privileges you must run this command using an |authtoken| with
52 admin rights.
51 admin rights.
53
52
54 :param apiuser: This is filled automatically from the |authtoken|.
53 :param apiuser: This is filled automatically from the |authtoken|.
55 :type apiuser: AuthUser
54 :type apiuser: AuthUser
56 :param userid: Sets the userid for which data will be returned.
55 :param userid: Sets the userid for which data will be returned.
57 :type userid: Optional(str or int)
56 :type userid: Optional(str or int)
58
57
59 Example output:
58 Example output:
60
59
61 .. code-block:: bash
60 .. code-block:: bash
62
61
63 {
62 {
64 "error": null,
63 "error": null,
65 "id": <id>,
64 "id": <id>,
66 "result": {
65 "result": {
67 "active": true,
66 "active": true,
68 "admin": false,
67 "admin": false,
69 "api_keys": [ list of keys ],
68 "api_keys": [ list of keys ],
70 "auth_tokens": [ list of tokens with details ],
69 "auth_tokens": [ list of tokens with details ],
71 "email": "user@example.com",
70 "email": "user@example.com",
72 "emails": [
71 "emails": [
73 "user@example.com"
72 "user@example.com"
74 ],
73 ],
75 "extern_name": "rhodecode",
74 "extern_name": "rhodecode",
76 "extern_type": "rhodecode",
75 "extern_type": "rhodecode",
77 "firstname": "username",
76 "firstname": "username",
78 "description": "user description",
77 "description": "user description",
79 "ip_addresses": [],
78 "ip_addresses": [],
80 "language": null,
79 "language": null,
81 "last_login": "Timestamp",
80 "last_login": "Timestamp",
82 "last_activity": "Timestamp",
81 "last_activity": "Timestamp",
83 "lastname": "surnae",
82 "lastname": "surnae",
84 "permissions": <deprecated>,
83 "permissions": <deprecated>,
85 "permissions_summary": {
84 "permissions_summary": {
86 "global": [
85 "global": [
87 "hg.inherit_default_perms.true",
86 "hg.inherit_default_perms.true",
88 "usergroup.read",
87 "usergroup.read",
89 "hg.repogroup.create.false",
88 "hg.repogroup.create.false",
90 "hg.create.none",
89 "hg.create.none",
91 "hg.password_reset.enabled",
90 "hg.password_reset.enabled",
92 "hg.extern_activate.manual",
91 "hg.extern_activate.manual",
93 "hg.create.write_on_repogroup.false",
92 "hg.create.write_on_repogroup.false",
94 "hg.usergroup.create.false",
93 "hg.usergroup.create.false",
95 "group.none",
94 "group.none",
96 "repository.none",
95 "repository.none",
97 "hg.register.none",
96 "hg.register.none",
98 "hg.fork.repository"
97 "hg.fork.repository"
99 ],
98 ],
100 "repositories": { "username/example": "repository.write"},
99 "repositories": { "username/example": "repository.write"},
101 "repositories_groups": { "user-group/repo": "group.none" },
100 "repositories_groups": { "user-group/repo": "group.none" },
102 "user_groups": { "user_group_name": "usergroup.read" }
101 "user_groups": { "user_group_name": "usergroup.read" }
103 }
102 }
104 "user_id": 32,
103 "user_id": 32,
105 "username": "username"
104 "username": "username"
106 }
105 }
107 }
106 }
108 """
107 """
109
108
110 if not has_superadmin_permission(apiuser):
109 if not has_superadmin_permission(apiuser):
111 # make sure normal user does not pass someone else userid,
110 # make sure normal user does not pass someone else userid,
112 # he is not allowed to do that
111 # he is not allowed to do that
113 if not isinstance(userid, Optional) and userid != apiuser.user_id:
112 if not isinstance(userid, Optional) and userid != apiuser.user_id:
114 raise JSONRPCError('userid is not the same as your user')
113 raise JSONRPCError('userid is not the same as your user')
115
114
116 userid = Optional.extract(userid, evaluate_locals=locals())
115 userid = Optional.extract(userid, evaluate_locals=locals())
117 userid = getattr(userid, 'user_id', userid)
116 userid = getattr(userid, 'user_id', userid)
118
117
119 user = get_user_or_error(userid)
118 user = get_user_or_error(userid)
120 data = user.get_api_data(include_secrets=True)
119 data = user.get_api_data(include_secrets=True)
121 permissions = AuthUser(user_id=user.user_id).permissions
120 permissions = AuthUser(user_id=user.user_id).permissions
122 data['permissions'] = permissions # TODO(marcink): should be deprecated
121 data['permissions'] = permissions # TODO(marcink): should be deprecated
123 data['permissions_summary'] = permissions
122 data['permissions_summary'] = permissions
124 return data
123 return data
125
124
126
125
127 @jsonrpc_method()
126 @jsonrpc_method()
128 def get_users(request, apiuser):
127 def get_users(request, apiuser):
129 """
128 """
130 Lists all users in the |RCE| user database.
129 Lists all users in the |RCE| user database.
131
130
132 This command can only be run using an |authtoken| with admin rights to
131 This command can only be run using an |authtoken| with admin rights to
133 the specified repository.
132 the specified repository.
134
133
135 This command takes the following options:
134 This command takes the following options:
136
135
137 :param apiuser: This is filled automatically from the |authtoken|.
136 :param apiuser: This is filled automatically from the |authtoken|.
138 :type apiuser: AuthUser
137 :type apiuser: AuthUser
139
138
140 Example output:
139 Example output:
141
140
142 .. code-block:: bash
141 .. code-block:: bash
143
142
144 id : <id_given_in_input>
143 id : <id_given_in_input>
145 result: [<user_object>, ...]
144 result: [<user_object>, ...]
146 error: null
145 error: null
147 """
146 """
148
147
149 if not has_superadmin_permission(apiuser):
148 if not has_superadmin_permission(apiuser):
150 raise JSONRPCForbidden()
149 raise JSONRPCForbidden()
151
150
152 result = []
151 result = []
153 users_list = User.query().order_by(User.username) \
152 users_list = User.query().order_by(User.username) \
154 .filter(User.username != User.DEFAULT_USER) \
153 .filter(User.username != User.DEFAULT_USER) \
155 .all()
154 .all()
156 for user in users_list:
155 for user in users_list:
157 result.append(user.get_api_data(include_secrets=True))
156 result.append(user.get_api_data(include_secrets=True))
158 return result
157 return result
159
158
160
159
161 @jsonrpc_method()
160 @jsonrpc_method()
162 def create_user(request, apiuser, username, email, password=Optional(''),
161 def create_user(request, apiuser, username, email, password=Optional(''),
163 firstname=Optional(''), lastname=Optional(''), description=Optional(''),
162 firstname=Optional(''), lastname=Optional(''), description=Optional(''),
164 active=Optional(True), admin=Optional(False),
163 active=Optional(True), admin=Optional(False),
165 extern_name=Optional('rhodecode'),
164 extern_name=Optional('rhodecode'),
166 extern_type=Optional('rhodecode'),
165 extern_type=Optional('rhodecode'),
167 force_password_change=Optional(False),
166 force_password_change=Optional(False),
168 create_personal_repo_group=Optional(None)):
167 create_personal_repo_group=Optional(None)):
169 """
168 """
170 Creates a new user and returns the new user object.
169 Creates a new user and returns the new user object.
171
170
172 This command can only be run using an |authtoken| with admin rights to
171 This command can only be run using an |authtoken| with admin rights to
173 the specified repository.
172 the specified repository.
174
173
175 This command takes the following options:
174 This command takes the following options:
176
175
177 :param apiuser: This is filled automatically from the |authtoken|.
176 :param apiuser: This is filled automatically from the |authtoken|.
178 :type apiuser: AuthUser
177 :type apiuser: AuthUser
179 :param username: Set the new username.
178 :param username: Set the new username.
180 :type username: str or int
179 :type username: str or int
181 :param email: Set the user email address.
180 :param email: Set the user email address.
182 :type email: str
181 :type email: str
183 :param password: Set the new user password.
182 :param password: Set the new user password.
184 :type password: Optional(str)
183 :type password: Optional(str)
185 :param firstname: Set the new user firstname.
184 :param firstname: Set the new user firstname.
186 :type firstname: Optional(str)
185 :type firstname: Optional(str)
187 :param lastname: Set the new user surname.
186 :param lastname: Set the new user surname.
188 :type lastname: Optional(str)
187 :type lastname: Optional(str)
189 :param description: Set user description, or short bio. Metatags are allowed.
188 :param description: Set user description, or short bio. Metatags are allowed.
190 :type description: Optional(str)
189 :type description: Optional(str)
191 :param active: Set the user as active.
190 :param active: Set the user as active.
192 :type active: Optional(``True`` | ``False``)
191 :type active: Optional(``True`` | ``False``)
193 :param admin: Give the new user admin rights.
192 :param admin: Give the new user admin rights.
194 :type admin: Optional(``True`` | ``False``)
193 :type admin: Optional(``True`` | ``False``)
195 :param extern_name: Set the authentication plugin name.
194 :param extern_name: Set the authentication plugin name.
196 Using LDAP this is filled with LDAP UID.
195 Using LDAP this is filled with LDAP UID.
197 :type extern_name: Optional(str)
196 :type extern_name: Optional(str)
198 :param extern_type: Set the new user authentication plugin.
197 :param extern_type: Set the new user authentication plugin.
199 :type extern_type: Optional(str)
198 :type extern_type: Optional(str)
200 :param force_password_change: Force the new user to change password
199 :param force_password_change: Force the new user to change password
201 on next login.
200 on next login.
202 :type force_password_change: Optional(``True`` | ``False``)
201 :type force_password_change: Optional(``True`` | ``False``)
203 :param create_personal_repo_group: Create personal repo group for this user
202 :param create_personal_repo_group: Create personal repo group for this user
204 :type create_personal_repo_group: Optional(``True`` | ``False``)
203 :type create_personal_repo_group: Optional(``True`` | ``False``)
205
204
206 Example output:
205 Example output:
207
206
208 .. code-block:: bash
207 .. code-block:: bash
209
208
210 id : <id_given_in_input>
209 id : <id_given_in_input>
211 result: {
210 result: {
212 "msg" : "created new user `<username>`",
211 "msg" : "created new user `<username>`",
213 "user": <user_obj>
212 "user": <user_obj>
214 }
213 }
215 error: null
214 error: null
216
215
217 Example error output:
216 Example error output:
218
217
219 .. code-block:: bash
218 .. code-block:: bash
220
219
221 id : <id_given_in_input>
220 id : <id_given_in_input>
222 result : null
221 result : null
223 error : {
222 error : {
224 "user `<username>` already exist"
223 "user `<username>` already exist"
225 or
224 or
226 "email `<email>` already exist"
225 "email `<email>` already exist"
227 or
226 or
228 "failed to create user `<username>`"
227 "failed to create user `<username>`"
229 }
228 }
230
229
231 """
230 """
232 if not has_superadmin_permission(apiuser):
231 if not has_superadmin_permission(apiuser):
233 raise JSONRPCForbidden()
232 raise JSONRPCForbidden()
234
233
235 if UserModel().get_by_username(username):
234 if UserModel().get_by_username(username):
236 raise JSONRPCError("user `%s` already exist" % (username,))
235 raise JSONRPCError("user `%s` already exist" % (username,))
237
236
238 if UserModel().get_by_email(email, case_insensitive=True):
237 if UserModel().get_by_email(email, case_insensitive=True):
239 raise JSONRPCError("email `%s` already exist" % (email,))
238 raise JSONRPCError("email `%s` already exist" % (email,))
240
239
241 # generate random password if we actually given the
240 # generate random password if we actually given the
242 # extern_name and it's not rhodecode
241 # extern_name and it's not rhodecode
243 if (not isinstance(extern_name, Optional) and
242 if (not isinstance(extern_name, Optional) and
244 Optional.extract(extern_name) != 'rhodecode'):
243 Optional.extract(extern_name) != 'rhodecode'):
245 # generate temporary password if user is external
244 # generate temporary password if user is external
246 password = PasswordGenerator().gen_password(length=16)
245 password = PasswordGenerator().gen_password(length=16)
247 create_repo_group = Optional.extract(create_personal_repo_group)
246 create_repo_group = Optional.extract(create_personal_repo_group)
248 if isinstance(create_repo_group, compat.string_types):
247 if isinstance(create_repo_group, str):
249 create_repo_group = str2bool(create_repo_group)
248 create_repo_group = str2bool(create_repo_group)
250
249
251 username = Optional.extract(username)
250 username = Optional.extract(username)
252 password = Optional.extract(password)
251 password = Optional.extract(password)
253 email = Optional.extract(email)
252 email = Optional.extract(email)
254 first_name = Optional.extract(firstname)
253 first_name = Optional.extract(firstname)
255 last_name = Optional.extract(lastname)
254 last_name = Optional.extract(lastname)
256 description = Optional.extract(description)
255 description = Optional.extract(description)
257 active = Optional.extract(active)
256 active = Optional.extract(active)
258 admin = Optional.extract(admin)
257 admin = Optional.extract(admin)
259 extern_type = Optional.extract(extern_type)
258 extern_type = Optional.extract(extern_type)
260 extern_name = Optional.extract(extern_name)
259 extern_name = Optional.extract(extern_name)
261
260
262 schema = user_schema.UserSchema().bind(
261 schema = user_schema.UserSchema().bind(
263 # user caller
262 # user caller
264 user=apiuser)
263 user=apiuser)
265 try:
264 try:
266 schema_data = schema.deserialize(dict(
265 schema_data = schema.deserialize(dict(
267 username=username,
266 username=username,
268 email=email,
267 email=email,
269 password=password,
268 password=password,
270 first_name=first_name,
269 first_name=first_name,
271 last_name=last_name,
270 last_name=last_name,
272 active=active,
271 active=active,
273 admin=admin,
272 admin=admin,
274 description=description,
273 description=description,
275 extern_type=extern_type,
274 extern_type=extern_type,
276 extern_name=extern_name,
275 extern_name=extern_name,
277 ))
276 ))
278 except validation_schema.Invalid as err:
277 except validation_schema.Invalid as err:
279 raise JSONRPCValidationError(colander_exc=err)
278 raise JSONRPCValidationError(colander_exc=err)
280
279
281 try:
280 try:
282 user = UserModel().create_or_update(
281 user = UserModel().create_or_update(
283 username=schema_data['username'],
282 username=schema_data['username'],
284 password=schema_data['password'],
283 password=schema_data['password'],
285 email=schema_data['email'],
284 email=schema_data['email'],
286 firstname=schema_data['first_name'],
285 firstname=schema_data['first_name'],
287 lastname=schema_data['last_name'],
286 lastname=schema_data['last_name'],
288 description=schema_data['description'],
287 description=schema_data['description'],
289 active=schema_data['active'],
288 active=schema_data['active'],
290 admin=schema_data['admin'],
289 admin=schema_data['admin'],
291 extern_type=schema_data['extern_type'],
290 extern_type=schema_data['extern_type'],
292 extern_name=schema_data['extern_name'],
291 extern_name=schema_data['extern_name'],
293 force_password_change=Optional.extract(force_password_change),
292 force_password_change=Optional.extract(force_password_change),
294 create_repo_group=create_repo_group
293 create_repo_group=create_repo_group
295 )
294 )
296 Session().flush()
295 Session().flush()
297 creation_data = user.get_api_data()
296 creation_data = user.get_api_data()
298 audit_logger.store_api(
297 audit_logger.store_api(
299 'user.create', action_data={'data': creation_data},
298 'user.create', action_data={'data': creation_data},
300 user=apiuser)
299 user=apiuser)
301
300
302 Session().commit()
301 Session().commit()
303 return {
302 return {
304 'msg': 'created new user `%s`' % username,
303 'msg': 'created new user `%s`' % username,
305 'user': user.get_api_data(include_secrets=True)
304 'user': user.get_api_data(include_secrets=True)
306 }
305 }
307 except Exception:
306 except Exception:
308 log.exception('Error occurred during creation of user')
307 log.exception('Error occurred during creation of user')
309 raise JSONRPCError('failed to create user `%s`' % (username,))
308 raise JSONRPCError('failed to create user `%s`' % (username,))
310
309
311
310
312 @jsonrpc_method()
311 @jsonrpc_method()
313 def update_user(request, apiuser, userid, username=Optional(None),
312 def update_user(request, apiuser, userid, username=Optional(None),
314 email=Optional(None), password=Optional(None),
313 email=Optional(None), password=Optional(None),
315 firstname=Optional(None), lastname=Optional(None),
314 firstname=Optional(None), lastname=Optional(None),
316 description=Optional(None), active=Optional(None), admin=Optional(None),
315 description=Optional(None), active=Optional(None), admin=Optional(None),
317 extern_type=Optional(None), extern_name=Optional(None), ):
316 extern_type=Optional(None), extern_name=Optional(None), ):
318 """
317 """
319 Updates the details for the specified user, if that user exists.
318 Updates the details for the specified user, if that user exists.
320
319
321 This command can only be run using an |authtoken| with admin rights to
320 This command can only be run using an |authtoken| with admin rights to
322 the specified repository.
321 the specified repository.
323
322
324 This command takes the following options:
323 This command takes the following options:
325
324
326 :param apiuser: This is filled automatically from |authtoken|.
325 :param apiuser: This is filled automatically from |authtoken|.
327 :type apiuser: AuthUser
326 :type apiuser: AuthUser
328 :param userid: Set the ``userid`` to update.
327 :param userid: Set the ``userid`` to update.
329 :type userid: str or int
328 :type userid: str or int
330 :param username: Set the new username.
329 :param username: Set the new username.
331 :type username: str or int
330 :type username: str or int
332 :param email: Set the new email.
331 :param email: Set the new email.
333 :type email: str
332 :type email: str
334 :param password: Set the new password.
333 :param password: Set the new password.
335 :type password: Optional(str)
334 :type password: Optional(str)
336 :param firstname: Set the new first name.
335 :param firstname: Set the new first name.
337 :type firstname: Optional(str)
336 :type firstname: Optional(str)
338 :param lastname: Set the new surname.
337 :param lastname: Set the new surname.
339 :type lastname: Optional(str)
338 :type lastname: Optional(str)
340 :param description: Set user description, or short bio. Metatags are allowed.
339 :param description: Set user description, or short bio. Metatags are allowed.
341 :type description: Optional(str)
340 :type description: Optional(str)
342 :param active: Set the new user as active.
341 :param active: Set the new user as active.
343 :type active: Optional(``True`` | ``False``)
342 :type active: Optional(``True`` | ``False``)
344 :param admin: Give the user admin rights.
343 :param admin: Give the user admin rights.
345 :type admin: Optional(``True`` | ``False``)
344 :type admin: Optional(``True`` | ``False``)
346 :param extern_name: Set the authentication plugin user name.
345 :param extern_name: Set the authentication plugin user name.
347 Using LDAP this is filled with LDAP UID.
346 Using LDAP this is filled with LDAP UID.
348 :type extern_name: Optional(str)
347 :type extern_name: Optional(str)
349 :param extern_type: Set the authentication plugin type.
348 :param extern_type: Set the authentication plugin type.
350 :type extern_type: Optional(str)
349 :type extern_type: Optional(str)
351
350
352
351
353 Example output:
352 Example output:
354
353
355 .. code-block:: bash
354 .. code-block:: bash
356
355
357 id : <id_given_in_input>
356 id : <id_given_in_input>
358 result: {
357 result: {
359 "msg" : "updated user ID:<userid> <username>",
358 "msg" : "updated user ID:<userid> <username>",
360 "user": <user_object>,
359 "user": <user_object>,
361 }
360 }
362 error: null
361 error: null
363
362
364 Example error output:
363 Example error output:
365
364
366 .. code-block:: bash
365 .. code-block:: bash
367
366
368 id : <id_given_in_input>
367 id : <id_given_in_input>
369 result : null
368 result : null
370 error : {
369 error : {
371 "failed to update user `<username>`"
370 "failed to update user `<username>`"
372 }
371 }
373
372
374 """
373 """
375 if not has_superadmin_permission(apiuser):
374 if not has_superadmin_permission(apiuser):
376 raise JSONRPCForbidden()
375 raise JSONRPCForbidden()
377
376
378 user = get_user_or_error(userid)
377 user = get_user_or_error(userid)
379 old_data = user.get_api_data()
378 old_data = user.get_api_data()
380 # only non optional arguments will be stored in updates
379 # only non optional arguments will be stored in updates
381 updates = {}
380 updates = {}
382
381
383 try:
382 try:
384
383
385 store_update(updates, username, 'username')
384 store_update(updates, username, 'username')
386 store_update(updates, password, 'password')
385 store_update(updates, password, 'password')
387 store_update(updates, email, 'email')
386 store_update(updates, email, 'email')
388 store_update(updates, firstname, 'name')
387 store_update(updates, firstname, 'name')
389 store_update(updates, lastname, 'lastname')
388 store_update(updates, lastname, 'lastname')
390 store_update(updates, description, 'description')
389 store_update(updates, description, 'description')
391 store_update(updates, active, 'active')
390 store_update(updates, active, 'active')
392 store_update(updates, admin, 'admin')
391 store_update(updates, admin, 'admin')
393 store_update(updates, extern_name, 'extern_name')
392 store_update(updates, extern_name, 'extern_name')
394 store_update(updates, extern_type, 'extern_type')
393 store_update(updates, extern_type, 'extern_type')
395
394
396 user = UserModel().update_user(user, **updates)
395 user = UserModel().update_user(user, **updates)
397 audit_logger.store_api(
396 audit_logger.store_api(
398 'user.edit', action_data={'old_data': old_data},
397 'user.edit', action_data={'old_data': old_data},
399 user=apiuser)
398 user=apiuser)
400 Session().commit()
399 Session().commit()
401 return {
400 return {
402 'msg': 'updated user ID:%s %s' % (user.user_id, user.username),
401 'msg': 'updated user ID:%s %s' % (user.user_id, user.username),
403 'user': user.get_api_data(include_secrets=True)
402 'user': user.get_api_data(include_secrets=True)
404 }
403 }
405 except DefaultUserException:
404 except DefaultUserException:
406 log.exception("Default user edit exception")
405 log.exception("Default user edit exception")
407 raise JSONRPCError('editing default user is forbidden')
406 raise JSONRPCError('editing default user is forbidden')
408 except Exception:
407 except Exception:
409 log.exception("Error occurred during update of user")
408 log.exception("Error occurred during update of user")
410 raise JSONRPCError('failed to update user `%s`' % (userid,))
409 raise JSONRPCError('failed to update user `%s`' % (userid,))
411
410
412
411
413 @jsonrpc_method()
412 @jsonrpc_method()
414 def delete_user(request, apiuser, userid):
413 def delete_user(request, apiuser, userid):
415 """
414 """
416 Deletes the specified user from the |RCE| user database.
415 Deletes the specified user from the |RCE| user database.
417
416
418 This command can only be run using an |authtoken| with admin rights to
417 This command can only be run using an |authtoken| with admin rights to
419 the specified repository.
418 the specified repository.
420
419
421 .. important::
420 .. important::
422
421
423 Ensure all open pull requests and open code review
422 Ensure all open pull requests and open code review
424 requests to this user are close.
423 requests to this user are close.
425
424
426 Also ensure all repositories, or repository groups owned by this
425 Also ensure all repositories, or repository groups owned by this
427 user are reassigned before deletion.
426 user are reassigned before deletion.
428
427
429 This command takes the following options:
428 This command takes the following options:
430
429
431 :param apiuser: This is filled automatically from the |authtoken|.
430 :param apiuser: This is filled automatically from the |authtoken|.
432 :type apiuser: AuthUser
431 :type apiuser: AuthUser
433 :param userid: Set the user to delete.
432 :param userid: Set the user to delete.
434 :type userid: str or int
433 :type userid: str or int
435
434
436 Example output:
435 Example output:
437
436
438 .. code-block:: bash
437 .. code-block:: bash
439
438
440 id : <id_given_in_input>
439 id : <id_given_in_input>
441 result: {
440 result: {
442 "msg" : "deleted user ID:<userid> <username>",
441 "msg" : "deleted user ID:<userid> <username>",
443 "user": null
442 "user": null
444 }
443 }
445 error: null
444 error: null
446
445
447 Example error output:
446 Example error output:
448
447
449 .. code-block:: bash
448 .. code-block:: bash
450
449
451 id : <id_given_in_input>
450 id : <id_given_in_input>
452 result : null
451 result : null
453 error : {
452 error : {
454 "failed to delete user ID:<userid> <username>"
453 "failed to delete user ID:<userid> <username>"
455 }
454 }
456
455
457 """
456 """
458 if not has_superadmin_permission(apiuser):
457 if not has_superadmin_permission(apiuser):
459 raise JSONRPCForbidden()
458 raise JSONRPCForbidden()
460
459
461 user = get_user_or_error(userid)
460 user = get_user_or_error(userid)
462 old_data = user.get_api_data()
461 old_data = user.get_api_data()
463 try:
462 try:
464 UserModel().delete(userid)
463 UserModel().delete(userid)
465 audit_logger.store_api(
464 audit_logger.store_api(
466 'user.delete', action_data={'old_data': old_data},
465 'user.delete', action_data={'old_data': old_data},
467 user=apiuser)
466 user=apiuser)
468
467
469 Session().commit()
468 Session().commit()
470 return {
469 return {
471 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username),
470 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username),
472 'user': None
471 'user': None
473 }
472 }
474 except Exception:
473 except Exception:
475 log.exception("Error occurred during deleting of user")
474 log.exception("Error occurred during deleting of user")
476 raise JSONRPCError(
475 raise JSONRPCError(
477 'failed to delete user ID:%s %s' % (user.user_id, user.username))
476 'failed to delete user ID:%s %s' % (user.user_id, user.username))
478
477
479
478
480 @jsonrpc_method()
479 @jsonrpc_method()
481 def get_user_locks(request, apiuser, userid=Optional(OAttr('apiuser'))):
480 def get_user_locks(request, apiuser, userid=Optional(OAttr('apiuser'))):
482 """
481 """
483 Displays all repositories locked by the specified user.
482 Displays all repositories locked by the specified user.
484
483
485 * If this command is run by a non-admin user, it returns
484 * If this command is run by a non-admin user, it returns
486 a list of |repos| locked by that user.
485 a list of |repos| locked by that user.
487
486
488 This command takes the following options:
487 This command takes the following options:
489
488
490 :param apiuser: This is filled automatically from the |authtoken|.
489 :param apiuser: This is filled automatically from the |authtoken|.
491 :type apiuser: AuthUser
490 :type apiuser: AuthUser
492 :param userid: Sets the userid whose list of locked |repos| will be
491 :param userid: Sets the userid whose list of locked |repos| will be
493 displayed.
492 displayed.
494 :type userid: Optional(str or int)
493 :type userid: Optional(str or int)
495
494
496 Example output:
495 Example output:
497
496
498 .. code-block:: bash
497 .. code-block:: bash
499
498
500 id : <id_given_in_input>
499 id : <id_given_in_input>
501 result : {
500 result : {
502 [repo_object, repo_object,...]
501 [repo_object, repo_object,...]
503 }
502 }
504 error : null
503 error : null
505 """
504 """
506
505
507 include_secrets = False
506 include_secrets = False
508 if not has_superadmin_permission(apiuser):
507 if not has_superadmin_permission(apiuser):
509 # make sure normal user does not pass someone else userid,
508 # make sure normal user does not pass someone else userid,
510 # he is not allowed to do that
509 # he is not allowed to do that
511 if not isinstance(userid, Optional) and userid != apiuser.user_id:
510 if not isinstance(userid, Optional) and userid != apiuser.user_id:
512 raise JSONRPCError('userid is not the same as your user')
511 raise JSONRPCError('userid is not the same as your user')
513 else:
512 else:
514 include_secrets = True
513 include_secrets = True
515
514
516 userid = Optional.extract(userid, evaluate_locals=locals())
515 userid = Optional.extract(userid, evaluate_locals=locals())
517 userid = getattr(userid, 'user_id', userid)
516 userid = getattr(userid, 'user_id', userid)
518 user = get_user_or_error(userid)
517 user = get_user_or_error(userid)
519
518
520 ret = []
519 ret = []
521
520
522 # show all locks
521 # show all locks
523 for r in Repository.getAll():
522 for r in Repository.getAll():
524 _user_id, _time, _reason = r.locked
523 _user_id, _time, _reason = r.locked
525 if _user_id and _time:
524 if _user_id and _time:
526 _api_data = r.get_api_data(include_secrets=include_secrets)
525 _api_data = r.get_api_data(include_secrets=include_secrets)
527 # if we use user filter just show the locks for this user
526 # if we use user filter just show the locks for this user
528 if safe_int(_user_id) == user.user_id:
527 if safe_int(_user_id) == user.user_id:
529 ret.append(_api_data)
528 ret.append(_api_data)
530
529
531 return ret
530 return ret
532
531
533
532
534 @jsonrpc_method()
533 @jsonrpc_method()
535 def get_user_audit_logs(request, apiuser, userid=Optional(OAttr('apiuser'))):
534 def get_user_audit_logs(request, apiuser, userid=Optional(OAttr('apiuser'))):
536 """
535 """
537 Fetches all action logs made by the specified user.
536 Fetches all action logs made by the specified user.
538
537
539 This command takes the following options:
538 This command takes the following options:
540
539
541 :param apiuser: This is filled automatically from the |authtoken|.
540 :param apiuser: This is filled automatically from the |authtoken|.
542 :type apiuser: AuthUser
541 :type apiuser: AuthUser
543 :param userid: Sets the userid whose list of locked |repos| will be
542 :param userid: Sets the userid whose list of locked |repos| will be
544 displayed.
543 displayed.
545 :type userid: Optional(str or int)
544 :type userid: Optional(str or int)
546
545
547 Example output:
546 Example output:
548
547
549 .. code-block:: bash
548 .. code-block:: bash
550
549
551 id : <id_given_in_input>
550 id : <id_given_in_input>
552 result : {
551 result : {
553 [action, action,...]
552 [action, action,...]
554 }
553 }
555 error : null
554 error : null
556 """
555 """
557
556
558 if not has_superadmin_permission(apiuser):
557 if not has_superadmin_permission(apiuser):
559 # make sure normal user does not pass someone else userid,
558 # make sure normal user does not pass someone else userid,
560 # he is not allowed to do that
559 # he is not allowed to do that
561 if not isinstance(userid, Optional) and userid != apiuser.user_id:
560 if not isinstance(userid, Optional) and userid != apiuser.user_id:
562 raise JSONRPCError('userid is not the same as your user')
561 raise JSONRPCError('userid is not the same as your user')
563
562
564 userid = Optional.extract(userid, evaluate_locals=locals())
563 userid = Optional.extract(userid, evaluate_locals=locals())
565 userid = getattr(userid, 'user_id', userid)
564 userid = getattr(userid, 'user_id', userid)
566 user = get_user_or_error(userid)
565 user = get_user_or_error(userid)
567
566
568 ret = []
567 ret = []
569
568
570 # show all user actions
569 # show all user actions
571 for entry in UserModel().get_user_log(user, filter_term=None):
570 for entry in UserModel().get_user_log(user, filter_term=None):
572 ret.append(entry)
571 ret.append(entry)
573 return ret
572 return ret
@@ -1,840 +1,839 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23 import operator
23 import operator
24
24
25 from pyramid import compat
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27
26
28 from rhodecode.lib import helpers as h, diffs, rc_cache
27 from rhodecode.lib import helpers as h, diffs, rc_cache
29 from rhodecode.lib.utils import repo_name_slug
28 from rhodecode.lib.utils import repo_name_slug
30 from rhodecode.lib.utils2 import (
29 from rhodecode.lib.utils2 import (
31 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
32 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
33 from rhodecode.lib.vcs.backends.base import EmptyCommit
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
34 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
35 from rhodecode.model import repo
34 from rhodecode.model import repo
36 from rhodecode.model import repo_group
35 from rhodecode.model import repo_group
37 from rhodecode.model import user_group
36 from rhodecode.model import user_group
38 from rhodecode.model import user
37 from rhodecode.model import user
39 from rhodecode.model.db import User
38 from rhodecode.model.db import User
40 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.scm import ScmModel
41 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
40 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
42 from rhodecode.model.repo import ReadmeFinder
41 from rhodecode.model.repo import ReadmeFinder
43
42
44 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
45
44
46
45
47 ADMIN_PREFIX = '/_admin'
46 ADMIN_PREFIX = '/_admin'
48 STATIC_FILE_PREFIX = '/_static'
47 STATIC_FILE_PREFIX = '/_static'
49
48
50 URL_NAME_REQUIREMENTS = {
49 URL_NAME_REQUIREMENTS = {
51 # group name can have a slash in them, but they must not end with a slash
50 # group name can have a slash in them, but they must not end with a slash
52 'group_name': r'.*?[^/]',
51 'group_name': r'.*?[^/]',
53 'repo_group_name': r'.*?[^/]',
52 'repo_group_name': r'.*?[^/]',
54 # repo names can have a slash in them, but they must not end with a slash
53 # repo names can have a slash in them, but they must not end with a slash
55 'repo_name': r'.*?[^/]',
54 'repo_name': r'.*?[^/]',
56 # file path eats up everything at the end
55 # file path eats up everything at the end
57 'f_path': r'.*',
56 'f_path': r'.*',
58 # reference types
57 # reference types
59 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
60 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
61 }
60 }
62
61
63
62
64 def add_route_with_slash(config,name, pattern, **kw):
63 def add_route_with_slash(config,name, pattern, **kw):
65 config.add_route(name, pattern, **kw)
64 config.add_route(name, pattern, **kw)
66 if not pattern.endswith('/'):
65 if not pattern.endswith('/'):
67 config.add_route(name + '_slash', pattern + '/', **kw)
66 config.add_route(name + '_slash', pattern + '/', **kw)
68
67
69
68
70 def add_route_requirements(route_path, requirements=None):
69 def add_route_requirements(route_path, requirements=None):
71 """
70 """
72 Adds regex requirements to pyramid routes using a mapping dict
71 Adds regex requirements to pyramid routes using a mapping dict
73 e.g::
72 e.g::
74 add_route_requirements('{repo_name}/settings')
73 add_route_requirements('{repo_name}/settings')
75 """
74 """
76 requirements = requirements or URL_NAME_REQUIREMENTS
75 requirements = requirements or URL_NAME_REQUIREMENTS
77 for key, regex in requirements.items():
76 for key, regex in requirements.items():
78 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
79 return route_path
78 return route_path
80
79
81
80
82 def get_format_ref_id(repo):
81 def get_format_ref_id(repo):
83 """Returns a `repo` specific reference formatter function"""
82 """Returns a `repo` specific reference formatter function"""
84 if h.is_svn(repo):
83 if h.is_svn(repo):
85 return _format_ref_id_svn
84 return _format_ref_id_svn
86 else:
85 else:
87 return _format_ref_id
86 return _format_ref_id
88
87
89
88
90 def _format_ref_id(name, raw_id):
89 def _format_ref_id(name, raw_id):
91 """Default formatting of a given reference `name`"""
90 """Default formatting of a given reference `name`"""
92 return name
91 return name
93
92
94
93
95 def _format_ref_id_svn(name, raw_id):
94 def _format_ref_id_svn(name, raw_id):
96 """Special way of formatting a reference for Subversion including path"""
95 """Special way of formatting a reference for Subversion including path"""
97 return '%s@%s' % (name, raw_id)
96 return '%s@%s' % (name, raw_id)
98
97
99
98
100 class TemplateArgs(StrictAttributeDict):
99 class TemplateArgs(StrictAttributeDict):
101 pass
100 pass
102
101
103
102
104 class BaseAppView(object):
103 class BaseAppView(object):
105
104
106 def __init__(self, context, request):
105 def __init__(self, context, request):
107 self.request = request
106 self.request = request
108 self.context = context
107 self.context = context
109 self.session = request.session
108 self.session = request.session
110 if not hasattr(request, 'user'):
109 if not hasattr(request, 'user'):
111 # NOTE(marcink): edge case, we ended up in matched route
110 # NOTE(marcink): edge case, we ended up in matched route
112 # but probably of web-app context, e.g API CALL/VCS CALL
111 # but probably of web-app context, e.g API CALL/VCS CALL
113 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
114 log.warning('Unable to process request `%s` in this scope', request)
113 log.warning('Unable to process request `%s` in this scope', request)
115 raise HTTPBadRequest()
114 raise HTTPBadRequest()
116
115
117 self._rhodecode_user = request.user # auth user
116 self._rhodecode_user = request.user # auth user
118 self._rhodecode_db_user = self._rhodecode_user.get_instance()
117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
119 self._maybe_needs_password_change(
118 self._maybe_needs_password_change(
120 request.matched_route.name, self._rhodecode_db_user)
119 request.matched_route.name, self._rhodecode_db_user)
121
120
122 def _maybe_needs_password_change(self, view_name, user_obj):
121 def _maybe_needs_password_change(self, view_name, user_obj):
123
122
124 dont_check_views = [
123 dont_check_views = [
125 'channelstream_connect'
124 'channelstream_connect'
126 ]
125 ]
127 if view_name in dont_check_views:
126 if view_name in dont_check_views:
128 return
127 return
129
128
130 log.debug('Checking if user %s needs password change on view %s',
129 log.debug('Checking if user %s needs password change on view %s',
131 user_obj, view_name)
130 user_obj, view_name)
132
131
133 skip_user_views = [
132 skip_user_views = [
134 'logout', 'login',
133 'logout', 'login',
135 'my_account_password', 'my_account_password_update'
134 'my_account_password', 'my_account_password_update'
136 ]
135 ]
137
136
138 if not user_obj:
137 if not user_obj:
139 return
138 return
140
139
141 if user_obj.username == User.DEFAULT_USER:
140 if user_obj.username == User.DEFAULT_USER:
142 return
141 return
143
142
144 now = time.time()
143 now = time.time()
145 should_change = user_obj.user_data.get('force_password_change')
144 should_change = user_obj.user_data.get('force_password_change')
146 change_after = safe_int(should_change) or 0
145 change_after = safe_int(should_change) or 0
147 if should_change and now > change_after:
146 if should_change and now > change_after:
148 log.debug('User %s requires password change', user_obj)
147 log.debug('User %s requires password change', user_obj)
149 h.flash('You are required to change your password', 'warning',
148 h.flash('You are required to change your password', 'warning',
150 ignore_duplicate=True)
149 ignore_duplicate=True)
151
150
152 if view_name not in skip_user_views:
151 if view_name not in skip_user_views:
153 raise HTTPFound(
152 raise HTTPFound(
154 self.request.route_path('my_account_password'))
153 self.request.route_path('my_account_password'))
155
154
156 def _log_creation_exception(self, e, repo_name):
155 def _log_creation_exception(self, e, repo_name):
157 _ = self.request.translate
156 _ = self.request.translate
158 reason = None
157 reason = None
159 if len(e.args) == 2:
158 if len(e.args) == 2:
160 reason = e.args[1]
159 reason = e.args[1]
161
160
162 if reason == 'INVALID_CERTIFICATE':
161 if reason == 'INVALID_CERTIFICATE':
163 log.exception(
162 log.exception(
164 'Exception creating a repository: invalid certificate')
163 'Exception creating a repository: invalid certificate')
165 msg = (_('Error creating repository %s: invalid certificate')
164 msg = (_('Error creating repository %s: invalid certificate')
166 % repo_name)
165 % repo_name)
167 else:
166 else:
168 log.exception("Exception creating a repository")
167 log.exception("Exception creating a repository")
169 msg = (_('Error creating repository %s')
168 msg = (_('Error creating repository %s')
170 % repo_name)
169 % repo_name)
171 return msg
170 return msg
172
171
173 def _get_local_tmpl_context(self, include_app_defaults=True):
172 def _get_local_tmpl_context(self, include_app_defaults=True):
174 c = TemplateArgs()
173 c = TemplateArgs()
175 c.auth_user = self.request.user
174 c.auth_user = self.request.user
176 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
175 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
177 c.rhodecode_user = self.request.user
176 c.rhodecode_user = self.request.user
178
177
179 if include_app_defaults:
178 if include_app_defaults:
180 from rhodecode.lib.base import attach_context_attributes
179 from rhodecode.lib.base import attach_context_attributes
181 attach_context_attributes(c, self.request, self.request.user.user_id)
180 attach_context_attributes(c, self.request, self.request.user.user_id)
182
181
183 c.is_super_admin = c.auth_user.is_admin
182 c.is_super_admin = c.auth_user.is_admin
184
183
185 c.can_create_repo = c.is_super_admin
184 c.can_create_repo = c.is_super_admin
186 c.can_create_repo_group = c.is_super_admin
185 c.can_create_repo_group = c.is_super_admin
187 c.can_create_user_group = c.is_super_admin
186 c.can_create_user_group = c.is_super_admin
188
187
189 c.is_delegated_admin = False
188 c.is_delegated_admin = False
190
189
191 if not c.auth_user.is_default and not c.is_super_admin:
190 if not c.auth_user.is_default and not c.is_super_admin:
192 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
191 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
193 user=self.request.user)
192 user=self.request.user)
194 repositories = c.auth_user.repositories_admin or c.can_create_repo
193 repositories = c.auth_user.repositories_admin or c.can_create_repo
195
194
196 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
195 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
197 user=self.request.user)
196 user=self.request.user)
198 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
197 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
199
198
200 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
199 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
201 user=self.request.user)
200 user=self.request.user)
202 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
201 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
203 # delegated admin can create, or manage some objects
202 # delegated admin can create, or manage some objects
204 c.is_delegated_admin = repositories or repository_groups or user_groups
203 c.is_delegated_admin = repositories or repository_groups or user_groups
205 return c
204 return c
206
205
207 def _get_template_context(self, tmpl_args, **kwargs):
206 def _get_template_context(self, tmpl_args, **kwargs):
208
207
209 local_tmpl_args = {
208 local_tmpl_args = {
210 'defaults': {},
209 'defaults': {},
211 'errors': {},
210 'errors': {},
212 'c': tmpl_args
211 'c': tmpl_args
213 }
212 }
214 local_tmpl_args.update(kwargs)
213 local_tmpl_args.update(kwargs)
215 return local_tmpl_args
214 return local_tmpl_args
216
215
217 def load_default_context(self):
216 def load_default_context(self):
218 """
217 """
219 example:
218 example:
220
219
221 def load_default_context(self):
220 def load_default_context(self):
222 c = self._get_local_tmpl_context()
221 c = self._get_local_tmpl_context()
223 c.custom_var = 'foobar'
222 c.custom_var = 'foobar'
224
223
225 return c
224 return c
226 """
225 """
227 raise NotImplementedError('Needs implementation in view class')
226 raise NotImplementedError('Needs implementation in view class')
228
227
229
228
230 class RepoAppView(BaseAppView):
229 class RepoAppView(BaseAppView):
231
230
232 def __init__(self, context, request):
231 def __init__(self, context, request):
233 super(RepoAppView, self).__init__(context, request)
232 super(RepoAppView, self).__init__(context, request)
234 self.db_repo = request.db_repo
233 self.db_repo = request.db_repo
235 self.db_repo_name = self.db_repo.repo_name
234 self.db_repo_name = self.db_repo.repo_name
236 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
235 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
237 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
236 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
238 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
237 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
239
238
240 def _handle_missing_requirements(self, error):
239 def _handle_missing_requirements(self, error):
241 log.error(
240 log.error(
242 'Requirements are missing for repository %s: %s',
241 'Requirements are missing for repository %s: %s',
243 self.db_repo_name, safe_unicode(error))
242 self.db_repo_name, safe_unicode(error))
244
243
245 def _get_local_tmpl_context(self, include_app_defaults=True):
244 def _get_local_tmpl_context(self, include_app_defaults=True):
246 _ = self.request.translate
245 _ = self.request.translate
247 c = super(RepoAppView, self)._get_local_tmpl_context(
246 c = super(RepoAppView, self)._get_local_tmpl_context(
248 include_app_defaults=include_app_defaults)
247 include_app_defaults=include_app_defaults)
249
248
250 # register common vars for this type of view
249 # register common vars for this type of view
251 c.rhodecode_db_repo = self.db_repo
250 c.rhodecode_db_repo = self.db_repo
252 c.repo_name = self.db_repo_name
251 c.repo_name = self.db_repo_name
253 c.repository_pull_requests = self.db_repo_pull_requests
252 c.repository_pull_requests = self.db_repo_pull_requests
254 c.repository_artifacts = self.db_repo_artifacts
253 c.repository_artifacts = self.db_repo_artifacts
255 c.repository_is_user_following = ScmModel().is_following_repo(
254 c.repository_is_user_following = ScmModel().is_following_repo(
256 self.db_repo_name, self._rhodecode_user.user_id)
255 self.db_repo_name, self._rhodecode_user.user_id)
257 self.path_filter = PathFilter(None)
256 self.path_filter = PathFilter(None)
258
257
259 c.repository_requirements_missing = {}
258 c.repository_requirements_missing = {}
260 try:
259 try:
261 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
260 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
262 # NOTE(marcink):
261 # NOTE(marcink):
263 # comparison to None since if it's an object __bool__ is expensive to
262 # comparison to None since if it's an object __bool__ is expensive to
264 # calculate
263 # calculate
265 if self.rhodecode_vcs_repo is not None:
264 if self.rhodecode_vcs_repo is not None:
266 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
265 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
267 c.auth_user.username)
266 c.auth_user.username)
268 self.path_filter = PathFilter(path_perms)
267 self.path_filter = PathFilter(path_perms)
269 except RepositoryRequirementError as e:
268 except RepositoryRequirementError as e:
270 c.repository_requirements_missing = {'error': str(e)}
269 c.repository_requirements_missing = {'error': str(e)}
271 self._handle_missing_requirements(e)
270 self._handle_missing_requirements(e)
272 self.rhodecode_vcs_repo = None
271 self.rhodecode_vcs_repo = None
273
272
274 c.path_filter = self.path_filter # used by atom_feed_entry.mako
273 c.path_filter = self.path_filter # used by atom_feed_entry.mako
275
274
276 if self.rhodecode_vcs_repo is None:
275 if self.rhodecode_vcs_repo is None:
277 # unable to fetch this repo as vcs instance, report back to user
276 # unable to fetch this repo as vcs instance, report back to user
278 log.debug('Repository was not found on filesystem, check if it exists or is not damaged')
277 log.debug('Repository was not found on filesystem, check if it exists or is not damaged')
279 h.flash(_(
278 h.flash(_(
280 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
279 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
281 "Please check if it exist, or is not damaged.") %
280 "Please check if it exist, or is not damaged.") %
282 {'repo_name': c.repo_name},
281 {'repo_name': c.repo_name},
283 category='error', ignore_duplicate=True)
282 category='error', ignore_duplicate=True)
284 if c.repository_requirements_missing:
283 if c.repository_requirements_missing:
285 route = self.request.matched_route.name
284 route = self.request.matched_route.name
286 if route.startswith(('edit_repo', 'repo_summary')):
285 if route.startswith(('edit_repo', 'repo_summary')):
287 # allow summary and edit repo on missing requirements
286 # allow summary and edit repo on missing requirements
288 return c
287 return c
289
288
290 raise HTTPFound(
289 raise HTTPFound(
291 h.route_path('repo_summary', repo_name=self.db_repo_name))
290 h.route_path('repo_summary', repo_name=self.db_repo_name))
292
291
293 else: # redirect if we don't show missing requirements
292 else: # redirect if we don't show missing requirements
294 raise HTTPFound(h.route_path('home'))
293 raise HTTPFound(h.route_path('home'))
295
294
296 c.has_origin_repo_read_perm = False
295 c.has_origin_repo_read_perm = False
297 if self.db_repo.fork:
296 if self.db_repo.fork:
298 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
297 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
299 'repository.write', 'repository.read', 'repository.admin')(
298 'repository.write', 'repository.read', 'repository.admin')(
300 self.db_repo.fork.repo_name, 'summary fork link')
299 self.db_repo.fork.repo_name, 'summary fork link')
301
300
302 return c
301 return c
303
302
304 def _get_f_path_unchecked(self, matchdict, default=None):
303 def _get_f_path_unchecked(self, matchdict, default=None):
305 """
304 """
306 Should only be used by redirects, everything else should call _get_f_path
305 Should only be used by redirects, everything else should call _get_f_path
307 """
306 """
308 f_path = matchdict.get('f_path')
307 f_path = matchdict.get('f_path')
309 if f_path:
308 if f_path:
310 # fix for multiple initial slashes that causes errors for GIT
309 # fix for multiple initial slashes that causes errors for GIT
311 return f_path.lstrip('/')
310 return f_path.lstrip('/')
312
311
313 return default
312 return default
314
313
315 def _get_f_path(self, matchdict, default=None):
314 def _get_f_path(self, matchdict, default=None):
316 f_path_match = self._get_f_path_unchecked(matchdict, default)
315 f_path_match = self._get_f_path_unchecked(matchdict, default)
317 return self.path_filter.assert_path_permissions(f_path_match)
316 return self.path_filter.assert_path_permissions(f_path_match)
318
317
319 def _get_general_setting(self, target_repo, settings_key, default=False):
318 def _get_general_setting(self, target_repo, settings_key, default=False):
320 settings_model = VcsSettingsModel(repo=target_repo)
319 settings_model = VcsSettingsModel(repo=target_repo)
321 settings = settings_model.get_general_settings()
320 settings = settings_model.get_general_settings()
322 return settings.get(settings_key, default)
321 return settings.get(settings_key, default)
323
322
324 def _get_repo_setting(self, target_repo, settings_key, default=False):
323 def _get_repo_setting(self, target_repo, settings_key, default=False):
325 settings_model = VcsSettingsModel(repo=target_repo)
324 settings_model = VcsSettingsModel(repo=target_repo)
326 settings = settings_model.get_repo_settings_inherited()
325 settings = settings_model.get_repo_settings_inherited()
327 return settings.get(settings_key, default)
326 return settings.get(settings_key, default)
328
327
329 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
328 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
330 log.debug('Looking for README file at path %s', path)
329 log.debug('Looking for README file at path %s', path)
331 if commit_id:
330 if commit_id:
332 landing_commit_id = commit_id
331 landing_commit_id = commit_id
333 else:
332 else:
334 landing_commit = db_repo.get_landing_commit()
333 landing_commit = db_repo.get_landing_commit()
335 if isinstance(landing_commit, EmptyCommit):
334 if isinstance(landing_commit, EmptyCommit):
336 return None, None
335 return None, None
337 landing_commit_id = landing_commit.raw_id
336 landing_commit_id = landing_commit.raw_id
338
337
339 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
338 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
340 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
339 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
341 start = time.time()
340 start = time.time()
342
341
343 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
342 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
344 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
343 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
345 readme_data = None
344 readme_data = None
346 readme_filename = None
345 readme_filename = None
347
346
348 commit = db_repo.get_commit(_commit_id)
347 commit = db_repo.get_commit(_commit_id)
349 log.debug("Searching for a README file at commit %s.", _commit_id)
348 log.debug("Searching for a README file at commit %s.", _commit_id)
350 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
349 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
351
350
352 if readme_node:
351 if readme_node:
353 log.debug('Found README node: %s', readme_node)
352 log.debug('Found README node: %s', readme_node)
354 relative_urls = {
353 relative_urls = {
355 'raw': h.route_path(
354 'raw': h.route_path(
356 'repo_file_raw', repo_name=_repo_name,
355 'repo_file_raw', repo_name=_repo_name,
357 commit_id=commit.raw_id, f_path=readme_node.path),
356 commit_id=commit.raw_id, f_path=readme_node.path),
358 'standard': h.route_path(
357 'standard': h.route_path(
359 'repo_files', repo_name=_repo_name,
358 'repo_files', repo_name=_repo_name,
360 commit_id=commit.raw_id, f_path=readme_node.path),
359 commit_id=commit.raw_id, f_path=readme_node.path),
361 }
360 }
362 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
361 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
363 readme_filename = readme_node.unicode_path
362 readme_filename = readme_node.unicode_path
364
363
365 return readme_data, readme_filename
364 return readme_data, readme_filename
366
365
367 readme_data, readme_filename = generate_repo_readme(
366 readme_data, readme_filename = generate_repo_readme(
368 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
367 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
369 compute_time = time.time() - start
368 compute_time = time.time() - start
370 log.debug('Repo README for path %s generated and computed in %.4fs',
369 log.debug('Repo README for path %s generated and computed in %.4fs',
371 path, compute_time)
370 path, compute_time)
372 return readme_data, readme_filename
371 return readme_data, readme_filename
373
372
374 def _render_readme_or_none(self, commit, readme_node, relative_urls):
373 def _render_readme_or_none(self, commit, readme_node, relative_urls):
375 log.debug('Found README file `%s` rendering...', readme_node.path)
374 log.debug('Found README file `%s` rendering...', readme_node.path)
376 renderer = MarkupRenderer()
375 renderer = MarkupRenderer()
377 try:
376 try:
378 html_source = renderer.render(
377 html_source = renderer.render(
379 readme_node.content, filename=readme_node.path)
378 readme_node.content, filename=readme_node.path)
380 if relative_urls:
379 if relative_urls:
381 return relative_links(html_source, relative_urls)
380 return relative_links(html_source, relative_urls)
382 return html_source
381 return html_source
383 except Exception:
382 except Exception:
384 log.exception(
383 log.exception(
385 "Exception while trying to render the README")
384 "Exception while trying to render the README")
386
385
387 def get_recache_flag(self):
386 def get_recache_flag(self):
388 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
387 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
389 flag_val = self.request.GET.get(flag_name)
388 flag_val = self.request.GET.get(flag_name)
390 if str2bool(flag_val):
389 if str2bool(flag_val):
391 return True
390 return True
392 return False
391 return False
393
392
394 def get_commit_preload_attrs(cls):
393 def get_commit_preload_attrs(cls):
395 pre_load = ['author', 'branch', 'date', 'message', 'parents',
394 pre_load = ['author', 'branch', 'date', 'message', 'parents',
396 'obsolete', 'phase', 'hidden']
395 'obsolete', 'phase', 'hidden']
397 return pre_load
396 return pre_load
398
397
399
398
400 class PathFilter(object):
399 class PathFilter(object):
401
400
402 # Expects and instance of BasePathPermissionChecker or None
401 # Expects and instance of BasePathPermissionChecker or None
403 def __init__(self, permission_checker):
402 def __init__(self, permission_checker):
404 self.permission_checker = permission_checker
403 self.permission_checker = permission_checker
405
404
406 def assert_path_permissions(self, path):
405 def assert_path_permissions(self, path):
407 if self.path_access_allowed(path):
406 if self.path_access_allowed(path):
408 return path
407 return path
409 raise HTTPForbidden()
408 raise HTTPForbidden()
410
409
411 def path_access_allowed(self, path):
410 def path_access_allowed(self, path):
412 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
411 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
413 if self.permission_checker:
412 if self.permission_checker:
414 has_access = path and self.permission_checker.has_access(path)
413 has_access = path and self.permission_checker.has_access(path)
415 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
414 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
416 return has_access
415 return has_access
417
416
418 log.debug('ACL permissions checker not enabled, skipping...')
417 log.debug('ACL permissions checker not enabled, skipping...')
419 return True
418 return True
420
419
421 def filter_patchset(self, patchset):
420 def filter_patchset(self, patchset):
422 if not self.permission_checker or not patchset:
421 if not self.permission_checker or not patchset:
423 return patchset, False
422 return patchset, False
424 had_filtered = False
423 had_filtered = False
425 filtered_patchset = []
424 filtered_patchset = []
426 for patch in patchset:
425 for patch in patchset:
427 filename = patch.get('filename', None)
426 filename = patch.get('filename', None)
428 if not filename or self.permission_checker.has_access(filename):
427 if not filename or self.permission_checker.has_access(filename):
429 filtered_patchset.append(patch)
428 filtered_patchset.append(patch)
430 else:
429 else:
431 had_filtered = True
430 had_filtered = True
432 if had_filtered:
431 if had_filtered:
433 if isinstance(patchset, diffs.LimitedDiffContainer):
432 if isinstance(patchset, diffs.LimitedDiffContainer):
434 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
433 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
435 return filtered_patchset, True
434 return filtered_patchset, True
436 else:
435 else:
437 return patchset, False
436 return patchset, False
438
437
439 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
438 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
440 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
439 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
441 result = diffset.render_patchset(
440 result = diffset.render_patchset(
442 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
441 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
443 result.has_hidden_changes = has_hidden_changes
442 result.has_hidden_changes = has_hidden_changes
444 return result
443 return result
445
444
446 def get_raw_patch(self, diff_processor):
445 def get_raw_patch(self, diff_processor):
447 if self.permission_checker is None:
446 if self.permission_checker is None:
448 return diff_processor.as_raw()
447 return diff_processor.as_raw()
449 elif self.permission_checker.has_full_access:
448 elif self.permission_checker.has_full_access:
450 return diff_processor.as_raw()
449 return diff_processor.as_raw()
451 else:
450 else:
452 return '# Repository has user-specific filters, raw patch generation is disabled.'
451 return '# Repository has user-specific filters, raw patch generation is disabled.'
453
452
454 @property
453 @property
455 def is_enabled(self):
454 def is_enabled(self):
456 return self.permission_checker is not None
455 return self.permission_checker is not None
457
456
458
457
459 class RepoGroupAppView(BaseAppView):
458 class RepoGroupAppView(BaseAppView):
460 def __init__(self, context, request):
459 def __init__(self, context, request):
461 super(RepoGroupAppView, self).__init__(context, request)
460 super(RepoGroupAppView, self).__init__(context, request)
462 self.db_repo_group = request.db_repo_group
461 self.db_repo_group = request.db_repo_group
463 self.db_repo_group_name = self.db_repo_group.group_name
462 self.db_repo_group_name = self.db_repo_group.group_name
464
463
465 def _get_local_tmpl_context(self, include_app_defaults=True):
464 def _get_local_tmpl_context(self, include_app_defaults=True):
466 _ = self.request.translate
465 _ = self.request.translate
467 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
466 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
468 include_app_defaults=include_app_defaults)
467 include_app_defaults=include_app_defaults)
469 c.repo_group = self.db_repo_group
468 c.repo_group = self.db_repo_group
470 return c
469 return c
471
470
472 def _revoke_perms_on_yourself(self, form_result):
471 def _revoke_perms_on_yourself(self, form_result):
473 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
472 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
474 form_result['perm_updates'])
473 form_result['perm_updates'])
475 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
474 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
476 form_result['perm_additions'])
475 form_result['perm_additions'])
477 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
476 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
478 form_result['perm_deletions'])
477 form_result['perm_deletions'])
479 admin_perm = 'group.admin'
478 admin_perm = 'group.admin'
480 if _updates and _updates[0][1] != admin_perm or \
479 if _updates and _updates[0][1] != admin_perm or \
481 _additions and _additions[0][1] != admin_perm or \
480 _additions and _additions[0][1] != admin_perm or \
482 _deletions and _deletions[0][1] != admin_perm:
481 _deletions and _deletions[0][1] != admin_perm:
483 return True
482 return True
484 return False
483 return False
485
484
486
485
487 class UserGroupAppView(BaseAppView):
486 class UserGroupAppView(BaseAppView):
488 def __init__(self, context, request):
487 def __init__(self, context, request):
489 super(UserGroupAppView, self).__init__(context, request)
488 super(UserGroupAppView, self).__init__(context, request)
490 self.db_user_group = request.db_user_group
489 self.db_user_group = request.db_user_group
491 self.db_user_group_name = self.db_user_group.users_group_name
490 self.db_user_group_name = self.db_user_group.users_group_name
492
491
493
492
494 class UserAppView(BaseAppView):
493 class UserAppView(BaseAppView):
495 def __init__(self, context, request):
494 def __init__(self, context, request):
496 super(UserAppView, self).__init__(context, request)
495 super(UserAppView, self).__init__(context, request)
497 self.db_user = request.db_user
496 self.db_user = request.db_user
498 self.db_user_id = self.db_user.user_id
497 self.db_user_id = self.db_user.user_id
499
498
500 _ = self.request.translate
499 _ = self.request.translate
501 if not request.db_user_supports_default:
500 if not request.db_user_supports_default:
502 if self.db_user.username == User.DEFAULT_USER:
501 if self.db_user.username == User.DEFAULT_USER:
503 h.flash(_("Editing user `{}` is disabled.".format(
502 h.flash(_("Editing user `{}` is disabled.".format(
504 User.DEFAULT_USER)), category='warning')
503 User.DEFAULT_USER)), category='warning')
505 raise HTTPFound(h.route_path('users'))
504 raise HTTPFound(h.route_path('users'))
506
505
507
506
508 class DataGridAppView(object):
507 class DataGridAppView(object):
509 """
508 """
510 Common class to have re-usable grid rendering components
509 Common class to have re-usable grid rendering components
511 """
510 """
512
511
513 def _extract_ordering(self, request, column_map=None):
512 def _extract_ordering(self, request, column_map=None):
514 column_map = column_map or {}
513 column_map = column_map or {}
515 column_index = safe_int(request.GET.get('order[0][column]'))
514 column_index = safe_int(request.GET.get('order[0][column]'))
516 order_dir = request.GET.get(
515 order_dir = request.GET.get(
517 'order[0][dir]', 'desc')
516 'order[0][dir]', 'desc')
518 order_by = request.GET.get(
517 order_by = request.GET.get(
519 'columns[%s][data][sort]' % column_index, 'name_raw')
518 'columns[%s][data][sort]' % column_index, 'name_raw')
520
519
521 # translate datatable to DB columns
520 # translate datatable to DB columns
522 order_by = column_map.get(order_by) or order_by
521 order_by = column_map.get(order_by) or order_by
523
522
524 search_q = request.GET.get('search[value]')
523 search_q = request.GET.get('search[value]')
525 return search_q, order_by, order_dir
524 return search_q, order_by, order_dir
526
525
527 def _extract_chunk(self, request):
526 def _extract_chunk(self, request):
528 start = safe_int(request.GET.get('start'), 0)
527 start = safe_int(request.GET.get('start'), 0)
529 length = safe_int(request.GET.get('length'), 25)
528 length = safe_int(request.GET.get('length'), 25)
530 draw = safe_int(request.GET.get('draw'))
529 draw = safe_int(request.GET.get('draw'))
531 return draw, start, length
530 return draw, start, length
532
531
533 def _get_order_col(self, order_by, model):
532 def _get_order_col(self, order_by, model):
534 if isinstance(order_by, compat.string_types):
533 if isinstance(order_by, str):
535 try:
534 try:
536 return operator.attrgetter(order_by)(model)
535 return operator.attrgetter(order_by)(model)
537 except AttributeError:
536 except AttributeError:
538 return None
537 return None
539 else:
538 else:
540 return order_by
539 return order_by
541
540
542
541
543 class BaseReferencesView(RepoAppView):
542 class BaseReferencesView(RepoAppView):
544 """
543 """
545 Base for reference view for branches, tags and bookmarks.
544 Base for reference view for branches, tags and bookmarks.
546 """
545 """
547 def load_default_context(self):
546 def load_default_context(self):
548 c = self._get_local_tmpl_context()
547 c = self._get_local_tmpl_context()
549 return c
548 return c
550
549
551 def load_refs_context(self, ref_items, partials_template):
550 def load_refs_context(self, ref_items, partials_template):
552 _render = self.request.get_partial_renderer(partials_template)
551 _render = self.request.get_partial_renderer(partials_template)
553 pre_load = ["author", "date", "message", "parents"]
552 pre_load = ["author", "date", "message", "parents"]
554
553
555 is_svn = h.is_svn(self.rhodecode_vcs_repo)
554 is_svn = h.is_svn(self.rhodecode_vcs_repo)
556 is_hg = h.is_hg(self.rhodecode_vcs_repo)
555 is_hg = h.is_hg(self.rhodecode_vcs_repo)
557
556
558 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
557 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
559
558
560 closed_refs = {}
559 closed_refs = {}
561 if is_hg:
560 if is_hg:
562 closed_refs = self.rhodecode_vcs_repo.branches_closed
561 closed_refs = self.rhodecode_vcs_repo.branches_closed
563
562
564 data = []
563 data = []
565 for ref_name, commit_id in ref_items:
564 for ref_name, commit_id in ref_items:
566 commit = self.rhodecode_vcs_repo.get_commit(
565 commit = self.rhodecode_vcs_repo.get_commit(
567 commit_id=commit_id, pre_load=pre_load)
566 commit_id=commit_id, pre_load=pre_load)
568 closed = ref_name in closed_refs
567 closed = ref_name in closed_refs
569
568
570 # TODO: johbo: Unify generation of reference links
569 # TODO: johbo: Unify generation of reference links
571 use_commit_id = '/' in ref_name or is_svn
570 use_commit_id = '/' in ref_name or is_svn
572
571
573 if use_commit_id:
572 if use_commit_id:
574 files_url = h.route_path(
573 files_url = h.route_path(
575 'repo_files',
574 'repo_files',
576 repo_name=self.db_repo_name,
575 repo_name=self.db_repo_name,
577 f_path=ref_name if is_svn else '',
576 f_path=ref_name if is_svn else '',
578 commit_id=commit_id,
577 commit_id=commit_id,
579 _query=dict(at=ref_name)
578 _query=dict(at=ref_name)
580 )
579 )
581
580
582 else:
581 else:
583 files_url = h.route_path(
582 files_url = h.route_path(
584 'repo_files',
583 'repo_files',
585 repo_name=self.db_repo_name,
584 repo_name=self.db_repo_name,
586 f_path=ref_name if is_svn else '',
585 f_path=ref_name if is_svn else '',
587 commit_id=ref_name,
586 commit_id=ref_name,
588 _query=dict(at=ref_name)
587 _query=dict(at=ref_name)
589 )
588 )
590
589
591 data.append({
590 data.append({
592 "name": _render('name', ref_name, files_url, closed),
591 "name": _render('name', ref_name, files_url, closed),
593 "name_raw": ref_name,
592 "name_raw": ref_name,
594 "date": _render('date', commit.date),
593 "date": _render('date', commit.date),
595 "date_raw": datetime_to_time(commit.date),
594 "date_raw": datetime_to_time(commit.date),
596 "author": _render('author', commit.author),
595 "author": _render('author', commit.author),
597 "commit": _render(
596 "commit": _render(
598 'commit', commit.message, commit.raw_id, commit.idx),
597 'commit', commit.message, commit.raw_id, commit.idx),
599 "commit_raw": commit.idx,
598 "commit_raw": commit.idx,
600 "compare": _render(
599 "compare": _render(
601 'compare', format_ref_id(ref_name, commit.raw_id)),
600 'compare', format_ref_id(ref_name, commit.raw_id)),
602 })
601 })
603
602
604 return data
603 return data
605
604
606
605
607 class RepoRoutePredicate(object):
606 class RepoRoutePredicate(object):
608 def __init__(self, val, config):
607 def __init__(self, val, config):
609 self.val = val
608 self.val = val
610
609
611 def text(self):
610 def text(self):
612 return 'repo_route = %s' % self.val
611 return 'repo_route = %s' % self.val
613
612
614 phash = text
613 phash = text
615
614
616 def __call__(self, info, request):
615 def __call__(self, info, request):
617 if hasattr(request, 'vcs_call'):
616 if hasattr(request, 'vcs_call'):
618 # skip vcs calls
617 # skip vcs calls
619 return
618 return
620
619
621 repo_name = info['match']['repo_name']
620 repo_name = info['match']['repo_name']
622
621
623 repo_name_parts = repo_name.split('/')
622 repo_name_parts = repo_name.split('/')
624 repo_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_name_parts)]
623 repo_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_name_parts)]
625
624
626 if repo_name_parts != repo_slugs:
625 if repo_name_parts != repo_slugs:
627 # short-skip if the repo-name doesn't follow slug rule
626 # short-skip if the repo-name doesn't follow slug rule
628 log.warning('repo_name: %s is different than slug %s', repo_name_parts, repo_slugs)
627 log.warning('repo_name: %s is different than slug %s', repo_name_parts, repo_slugs)
629 return False
628 return False
630
629
631 repo_model = repo.RepoModel()
630 repo_model = repo.RepoModel()
632
631
633 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
632 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
634
633
635 def redirect_if_creating(route_info, db_repo):
634 def redirect_if_creating(route_info, db_repo):
636 skip_views = ['edit_repo_advanced_delete']
635 skip_views = ['edit_repo_advanced_delete']
637 route = route_info['route']
636 route = route_info['route']
638 # we should skip delete view so we can actually "remove" repositories
637 # we should skip delete view so we can actually "remove" repositories
639 # if they get stuck in creating state.
638 # if they get stuck in creating state.
640 if route.name in skip_views:
639 if route.name in skip_views:
641 return
640 return
642
641
643 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
642 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
644 repo_creating_url = request.route_path(
643 repo_creating_url = request.route_path(
645 'repo_creating', repo_name=db_repo.repo_name)
644 'repo_creating', repo_name=db_repo.repo_name)
646 raise HTTPFound(repo_creating_url)
645 raise HTTPFound(repo_creating_url)
647
646
648 if by_name_match:
647 if by_name_match:
649 # register this as request object we can re-use later
648 # register this as request object we can re-use later
650 request.db_repo = by_name_match
649 request.db_repo = by_name_match
651 redirect_if_creating(info, by_name_match)
650 redirect_if_creating(info, by_name_match)
652 return True
651 return True
653
652
654 by_id_match = repo_model.get_repo_by_id(repo_name)
653 by_id_match = repo_model.get_repo_by_id(repo_name)
655 if by_id_match:
654 if by_id_match:
656 request.db_repo = by_id_match
655 request.db_repo = by_id_match
657 redirect_if_creating(info, by_id_match)
656 redirect_if_creating(info, by_id_match)
658 return True
657 return True
659
658
660 return False
659 return False
661
660
662
661
663 class RepoForbidArchivedRoutePredicate(object):
662 class RepoForbidArchivedRoutePredicate(object):
664 def __init__(self, val, config):
663 def __init__(self, val, config):
665 self.val = val
664 self.val = val
666
665
667 def text(self):
666 def text(self):
668 return 'repo_forbid_archived = %s' % self.val
667 return 'repo_forbid_archived = %s' % self.val
669
668
670 phash = text
669 phash = text
671
670
672 def __call__(self, info, request):
671 def __call__(self, info, request):
673 _ = request.translate
672 _ = request.translate
674 rhodecode_db_repo = request.db_repo
673 rhodecode_db_repo = request.db_repo
675
674
676 log.debug(
675 log.debug(
677 '%s checking if archived flag for repo for %s',
676 '%s checking if archived flag for repo for %s',
678 self.__class__.__name__, rhodecode_db_repo.repo_name)
677 self.__class__.__name__, rhodecode_db_repo.repo_name)
679
678
680 if rhodecode_db_repo.archived:
679 if rhodecode_db_repo.archived:
681 log.warning('Current view is not supported for archived repo:%s',
680 log.warning('Current view is not supported for archived repo:%s',
682 rhodecode_db_repo.repo_name)
681 rhodecode_db_repo.repo_name)
683
682
684 h.flash(
683 h.flash(
685 h.literal(_('Action not supported for archived repository.')),
684 h.literal(_('Action not supported for archived repository.')),
686 category='warning')
685 category='warning')
687 summary_url = request.route_path(
686 summary_url = request.route_path(
688 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
687 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
689 raise HTTPFound(summary_url)
688 raise HTTPFound(summary_url)
690 return True
689 return True
691
690
692
691
693 class RepoTypeRoutePredicate(object):
692 class RepoTypeRoutePredicate(object):
694 def __init__(self, val, config):
693 def __init__(self, val, config):
695 self.val = val or ['hg', 'git', 'svn']
694 self.val = val or ['hg', 'git', 'svn']
696
695
697 def text(self):
696 def text(self):
698 return 'repo_accepted_type = %s' % self.val
697 return 'repo_accepted_type = %s' % self.val
699
698
700 phash = text
699 phash = text
701
700
702 def __call__(self, info, request):
701 def __call__(self, info, request):
703 if hasattr(request, 'vcs_call'):
702 if hasattr(request, 'vcs_call'):
704 # skip vcs calls
703 # skip vcs calls
705 return
704 return
706
705
707 rhodecode_db_repo = request.db_repo
706 rhodecode_db_repo = request.db_repo
708
707
709 log.debug(
708 log.debug(
710 '%s checking repo type for %s in %s',
709 '%s checking repo type for %s in %s',
711 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
710 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
712
711
713 if rhodecode_db_repo.repo_type in self.val:
712 if rhodecode_db_repo.repo_type in self.val:
714 return True
713 return True
715 else:
714 else:
716 log.warning('Current view is not supported for repo type:%s',
715 log.warning('Current view is not supported for repo type:%s',
717 rhodecode_db_repo.repo_type)
716 rhodecode_db_repo.repo_type)
718 return False
717 return False
719
718
720
719
721 class RepoGroupRoutePredicate(object):
720 class RepoGroupRoutePredicate(object):
722 def __init__(self, val, config):
721 def __init__(self, val, config):
723 self.val = val
722 self.val = val
724
723
725 def text(self):
724 def text(self):
726 return 'repo_group_route = %s' % self.val
725 return 'repo_group_route = %s' % self.val
727
726
728 phash = text
727 phash = text
729
728
730 def __call__(self, info, request):
729 def __call__(self, info, request):
731 if hasattr(request, 'vcs_call'):
730 if hasattr(request, 'vcs_call'):
732 # skip vcs calls
731 # skip vcs calls
733 return
732 return
734
733
735 repo_group_name = info['match']['repo_group_name']
734 repo_group_name = info['match']['repo_group_name']
736
735
737 repo_group_name_parts = repo_group_name.split('/')
736 repo_group_name_parts = repo_group_name.split('/')
738 repo_group_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_group_name_parts)]
737 repo_group_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_group_name_parts)]
739 if repo_group_name_parts != repo_group_slugs:
738 if repo_group_name_parts != repo_group_slugs:
740 # short-skip if the repo-name doesn't follow slug rule
739 # short-skip if the repo-name doesn't follow slug rule
741 log.warning('repo_group_name: %s is different than slug %s', repo_group_name_parts, repo_group_slugs)
740 log.warning('repo_group_name: %s is different than slug %s', repo_group_name_parts, repo_group_slugs)
742 return False
741 return False
743
742
744 repo_group_model = repo_group.RepoGroupModel()
743 repo_group_model = repo_group.RepoGroupModel()
745 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
744 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
746
745
747 if by_name_match:
746 if by_name_match:
748 # register this as request object we can re-use later
747 # register this as request object we can re-use later
749 request.db_repo_group = by_name_match
748 request.db_repo_group = by_name_match
750 return True
749 return True
751
750
752 return False
751 return False
753
752
754
753
755 class UserGroupRoutePredicate(object):
754 class UserGroupRoutePredicate(object):
756 def __init__(self, val, config):
755 def __init__(self, val, config):
757 self.val = val
756 self.val = val
758
757
759 def text(self):
758 def text(self):
760 return 'user_group_route = %s' % self.val
759 return 'user_group_route = %s' % self.val
761
760
762 phash = text
761 phash = text
763
762
764 def __call__(self, info, request):
763 def __call__(self, info, request):
765 if hasattr(request, 'vcs_call'):
764 if hasattr(request, 'vcs_call'):
766 # skip vcs calls
765 # skip vcs calls
767 return
766 return
768
767
769 user_group_id = info['match']['user_group_id']
768 user_group_id = info['match']['user_group_id']
770 user_group_model = user_group.UserGroup()
769 user_group_model = user_group.UserGroup()
771 by_id_match = user_group_model.get(user_group_id, cache=False)
770 by_id_match = user_group_model.get(user_group_id, cache=False)
772
771
773 if by_id_match:
772 if by_id_match:
774 # register this as request object we can re-use later
773 # register this as request object we can re-use later
775 request.db_user_group = by_id_match
774 request.db_user_group = by_id_match
776 return True
775 return True
777
776
778 return False
777 return False
779
778
780
779
781 class UserRoutePredicateBase(object):
780 class UserRoutePredicateBase(object):
782 supports_default = None
781 supports_default = None
783
782
784 def __init__(self, val, config):
783 def __init__(self, val, config):
785 self.val = val
784 self.val = val
786
785
787 def text(self):
786 def text(self):
788 raise NotImplementedError()
787 raise NotImplementedError()
789
788
790 def __call__(self, info, request):
789 def __call__(self, info, request):
791 if hasattr(request, 'vcs_call'):
790 if hasattr(request, 'vcs_call'):
792 # skip vcs calls
791 # skip vcs calls
793 return
792 return
794
793
795 user_id = info['match']['user_id']
794 user_id = info['match']['user_id']
796 user_model = user.User()
795 user_model = user.User()
797 by_id_match = user_model.get(user_id, cache=False)
796 by_id_match = user_model.get(user_id, cache=False)
798
797
799 if by_id_match:
798 if by_id_match:
800 # register this as request object we can re-use later
799 # register this as request object we can re-use later
801 request.db_user = by_id_match
800 request.db_user = by_id_match
802 request.db_user_supports_default = self.supports_default
801 request.db_user_supports_default = self.supports_default
803 return True
802 return True
804
803
805 return False
804 return False
806
805
807
806
808 class UserRoutePredicate(UserRoutePredicateBase):
807 class UserRoutePredicate(UserRoutePredicateBase):
809 supports_default = False
808 supports_default = False
810
809
811 def text(self):
810 def text(self):
812 return 'user_route = %s' % self.val
811 return 'user_route = %s' % self.val
813
812
814 phash = text
813 phash = text
815
814
816
815
817 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
816 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
818 supports_default = True
817 supports_default = True
819
818
820 def text(self):
819 def text(self):
821 return 'user_with_default_route = %s' % self.val
820 return 'user_with_default_route = %s' % self.val
822
821
823 phash = text
822 phash = text
824
823
825
824
826 def includeme(config):
825 def includeme(config):
827 config.add_route_predicate(
826 config.add_route_predicate(
828 'repo_route', RepoRoutePredicate)
827 'repo_route', RepoRoutePredicate)
829 config.add_route_predicate(
828 config.add_route_predicate(
830 'repo_accepted_types', RepoTypeRoutePredicate)
829 'repo_accepted_types', RepoTypeRoutePredicate)
831 config.add_route_predicate(
830 config.add_route_predicate(
832 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
831 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
833 config.add_route_predicate(
832 config.add_route_predicate(
834 'repo_group_route', RepoGroupRoutePredicate)
833 'repo_group_route', RepoGroupRoutePredicate)
835 config.add_route_predicate(
834 config.add_route_predicate(
836 'user_group_route', UserGroupRoutePredicate)
835 'user_group_route', UserGroupRoutePredicate)
837 config.add_route_predicate(
836 config.add_route_predicate(
838 'user_route_with_default', UserRouteWithDefaultPredicate)
837 'user_route_with_default', UserRouteWithDefaultPredicate)
839 config.add_route_predicate(
838 config.add_route_predicate(
840 'user_route', UserRoutePredicate)
839 'user_route', UserRoutePredicate)
@@ -1,91 +1,90 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
20 import os
21 import logging
21 import logging
22 from pyramid import compat
23
22
24 # Do not use `from rhodecode import events` here, it will be overridden by the
23 # Do not use `from rhodecode import events` here, it will be overridden by the
25 # events module in this package due to pythons import mechanism.
24 # events module in this package due to pythons import mechanism.
26 from rhodecode.events import RepoGroupEvent
25 from rhodecode.events import RepoGroupEvent
27 from rhodecode.subscribers import AsyncSubprocessSubscriber
26 from rhodecode.subscribers import AsyncSubprocessSubscriber
28 from rhodecode.config.settings_maker import SettingsMaker
27 from rhodecode.config.settings_maker import SettingsMaker
29
28
30 from .events import ModDavSvnConfigChange
29 from .events import ModDavSvnConfigChange
31 from .subscribers import generate_config_subscriber
30 from .subscribers import generate_config_subscriber
32 from . import config_keys
31 from . import config_keys
33
32
34
33
35 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
36
35
37
36
38 def _sanitize_settings_and_apply_defaults(settings):
37 def _sanitize_settings_and_apply_defaults(settings):
39 """
38 """
40 Set defaults, convert to python types and validate settings.
39 Set defaults, convert to python types and validate settings.
41 """
40 """
42 settings_maker = SettingsMaker(settings)
41 settings_maker = SettingsMaker(settings)
43 settings_maker.make_setting(config_keys.generate_config, False, parser='bool')
42 settings_maker.make_setting(config_keys.generate_config, False, parser='bool')
44 settings_maker.make_setting(config_keys.list_parent_path, True, parser='bool')
43 settings_maker.make_setting(config_keys.list_parent_path, True, parser='bool')
45 settings_maker.make_setting(config_keys.reload_timeout, 10, parser='bool')
44 settings_maker.make_setting(config_keys.reload_timeout, 10, parser='bool')
46 settings_maker.make_setting(config_keys.config_file_path, '')
45 settings_maker.make_setting(config_keys.config_file_path, '')
47 settings_maker.make_setting(config_keys.location_root, '/')
46 settings_maker.make_setting(config_keys.location_root, '/')
48 settings_maker.make_setting(config_keys.reload_command, '')
47 settings_maker.make_setting(config_keys.reload_command, '')
49 settings_maker.make_setting(config_keys.template, '')
48 settings_maker.make_setting(config_keys.template, '')
50
49
51 settings_maker.env_expand()
50 settings_maker.env_expand()
52
51
53 # Convert negative timeout values to zero.
52 # Convert negative timeout values to zero.
54 if settings[config_keys.reload_timeout] < 0:
53 if settings[config_keys.reload_timeout] < 0:
55 settings[config_keys.reload_timeout] = 0
54 settings[config_keys.reload_timeout] = 0
56
55
57 # Append path separator to location root.
56 # Append path separator to location root.
58 settings[config_keys.location_root] = _append_path_sep(
57 settings[config_keys.location_root] = _append_path_sep(
59 settings[config_keys.location_root])
58 settings[config_keys.location_root])
60
59
61 # Validate settings.
60 # Validate settings.
62 if settings[config_keys.generate_config]:
61 if settings[config_keys.generate_config]:
63 assert len(settings[config_keys.config_file_path]) > 0
62 assert len(settings[config_keys.config_file_path]) > 0
64
63
65
64
66 def _append_path_sep(path):
65 def _append_path_sep(path):
67 """
66 """
68 Append the path separator if missing.
67 Append the path separator if missing.
69 """
68 """
70 if isinstance(path, compat.string_types) and not path.endswith(os.path.sep):
69 if isinstance(path, str) and not path.endswith(os.path.sep):
71 path += os.path.sep
70 path += os.path.sep
72 return path
71 return path
73
72
74
73
75 def includeme(config):
74 def includeme(config):
76 settings = config.registry.settings
75 settings = config.registry.settings
77 _sanitize_settings_and_apply_defaults(settings)
76 _sanitize_settings_and_apply_defaults(settings)
78
77
79 if settings[config_keys.generate_config]:
78 if settings[config_keys.generate_config]:
80 # Add subscriber to generate the Apache mod dav svn configuration on
79 # Add subscriber to generate the Apache mod dav svn configuration on
81 # repository group events.
80 # repository group events.
82 config.add_subscriber(generate_config_subscriber, RepoGroupEvent)
81 config.add_subscriber(generate_config_subscriber, RepoGroupEvent)
83
82
84 # If a reload command is set add a subscriber to execute it on
83 # If a reload command is set add a subscriber to execute it on
85 # configuration changes.
84 # configuration changes.
86 reload_cmd = settings[config_keys.reload_command]
85 reload_cmd = settings[config_keys.reload_command]
87 if reload_cmd:
86 if reload_cmd:
88 reload_timeout = settings[config_keys.reload_timeout] or None
87 reload_timeout = settings[config_keys.reload_timeout] or None
89 reload_subscriber = AsyncSubprocessSubscriber(
88 reload_subscriber = AsyncSubprocessSubscriber(
90 cmd=reload_cmd, timeout=reload_timeout)
89 cmd=reload_cmd, timeout=reload_timeout)
91 config.add_subscriber(reload_subscriber, ModDavSvnConfigChange)
90 config.add_subscriber(reload_subscriber, ModDavSvnConfigChange)
@@ -1,415 +1,414 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode task modules, containing all task that suppose to be run
22 RhodeCode task modules, containing all task that suppose to be run
23 by celery daemon
23 by celery daemon
24 """
24 """
25
25
26 import os
26 import os
27 import time
27 import time
28
28
29 from pyramid import compat
30 from pyramid_mailer.mailer import Mailer
29 from pyramid_mailer.mailer import Mailer
31 from pyramid_mailer.message import Message
30 from pyramid_mailer.message import Message
32 from email.utils import formatdate
31 from email.utils import formatdate
33
32
34 import rhodecode
33 import rhodecode
35 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
36 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask, run_task
35 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask, run_task
37 from rhodecode.lib import hooks_base
36 from rhodecode.lib import hooks_base
38 from rhodecode.lib.utils2 import safe_int, str2bool, aslist
37 from rhodecode.lib.utils2 import safe_int, str2bool, aslist
39 from rhodecode.lib.statsd_client import StatsdClient
38 from rhodecode.lib.statsd_client import StatsdClient
40 from rhodecode.model.db import (
39 from rhodecode.model.db import (
41 Session, IntegrityError, true, Repository, RepoGroup, User)
40 Session, IntegrityError, true, Repository, RepoGroup, User)
42 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.permission import PermissionModel
43
42
44
43
45 @async_task(ignore_result=True, base=RequestContextTask)
44 @async_task(ignore_result=True, base=RequestContextTask)
46 def send_email(recipients, subject, body='', html_body='', email_config=None,
45 def send_email(recipients, subject, body='', html_body='', email_config=None,
47 extra_headers=None):
46 extra_headers=None):
48 """
47 """
49 Sends an email with defined parameters from the .ini files.
48 Sends an email with defined parameters from the .ini files.
50
49
51 :param recipients: list of recipients, it this is empty the defined email
50 :param recipients: list of recipients, it this is empty the defined email
52 address from field 'email_to' is used instead
51 address from field 'email_to' is used instead
53 :param subject: subject of the mail
52 :param subject: subject of the mail
54 :param body: body of the mail
53 :param body: body of the mail
55 :param html_body: html version of body
54 :param html_body: html version of body
56 :param email_config: specify custom configuration for mailer
55 :param email_config: specify custom configuration for mailer
57 :param extra_headers: specify custom headers
56 :param extra_headers: specify custom headers
58 """
57 """
59 log = get_logger(send_email)
58 log = get_logger(send_email)
60
59
61 email_config = email_config or rhodecode.CONFIG
60 email_config = email_config or rhodecode.CONFIG
62
61
63 mail_server = email_config.get('smtp_server') or None
62 mail_server = email_config.get('smtp_server') or None
64 if mail_server is None:
63 if mail_server is None:
65 log.error("SMTP server information missing. Sending email failed. "
64 log.error("SMTP server information missing. Sending email failed. "
66 "Make sure that `smtp_server` variable is configured "
65 "Make sure that `smtp_server` variable is configured "
67 "inside the .ini file")
66 "inside the .ini file")
68 return False
67 return False
69
68
70 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
69 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
71
70
72 if recipients:
71 if recipients:
73 if isinstance(recipients, compat.string_types):
72 if isinstance(recipients, str):
74 recipients = recipients.split(',')
73 recipients = recipients.split(',')
75 else:
74 else:
76 # if recipients are not defined we send to email_config + all admins
75 # if recipients are not defined we send to email_config + all admins
77 admins = []
76 admins = []
78 for u in User.query().filter(User.admin == true()).all():
77 for u in User.query().filter(User.admin == true()).all():
79 if u.email:
78 if u.email:
80 admins.append(u.email)
79 admins.append(u.email)
81 recipients = []
80 recipients = []
82 config_email = email_config.get('email_to')
81 config_email = email_config.get('email_to')
83 if config_email:
82 if config_email:
84 recipients += [config_email]
83 recipients += [config_email]
85 recipients += admins
84 recipients += admins
86
85
87 # translate our LEGACY config into the one that pyramid_mailer supports
86 # translate our LEGACY config into the one that pyramid_mailer supports
88 email_conf = dict(
87 email_conf = dict(
89 host=mail_server,
88 host=mail_server,
90 port=email_config.get('smtp_port', 25),
89 port=email_config.get('smtp_port', 25),
91 username=email_config.get('smtp_username'),
90 username=email_config.get('smtp_username'),
92 password=email_config.get('smtp_password'),
91 password=email_config.get('smtp_password'),
93
92
94 tls=str2bool(email_config.get('smtp_use_tls')),
93 tls=str2bool(email_config.get('smtp_use_tls')),
95 ssl=str2bool(email_config.get('smtp_use_ssl')),
94 ssl=str2bool(email_config.get('smtp_use_ssl')),
96
95
97 # SSL key file
96 # SSL key file
98 # keyfile='',
97 # keyfile='',
99
98
100 # SSL certificate file
99 # SSL certificate file
101 # certfile='',
100 # certfile='',
102
101
103 # Location of maildir
102 # Location of maildir
104 # queue_path='',
103 # queue_path='',
105
104
106 default_sender=email_config.get('app_email_from', 'RhodeCode-noreply@rhodecode.com'),
105 default_sender=email_config.get('app_email_from', 'RhodeCode-noreply@rhodecode.com'),
107
106
108 debug=str2bool(email_config.get('smtp_debug')),
107 debug=str2bool(email_config.get('smtp_debug')),
109 # /usr/sbin/sendmail Sendmail executable
108 # /usr/sbin/sendmail Sendmail executable
110 # sendmail_app='',
109 # sendmail_app='',
111
110
112 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
111 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
113 # sendmail_template='',
112 # sendmail_template='',
114 )
113 )
115
114
116 if extra_headers is None:
115 if extra_headers is None:
117 extra_headers = {}
116 extra_headers = {}
118
117
119 extra_headers.setdefault('Date', formatdate(time.time()))
118 extra_headers.setdefault('Date', formatdate(time.time()))
120
119
121 if 'thread_ids' in extra_headers:
120 if 'thread_ids' in extra_headers:
122 thread_ids = extra_headers.pop('thread_ids')
121 thread_ids = extra_headers.pop('thread_ids')
123 extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
122 extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
124
123
125 try:
124 try:
126 mailer = Mailer(**email_conf)
125 mailer = Mailer(**email_conf)
127
126
128 message = Message(subject=subject,
127 message = Message(subject=subject,
129 sender=email_conf['default_sender'],
128 sender=email_conf['default_sender'],
130 recipients=recipients,
129 recipients=recipients,
131 body=body, html=html_body,
130 body=body, html=html_body,
132 extra_headers=extra_headers)
131 extra_headers=extra_headers)
133 mailer.send_immediately(message)
132 mailer.send_immediately(message)
134 statsd = StatsdClient.statsd
133 statsd = StatsdClient.statsd
135 if statsd:
134 if statsd:
136 statsd.incr('rhodecode_email_sent_total')
135 statsd.incr('rhodecode_email_sent_total')
137
136
138 except Exception:
137 except Exception:
139 log.exception('Mail sending failed')
138 log.exception('Mail sending failed')
140 return False
139 return False
141 return True
140 return True
142
141
143
142
144 @async_task(ignore_result=True, base=RequestContextTask)
143 @async_task(ignore_result=True, base=RequestContextTask)
145 def create_repo(form_data, cur_user):
144 def create_repo(form_data, cur_user):
146 from rhodecode.model.repo import RepoModel
145 from rhodecode.model.repo import RepoModel
147 from rhodecode.model.user import UserModel
146 from rhodecode.model.user import UserModel
148 from rhodecode.model.scm import ScmModel
147 from rhodecode.model.scm import ScmModel
149 from rhodecode.model.settings import SettingsModel
148 from rhodecode.model.settings import SettingsModel
150
149
151 log = get_logger(create_repo)
150 log = get_logger(create_repo)
152
151
153 cur_user = UserModel()._get_user(cur_user)
152 cur_user = UserModel()._get_user(cur_user)
154 owner = cur_user
153 owner = cur_user
155
154
156 repo_name = form_data['repo_name']
155 repo_name = form_data['repo_name']
157 repo_name_full = form_data['repo_name_full']
156 repo_name_full = form_data['repo_name_full']
158 repo_type = form_data['repo_type']
157 repo_type = form_data['repo_type']
159 description = form_data['repo_description']
158 description = form_data['repo_description']
160 private = form_data['repo_private']
159 private = form_data['repo_private']
161 clone_uri = form_data.get('clone_uri')
160 clone_uri = form_data.get('clone_uri')
162 repo_group = safe_int(form_data['repo_group'])
161 repo_group = safe_int(form_data['repo_group'])
163 copy_fork_permissions = form_data.get('copy_permissions')
162 copy_fork_permissions = form_data.get('copy_permissions')
164 copy_group_permissions = form_data.get('repo_copy_permissions')
163 copy_group_permissions = form_data.get('repo_copy_permissions')
165 fork_of = form_data.get('fork_parent_id')
164 fork_of = form_data.get('fork_parent_id')
166 state = form_data.get('repo_state', Repository.STATE_PENDING)
165 state = form_data.get('repo_state', Repository.STATE_PENDING)
167
166
168 # repo creation defaults, private and repo_type are filled in form
167 # repo creation defaults, private and repo_type are filled in form
169 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
168 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
170 enable_statistics = form_data.get(
169 enable_statistics = form_data.get(
171 'enable_statistics', defs.get('repo_enable_statistics'))
170 'enable_statistics', defs.get('repo_enable_statistics'))
172 enable_locking = form_data.get(
171 enable_locking = form_data.get(
173 'enable_locking', defs.get('repo_enable_locking'))
172 'enable_locking', defs.get('repo_enable_locking'))
174 enable_downloads = form_data.get(
173 enable_downloads = form_data.get(
175 'enable_downloads', defs.get('repo_enable_downloads'))
174 'enable_downloads', defs.get('repo_enable_downloads'))
176
175
177 # set landing rev based on default branches for SCM
176 # set landing rev based on default branches for SCM
178 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
177 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
179
178
180 try:
179 try:
181 RepoModel()._create_repo(
180 RepoModel()._create_repo(
182 repo_name=repo_name_full,
181 repo_name=repo_name_full,
183 repo_type=repo_type,
182 repo_type=repo_type,
184 description=description,
183 description=description,
185 owner=owner,
184 owner=owner,
186 private=private,
185 private=private,
187 clone_uri=clone_uri,
186 clone_uri=clone_uri,
188 repo_group=repo_group,
187 repo_group=repo_group,
189 landing_rev=landing_ref,
188 landing_rev=landing_ref,
190 fork_of=fork_of,
189 fork_of=fork_of,
191 copy_fork_permissions=copy_fork_permissions,
190 copy_fork_permissions=copy_fork_permissions,
192 copy_group_permissions=copy_group_permissions,
191 copy_group_permissions=copy_group_permissions,
193 enable_statistics=enable_statistics,
192 enable_statistics=enable_statistics,
194 enable_locking=enable_locking,
193 enable_locking=enable_locking,
195 enable_downloads=enable_downloads,
194 enable_downloads=enable_downloads,
196 state=state
195 state=state
197 )
196 )
198 Session().commit()
197 Session().commit()
199
198
200 # now create this repo on Filesystem
199 # now create this repo on Filesystem
201 RepoModel()._create_filesystem_repo(
200 RepoModel()._create_filesystem_repo(
202 repo_name=repo_name,
201 repo_name=repo_name,
203 repo_type=repo_type,
202 repo_type=repo_type,
204 repo_group=RepoModel()._get_repo_group(repo_group),
203 repo_group=RepoModel()._get_repo_group(repo_group),
205 clone_uri=clone_uri,
204 clone_uri=clone_uri,
206 )
205 )
207 repo = Repository.get_by_repo_name(repo_name_full)
206 repo = Repository.get_by_repo_name(repo_name_full)
208 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
207 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
209
208
210 # update repo commit caches initially
209 # update repo commit caches initially
211 repo.update_commit_cache()
210 repo.update_commit_cache()
212
211
213 # set new created state
212 # set new created state
214 repo.set_state(Repository.STATE_CREATED)
213 repo.set_state(Repository.STATE_CREATED)
215 repo_id = repo.repo_id
214 repo_id = repo.repo_id
216 repo_data = repo.get_api_data()
215 repo_data = repo.get_api_data()
217
216
218 audit_logger.store(
217 audit_logger.store(
219 'repo.create', action_data={'data': repo_data},
218 'repo.create', action_data={'data': repo_data},
220 user=cur_user,
219 user=cur_user,
221 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
220 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
222
221
223 Session().commit()
222 Session().commit()
224
223
225 PermissionModel().trigger_permission_flush()
224 PermissionModel().trigger_permission_flush()
226
225
227 except Exception as e:
226 except Exception as e:
228 log.warning('Exception occurred when creating repository, '
227 log.warning('Exception occurred when creating repository, '
229 'doing cleanup...', exc_info=True)
228 'doing cleanup...', exc_info=True)
230 if isinstance(e, IntegrityError):
229 if isinstance(e, IntegrityError):
231 Session().rollback()
230 Session().rollback()
232
231
233 # rollback things manually !
232 # rollback things manually !
234 repo = Repository.get_by_repo_name(repo_name_full)
233 repo = Repository.get_by_repo_name(repo_name_full)
235 if repo:
234 if repo:
236 Repository.delete(repo.repo_id)
235 Repository.delete(repo.repo_id)
237 Session().commit()
236 Session().commit()
238 RepoModel()._delete_filesystem_repo(repo)
237 RepoModel()._delete_filesystem_repo(repo)
239 log.info('Cleanup of repo %s finished', repo_name_full)
238 log.info('Cleanup of repo %s finished', repo_name_full)
240 raise
239 raise
241
240
242 return True
241 return True
243
242
244
243
245 @async_task(ignore_result=True, base=RequestContextTask)
244 @async_task(ignore_result=True, base=RequestContextTask)
246 def create_repo_fork(form_data, cur_user):
245 def create_repo_fork(form_data, cur_user):
247 """
246 """
248 Creates a fork of repository using internal VCS methods
247 Creates a fork of repository using internal VCS methods
249 """
248 """
250 from rhodecode.model.repo import RepoModel
249 from rhodecode.model.repo import RepoModel
251 from rhodecode.model.user import UserModel
250 from rhodecode.model.user import UserModel
252
251
253 log = get_logger(create_repo_fork)
252 log = get_logger(create_repo_fork)
254
253
255 cur_user = UserModel()._get_user(cur_user)
254 cur_user = UserModel()._get_user(cur_user)
256 owner = cur_user
255 owner = cur_user
257
256
258 repo_name = form_data['repo_name'] # fork in this case
257 repo_name = form_data['repo_name'] # fork in this case
259 repo_name_full = form_data['repo_name_full']
258 repo_name_full = form_data['repo_name_full']
260 repo_type = form_data['repo_type']
259 repo_type = form_data['repo_type']
261 description = form_data['description']
260 description = form_data['description']
262 private = form_data['private']
261 private = form_data['private']
263 clone_uri = form_data.get('clone_uri')
262 clone_uri = form_data.get('clone_uri')
264 repo_group = safe_int(form_data['repo_group'])
263 repo_group = safe_int(form_data['repo_group'])
265 landing_ref = form_data['landing_rev']
264 landing_ref = form_data['landing_rev']
266 copy_fork_permissions = form_data.get('copy_permissions')
265 copy_fork_permissions = form_data.get('copy_permissions')
267 fork_id = safe_int(form_data.get('fork_parent_id'))
266 fork_id = safe_int(form_data.get('fork_parent_id'))
268
267
269 try:
268 try:
270 fork_of = RepoModel()._get_repo(fork_id)
269 fork_of = RepoModel()._get_repo(fork_id)
271 RepoModel()._create_repo(
270 RepoModel()._create_repo(
272 repo_name=repo_name_full,
271 repo_name=repo_name_full,
273 repo_type=repo_type,
272 repo_type=repo_type,
274 description=description,
273 description=description,
275 owner=owner,
274 owner=owner,
276 private=private,
275 private=private,
277 clone_uri=clone_uri,
276 clone_uri=clone_uri,
278 repo_group=repo_group,
277 repo_group=repo_group,
279 landing_rev=landing_ref,
278 landing_rev=landing_ref,
280 fork_of=fork_of,
279 fork_of=fork_of,
281 copy_fork_permissions=copy_fork_permissions
280 copy_fork_permissions=copy_fork_permissions
282 )
281 )
283
282
284 Session().commit()
283 Session().commit()
285
284
286 base_path = Repository.base_path()
285 base_path = Repository.base_path()
287 source_repo_path = os.path.join(base_path, fork_of.repo_name)
286 source_repo_path = os.path.join(base_path, fork_of.repo_name)
288
287
289 # now create this repo on Filesystem
288 # now create this repo on Filesystem
290 RepoModel()._create_filesystem_repo(
289 RepoModel()._create_filesystem_repo(
291 repo_name=repo_name,
290 repo_name=repo_name,
292 repo_type=repo_type,
291 repo_type=repo_type,
293 repo_group=RepoModel()._get_repo_group(repo_group),
292 repo_group=RepoModel()._get_repo_group(repo_group),
294 clone_uri=source_repo_path,
293 clone_uri=source_repo_path,
295 )
294 )
296 repo = Repository.get_by_repo_name(repo_name_full)
295 repo = Repository.get_by_repo_name(repo_name_full)
297 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
296 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
298
297
299 # update repo commit caches initially
298 # update repo commit caches initially
300 config = repo._config
299 config = repo._config
301 config.set('extensions', 'largefiles', '')
300 config.set('extensions', 'largefiles', '')
302 repo.update_commit_cache(config=config)
301 repo.update_commit_cache(config=config)
303
302
304 # set new created state
303 # set new created state
305 repo.set_state(Repository.STATE_CREATED)
304 repo.set_state(Repository.STATE_CREATED)
306
305
307 repo_id = repo.repo_id
306 repo_id = repo.repo_id
308 repo_data = repo.get_api_data()
307 repo_data = repo.get_api_data()
309 audit_logger.store(
308 audit_logger.store(
310 'repo.fork', action_data={'data': repo_data},
309 'repo.fork', action_data={'data': repo_data},
311 user=cur_user,
310 user=cur_user,
312 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
311 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
313
312
314 Session().commit()
313 Session().commit()
315 except Exception as e:
314 except Exception as e:
316 log.warning('Exception occurred when forking repository, '
315 log.warning('Exception occurred when forking repository, '
317 'doing cleanup...', exc_info=True)
316 'doing cleanup...', exc_info=True)
318 if isinstance(e, IntegrityError):
317 if isinstance(e, IntegrityError):
319 Session().rollback()
318 Session().rollback()
320
319
321 # rollback things manually !
320 # rollback things manually !
322 repo = Repository.get_by_repo_name(repo_name_full)
321 repo = Repository.get_by_repo_name(repo_name_full)
323 if repo:
322 if repo:
324 Repository.delete(repo.repo_id)
323 Repository.delete(repo.repo_id)
325 Session().commit()
324 Session().commit()
326 RepoModel()._delete_filesystem_repo(repo)
325 RepoModel()._delete_filesystem_repo(repo)
327 log.info('Cleanup of repo %s finished', repo_name_full)
326 log.info('Cleanup of repo %s finished', repo_name_full)
328 raise
327 raise
329
328
330 return True
329 return True
331
330
332
331
333 @async_task(ignore_result=True, base=RequestContextTask)
332 @async_task(ignore_result=True, base=RequestContextTask)
334 def repo_maintenance(repoid):
333 def repo_maintenance(repoid):
335 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
334 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
336 log = get_logger(repo_maintenance)
335 log = get_logger(repo_maintenance)
337 repo = Repository.get_by_id_or_repo_name(repoid)
336 repo = Repository.get_by_id_or_repo_name(repoid)
338 if repo:
337 if repo:
339 maintenance = repo_maintenance_lib.RepoMaintenance()
338 maintenance = repo_maintenance_lib.RepoMaintenance()
340 tasks = maintenance.get_tasks_for_repo(repo)
339 tasks = maintenance.get_tasks_for_repo(repo)
341 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
340 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
342 executed_types = maintenance.execute(repo)
341 executed_types = maintenance.execute(repo)
343 log.debug('Got execution results %s', executed_types)
342 log.debug('Got execution results %s', executed_types)
344 else:
343 else:
345 log.debug('Repo `%s` not found or without a clone_url', repoid)
344 log.debug('Repo `%s` not found or without a clone_url', repoid)
346
345
347
346
348 @async_task(ignore_result=True, base=RequestContextTask)
347 @async_task(ignore_result=True, base=RequestContextTask)
349 def check_for_update(send_email_notification=True, email_recipients=None):
348 def check_for_update(send_email_notification=True, email_recipients=None):
350 from rhodecode.model.update import UpdateModel
349 from rhodecode.model.update import UpdateModel
351 from rhodecode.model.notification import EmailNotificationModel
350 from rhodecode.model.notification import EmailNotificationModel
352
351
353 log = get_logger(check_for_update)
352 log = get_logger(check_for_update)
354 update_url = UpdateModel().get_update_url()
353 update_url = UpdateModel().get_update_url()
355 cur_ver = rhodecode.__version__
354 cur_ver = rhodecode.__version__
356
355
357 try:
356 try:
358 data = UpdateModel().get_update_data(update_url)
357 data = UpdateModel().get_update_data(update_url)
359
358
360 current_ver = UpdateModel().get_stored_version(fallback=cur_ver)
359 current_ver = UpdateModel().get_stored_version(fallback=cur_ver)
361 latest_ver = data['versions'][0]['version']
360 latest_ver = data['versions'][0]['version']
362 UpdateModel().store_version(latest_ver)
361 UpdateModel().store_version(latest_ver)
363
362
364 if send_email_notification:
363 if send_email_notification:
365 log.debug('Send email notification is enabled. '
364 log.debug('Send email notification is enabled. '
366 'Current RhodeCode version: %s, latest known: %s', current_ver, latest_ver)
365 'Current RhodeCode version: %s, latest known: %s', current_ver, latest_ver)
367 if UpdateModel().is_outdated(current_ver, latest_ver):
366 if UpdateModel().is_outdated(current_ver, latest_ver):
368
367
369 email_kwargs = {
368 email_kwargs = {
370 'current_ver': current_ver,
369 'current_ver': current_ver,
371 'latest_ver': latest_ver,
370 'latest_ver': latest_ver,
372 }
371 }
373
372
374 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
373 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
375 EmailNotificationModel.TYPE_UPDATE_AVAILABLE, **email_kwargs)
374 EmailNotificationModel.TYPE_UPDATE_AVAILABLE, **email_kwargs)
376
375
377 email_recipients = aslist(email_recipients, sep=',') or \
376 email_recipients = aslist(email_recipients, sep=',') or \
378 [user.email for user in User.get_all_super_admins()]
377 [user.email for user in User.get_all_super_admins()]
379 run_task(send_email, email_recipients, subject,
378 run_task(send_email, email_recipients, subject,
380 email_body_plaintext, email_body)
379 email_body_plaintext, email_body)
381
380
382 except Exception:
381 except Exception:
383 log.exception('Failed to check for update')
382 log.exception('Failed to check for update')
384 raise
383 raise
385
384
386
385
387 def sync_last_update_for_objects(*args, **kwargs):
386 def sync_last_update_for_objects(*args, **kwargs):
388 skip_repos = kwargs.get('skip_repos')
387 skip_repos = kwargs.get('skip_repos')
389 if not skip_repos:
388 if not skip_repos:
390 repos = Repository.query() \
389 repos = Repository.query() \
391 .order_by(Repository.group_id.asc())
390 .order_by(Repository.group_id.asc())
392
391
393 for repo in repos:
392 for repo in repos:
394 repo.update_commit_cache()
393 repo.update_commit_cache()
395
394
396 skip_groups = kwargs.get('skip_groups')
395 skip_groups = kwargs.get('skip_groups')
397 if not skip_groups:
396 if not skip_groups:
398 repo_groups = RepoGroup.query() \
397 repo_groups = RepoGroup.query() \
399 .filter(RepoGroup.group_parent_id == None)
398 .filter(RepoGroup.group_parent_id == None)
400
399
401 for root_gr in repo_groups:
400 for root_gr in repo_groups:
402 for repo_gr in reversed(root_gr.recursive_groups()):
401 for repo_gr in reversed(root_gr.recursive_groups()):
403 repo_gr.update_commit_cache()
402 repo_gr.update_commit_cache()
404
403
405
404
406 @async_task(ignore_result=True, base=RequestContextTask)
405 @async_task(ignore_result=True, base=RequestContextTask)
407 def sync_last_update(*args, **kwargs):
406 def sync_last_update(*args, **kwargs):
408 sync_last_update_for_objects(*args, **kwargs)
407 sync_last_update_for_objects(*args, **kwargs)
409
408
410
409
411 @async_task(ignore_result=False)
410 @async_task(ignore_result=False)
412 def beat_check(*args, **kwargs):
411 def beat_check(*args, **kwargs):
413 log = get_logger(beat_check)
412 log = get_logger(beat_check)
414 log.info('%r: Got args: %r and kwargs %r', beat_check, args, kwargs)
413 log.info('%r: Got args: %r and kwargs %r', beat_check, args, kwargs)
415 return time.time()
414 return time.time()
@@ -1,798 +1,797 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import difflib
22 import difflib
23 from itertools import groupby
23 from itertools import groupby
24
24
25 from pygments import lex
25 from pygments import lex
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 from pygments.lexers.special import TextLexer, Token
27 from pygments.lexers.special import TextLexer, Token
28 from pygments.lexers import get_lexer_by_name
28 from pygments.lexers import get_lexer_by_name
29 from pyramid import compat
30
29
31 from rhodecode.lib.helpers import (
30 from rhodecode.lib.helpers import (
32 get_lexer_for_filenode, html_escape, get_custom_lexer)
31 get_lexer_for_filenode, html_escape, get_custom_lexer)
33 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
32 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
34 from rhodecode.lib.vcs.nodes import FileNode
33 from rhodecode.lib.vcs.nodes import FileNode
35 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
34 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
36 from rhodecode.lib.diff_match_patch import diff_match_patch
35 from rhodecode.lib.diff_match_patch import diff_match_patch
37 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
36 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
38
37
39
38
40 plain_text_lexer = get_lexer_by_name(
39 plain_text_lexer = get_lexer_by_name(
41 'text', stripall=False, stripnl=False, ensurenl=False)
40 'text', stripall=False, stripnl=False, ensurenl=False)
42
41
43
42
44 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
45
44
46
45
47 def filenode_as_lines_tokens(filenode, lexer=None):
46 def filenode_as_lines_tokens(filenode, lexer=None):
48 org_lexer = lexer
47 org_lexer = lexer
49 lexer = lexer or get_lexer_for_filenode(filenode)
48 lexer = lexer or get_lexer_for_filenode(filenode)
50 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
49 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
51 lexer, filenode, org_lexer)
50 lexer, filenode, org_lexer)
52 content = filenode.content
51 content = filenode.content
53 tokens = tokenize_string(content, lexer)
52 tokens = tokenize_string(content, lexer)
54 lines = split_token_stream(tokens, content)
53 lines = split_token_stream(tokens, content)
55 rv = list(lines)
54 rv = list(lines)
56 return rv
55 return rv
57
56
58
57
59 def tokenize_string(content, lexer):
58 def tokenize_string(content, lexer):
60 """
59 """
61 Use pygments to tokenize some content based on a lexer
60 Use pygments to tokenize some content based on a lexer
62 ensuring all original new lines and whitespace is preserved
61 ensuring all original new lines and whitespace is preserved
63 """
62 """
64
63
65 lexer.stripall = False
64 lexer.stripall = False
66 lexer.stripnl = False
65 lexer.stripnl = False
67 lexer.ensurenl = False
66 lexer.ensurenl = False
68
67
69 if isinstance(lexer, TextLexer):
68 if isinstance(lexer, TextLexer):
70 lexed = [(Token.Text, content)]
69 lexed = [(Token.Text, content)]
71 else:
70 else:
72 lexed = lex(content, lexer)
71 lexed = lex(content, lexer)
73
72
74 for token_type, token_text in lexed:
73 for token_type, token_text in lexed:
75 yield pygment_token_class(token_type), token_text
74 yield pygment_token_class(token_type), token_text
76
75
77
76
78 def split_token_stream(tokens, content):
77 def split_token_stream(tokens, content):
79 """
78 """
80 Take a list of (TokenType, text) tuples and split them by a string
79 Take a list of (TokenType, text) tuples and split them by a string
81
80
82 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
81 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
83 [(TEXT, 'some'), (TEXT, 'text'),
82 [(TEXT, 'some'), (TEXT, 'text'),
84 (TEXT, 'more'), (TEXT, 'text')]
83 (TEXT, 'more'), (TEXT, 'text')]
85 """
84 """
86
85
87 token_buffer = []
86 token_buffer = []
88 for token_class, token_text in tokens:
87 for token_class, token_text in tokens:
89 parts = token_text.split('\n')
88 parts = token_text.split('\n')
90 for part in parts[:-1]:
89 for part in parts[:-1]:
91 token_buffer.append((token_class, part))
90 token_buffer.append((token_class, part))
92 yield token_buffer
91 yield token_buffer
93 token_buffer = []
92 token_buffer = []
94
93
95 token_buffer.append((token_class, parts[-1]))
94 token_buffer.append((token_class, parts[-1]))
96
95
97 if token_buffer:
96 if token_buffer:
98 yield token_buffer
97 yield token_buffer
99 elif content:
98 elif content:
100 # this is a special case, we have the content, but tokenization didn't produce
99 # this is a special case, we have the content, but tokenization didn't produce
101 # any results. THis can happen if know file extensions like .css have some bogus
100 # any results. THis can happen if know file extensions like .css have some bogus
102 # unicode content without any newline characters
101 # unicode content without any newline characters
103 yield [(pygment_token_class(Token.Text), content)]
102 yield [(pygment_token_class(Token.Text), content)]
104
103
105
104
106 def filenode_as_annotated_lines_tokens(filenode):
105 def filenode_as_annotated_lines_tokens(filenode):
107 """
106 """
108 Take a file node and return a list of annotations => lines, if no annotation
107 Take a file node and return a list of annotations => lines, if no annotation
109 is found, it will be None.
108 is found, it will be None.
110
109
111 eg:
110 eg:
112
111
113 [
112 [
114 (annotation1, [
113 (annotation1, [
115 (1, line1_tokens_list),
114 (1, line1_tokens_list),
116 (2, line2_tokens_list),
115 (2, line2_tokens_list),
117 ]),
116 ]),
118 (annotation2, [
117 (annotation2, [
119 (3, line1_tokens_list),
118 (3, line1_tokens_list),
120 ]),
119 ]),
121 (None, [
120 (None, [
122 (4, line1_tokens_list),
121 (4, line1_tokens_list),
123 ]),
122 ]),
124 (annotation1, [
123 (annotation1, [
125 (5, line1_tokens_list),
124 (5, line1_tokens_list),
126 (6, line2_tokens_list),
125 (6, line2_tokens_list),
127 ])
126 ])
128 ]
127 ]
129 """
128 """
130
129
131 commit_cache = {} # cache commit_getter lookups
130 commit_cache = {} # cache commit_getter lookups
132
131
133 def _get_annotation(commit_id, commit_getter):
132 def _get_annotation(commit_id, commit_getter):
134 if commit_id not in commit_cache:
133 if commit_id not in commit_cache:
135 commit_cache[commit_id] = commit_getter()
134 commit_cache[commit_id] = commit_getter()
136 return commit_cache[commit_id]
135 return commit_cache[commit_id]
137
136
138 annotation_lookup = {
137 annotation_lookup = {
139 line_no: _get_annotation(commit_id, commit_getter)
138 line_no: _get_annotation(commit_id, commit_getter)
140 for line_no, commit_id, commit_getter, line_content
139 for line_no, commit_id, commit_getter, line_content
141 in filenode.annotate
140 in filenode.annotate
142 }
141 }
143
142
144 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
143 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
145 for line_no, tokens
144 for line_no, tokens
146 in enumerate(filenode_as_lines_tokens(filenode), 1))
145 in enumerate(filenode_as_lines_tokens(filenode), 1))
147
146
148 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
147 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
149
148
150 for annotation, group in grouped_annotations_lines:
149 for annotation, group in grouped_annotations_lines:
151 yield (
150 yield (
152 annotation, [(line_no, tokens)
151 annotation, [(line_no, tokens)
153 for (_, line_no, tokens) in group]
152 for (_, line_no, tokens) in group]
154 )
153 )
155
154
156
155
157 def render_tokenstream(tokenstream):
156 def render_tokenstream(tokenstream):
158 result = []
157 result = []
159 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
158 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
160
159
161 if token_class:
160 if token_class:
162 result.append(u'<span class="%s">' % token_class)
161 result.append(u'<span class="%s">' % token_class)
163 else:
162 else:
164 result.append(u'<span>')
163 result.append(u'<span>')
165
164
166 for op_tag, token_text in token_ops_texts:
165 for op_tag, token_text in token_ops_texts:
167
166
168 if op_tag:
167 if op_tag:
169 result.append(u'<%s>' % op_tag)
168 result.append(u'<%s>' % op_tag)
170
169
171 # NOTE(marcink): in some cases of mixed encodings, we might run into
170 # NOTE(marcink): in some cases of mixed encodings, we might run into
172 # troubles in the html_escape, in this case we say unicode force on token_text
171 # troubles in the html_escape, in this case we say unicode force on token_text
173 # that would ensure "correct" data even with the cost of rendered
172 # that would ensure "correct" data even with the cost of rendered
174 try:
173 try:
175 escaped_text = html_escape(token_text)
174 escaped_text = html_escape(token_text)
176 except TypeError:
175 except TypeError:
177 escaped_text = html_escape(safe_unicode(token_text))
176 escaped_text = html_escape(safe_unicode(token_text))
178
177
179 # TODO: dan: investigate showing hidden characters like space/nl/tab
178 # TODO: dan: investigate showing hidden characters like space/nl/tab
180 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
179 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
181 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
180 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
182 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
181 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
183
182
184 result.append(escaped_text)
183 result.append(escaped_text)
185
184
186 if op_tag:
185 if op_tag:
187 result.append(u'</%s>' % op_tag)
186 result.append(u'</%s>' % op_tag)
188
187
189 result.append(u'</span>')
188 result.append(u'</span>')
190
189
191 html = ''.join(result)
190 html = ''.join(result)
192 return html
191 return html
193
192
194
193
195 def rollup_tokenstream(tokenstream):
194 def rollup_tokenstream(tokenstream):
196 """
195 """
197 Group a token stream of the format:
196 Group a token stream of the format:
198
197
199 ('class', 'op', 'text')
198 ('class', 'op', 'text')
200 or
199 or
201 ('class', 'text')
200 ('class', 'text')
202
201
203 into
202 into
204
203
205 [('class1',
204 [('class1',
206 [('op1', 'text'),
205 [('op1', 'text'),
207 ('op2', 'text')]),
206 ('op2', 'text')]),
208 ('class2',
207 ('class2',
209 [('op3', 'text')])]
208 [('op3', 'text')])]
210
209
211 This is used to get the minimal tags necessary when
210 This is used to get the minimal tags necessary when
212 rendering to html eg for a token stream ie.
211 rendering to html eg for a token stream ie.
213
212
214 <span class="A"><ins>he</ins>llo</span>
213 <span class="A"><ins>he</ins>llo</span>
215 vs
214 vs
216 <span class="A"><ins>he</ins></span><span class="A">llo</span>
215 <span class="A"><ins>he</ins></span><span class="A">llo</span>
217
216
218 If a 2 tuple is passed in, the output op will be an empty string.
217 If a 2 tuple is passed in, the output op will be an empty string.
219
218
220 eg:
219 eg:
221
220
222 >>> rollup_tokenstream([('classA', '', 'h'),
221 >>> rollup_tokenstream([('classA', '', 'h'),
223 ('classA', 'del', 'ell'),
222 ('classA', 'del', 'ell'),
224 ('classA', '', 'o'),
223 ('classA', '', 'o'),
225 ('classB', '', ' '),
224 ('classB', '', ' '),
226 ('classA', '', 'the'),
225 ('classA', '', 'the'),
227 ('classA', '', 're'),
226 ('classA', '', 're'),
228 ])
227 ])
229
228
230 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
229 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
231 ('classB', [('', ' ')],
230 ('classB', [('', ' ')],
232 ('classA', [('', 'there')]]
231 ('classA', [('', 'there')]]
233
232
234 """
233 """
235 if tokenstream and len(tokenstream[0]) == 2:
234 if tokenstream and len(tokenstream[0]) == 2:
236 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
235 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
237
236
238 result = []
237 result = []
239 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
238 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
240 ops = []
239 ops = []
241 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
240 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
242 text_buffer = []
241 text_buffer = []
243 for t_class, t_op, t_text in token_text_list:
242 for t_class, t_op, t_text in token_text_list:
244 text_buffer.append(t_text)
243 text_buffer.append(t_text)
245 ops.append((token_op, ''.join(text_buffer)))
244 ops.append((token_op, ''.join(text_buffer)))
246 result.append((token_class, ops))
245 result.append((token_class, ops))
247 return result
246 return result
248
247
249
248
250 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
249 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
251 """
250 """
252 Converts a list of (token_class, token_text) tuples to a list of
251 Converts a list of (token_class, token_text) tuples to a list of
253 (token_class, token_op, token_text) tuples where token_op is one of
252 (token_class, token_op, token_text) tuples where token_op is one of
254 ('ins', 'del', '')
253 ('ins', 'del', '')
255
254
256 :param old_tokens: list of (token_class, token_text) tuples of old line
255 :param old_tokens: list of (token_class, token_text) tuples of old line
257 :param new_tokens: list of (token_class, token_text) tuples of new line
256 :param new_tokens: list of (token_class, token_text) tuples of new line
258 :param use_diff_match_patch: boolean, will use google's diff match patch
257 :param use_diff_match_patch: boolean, will use google's diff match patch
259 library which has options to 'smooth' out the character by character
258 library which has options to 'smooth' out the character by character
260 differences making nicer ins/del blocks
259 differences making nicer ins/del blocks
261 """
260 """
262
261
263 old_tokens_result = []
262 old_tokens_result = []
264 new_tokens_result = []
263 new_tokens_result = []
265
264
266 similarity = difflib.SequenceMatcher(None,
265 similarity = difflib.SequenceMatcher(None,
267 ''.join(token_text for token_class, token_text in old_tokens),
266 ''.join(token_text for token_class, token_text in old_tokens),
268 ''.join(token_text for token_class, token_text in new_tokens)
267 ''.join(token_text for token_class, token_text in new_tokens)
269 ).ratio()
268 ).ratio()
270
269
271 if similarity < 0.6: # return, the blocks are too different
270 if similarity < 0.6: # return, the blocks are too different
272 for token_class, token_text in old_tokens:
271 for token_class, token_text in old_tokens:
273 old_tokens_result.append((token_class, '', token_text))
272 old_tokens_result.append((token_class, '', token_text))
274 for token_class, token_text in new_tokens:
273 for token_class, token_text in new_tokens:
275 new_tokens_result.append((token_class, '', token_text))
274 new_tokens_result.append((token_class, '', token_text))
276 return old_tokens_result, new_tokens_result, similarity
275 return old_tokens_result, new_tokens_result, similarity
277
276
278 token_sequence_matcher = difflib.SequenceMatcher(None,
277 token_sequence_matcher = difflib.SequenceMatcher(None,
279 [x[1] for x in old_tokens],
278 [x[1] for x in old_tokens],
280 [x[1] for x in new_tokens])
279 [x[1] for x in new_tokens])
281
280
282 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
281 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
283 # check the differences by token block types first to give a more
282 # check the differences by token block types first to give a more
284 # nicer "block" level replacement vs character diffs
283 # nicer "block" level replacement vs character diffs
285
284
286 if tag == 'equal':
285 if tag == 'equal':
287 for token_class, token_text in old_tokens[o1:o2]:
286 for token_class, token_text in old_tokens[o1:o2]:
288 old_tokens_result.append((token_class, '', token_text))
287 old_tokens_result.append((token_class, '', token_text))
289 for token_class, token_text in new_tokens[n1:n2]:
288 for token_class, token_text in new_tokens[n1:n2]:
290 new_tokens_result.append((token_class, '', token_text))
289 new_tokens_result.append((token_class, '', token_text))
291 elif tag == 'delete':
290 elif tag == 'delete':
292 for token_class, token_text in old_tokens[o1:o2]:
291 for token_class, token_text in old_tokens[o1:o2]:
293 old_tokens_result.append((token_class, 'del', token_text))
292 old_tokens_result.append((token_class, 'del', token_text))
294 elif tag == 'insert':
293 elif tag == 'insert':
295 for token_class, token_text in new_tokens[n1:n2]:
294 for token_class, token_text in new_tokens[n1:n2]:
296 new_tokens_result.append((token_class, 'ins', token_text))
295 new_tokens_result.append((token_class, 'ins', token_text))
297 elif tag == 'replace':
296 elif tag == 'replace':
298 # if same type token blocks must be replaced, do a diff on the
297 # if same type token blocks must be replaced, do a diff on the
299 # characters in the token blocks to show individual changes
298 # characters in the token blocks to show individual changes
300
299
301 old_char_tokens = []
300 old_char_tokens = []
302 new_char_tokens = []
301 new_char_tokens = []
303 for token_class, token_text in old_tokens[o1:o2]:
302 for token_class, token_text in old_tokens[o1:o2]:
304 for char in token_text:
303 for char in token_text:
305 old_char_tokens.append((token_class, char))
304 old_char_tokens.append((token_class, char))
306
305
307 for token_class, token_text in new_tokens[n1:n2]:
306 for token_class, token_text in new_tokens[n1:n2]:
308 for char in token_text:
307 for char in token_text:
309 new_char_tokens.append((token_class, char))
308 new_char_tokens.append((token_class, char))
310
309
311 old_string = ''.join([token_text for
310 old_string = ''.join([token_text for
312 token_class, token_text in old_char_tokens])
311 token_class, token_text in old_char_tokens])
313 new_string = ''.join([token_text for
312 new_string = ''.join([token_text for
314 token_class, token_text in new_char_tokens])
313 token_class, token_text in new_char_tokens])
315
314
316 char_sequence = difflib.SequenceMatcher(
315 char_sequence = difflib.SequenceMatcher(
317 None, old_string, new_string)
316 None, old_string, new_string)
318 copcodes = char_sequence.get_opcodes()
317 copcodes = char_sequence.get_opcodes()
319 obuffer, nbuffer = [], []
318 obuffer, nbuffer = [], []
320
319
321 if use_diff_match_patch:
320 if use_diff_match_patch:
322 dmp = diff_match_patch()
321 dmp = diff_match_patch()
323 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
322 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
324 reps = dmp.diff_main(old_string, new_string)
323 reps = dmp.diff_main(old_string, new_string)
325 dmp.diff_cleanupEfficiency(reps)
324 dmp.diff_cleanupEfficiency(reps)
326
325
327 a, b = 0, 0
326 a, b = 0, 0
328 for op, rep in reps:
327 for op, rep in reps:
329 l = len(rep)
328 l = len(rep)
330 if op == 0:
329 if op == 0:
331 for i, c in enumerate(rep):
330 for i, c in enumerate(rep):
332 obuffer.append((old_char_tokens[a+i][0], '', c))
331 obuffer.append((old_char_tokens[a+i][0], '', c))
333 nbuffer.append((new_char_tokens[b+i][0], '', c))
332 nbuffer.append((new_char_tokens[b+i][0], '', c))
334 a += l
333 a += l
335 b += l
334 b += l
336 elif op == -1:
335 elif op == -1:
337 for i, c in enumerate(rep):
336 for i, c in enumerate(rep):
338 obuffer.append((old_char_tokens[a+i][0], 'del', c))
337 obuffer.append((old_char_tokens[a+i][0], 'del', c))
339 a += l
338 a += l
340 elif op == 1:
339 elif op == 1:
341 for i, c in enumerate(rep):
340 for i, c in enumerate(rep):
342 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
341 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
343 b += l
342 b += l
344 else:
343 else:
345 for ctag, co1, co2, cn1, cn2 in copcodes:
344 for ctag, co1, co2, cn1, cn2 in copcodes:
346 if ctag == 'equal':
345 if ctag == 'equal':
347 for token_class, token_text in old_char_tokens[co1:co2]:
346 for token_class, token_text in old_char_tokens[co1:co2]:
348 obuffer.append((token_class, '', token_text))
347 obuffer.append((token_class, '', token_text))
349 for token_class, token_text in new_char_tokens[cn1:cn2]:
348 for token_class, token_text in new_char_tokens[cn1:cn2]:
350 nbuffer.append((token_class, '', token_text))
349 nbuffer.append((token_class, '', token_text))
351 elif ctag == 'delete':
350 elif ctag == 'delete':
352 for token_class, token_text in old_char_tokens[co1:co2]:
351 for token_class, token_text in old_char_tokens[co1:co2]:
353 obuffer.append((token_class, 'del', token_text))
352 obuffer.append((token_class, 'del', token_text))
354 elif ctag == 'insert':
353 elif ctag == 'insert':
355 for token_class, token_text in new_char_tokens[cn1:cn2]:
354 for token_class, token_text in new_char_tokens[cn1:cn2]:
356 nbuffer.append((token_class, 'ins', token_text))
355 nbuffer.append((token_class, 'ins', token_text))
357 elif ctag == 'replace':
356 elif ctag == 'replace':
358 for token_class, token_text in old_char_tokens[co1:co2]:
357 for token_class, token_text in old_char_tokens[co1:co2]:
359 obuffer.append((token_class, 'del', token_text))
358 obuffer.append((token_class, 'del', token_text))
360 for token_class, token_text in new_char_tokens[cn1:cn2]:
359 for token_class, token_text in new_char_tokens[cn1:cn2]:
361 nbuffer.append((token_class, 'ins', token_text))
360 nbuffer.append((token_class, 'ins', token_text))
362
361
363 old_tokens_result.extend(obuffer)
362 old_tokens_result.extend(obuffer)
364 new_tokens_result.extend(nbuffer)
363 new_tokens_result.extend(nbuffer)
365
364
366 return old_tokens_result, new_tokens_result, similarity
365 return old_tokens_result, new_tokens_result, similarity
367
366
368
367
369 def diffset_node_getter(commit):
368 def diffset_node_getter(commit):
370 def get_node(fname):
369 def get_node(fname):
371 try:
370 try:
372 return commit.get_node(fname)
371 return commit.get_node(fname)
373 except NodeDoesNotExistError:
372 except NodeDoesNotExistError:
374 return None
373 return None
375
374
376 return get_node
375 return get_node
377
376
378
377
379 class DiffSet(object):
378 class DiffSet(object):
380 """
379 """
381 An object for parsing the diff result from diffs.DiffProcessor and
380 An object for parsing the diff result from diffs.DiffProcessor and
382 adding highlighting, side by side/unified renderings and line diffs
381 adding highlighting, side by side/unified renderings and line diffs
383 """
382 """
384
383
385 HL_REAL = 'REAL' # highlights using original file, slow
384 HL_REAL = 'REAL' # highlights using original file, slow
386 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
385 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
387 # in the case of multiline code
386 # in the case of multiline code
388 HL_NONE = 'NONE' # no highlighting, fastest
387 HL_NONE = 'NONE' # no highlighting, fastest
389
388
390 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
389 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
391 source_repo_name=None,
390 source_repo_name=None,
392 source_node_getter=lambda filename: None,
391 source_node_getter=lambda filename: None,
393 target_repo_name=None,
392 target_repo_name=None,
394 target_node_getter=lambda filename: None,
393 target_node_getter=lambda filename: None,
395 source_nodes=None, target_nodes=None,
394 source_nodes=None, target_nodes=None,
396 # files over this size will use fast highlighting
395 # files over this size will use fast highlighting
397 max_file_size_limit=150 * 1024,
396 max_file_size_limit=150 * 1024,
398 ):
397 ):
399
398
400 self.highlight_mode = highlight_mode
399 self.highlight_mode = highlight_mode
401 self.highlighted_filenodes = {
400 self.highlighted_filenodes = {
402 'before': {},
401 'before': {},
403 'after': {}
402 'after': {}
404 }
403 }
405 self.source_node_getter = source_node_getter
404 self.source_node_getter = source_node_getter
406 self.target_node_getter = target_node_getter
405 self.target_node_getter = target_node_getter
407 self.source_nodes = source_nodes or {}
406 self.source_nodes = source_nodes or {}
408 self.target_nodes = target_nodes or {}
407 self.target_nodes = target_nodes or {}
409 self.repo_name = repo_name
408 self.repo_name = repo_name
410 self.target_repo_name = target_repo_name or repo_name
409 self.target_repo_name = target_repo_name or repo_name
411 self.source_repo_name = source_repo_name or repo_name
410 self.source_repo_name = source_repo_name or repo_name
412 self.max_file_size_limit = max_file_size_limit
411 self.max_file_size_limit = max_file_size_limit
413
412
414 def render_patchset(self, patchset, source_ref=None, target_ref=None):
413 def render_patchset(self, patchset, source_ref=None, target_ref=None):
415 diffset = AttributeDict(dict(
414 diffset = AttributeDict(dict(
416 lines_added=0,
415 lines_added=0,
417 lines_deleted=0,
416 lines_deleted=0,
418 changed_files=0,
417 changed_files=0,
419 files=[],
418 files=[],
420 file_stats={},
419 file_stats={},
421 limited_diff=isinstance(patchset, LimitedDiffContainer),
420 limited_diff=isinstance(patchset, LimitedDiffContainer),
422 repo_name=self.repo_name,
421 repo_name=self.repo_name,
423 target_repo_name=self.target_repo_name,
422 target_repo_name=self.target_repo_name,
424 source_repo_name=self.source_repo_name,
423 source_repo_name=self.source_repo_name,
425 source_ref=source_ref,
424 source_ref=source_ref,
426 target_ref=target_ref,
425 target_ref=target_ref,
427 ))
426 ))
428 for patch in patchset:
427 for patch in patchset:
429 diffset.file_stats[patch['filename']] = patch['stats']
428 diffset.file_stats[patch['filename']] = patch['stats']
430 filediff = self.render_patch(patch)
429 filediff = self.render_patch(patch)
431 filediff.diffset = StrictAttributeDict(dict(
430 filediff.diffset = StrictAttributeDict(dict(
432 source_ref=diffset.source_ref,
431 source_ref=diffset.source_ref,
433 target_ref=diffset.target_ref,
432 target_ref=diffset.target_ref,
434 repo_name=diffset.repo_name,
433 repo_name=diffset.repo_name,
435 source_repo_name=diffset.source_repo_name,
434 source_repo_name=diffset.source_repo_name,
436 target_repo_name=diffset.target_repo_name,
435 target_repo_name=diffset.target_repo_name,
437 ))
436 ))
438 diffset.files.append(filediff)
437 diffset.files.append(filediff)
439 diffset.changed_files += 1
438 diffset.changed_files += 1
440 if not patch['stats']['binary']:
439 if not patch['stats']['binary']:
441 diffset.lines_added += patch['stats']['added']
440 diffset.lines_added += patch['stats']['added']
442 diffset.lines_deleted += patch['stats']['deleted']
441 diffset.lines_deleted += patch['stats']['deleted']
443
442
444 return diffset
443 return diffset
445
444
446 _lexer_cache = {}
445 _lexer_cache = {}
447
446
448 def _get_lexer_for_filename(self, filename, filenode=None):
447 def _get_lexer_for_filename(self, filename, filenode=None):
449 # cached because we might need to call it twice for source/target
448 # cached because we might need to call it twice for source/target
450 if filename not in self._lexer_cache:
449 if filename not in self._lexer_cache:
451 if filenode:
450 if filenode:
452 lexer = filenode.lexer
451 lexer = filenode.lexer
453 extension = filenode.extension
452 extension = filenode.extension
454 else:
453 else:
455 lexer = FileNode.get_lexer(filename=filename)
454 lexer = FileNode.get_lexer(filename=filename)
456 extension = filename.split('.')[-1]
455 extension = filename.split('.')[-1]
457
456
458 lexer = get_custom_lexer(extension) or lexer
457 lexer = get_custom_lexer(extension) or lexer
459 self._lexer_cache[filename] = lexer
458 self._lexer_cache[filename] = lexer
460 return self._lexer_cache[filename]
459 return self._lexer_cache[filename]
461
460
462 def render_patch(self, patch):
461 def render_patch(self, patch):
463 log.debug('rendering diff for %r', patch['filename'])
462 log.debug('rendering diff for %r', patch['filename'])
464
463
465 source_filename = patch['original_filename']
464 source_filename = patch['original_filename']
466 target_filename = patch['filename']
465 target_filename = patch['filename']
467
466
468 source_lexer = plain_text_lexer
467 source_lexer = plain_text_lexer
469 target_lexer = plain_text_lexer
468 target_lexer = plain_text_lexer
470
469
471 if not patch['stats']['binary']:
470 if not patch['stats']['binary']:
472 node_hl_mode = self.HL_NONE if patch['chunks'] == [] else None
471 node_hl_mode = self.HL_NONE if patch['chunks'] == [] else None
473 hl_mode = node_hl_mode or self.highlight_mode
472 hl_mode = node_hl_mode or self.highlight_mode
474
473
475 if hl_mode == self.HL_REAL:
474 if hl_mode == self.HL_REAL:
476 if (source_filename and patch['operation'] in ('D', 'M')
475 if (source_filename and patch['operation'] in ('D', 'M')
477 and source_filename not in self.source_nodes):
476 and source_filename not in self.source_nodes):
478 self.source_nodes[source_filename] = (
477 self.source_nodes[source_filename] = (
479 self.source_node_getter(source_filename))
478 self.source_node_getter(source_filename))
480
479
481 if (target_filename and patch['operation'] in ('A', 'M')
480 if (target_filename and patch['operation'] in ('A', 'M')
482 and target_filename not in self.target_nodes):
481 and target_filename not in self.target_nodes):
483 self.target_nodes[target_filename] = (
482 self.target_nodes[target_filename] = (
484 self.target_node_getter(target_filename))
483 self.target_node_getter(target_filename))
485
484
486 elif hl_mode == self.HL_FAST:
485 elif hl_mode == self.HL_FAST:
487 source_lexer = self._get_lexer_for_filename(source_filename)
486 source_lexer = self._get_lexer_for_filename(source_filename)
488 target_lexer = self._get_lexer_for_filename(target_filename)
487 target_lexer = self._get_lexer_for_filename(target_filename)
489
488
490 source_file = self.source_nodes.get(source_filename, source_filename)
489 source_file = self.source_nodes.get(source_filename, source_filename)
491 target_file = self.target_nodes.get(target_filename, target_filename)
490 target_file = self.target_nodes.get(target_filename, target_filename)
492 raw_id_uid = ''
491 raw_id_uid = ''
493 if self.source_nodes.get(source_filename):
492 if self.source_nodes.get(source_filename):
494 raw_id_uid = self.source_nodes[source_filename].commit.raw_id
493 raw_id_uid = self.source_nodes[source_filename].commit.raw_id
495
494
496 if not raw_id_uid and self.target_nodes.get(target_filename):
495 if not raw_id_uid and self.target_nodes.get(target_filename):
497 # in case this is a new file we only have it in target
496 # in case this is a new file we only have it in target
498 raw_id_uid = self.target_nodes[target_filename].commit.raw_id
497 raw_id_uid = self.target_nodes[target_filename].commit.raw_id
499
498
500 source_filenode, target_filenode = None, None
499 source_filenode, target_filenode = None, None
501
500
502 # TODO: dan: FileNode.lexer works on the content of the file - which
501 # TODO: dan: FileNode.lexer works on the content of the file - which
503 # can be slow - issue #4289 explains a lexer clean up - which once
502 # can be slow - issue #4289 explains a lexer clean up - which once
504 # done can allow caching a lexer for a filenode to avoid the file lookup
503 # done can allow caching a lexer for a filenode to avoid the file lookup
505 if isinstance(source_file, FileNode):
504 if isinstance(source_file, FileNode):
506 source_filenode = source_file
505 source_filenode = source_file
507 #source_lexer = source_file.lexer
506 #source_lexer = source_file.lexer
508 source_lexer = self._get_lexer_for_filename(source_filename)
507 source_lexer = self._get_lexer_for_filename(source_filename)
509 source_file.lexer = source_lexer
508 source_file.lexer = source_lexer
510
509
511 if isinstance(target_file, FileNode):
510 if isinstance(target_file, FileNode):
512 target_filenode = target_file
511 target_filenode = target_file
513 #target_lexer = target_file.lexer
512 #target_lexer = target_file.lexer
514 target_lexer = self._get_lexer_for_filename(target_filename)
513 target_lexer = self._get_lexer_for_filename(target_filename)
515 target_file.lexer = target_lexer
514 target_file.lexer = target_lexer
516
515
517 source_file_path, target_file_path = None, None
516 source_file_path, target_file_path = None, None
518
517
519 if source_filename != '/dev/null':
518 if source_filename != '/dev/null':
520 source_file_path = source_filename
519 source_file_path = source_filename
521 if target_filename != '/dev/null':
520 if target_filename != '/dev/null':
522 target_file_path = target_filename
521 target_file_path = target_filename
523
522
524 source_file_type = source_lexer.name
523 source_file_type = source_lexer.name
525 target_file_type = target_lexer.name
524 target_file_type = target_lexer.name
526
525
527 filediff = AttributeDict({
526 filediff = AttributeDict({
528 'source_file_path': source_file_path,
527 'source_file_path': source_file_path,
529 'target_file_path': target_file_path,
528 'target_file_path': target_file_path,
530 'source_filenode': source_filenode,
529 'source_filenode': source_filenode,
531 'target_filenode': target_filenode,
530 'target_filenode': target_filenode,
532 'source_file_type': target_file_type,
531 'source_file_type': target_file_type,
533 'target_file_type': source_file_type,
532 'target_file_type': source_file_type,
534 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
533 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
535 'operation': patch['operation'],
534 'operation': patch['operation'],
536 'source_mode': patch['stats']['old_mode'],
535 'source_mode': patch['stats']['old_mode'],
537 'target_mode': patch['stats']['new_mode'],
536 'target_mode': patch['stats']['new_mode'],
538 'limited_diff': patch['is_limited_diff'],
537 'limited_diff': patch['is_limited_diff'],
539 'hunks': [],
538 'hunks': [],
540 'hunk_ops': None,
539 'hunk_ops': None,
541 'diffset': self,
540 'diffset': self,
542 'raw_id': raw_id_uid,
541 'raw_id': raw_id_uid,
543 })
542 })
544
543
545 file_chunks = patch['chunks'][1:]
544 file_chunks = patch['chunks'][1:]
546 for i, hunk in enumerate(file_chunks, 1):
545 for i, hunk in enumerate(file_chunks, 1):
547 hunkbit = self.parse_hunk(hunk, source_file, target_file)
546 hunkbit = self.parse_hunk(hunk, source_file, target_file)
548 hunkbit.source_file_path = source_file_path
547 hunkbit.source_file_path = source_file_path
549 hunkbit.target_file_path = target_file_path
548 hunkbit.target_file_path = target_file_path
550 hunkbit.index = i
549 hunkbit.index = i
551 filediff.hunks.append(hunkbit)
550 filediff.hunks.append(hunkbit)
552
551
553 # Simulate hunk on OPS type line which doesn't really contain any diff
552 # Simulate hunk on OPS type line which doesn't really contain any diff
554 # this allows commenting on those
553 # this allows commenting on those
555 if not file_chunks:
554 if not file_chunks:
556 actions = []
555 actions = []
557 for op_id, op_text in filediff.patch['stats']['ops'].items():
556 for op_id, op_text in filediff.patch['stats']['ops'].items():
558 if op_id == DEL_FILENODE:
557 if op_id == DEL_FILENODE:
559 actions.append(u'file was removed')
558 actions.append(u'file was removed')
560 elif op_id == BIN_FILENODE:
559 elif op_id == BIN_FILENODE:
561 actions.append(u'binary diff hidden')
560 actions.append(u'binary diff hidden')
562 else:
561 else:
563 actions.append(safe_unicode(op_text))
562 actions.append(safe_unicode(op_text))
564 action_line = u'NO CONTENT: ' + \
563 action_line = u'NO CONTENT: ' + \
565 u', '.join(actions) or u'UNDEFINED_ACTION'
564 u', '.join(actions) or u'UNDEFINED_ACTION'
566
565
567 hunk_ops = {'source_length': 0, 'source_start': 0,
566 hunk_ops = {'source_length': 0, 'source_start': 0,
568 'lines': [
567 'lines': [
569 {'new_lineno': 0, 'old_lineno': 1,
568 {'new_lineno': 0, 'old_lineno': 1,
570 'action': 'unmod-no-hl', 'line': action_line}
569 'action': 'unmod-no-hl', 'line': action_line}
571 ],
570 ],
572 'section_header': u'', 'target_start': 1, 'target_length': 1}
571 'section_header': u'', 'target_start': 1, 'target_length': 1}
573
572
574 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
573 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
575 hunkbit.source_file_path = source_file_path
574 hunkbit.source_file_path = source_file_path
576 hunkbit.target_file_path = target_file_path
575 hunkbit.target_file_path = target_file_path
577 filediff.hunk_ops = hunkbit
576 filediff.hunk_ops = hunkbit
578 return filediff
577 return filediff
579
578
580 def parse_hunk(self, hunk, source_file, target_file):
579 def parse_hunk(self, hunk, source_file, target_file):
581 result = AttributeDict(dict(
580 result = AttributeDict(dict(
582 source_start=hunk['source_start'],
581 source_start=hunk['source_start'],
583 source_length=hunk['source_length'],
582 source_length=hunk['source_length'],
584 target_start=hunk['target_start'],
583 target_start=hunk['target_start'],
585 target_length=hunk['target_length'],
584 target_length=hunk['target_length'],
586 section_header=hunk['section_header'],
585 section_header=hunk['section_header'],
587 lines=[],
586 lines=[],
588 ))
587 ))
589 before, after = [], []
588 before, after = [], []
590
589
591 for line in hunk['lines']:
590 for line in hunk['lines']:
592 if line['action'] in ['unmod', 'unmod-no-hl']:
591 if line['action'] in ['unmod', 'unmod-no-hl']:
593 no_hl = line['action'] == 'unmod-no-hl'
592 no_hl = line['action'] == 'unmod-no-hl'
594 result.lines.extend(
593 result.lines.extend(
595 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
594 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
596 after.append(line)
595 after.append(line)
597 before.append(line)
596 before.append(line)
598 elif line['action'] == 'add':
597 elif line['action'] == 'add':
599 after.append(line)
598 after.append(line)
600 elif line['action'] == 'del':
599 elif line['action'] == 'del':
601 before.append(line)
600 before.append(line)
602 elif line['action'] == 'old-no-nl':
601 elif line['action'] == 'old-no-nl':
603 before.append(line)
602 before.append(line)
604 elif line['action'] == 'new-no-nl':
603 elif line['action'] == 'new-no-nl':
605 after.append(line)
604 after.append(line)
606
605
607 all_actions = [x['action'] for x in after] + [x['action'] for x in before]
606 all_actions = [x['action'] for x in after] + [x['action'] for x in before]
608 no_hl = {x for x in all_actions} == {'unmod-no-hl'}
607 no_hl = {x for x in all_actions} == {'unmod-no-hl'}
609 result.lines.extend(
608 result.lines.extend(
610 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
609 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
611 # NOTE(marcink): we must keep list() call here so we can cache the result...
610 # NOTE(marcink): we must keep list() call here so we can cache the result...
612 result.unified = list(self.as_unified(result.lines))
611 result.unified = list(self.as_unified(result.lines))
613 result.sideside = result.lines
612 result.sideside = result.lines
614
613
615 return result
614 return result
616
615
617 def parse_lines(self, before_lines, after_lines, source_file, target_file,
616 def parse_lines(self, before_lines, after_lines, source_file, target_file,
618 no_hl=False):
617 no_hl=False):
619 # TODO: dan: investigate doing the diff comparison and fast highlighting
618 # TODO: dan: investigate doing the diff comparison and fast highlighting
620 # on the entire before and after buffered block lines rather than by
619 # on the entire before and after buffered block lines rather than by
621 # line, this means we can get better 'fast' highlighting if the context
620 # line, this means we can get better 'fast' highlighting if the context
622 # allows it - eg.
621 # allows it - eg.
623 # line 4: """
622 # line 4: """
624 # line 5: this gets highlighted as a string
623 # line 5: this gets highlighted as a string
625 # line 6: """
624 # line 6: """
626
625
627 lines = []
626 lines = []
628
627
629 before_newline = AttributeDict()
628 before_newline = AttributeDict()
630 after_newline = AttributeDict()
629 after_newline = AttributeDict()
631 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
630 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
632 before_newline_line = before_lines.pop(-1)
631 before_newline_line = before_lines.pop(-1)
633 before_newline.content = '\n {}'.format(
632 before_newline.content = '\n {}'.format(
634 render_tokenstream(
633 render_tokenstream(
635 [(x[0], '', x[1])
634 [(x[0], '', x[1])
636 for x in [('nonl', before_newline_line['line'])]]))
635 for x in [('nonl', before_newline_line['line'])]]))
637
636
638 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
637 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
639 after_newline_line = after_lines.pop(-1)
638 after_newline_line = after_lines.pop(-1)
640 after_newline.content = '\n {}'.format(
639 after_newline.content = '\n {}'.format(
641 render_tokenstream(
640 render_tokenstream(
642 [(x[0], '', x[1])
641 [(x[0], '', x[1])
643 for x in [('nonl', after_newline_line['line'])]]))
642 for x in [('nonl', after_newline_line['line'])]]))
644
643
645 while before_lines or after_lines:
644 while before_lines or after_lines:
646 before, after = None, None
645 before, after = None, None
647 before_tokens, after_tokens = None, None
646 before_tokens, after_tokens = None, None
648
647
649 if before_lines:
648 if before_lines:
650 before = before_lines.pop(0)
649 before = before_lines.pop(0)
651 if after_lines:
650 if after_lines:
652 after = after_lines.pop(0)
651 after = after_lines.pop(0)
653
652
654 original = AttributeDict()
653 original = AttributeDict()
655 modified = AttributeDict()
654 modified = AttributeDict()
656
655
657 if before:
656 if before:
658 if before['action'] == 'old-no-nl':
657 if before['action'] == 'old-no-nl':
659 before_tokens = [('nonl', before['line'])]
658 before_tokens = [('nonl', before['line'])]
660 else:
659 else:
661 before_tokens = self.get_line_tokens(
660 before_tokens = self.get_line_tokens(
662 line_text=before['line'], line_number=before['old_lineno'],
661 line_text=before['line'], line_number=before['old_lineno'],
663 input_file=source_file, no_hl=no_hl, source='before')
662 input_file=source_file, no_hl=no_hl, source='before')
664 original.lineno = before['old_lineno']
663 original.lineno = before['old_lineno']
665 original.content = before['line']
664 original.content = before['line']
666 original.action = self.action_to_op(before['action'])
665 original.action = self.action_to_op(before['action'])
667
666
668 original.get_comment_args = (
667 original.get_comment_args = (
669 source_file, 'o', before['old_lineno'])
668 source_file, 'o', before['old_lineno'])
670
669
671 if after:
670 if after:
672 if after['action'] == 'new-no-nl':
671 if after['action'] == 'new-no-nl':
673 after_tokens = [('nonl', after['line'])]
672 after_tokens = [('nonl', after['line'])]
674 else:
673 else:
675 after_tokens = self.get_line_tokens(
674 after_tokens = self.get_line_tokens(
676 line_text=after['line'], line_number=after['new_lineno'],
675 line_text=after['line'], line_number=after['new_lineno'],
677 input_file=target_file, no_hl=no_hl, source='after')
676 input_file=target_file, no_hl=no_hl, source='after')
678 modified.lineno = after['new_lineno']
677 modified.lineno = after['new_lineno']
679 modified.content = after['line']
678 modified.content = after['line']
680 modified.action = self.action_to_op(after['action'])
679 modified.action = self.action_to_op(after['action'])
681
680
682 modified.get_comment_args = (target_file, 'n', after['new_lineno'])
681 modified.get_comment_args = (target_file, 'n', after['new_lineno'])
683
682
684 # diff the lines
683 # diff the lines
685 if before_tokens and after_tokens:
684 if before_tokens and after_tokens:
686 o_tokens, m_tokens, similarity = tokens_diff(
685 o_tokens, m_tokens, similarity = tokens_diff(
687 before_tokens, after_tokens)
686 before_tokens, after_tokens)
688 original.content = render_tokenstream(o_tokens)
687 original.content = render_tokenstream(o_tokens)
689 modified.content = render_tokenstream(m_tokens)
688 modified.content = render_tokenstream(m_tokens)
690 elif before_tokens:
689 elif before_tokens:
691 original.content = render_tokenstream(
690 original.content = render_tokenstream(
692 [(x[0], '', x[1]) for x in before_tokens])
691 [(x[0], '', x[1]) for x in before_tokens])
693 elif after_tokens:
692 elif after_tokens:
694 modified.content = render_tokenstream(
693 modified.content = render_tokenstream(
695 [(x[0], '', x[1]) for x in after_tokens])
694 [(x[0], '', x[1]) for x in after_tokens])
696
695
697 if not before_lines and before_newline:
696 if not before_lines and before_newline:
698 original.content += before_newline.content
697 original.content += before_newline.content
699 before_newline = None
698 before_newline = None
700 if not after_lines and after_newline:
699 if not after_lines and after_newline:
701 modified.content += after_newline.content
700 modified.content += after_newline.content
702 after_newline = None
701 after_newline = None
703
702
704 lines.append(AttributeDict({
703 lines.append(AttributeDict({
705 'original': original,
704 'original': original,
706 'modified': modified,
705 'modified': modified,
707 }))
706 }))
708
707
709 return lines
708 return lines
710
709
711 def get_line_tokens(self, line_text, line_number, input_file=None, no_hl=False, source=''):
710 def get_line_tokens(self, line_text, line_number, input_file=None, no_hl=False, source=''):
712 filenode = None
711 filenode = None
713 filename = None
712 filename = None
714
713
715 if isinstance(input_file, compat.string_types):
714 if isinstance(input_file, str):
716 filename = input_file
715 filename = input_file
717 elif isinstance(input_file, FileNode):
716 elif isinstance(input_file, FileNode):
718 filenode = input_file
717 filenode = input_file
719 filename = input_file.unicode_path
718 filename = input_file.unicode_path
720
719
721 hl_mode = self.HL_NONE if no_hl else self.highlight_mode
720 hl_mode = self.HL_NONE if no_hl else self.highlight_mode
722 if hl_mode == self.HL_REAL and filenode:
721 if hl_mode == self.HL_REAL and filenode:
723 lexer = self._get_lexer_for_filename(filename)
722 lexer = self._get_lexer_for_filename(filename)
724 file_size_allowed = input_file.size < self.max_file_size_limit
723 file_size_allowed = input_file.size < self.max_file_size_limit
725 if line_number and file_size_allowed:
724 if line_number and file_size_allowed:
726 return self.get_tokenized_filenode_line(input_file, line_number, lexer, source)
725 return self.get_tokenized_filenode_line(input_file, line_number, lexer, source)
727
726
728 if hl_mode in (self.HL_REAL, self.HL_FAST) and filename:
727 if hl_mode in (self.HL_REAL, self.HL_FAST) and filename:
729 lexer = self._get_lexer_for_filename(filename)
728 lexer = self._get_lexer_for_filename(filename)
730 return list(tokenize_string(line_text, lexer))
729 return list(tokenize_string(line_text, lexer))
731
730
732 return list(tokenize_string(line_text, plain_text_lexer))
731 return list(tokenize_string(line_text, plain_text_lexer))
733
732
734 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None, source=''):
733 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None, source=''):
735
734
736 def tokenize(_filenode):
735 def tokenize(_filenode):
737 self.highlighted_filenodes[source][filenode] = filenode_as_lines_tokens(filenode, lexer)
736 self.highlighted_filenodes[source][filenode] = filenode_as_lines_tokens(filenode, lexer)
738
737
739 if filenode not in self.highlighted_filenodes[source]:
738 if filenode not in self.highlighted_filenodes[source]:
740 tokenize(filenode)
739 tokenize(filenode)
741
740
742 try:
741 try:
743 return self.highlighted_filenodes[source][filenode][line_number - 1]
742 return self.highlighted_filenodes[source][filenode][line_number - 1]
744 except Exception:
743 except Exception:
745 log.exception('diff rendering error')
744 log.exception('diff rendering error')
746 return [('', u'L{}: rhodecode diff rendering error'.format(line_number))]
745 return [('', u'L{}: rhodecode diff rendering error'.format(line_number))]
747
746
748 def action_to_op(self, action):
747 def action_to_op(self, action):
749 return {
748 return {
750 'add': '+',
749 'add': '+',
751 'del': '-',
750 'del': '-',
752 'unmod': ' ',
751 'unmod': ' ',
753 'unmod-no-hl': ' ',
752 'unmod-no-hl': ' ',
754 'old-no-nl': ' ',
753 'old-no-nl': ' ',
755 'new-no-nl': ' ',
754 'new-no-nl': ' ',
756 }.get(action, action)
755 }.get(action, action)
757
756
758 def as_unified(self, lines):
757 def as_unified(self, lines):
759 """
758 """
760 Return a generator that yields the lines of a diff in unified order
759 Return a generator that yields the lines of a diff in unified order
761 """
760 """
762 def generator():
761 def generator():
763 buf = []
762 buf = []
764 for line in lines:
763 for line in lines:
765
764
766 if buf and not line.original or line.original.action == ' ':
765 if buf and not line.original or line.original.action == ' ':
767 for b in buf:
766 for b in buf:
768 yield b
767 yield b
769 buf = []
768 buf = []
770
769
771 if line.original:
770 if line.original:
772 if line.original.action == ' ':
771 if line.original.action == ' ':
773 yield (line.original.lineno, line.modified.lineno,
772 yield (line.original.lineno, line.modified.lineno,
774 line.original.action, line.original.content,
773 line.original.action, line.original.content,
775 line.original.get_comment_args)
774 line.original.get_comment_args)
776 continue
775 continue
777
776
778 if line.original.action == '-':
777 if line.original.action == '-':
779 yield (line.original.lineno, None,
778 yield (line.original.lineno, None,
780 line.original.action, line.original.content,
779 line.original.action, line.original.content,
781 line.original.get_comment_args)
780 line.original.get_comment_args)
782
781
783 if line.modified.action == '+':
782 if line.modified.action == '+':
784 buf.append((
783 buf.append((
785 None, line.modified.lineno,
784 None, line.modified.lineno,
786 line.modified.action, line.modified.content,
785 line.modified.action, line.modified.content,
787 line.modified.get_comment_args))
786 line.modified.get_comment_args))
788 continue
787 continue
789
788
790 if line.modified:
789 if line.modified:
791 yield (None, line.modified.lineno,
790 yield (None, line.modified.lineno,
792 line.modified.action, line.modified.content,
791 line.modified.action, line.modified.content,
793 line.modified.get_comment_args)
792 line.modified.get_comment_args)
794
793
795 for b in buf:
794 for b in buf:
796 yield b
795 yield b
797
796
798 return generator()
797 return generator()
@@ -1,32 +1,30 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from pyramid import compat
22
23
21
24 def strip_whitespace(value):
22 def strip_whitespace(value):
25 """
23 """
26 Removes leading/trailing whitespace, newlines, and tabs from the value.
24 Removes leading/trailing whitespace, newlines, and tabs from the value.
27 Implements the `colander.interface.Preparer` interface.
25 Implements the `colander.interface.Preparer` interface.
28 """
26 """
29 if isinstance(value, compat.string_types):
27 if isinstance(value, str):
30 return value.strip(' \t\n\r')
28 return value.strip(' \t\n\r')
31 else:
29 else:
32 return value
30 return value
@@ -1,669 +1,668 b''
1 """
1 """
2 Schema module providing common schema operations.
2 Schema module providing common schema operations.
3 """
3 """
4 import abc
4 import abc
5 try: # Python 3
5 try: # Python 3
6 from collections.abc import MutableMapping as DictMixin
6 from collections.abc import MutableMapping as DictMixin
7 except ImportError: # Python 2
7 except ImportError: # Python 2
8 from UserDict import DictMixin
8 from UserDict import DictMixin
9 import warnings
9 import warnings
10
10
11 import sqlalchemy
11 import sqlalchemy
12
12
13 from sqlalchemy.schema import ForeignKeyConstraint
13 from sqlalchemy.schema import ForeignKeyConstraint
14 from sqlalchemy.schema import UniqueConstraint
14 from sqlalchemy.schema import UniqueConstraint
15 from pyramid import compat
16
15
17 from rhodecode.lib.dbmigrate.migrate.exceptions import *
16 from rhodecode.lib.dbmigrate.migrate.exceptions import *
18 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07, SQLA_08
17 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07, SQLA_08
19 from rhodecode.lib.dbmigrate.migrate.changeset import util
18 from rhodecode.lib.dbmigrate.migrate.changeset import util
20 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (
19 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (
21 get_engine_visitor, run_single_visitor)
20 get_engine_visitor, run_single_visitor)
22
21
23
22
24 __all__ = [
23 __all__ = [
25 'create_column',
24 'create_column',
26 'drop_column',
25 'drop_column',
27 'alter_column',
26 'alter_column',
28 'rename_table',
27 'rename_table',
29 'rename_index',
28 'rename_index',
30 'ChangesetTable',
29 'ChangesetTable',
31 'ChangesetColumn',
30 'ChangesetColumn',
32 'ChangesetIndex',
31 'ChangesetIndex',
33 'ChangesetDefaultClause',
32 'ChangesetDefaultClause',
34 'ColumnDelta',
33 'ColumnDelta',
35 ]
34 ]
36
35
37 def create_column(column, table=None, *p, **kw):
36 def create_column(column, table=None, *p, **kw):
38 """Create a column, given the table.
37 """Create a column, given the table.
39
38
40 API to :meth:`ChangesetColumn.create`.
39 API to :meth:`ChangesetColumn.create`.
41 """
40 """
42 if table is not None:
41 if table is not None:
43 return table.create_column(column, *p, **kw)
42 return table.create_column(column, *p, **kw)
44 return column.create(*p, **kw)
43 return column.create(*p, **kw)
45
44
46
45
47 def drop_column(column, table=None, *p, **kw):
46 def drop_column(column, table=None, *p, **kw):
48 """Drop a column, given the table.
47 """Drop a column, given the table.
49
48
50 API to :meth:`ChangesetColumn.drop`.
49 API to :meth:`ChangesetColumn.drop`.
51 """
50 """
52 if table is not None:
51 if table is not None:
53 return table.drop_column(column, *p, **kw)
52 return table.drop_column(column, *p, **kw)
54 return column.drop(*p, **kw)
53 return column.drop(*p, **kw)
55
54
56
55
57 def rename_table(table, name, engine=None, **kw):
56 def rename_table(table, name, engine=None, **kw):
58 """Rename a table.
57 """Rename a table.
59
58
60 If Table instance is given, engine is not used.
59 If Table instance is given, engine is not used.
61
60
62 API to :meth:`ChangesetTable.rename`.
61 API to :meth:`ChangesetTable.rename`.
63
62
64 :param table: Table to be renamed.
63 :param table: Table to be renamed.
65 :param name: New name for Table.
64 :param name: New name for Table.
66 :param engine: Engine instance.
65 :param engine: Engine instance.
67 :type table: string or Table instance
66 :type table: string or Table instance
68 :type name: string
67 :type name: string
69 :type engine: obj
68 :type engine: obj
70 """
69 """
71 table = _to_table(table, engine)
70 table = _to_table(table, engine)
72 table.rename(name, **kw)
71 table.rename(name, **kw)
73
72
74
73
75 def rename_index(index, name, table=None, engine=None, **kw):
74 def rename_index(index, name, table=None, engine=None, **kw):
76 """Rename an index.
75 """Rename an index.
77
76
78 If Index instance is given,
77 If Index instance is given,
79 table and engine are not used.
78 table and engine are not used.
80
79
81 API to :meth:`ChangesetIndex.rename`.
80 API to :meth:`ChangesetIndex.rename`.
82
81
83 :param index: Index to be renamed.
82 :param index: Index to be renamed.
84 :param name: New name for index.
83 :param name: New name for index.
85 :param table: Table to which Index is reffered.
84 :param table: Table to which Index is reffered.
86 :param engine: Engine instance.
85 :param engine: Engine instance.
87 :type index: string or Index instance
86 :type index: string or Index instance
88 :type name: string
87 :type name: string
89 :type table: string or Table instance
88 :type table: string or Table instance
90 :type engine: obj
89 :type engine: obj
91 """
90 """
92 index = _to_index(index, table, engine)
91 index = _to_index(index, table, engine)
93 index.rename(name, **kw)
92 index.rename(name, **kw)
94
93
95
94
96 def alter_column(*p, **k):
95 def alter_column(*p, **k):
97 """Alter a column.
96 """Alter a column.
98
97
99 This is a helper function that creates a :class:`ColumnDelta` and
98 This is a helper function that creates a :class:`ColumnDelta` and
100 runs it.
99 runs it.
101
100
102 :argument column:
101 :argument column:
103 The name of the column to be altered or a
102 The name of the column to be altered or a
104 :class:`ChangesetColumn` column representing it.
103 :class:`ChangesetColumn` column representing it.
105
104
106 :param table:
105 :param table:
107 A :class:`~sqlalchemy.schema.Table` or table name to
106 A :class:`~sqlalchemy.schema.Table` or table name to
108 for the table where the column will be changed.
107 for the table where the column will be changed.
109
108
110 :param engine:
109 :param engine:
111 The :class:`~sqlalchemy.engine.base.Engine` to use for table
110 The :class:`~sqlalchemy.engine.base.Engine` to use for table
112 reflection and schema alterations.
111 reflection and schema alterations.
113
112
114 :returns: A :class:`ColumnDelta` instance representing the change.
113 :returns: A :class:`ColumnDelta` instance representing the change.
115
114
116
115
117 """
116 """
118
117
119 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
118 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
120 k['table'] = p[0].table
119 k['table'] = p[0].table
121 if 'engine' not in k:
120 if 'engine' not in k:
122 k['engine'] = k['table'].bind
121 k['engine'] = k['table'].bind
123
122
124 # deprecation
123 # deprecation
125 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
124 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
126 warnings.warn(
125 warnings.warn(
127 "Passing a Column object to alter_column is deprecated."
126 "Passing a Column object to alter_column is deprecated."
128 " Just pass in keyword parameters instead.",
127 " Just pass in keyword parameters instead.",
129 MigrateDeprecationWarning
128 MigrateDeprecationWarning
130 )
129 )
131 engine = k['engine']
130 engine = k['engine']
132
131
133 # enough tests seem to break when metadata is always altered
132 # enough tests seem to break when metadata is always altered
134 # that this crutch has to be left in until they can be sorted
133 # that this crutch has to be left in until they can be sorted
135 # out
134 # out
136 k['alter_metadata']=True
135 k['alter_metadata']=True
137
136
138 delta = ColumnDelta(*p, **k)
137 delta = ColumnDelta(*p, **k)
139
138
140 visitorcallable = get_engine_visitor(engine, 'schemachanger')
139 visitorcallable = get_engine_visitor(engine, 'schemachanger')
141 engine._run_visitor(visitorcallable, delta)
140 engine._run_visitor(visitorcallable, delta)
142
141
143 return delta
142 return delta
144
143
145
144
146 def _to_table(table, engine=None):
145 def _to_table(table, engine=None):
147 """Return if instance of Table, else construct new with metadata"""
146 """Return if instance of Table, else construct new with metadata"""
148 if isinstance(table, sqlalchemy.Table):
147 if isinstance(table, sqlalchemy.Table):
149 return table
148 return table
150
149
151 # Given: table name, maybe an engine
150 # Given: table name, maybe an engine
152 meta = sqlalchemy.MetaData()
151 meta = sqlalchemy.MetaData()
153 if engine is not None:
152 if engine is not None:
154 meta.bind = engine
153 meta.bind = engine
155 return sqlalchemy.Table(table, meta)
154 return sqlalchemy.Table(table, meta)
156
155
157
156
158 def _to_index(index, table=None, engine=None):
157 def _to_index(index, table=None, engine=None):
159 """Return if instance of Index, else construct new with metadata"""
158 """Return if instance of Index, else construct new with metadata"""
160 if isinstance(index, sqlalchemy.Index):
159 if isinstance(index, sqlalchemy.Index):
161 return index
160 return index
162
161
163 # Given: index name; table name required
162 # Given: index name; table name required
164 table = _to_table(table, engine)
163 table = _to_table(table, engine)
165 ret = sqlalchemy.Index(index)
164 ret = sqlalchemy.Index(index)
166 ret.table = table
165 ret.table = table
167 return ret
166 return ret
168
167
169
168
170 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
169 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
171 """Extracts the differences between two columns/column-parameters
170 """Extracts the differences between two columns/column-parameters
172
171
173 May receive parameters arranged in several different ways:
172 May receive parameters arranged in several different ways:
174
173
175 * **current_column, new_column, \*p, \*\*kw**
174 * **current_column, new_column, \*p, \*\*kw**
176 Additional parameters can be specified to override column
175 Additional parameters can be specified to override column
177 differences.
176 differences.
178
177
179 * **current_column, \*p, \*\*kw**
178 * **current_column, \*p, \*\*kw**
180 Additional parameters alter current_column. Table name is extracted
179 Additional parameters alter current_column. Table name is extracted
181 from current_column object.
180 from current_column object.
182 Name is changed to current_column.name from current_name,
181 Name is changed to current_column.name from current_name,
183 if current_name is specified.
182 if current_name is specified.
184
183
185 * **current_col_name, \*p, \*\*kw**
184 * **current_col_name, \*p, \*\*kw**
186 Table kw must specified.
185 Table kw must specified.
187
186
188 :param table: Table at which current Column should be bound to.\
187 :param table: Table at which current Column should be bound to.\
189 If table name is given, reflection will be used.
188 If table name is given, reflection will be used.
190 :type table: string or Table instance
189 :type table: string or Table instance
191
190
192 :param metadata: A :class:`MetaData` instance to store
191 :param metadata: A :class:`MetaData` instance to store
193 reflected table names
192 reflected table names
194
193
195 :param engine: When reflecting tables, either engine or metadata must \
194 :param engine: When reflecting tables, either engine or metadata must \
196 be specified to acquire engine object.
195 be specified to acquire engine object.
197 :type engine: :class:`Engine` instance
196 :type engine: :class:`Engine` instance
198 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
197 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
199 `result_column` through :func:`dict` alike object.
198 `result_column` through :func:`dict` alike object.
200
199
201 * :class:`ColumnDelta`.result_column is altered column with new attributes
200 * :class:`ColumnDelta`.result_column is altered column with new attributes
202
201
203 * :class:`ColumnDelta`.current_name is current name of column in db
202 * :class:`ColumnDelta`.current_name is current name of column in db
204
203
205
204
206 """
205 """
207
206
208 # Column attributes that can be altered
207 # Column attributes that can be altered
209 diff_keys = ('name', 'type', 'primary_key', 'nullable',
208 diff_keys = ('name', 'type', 'primary_key', 'nullable',
210 'server_onupdate', 'server_default', 'autoincrement')
209 'server_onupdate', 'server_default', 'autoincrement')
211 diffs = dict()
210 diffs = dict()
212 __visit_name__ = 'column'
211 __visit_name__ = 'column'
213
212
214 def __init__(self, *p, **kw):
213 def __init__(self, *p, **kw):
215 # 'alter_metadata' is not a public api. It exists purely
214 # 'alter_metadata' is not a public api. It exists purely
216 # as a crutch until the tests that fail when 'alter_metadata'
215 # as a crutch until the tests that fail when 'alter_metadata'
217 # behaviour always happens can be sorted out
216 # behaviour always happens can be sorted out
218 self.alter_metadata = kw.pop("alter_metadata", False)
217 self.alter_metadata = kw.pop("alter_metadata", False)
219
218
220 self.meta = kw.pop("metadata", None)
219 self.meta = kw.pop("metadata", None)
221 self.engine = kw.pop("engine", None)
220 self.engine = kw.pop("engine", None)
222
221
223 # Things are initialized differently depending on how many column
222 # Things are initialized differently depending on how many column
224 # parameters are given. Figure out how many and call the appropriate
223 # parameters are given. Figure out how many and call the appropriate
225 # method.
224 # method.
226 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
225 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
227 # At least one column specified
226 # At least one column specified
228 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
227 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
229 # Two columns specified
228 # Two columns specified
230 diffs = self.compare_2_columns(*p, **kw)
229 diffs = self.compare_2_columns(*p, **kw)
231 else:
230 else:
232 # Exactly one column specified
231 # Exactly one column specified
233 diffs = self.compare_1_column(*p, **kw)
232 diffs = self.compare_1_column(*p, **kw)
234 else:
233 else:
235 # Zero columns specified
234 # Zero columns specified
236 if not len(p) or not isinstance(p[0], compat.string_types):
235 if not len(p) or not isinstance(p[0], str):
237 raise ValueError("First argument must be column name")
236 raise ValueError("First argument must be column name")
238 diffs = self.compare_parameters(*p, **kw)
237 diffs = self.compare_parameters(*p, **kw)
239
238
240 self.apply_diffs(diffs)
239 self.apply_diffs(diffs)
241
240
242 def __repr__(self):
241 def __repr__(self):
243 return '<ColumnDelta altermetadata=%r, %s>' % (
242 return '<ColumnDelta altermetadata=%r, %s>' % (
244 self.alter_metadata,
243 self.alter_metadata,
245 super(ColumnDelta, self).__repr__()
244 super(ColumnDelta, self).__repr__()
246 )
245 )
247
246
248 def __getitem__(self, key):
247 def __getitem__(self, key):
249 if key not in self.keys():
248 if key not in self.keys():
250 raise KeyError("No such diff key, available: %s" % self.diffs )
249 raise KeyError("No such diff key, available: %s" % self.diffs )
251 return getattr(self.result_column, key)
250 return getattr(self.result_column, key)
252
251
253 def __setitem__(self, key, value):
252 def __setitem__(self, key, value):
254 if key not in self.keys():
253 if key not in self.keys():
255 raise KeyError("No such diff key, available: %s" % self.diffs )
254 raise KeyError("No such diff key, available: %s" % self.diffs )
256 setattr(self.result_column, key, value)
255 setattr(self.result_column, key, value)
257
256
258 def __delitem__(self, key):
257 def __delitem__(self, key):
259 raise NotImplementedError
258 raise NotImplementedError
260
259
261 def __len__(self):
260 def __len__(self):
262 raise NotImplementedError
261 raise NotImplementedError
263
262
264 def __iter__(self):
263 def __iter__(self):
265 raise NotImplementedError
264 raise NotImplementedError
266
265
267 def keys(self):
266 def keys(self):
268 return self.diffs.keys()
267 return self.diffs.keys()
269
268
270 def compare_parameters(self, current_name, *p, **k):
269 def compare_parameters(self, current_name, *p, **k):
271 """Compares Column objects with reflection"""
270 """Compares Column objects with reflection"""
272 self.table = k.pop('table')
271 self.table = k.pop('table')
273 self.result_column = self._table.c.get(current_name)
272 self.result_column = self._table.c.get(current_name)
274 if len(p):
273 if len(p):
275 k = self._extract_parameters(p, k, self.result_column)
274 k = self._extract_parameters(p, k, self.result_column)
276 return k
275 return k
277
276
278 def compare_1_column(self, col, *p, **k):
277 def compare_1_column(self, col, *p, **k):
279 """Compares one Column object"""
278 """Compares one Column object"""
280 self.table = k.pop('table', None)
279 self.table = k.pop('table', None)
281 if self.table is None:
280 if self.table is None:
282 self.table = col.table
281 self.table = col.table
283 self.result_column = col
282 self.result_column = col
284 if len(p):
283 if len(p):
285 k = self._extract_parameters(p, k, self.result_column)
284 k = self._extract_parameters(p, k, self.result_column)
286 return k
285 return k
287
286
288 def compare_2_columns(self, old_col, new_col, *p, **k):
287 def compare_2_columns(self, old_col, new_col, *p, **k):
289 """Compares two Column objects"""
288 """Compares two Column objects"""
290 self.process_column(new_col)
289 self.process_column(new_col)
291 self.table = k.pop('table', None)
290 self.table = k.pop('table', None)
292 # we cannot use bool() on table in SA06
291 # we cannot use bool() on table in SA06
293 if self.table is None:
292 if self.table is None:
294 self.table = old_col.table
293 self.table = old_col.table
295 if self.table is None:
294 if self.table is None:
296 new_col.table
295 new_col.table
297 self.result_column = old_col
296 self.result_column = old_col
298
297
299 # set differences
298 # set differences
300 # leave out some stuff for later comp
299 # leave out some stuff for later comp
301 for key in (set(self.diff_keys) - set(('type',))):
300 for key in (set(self.diff_keys) - set(('type',))):
302 val = getattr(new_col, key, None)
301 val = getattr(new_col, key, None)
303 if getattr(self.result_column, key, None) != val:
302 if getattr(self.result_column, key, None) != val:
304 k.setdefault(key, val)
303 k.setdefault(key, val)
305
304
306 # inspect types
305 # inspect types
307 if not self.are_column_types_eq(self.result_column.type, new_col.type):
306 if not self.are_column_types_eq(self.result_column.type, new_col.type):
308 k.setdefault('type', new_col.type)
307 k.setdefault('type', new_col.type)
309
308
310 if len(p):
309 if len(p):
311 k = self._extract_parameters(p, k, self.result_column)
310 k = self._extract_parameters(p, k, self.result_column)
312 return k
311 return k
313
312
314 def apply_diffs(self, diffs):
313 def apply_diffs(self, diffs):
315 """Populate dict and column object with new values"""
314 """Populate dict and column object with new values"""
316 self.diffs = diffs
315 self.diffs = diffs
317 for key in self.diff_keys:
316 for key in self.diff_keys:
318 if key in diffs:
317 if key in diffs:
319 setattr(self.result_column, key, diffs[key])
318 setattr(self.result_column, key, diffs[key])
320
319
321 self.process_column(self.result_column)
320 self.process_column(self.result_column)
322
321
323 # create an instance of class type if not yet
322 # create an instance of class type if not yet
324 if 'type' in diffs and callable(self.result_column.type):
323 if 'type' in diffs and callable(self.result_column.type):
325 self.result_column.type = self.result_column.type()
324 self.result_column.type = self.result_column.type()
326
325
327 # add column to the table
326 # add column to the table
328 if self.table is not None and self.alter_metadata:
327 if self.table is not None and self.alter_metadata:
329 self.result_column.add_to_table(self.table)
328 self.result_column.add_to_table(self.table)
330
329
331 def are_column_types_eq(self, old_type, new_type):
330 def are_column_types_eq(self, old_type, new_type):
332 """Compares two types to be equal"""
331 """Compares two types to be equal"""
333 ret = old_type.__class__ == new_type.__class__
332 ret = old_type.__class__ == new_type.__class__
334
333
335 # String length is a special case
334 # String length is a special case
336 if ret and isinstance(new_type, sqlalchemy.types.String):
335 if ret and isinstance(new_type, sqlalchemy.types.String):
337 ret = (getattr(old_type, 'length', None) == \
336 ret = (getattr(old_type, 'length', None) == \
338 getattr(new_type, 'length', None))
337 getattr(new_type, 'length', None))
339 return ret
338 return ret
340
339
341 def _extract_parameters(self, p, k, column):
340 def _extract_parameters(self, p, k, column):
342 """Extracts data from p and modifies diffs"""
341 """Extracts data from p and modifies diffs"""
343 p = list(p)
342 p = list(p)
344 while len(p):
343 while len(p):
345 if isinstance(p[0], compat.string_types):
344 if isinstance(p[0], str):
346 k.setdefault('name', p.pop(0))
345 k.setdefault('name', p.pop(0))
347 elif isinstance(p[0], sqlalchemy.types.TypeEngine):
346 elif isinstance(p[0], sqlalchemy.types.TypeEngine):
348 k.setdefault('type', p.pop(0))
347 k.setdefault('type', p.pop(0))
349 elif callable(p[0]):
348 elif callable(p[0]):
350 p[0] = p[0]()
349 p[0] = p[0]()
351 else:
350 else:
352 break
351 break
353
352
354 if len(p):
353 if len(p):
355 new_col = column.copy_fixed()
354 new_col = column.copy_fixed()
356 new_col._init_items(*p)
355 new_col._init_items(*p)
357 k = self.compare_2_columns(column, new_col, **k)
356 k = self.compare_2_columns(column, new_col, **k)
358 return k
357 return k
359
358
360 def process_column(self, column):
359 def process_column(self, column):
361 """Processes default values for column"""
360 """Processes default values for column"""
362 # XXX: this is a snippet from SA processing of positional parameters
361 # XXX: this is a snippet from SA processing of positional parameters
363 toinit = list()
362 toinit = list()
364
363
365 if column.server_default is not None:
364 if column.server_default is not None:
366 if isinstance(column.server_default, sqlalchemy.FetchedValue):
365 if isinstance(column.server_default, sqlalchemy.FetchedValue):
367 toinit.append(column.server_default)
366 toinit.append(column.server_default)
368 else:
367 else:
369 toinit.append(sqlalchemy.DefaultClause(column.server_default))
368 toinit.append(sqlalchemy.DefaultClause(column.server_default))
370 if column.server_onupdate is not None:
369 if column.server_onupdate is not None:
371 if isinstance(column.server_onupdate, FetchedValue):
370 if isinstance(column.server_onupdate, FetchedValue):
372 toinit.append(column.server_default)
371 toinit.append(column.server_default)
373 else:
372 else:
374 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
373 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
375 for_update=True))
374 for_update=True))
376 if toinit:
375 if toinit:
377 column._init_items(*toinit)
376 column._init_items(*toinit)
378
377
379 def _get_table(self):
378 def _get_table(self):
380 return getattr(self, '_table', None)
379 return getattr(self, '_table', None)
381
380
382 def _set_table(self, table):
381 def _set_table(self, table):
383 if isinstance(table, compat.string_types):
382 if isinstance(table, str):
384 if self.alter_metadata:
383 if self.alter_metadata:
385 if not self.meta:
384 if not self.meta:
386 raise ValueError("metadata must be specified for table"
385 raise ValueError("metadata must be specified for table"
387 " reflection when using alter_metadata")
386 " reflection when using alter_metadata")
388 meta = self.meta
387 meta = self.meta
389 if self.engine:
388 if self.engine:
390 meta.bind = self.engine
389 meta.bind = self.engine
391 else:
390 else:
392 if not self.engine and not self.meta:
391 if not self.engine and not self.meta:
393 raise ValueError("engine or metadata must be specified"
392 raise ValueError("engine or metadata must be specified"
394 " to reflect tables")
393 " to reflect tables")
395 if not self.engine:
394 if not self.engine:
396 self.engine = self.meta.bind
395 self.engine = self.meta.bind
397 meta = sqlalchemy.MetaData(bind=self.engine)
396 meta = sqlalchemy.MetaData(bind=self.engine)
398 self._table = sqlalchemy.Table(table, meta, autoload=True)
397 self._table = sqlalchemy.Table(table, meta, autoload=True)
399 elif isinstance(table, sqlalchemy.Table):
398 elif isinstance(table, sqlalchemy.Table):
400 self._table = table
399 self._table = table
401 if not self.alter_metadata:
400 if not self.alter_metadata:
402 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
401 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
403 def _get_result_column(self):
402 def _get_result_column(self):
404 return getattr(self, '_result_column', None)
403 return getattr(self, '_result_column', None)
405
404
406 def _set_result_column(self, column):
405 def _set_result_column(self, column):
407 """Set Column to Table based on alter_metadata evaluation."""
406 """Set Column to Table based on alter_metadata evaluation."""
408 self.process_column(column)
407 self.process_column(column)
409 if not hasattr(self, 'current_name'):
408 if not hasattr(self, 'current_name'):
410 self.current_name = column.name
409 self.current_name = column.name
411 if self.alter_metadata:
410 if self.alter_metadata:
412 self._result_column = column
411 self._result_column = column
413 else:
412 else:
414 self._result_column = column.copy_fixed()
413 self._result_column = column.copy_fixed()
415
414
416 table = property(_get_table, _set_table)
415 table = property(_get_table, _set_table)
417 result_column = property(_get_result_column, _set_result_column)
416 result_column = property(_get_result_column, _set_result_column)
418
417
419
418
420 class ChangesetTable(object):
419 class ChangesetTable(object):
421 """Changeset extensions to SQLAlchemy tables."""
420 """Changeset extensions to SQLAlchemy tables."""
422
421
423 def create_column(self, column, *p, **kw):
422 def create_column(self, column, *p, **kw):
424 """Creates a column.
423 """Creates a column.
425
424
426 The column parameter may be a column definition or the name of
425 The column parameter may be a column definition or the name of
427 a column in this table.
426 a column in this table.
428
427
429 API to :meth:`ChangesetColumn.create`
428 API to :meth:`ChangesetColumn.create`
430
429
431 :param column: Column to be created
430 :param column: Column to be created
432 :type column: Column instance or string
431 :type column: Column instance or string
433 """
432 """
434 if not isinstance(column, sqlalchemy.Column):
433 if not isinstance(column, sqlalchemy.Column):
435 # It's a column name
434 # It's a column name
436 column = getattr(self.c, str(column))
435 column = getattr(self.c, str(column))
437 column.create(table=self, *p, **kw)
436 column.create(table=self, *p, **kw)
438
437
439 def drop_column(self, column, *p, **kw):
438 def drop_column(self, column, *p, **kw):
440 """Drop a column, given its name or definition.
439 """Drop a column, given its name or definition.
441
440
442 API to :meth:`ChangesetColumn.drop`
441 API to :meth:`ChangesetColumn.drop`
443
442
444 :param column: Column to be droped
443 :param column: Column to be droped
445 :type column: Column instance or string
444 :type column: Column instance or string
446 """
445 """
447 if not isinstance(column, sqlalchemy.Column):
446 if not isinstance(column, sqlalchemy.Column):
448 # It's a column name
447 # It's a column name
449 try:
448 try:
450 column = getattr(self.c, str(column))
449 column = getattr(self.c, str(column))
451 except AttributeError:
450 except AttributeError:
452 # That column isn't part of the table. We don't need
451 # That column isn't part of the table. We don't need
453 # its entire definition to drop the column, just its
452 # its entire definition to drop the column, just its
454 # name, so create a dummy column with the same name.
453 # name, so create a dummy column with the same name.
455 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
454 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
456 column.drop(table=self, *p, **kw)
455 column.drop(table=self, *p, **kw)
457
456
458 def rename(self, name, connection=None, **kwargs):
457 def rename(self, name, connection=None, **kwargs):
459 """Rename this table.
458 """Rename this table.
460
459
461 :param name: New name of the table.
460 :param name: New name of the table.
462 :type name: string
461 :type name: string
463 :param connection: reuse connection istead of creating new one.
462 :param connection: reuse connection istead of creating new one.
464 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
463 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
465 """
464 """
466 engine = self.bind
465 engine = self.bind
467 self.new_name = name
466 self.new_name = name
468 visitorcallable = get_engine_visitor(engine, 'schemachanger')
467 visitorcallable = get_engine_visitor(engine, 'schemachanger')
469 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
468 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
470
469
471 # Fix metadata registration
470 # Fix metadata registration
472 self.name = name
471 self.name = name
473 self.deregister()
472 self.deregister()
474 self._set_parent(self.metadata)
473 self._set_parent(self.metadata)
475
474
476 def _meta_key(self):
475 def _meta_key(self):
477 """Get the meta key for this table."""
476 """Get the meta key for this table."""
478 return sqlalchemy.schema._get_table_key(self.name, self.schema)
477 return sqlalchemy.schema._get_table_key(self.name, self.schema)
479
478
480 def deregister(self):
479 def deregister(self):
481 """Remove this table from its metadata"""
480 """Remove this table from its metadata"""
482 if SQLA_07:
481 if SQLA_07:
483 self.metadata._remove_table(self.name, self.schema)
482 self.metadata._remove_table(self.name, self.schema)
484 else:
483 else:
485 key = self._meta_key()
484 key = self._meta_key()
486 meta = self.metadata
485 meta = self.metadata
487 if key in meta.tables:
486 if key in meta.tables:
488 del meta.tables[key]
487 del meta.tables[key]
489
488
490
489
491 class ChangesetColumn(object):
490 class ChangesetColumn(object):
492 """Changeset extensions to SQLAlchemy columns."""
491 """Changeset extensions to SQLAlchemy columns."""
493
492
494 def alter(self, *p, **k):
493 def alter(self, *p, **k):
495 """Makes a call to :func:`alter_column` for the column this
494 """Makes a call to :func:`alter_column` for the column this
496 method is called on.
495 method is called on.
497 """
496 """
498 if 'table' not in k:
497 if 'table' not in k:
499 k['table'] = self.table
498 k['table'] = self.table
500 if 'engine' not in k:
499 if 'engine' not in k:
501 k['engine'] = k['table'].bind
500 k['engine'] = k['table'].bind
502 return alter_column(self, *p, **k)
501 return alter_column(self, *p, **k)
503
502
504 def create(self, table=None, index_name=None, unique_name=None,
503 def create(self, table=None, index_name=None, unique_name=None,
505 primary_key_name=None, populate_default=True, connection=None, **kwargs):
504 primary_key_name=None, populate_default=True, connection=None, **kwargs):
506 """Create this column in the database.
505 """Create this column in the database.
507
506
508 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
507 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
509 for most databases.
508 for most databases.
510
509
511 :param table: Table instance to create on.
510 :param table: Table instance to create on.
512 :param index_name: Creates :class:`ChangesetIndex` on this column.
511 :param index_name: Creates :class:`ChangesetIndex` on this column.
513 :param unique_name: Creates :class:\
512 :param unique_name: Creates :class:\
514 `~migrate.changeset.constraint.UniqueConstraint` on this column.
513 `~migrate.changeset.constraint.UniqueConstraint` on this column.
515 :param primary_key_name: Creates :class:\
514 :param primary_key_name: Creates :class:\
516 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
515 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
517 :param populate_default: If True, created column will be \
516 :param populate_default: If True, created column will be \
518 populated with defaults
517 populated with defaults
519 :param connection: reuse connection istead of creating new one.
518 :param connection: reuse connection istead of creating new one.
520 :type table: Table instance
519 :type table: Table instance
521 :type index_name: string
520 :type index_name: string
522 :type unique_name: string
521 :type unique_name: string
523 :type primary_key_name: string
522 :type primary_key_name: string
524 :type populate_default: bool
523 :type populate_default: bool
525 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
524 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
526
525
527 :returns: self
526 :returns: self
528 """
527 """
529 self.populate_default = populate_default
528 self.populate_default = populate_default
530 self.index_name = index_name
529 self.index_name = index_name
531 self.unique_name = unique_name
530 self.unique_name = unique_name
532 self.primary_key_name = primary_key_name
531 self.primary_key_name = primary_key_name
533 for cons in ('index_name', 'unique_name', 'primary_key_name'):
532 for cons in ('index_name', 'unique_name', 'primary_key_name'):
534 self._check_sanity_constraints(cons)
533 self._check_sanity_constraints(cons)
535
534
536 self.add_to_table(table)
535 self.add_to_table(table)
537 engine = self.table.bind
536 engine = self.table.bind
538 visitorcallable = get_engine_visitor(engine, 'columngenerator')
537 visitorcallable = get_engine_visitor(engine, 'columngenerator')
539 engine._run_visitor(visitorcallable, self, connection, **kwargs)
538 engine._run_visitor(visitorcallable, self, connection, **kwargs)
540
539
541 # TODO: reuse existing connection
540 # TODO: reuse existing connection
542 if self.populate_default and self.default is not None:
541 if self.populate_default and self.default is not None:
543 stmt = table.update().values({self: engine._execute_default(self.default)})
542 stmt = table.update().values({self: engine._execute_default(self.default)})
544 engine.execute(stmt)
543 engine.execute(stmt)
545
544
546 return self
545 return self
547
546
548 def drop(self, table=None, connection=None, **kwargs):
547 def drop(self, table=None, connection=None, **kwargs):
549 """Drop this column from the database, leaving its table intact.
548 """Drop this column from the database, leaving its table intact.
550
549
551 ``ALTER TABLE DROP COLUMN``, for most databases.
550 ``ALTER TABLE DROP COLUMN``, for most databases.
552
551
553 :param connection: reuse connection istead of creating new one.
552 :param connection: reuse connection istead of creating new one.
554 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
553 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
555 """
554 """
556 if table is not None:
555 if table is not None:
557 self.table = table
556 self.table = table
558 engine = self.table.bind
557 engine = self.table.bind
559 visitorcallable = get_engine_visitor(engine, 'columndropper')
558 visitorcallable = get_engine_visitor(engine, 'columndropper')
560 engine._run_visitor(visitorcallable, self, connection, **kwargs)
559 engine._run_visitor(visitorcallable, self, connection, **kwargs)
561 self.remove_from_table(self.table, unset_table=False)
560 self.remove_from_table(self.table, unset_table=False)
562 self.table = None
561 self.table = None
563 return self
562 return self
564
563
565 def add_to_table(self, table):
564 def add_to_table(self, table):
566 if table is not None and self.table is None:
565 if table is not None and self.table is None:
567 if SQLA_07:
566 if SQLA_07:
568 table.append_column(self)
567 table.append_column(self)
569 else:
568 else:
570 self._set_parent(table)
569 self._set_parent(table)
571
570
572 def _col_name_in_constraint(self,cons,name):
571 def _col_name_in_constraint(self,cons,name):
573 return False
572 return False
574
573
575 def remove_from_table(self, table, unset_table=True):
574 def remove_from_table(self, table, unset_table=True):
576 # TODO: remove primary keys, constraints, etc
575 # TODO: remove primary keys, constraints, etc
577 if unset_table:
576 if unset_table:
578 self.table = None
577 self.table = None
579
578
580 to_drop = set()
579 to_drop = set()
581 for index in table.indexes:
580 for index in table.indexes:
582 columns = []
581 columns = []
583 for col in index.columns:
582 for col in index.columns:
584 if col.name!=self.name:
583 if col.name!=self.name:
585 columns.append(col)
584 columns.append(col)
586 if columns:
585 if columns:
587 index.columns = columns
586 index.columns = columns
588 if SQLA_08:
587 if SQLA_08:
589 index.expressions = columns
588 index.expressions = columns
590 else:
589 else:
591 to_drop.add(index)
590 to_drop.add(index)
592 table.indexes = table.indexes - to_drop
591 table.indexes = table.indexes - to_drop
593
592
594 to_drop = set()
593 to_drop = set()
595 for cons in table.constraints:
594 for cons in table.constraints:
596 # TODO: deal with other types of constraint
595 # TODO: deal with other types of constraint
597 if isinstance(cons,(ForeignKeyConstraint,
596 if isinstance(cons,(ForeignKeyConstraint,
598 UniqueConstraint)):
597 UniqueConstraint)):
599 for col_name in cons.columns:
598 for col_name in cons.columns:
600 if not isinstance(col_name, compat.string_types):
599 if not isinstance(col_name, str):
601 col_name = col_name.name
600 col_name = col_name.name
602 if self.name==col_name:
601 if self.name==col_name:
603 to_drop.add(cons)
602 to_drop.add(cons)
604 table.constraints = table.constraints - to_drop
603 table.constraints = table.constraints - to_drop
605
604
606 if table.c.contains_column(self):
605 if table.c.contains_column(self):
607 if SQLA_07:
606 if SQLA_07:
608 table._columns.remove(self)
607 table._columns.remove(self)
609 else:
608 else:
610 table.c.remove(self)
609 table.c.remove(self)
611
610
612 # TODO: this is fixed in 0.6
611 # TODO: this is fixed in 0.6
613 def copy_fixed(self, **kw):
612 def copy_fixed(self, **kw):
614 """Create a copy of this ``Column``, with all attributes."""
613 """Create a copy of this ``Column``, with all attributes."""
615 q = util.safe_quote(self)
614 q = util.safe_quote(self)
616 return sqlalchemy.Column(self.name, self.type, self.default,
615 return sqlalchemy.Column(self.name, self.type, self.default,
617 key=self.key,
616 key=self.key,
618 primary_key=self.primary_key,
617 primary_key=self.primary_key,
619 nullable=self.nullable,
618 nullable=self.nullable,
620 quote=q,
619 quote=q,
621 index=self.index,
620 index=self.index,
622 unique=self.unique,
621 unique=self.unique,
623 onupdate=self.onupdate,
622 onupdate=self.onupdate,
624 autoincrement=self.autoincrement,
623 autoincrement=self.autoincrement,
625 server_default=self.server_default,
624 server_default=self.server_default,
626 server_onupdate=self.server_onupdate,
625 server_onupdate=self.server_onupdate,
627 *[c.copy(**kw) for c in self.constraints])
626 *[c.copy(**kw) for c in self.constraints])
628
627
629 def _check_sanity_constraints(self, name):
628 def _check_sanity_constraints(self, name):
630 """Check if constraints names are correct"""
629 """Check if constraints names are correct"""
631 obj = getattr(self, name)
630 obj = getattr(self, name)
632 if (getattr(self, name[:-5]) and not obj):
631 if (getattr(self, name[:-5]) and not obj):
633 raise InvalidConstraintError("Column.create() accepts index_name,"
632 raise InvalidConstraintError("Column.create() accepts index_name,"
634 " primary_key_name and unique_name to generate constraints")
633 " primary_key_name and unique_name to generate constraints")
635 if not isinstance(obj, compat.string_types) and obj is not None:
634 if not isinstance(obj, str) and obj is not None:
636 raise InvalidConstraintError(
635 raise InvalidConstraintError(
637 "%s argument for column must be constraint name" % name)
636 "%s argument for column must be constraint name" % name)
638
637
639
638
640 class ChangesetIndex(object):
639 class ChangesetIndex(object):
641 """Changeset extensions to SQLAlchemy Indexes."""
640 """Changeset extensions to SQLAlchemy Indexes."""
642
641
643 __visit_name__ = 'index'
642 __visit_name__ = 'index'
644
643
645 def rename(self, name, connection=None, **kwargs):
644 def rename(self, name, connection=None, **kwargs):
646 """Change the name of an index.
645 """Change the name of an index.
647
646
648 :param name: New name of the Index.
647 :param name: New name of the Index.
649 :type name: string
648 :type name: string
650 :param connection: reuse connection istead of creating new one.
649 :param connection: reuse connection istead of creating new one.
651 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
650 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
652 """
651 """
653 engine = self.table.bind
652 engine = self.table.bind
654 self.new_name = name
653 self.new_name = name
655 visitorcallable = get_engine_visitor(engine, 'schemachanger')
654 visitorcallable = get_engine_visitor(engine, 'schemachanger')
656 engine._run_visitor(visitorcallable, self, connection, **kwargs)
655 engine._run_visitor(visitorcallable, self, connection, **kwargs)
657 self.name = name
656 self.name = name
658
657
659
658
660 class ChangesetDefaultClause(object):
659 class ChangesetDefaultClause(object):
661 """Implements comparison between :class:`DefaultClause` instances"""
660 """Implements comparison between :class:`DefaultClause` instances"""
662
661
663 def __eq__(self, other):
662 def __eq__(self, other):
664 if isinstance(other, self.__class__):
663 if isinstance(other, self.__class__):
665 if self.arg == other.arg:
664 if self.arg == other.arg:
666 return True
665 return True
667
666
668 def __ne__(self, other):
667 def __ne__(self, other):
669 return not self.__eq__(other)
668 return not self.__eq__(other)
@@ -1,222 +1,221 b''
1 """
1 """
2 Database schema version management.
2 Database schema version management.
3 """
3 """
4 import sys
4 import sys
5 import logging
5 import logging
6
6
7 from sqlalchemy import (Table, Column, MetaData, String, Text, Integer,
7 from sqlalchemy import (Table, Column, MetaData, String, Text, Integer,
8 create_engine)
8 create_engine)
9 from sqlalchemy.sql import and_
9 from sqlalchemy.sql import and_
10 from sqlalchemy import exc as sa_exceptions
10 from sqlalchemy import exc as sa_exceptions
11 from sqlalchemy.sql import bindparam
11 from sqlalchemy.sql import bindparam
12 from pyramid import compat
13
12
14 from rhodecode.lib.dbmigrate.migrate import exceptions
13 from rhodecode.lib.dbmigrate.migrate import exceptions
15 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07
14 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07
16 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
15 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
17 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
16 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
18 from rhodecode.lib.dbmigrate.migrate.versioning.util import load_model
17 from rhodecode.lib.dbmigrate.migrate.versioning.util import load_model
19 from rhodecode.lib.dbmigrate.migrate.versioning.version import VerNum
18 from rhodecode.lib.dbmigrate.migrate.versioning.version import VerNum
20
19
21
20
22 log = logging.getLogger(__name__)
21 log = logging.getLogger(__name__)
23
22
24
23
25 class ControlledSchema(object):
24 class ControlledSchema(object):
26 """A database under version control"""
25 """A database under version control"""
27
26
28 def __init__(self, engine, repository):
27 def __init__(self, engine, repository):
29 if isinstance(repository, compat.string_types):
28 if isinstance(repository, str):
30 repository = Repository(repository)
29 repository = Repository(repository)
31 self.engine = engine
30 self.engine = engine
32 self.repository = repository
31 self.repository = repository
33 self.meta = MetaData(engine)
32 self.meta = MetaData(engine)
34 self.load()
33 self.load()
35
34
36 def __eq__(self, other):
35 def __eq__(self, other):
37 """Compare two schemas by repositories and versions"""
36 """Compare two schemas by repositories and versions"""
38 return (self.repository is other.repository \
37 return (self.repository is other.repository \
39 and self.version == other.version)
38 and self.version == other.version)
40
39
41 def load(self):
40 def load(self):
42 """Load controlled schema version info from DB"""
41 """Load controlled schema version info from DB"""
43 tname = self.repository.version_table
42 tname = self.repository.version_table
44 try:
43 try:
45 if not hasattr(self, 'table') or self.table is None:
44 if not hasattr(self, 'table') or self.table is None:
46 self.table = Table(tname, self.meta, autoload=True)
45 self.table = Table(tname, self.meta, autoload=True)
47
46
48 result = self.engine.execute(self.table.select(
47 result = self.engine.execute(self.table.select(
49 self.table.c.repository_id == str(self.repository.id)))
48 self.table.c.repository_id == str(self.repository.id)))
50
49
51 data = list(result)[0]
50 data = list(result)[0]
52 except:
51 except:
53 cls, exc, tb = sys.exc_info()
52 cls, exc, tb = sys.exc_info()
54 raise exceptions.DatabaseNotControlledError, exc.__str__(), tb
53 raise exceptions.DatabaseNotControlledError, exc.__str__(), tb
55
54
56 self.version = data['version']
55 self.version = data['version']
57 return data
56 return data
58
57
59 def drop(self):
58 def drop(self):
60 """
59 """
61 Remove version control from a database.
60 Remove version control from a database.
62 """
61 """
63 if SQLA_07:
62 if SQLA_07:
64 try:
63 try:
65 self.table.drop()
64 self.table.drop()
66 except sa_exceptions.DatabaseError:
65 except sa_exceptions.DatabaseError:
67 raise exceptions.DatabaseNotControlledError(str(self.table))
66 raise exceptions.DatabaseNotControlledError(str(self.table))
68 else:
67 else:
69 try:
68 try:
70 self.table.drop()
69 self.table.drop()
71 except (sa_exceptions.SQLError):
70 except (sa_exceptions.SQLError):
72 raise exceptions.DatabaseNotControlledError(str(self.table))
71 raise exceptions.DatabaseNotControlledError(str(self.table))
73
72
74 def changeset(self, version=None):
73 def changeset(self, version=None):
75 """API to Changeset creation.
74 """API to Changeset creation.
76
75
77 Uses self.version for start version and engine.name
76 Uses self.version for start version and engine.name
78 to get database name.
77 to get database name.
79 """
78 """
80 database = self.engine.name
79 database = self.engine.name
81 start_ver = self.version
80 start_ver = self.version
82 changeset = self.repository.changeset(database, start_ver, version)
81 changeset = self.repository.changeset(database, start_ver, version)
83 return changeset
82 return changeset
84
83
85 def runchange(self, ver, change, step):
84 def runchange(self, ver, change, step):
86 startver = ver
85 startver = ver
87 endver = ver + step
86 endver = ver + step
88 # Current database version must be correct! Don't run if corrupt!
87 # Current database version must be correct! Don't run if corrupt!
89 if self.version != startver:
88 if self.version != startver:
90 raise exceptions.InvalidVersionError("%s is not %s" % \
89 raise exceptions.InvalidVersionError("%s is not %s" % \
91 (self.version, startver))
90 (self.version, startver))
92 # Run the change
91 # Run the change
93 change.run(self.engine, step)
92 change.run(self.engine, step)
94
93
95 # Update/refresh database version
94 # Update/refresh database version
96 self.update_repository_table(startver, endver)
95 self.update_repository_table(startver, endver)
97 self.load()
96 self.load()
98
97
99 def update_repository_table(self, startver, endver):
98 def update_repository_table(self, startver, endver):
100 """Update version_table with new information"""
99 """Update version_table with new information"""
101 update = self.table.update(and_(self.table.c.version == int(startver),
100 update = self.table.update(and_(self.table.c.version == int(startver),
102 self.table.c.repository_id == str(self.repository.id)))
101 self.table.c.repository_id == str(self.repository.id)))
103 self.engine.execute(update, version=int(endver))
102 self.engine.execute(update, version=int(endver))
104
103
105 def upgrade(self, version=None):
104 def upgrade(self, version=None):
106 """
105 """
107 Upgrade (or downgrade) to a specified version, or latest version.
106 Upgrade (or downgrade) to a specified version, or latest version.
108 """
107 """
109 changeset = self.changeset(version)
108 changeset = self.changeset(version)
110 for ver, change in changeset:
109 for ver, change in changeset:
111 self.runchange(ver, change, changeset.step)
110 self.runchange(ver, change, changeset.step)
112
111
113 def update_db_from_model(self, model):
112 def update_db_from_model(self, model):
114 """
113 """
115 Modify the database to match the structure of the current Python model.
114 Modify the database to match the structure of the current Python model.
116 """
115 """
117 model = load_model(model)
116 model = load_model(model)
118
117
119 diff = schemadiff.getDiffOfModelAgainstDatabase(
118 diff = schemadiff.getDiffOfModelAgainstDatabase(
120 model, self.engine, excludeTables=[self.repository.version_table]
119 model, self.engine, excludeTables=[self.repository.version_table]
121 )
120 )
122 genmodel.ModelGenerator(diff,self.engine).runB2A()
121 genmodel.ModelGenerator(diff,self.engine).runB2A()
123
122
124 self.update_repository_table(self.version, int(self.repository.latest))
123 self.update_repository_table(self.version, int(self.repository.latest))
125
124
126 self.load()
125 self.load()
127
126
128 @classmethod
127 @classmethod
129 def create(cls, engine, repository, version=None):
128 def create(cls, engine, repository, version=None):
130 """
129 """
131 Declare a database to be under a repository's version control.
130 Declare a database to be under a repository's version control.
132
131
133 :raises: :exc:`DatabaseAlreadyControlledError`
132 :raises: :exc:`DatabaseAlreadyControlledError`
134 :returns: :class:`ControlledSchema`
133 :returns: :class:`ControlledSchema`
135 """
134 """
136 # Confirm that the version # is valid: positive, integer,
135 # Confirm that the version # is valid: positive, integer,
137 # exists in repos
136 # exists in repos
138 if isinstance(repository, compat.string_types):
137 if isinstance(repository, str):
139 repository = Repository(repository)
138 repository = Repository(repository)
140 version = cls._validate_version(repository, version)
139 version = cls._validate_version(repository, version)
141 table = cls._create_table_version(engine, repository, version)
140 table = cls._create_table_version(engine, repository, version)
142 # TODO: history table
141 # TODO: history table
143 # Load repository information and return
142 # Load repository information and return
144 return cls(engine, repository)
143 return cls(engine, repository)
145
144
146 @classmethod
145 @classmethod
147 def _validate_version(cls, repository, version):
146 def _validate_version(cls, repository, version):
148 """
147 """
149 Ensures this is a valid version number for this repository.
148 Ensures this is a valid version number for this repository.
150
149
151 :raises: :exc:`InvalidVersionError` if invalid
150 :raises: :exc:`InvalidVersionError` if invalid
152 :return: valid version number
151 :return: valid version number
153 """
152 """
154 if version is None:
153 if version is None:
155 version = 0
154 version = 0
156 try:
155 try:
157 version = VerNum(version) # raises valueerror
156 version = VerNum(version) # raises valueerror
158 if version < 0 or version > repository.latest:
157 if version < 0 or version > repository.latest:
159 raise ValueError()
158 raise ValueError()
160 except ValueError:
159 except ValueError:
161 raise exceptions.InvalidVersionError(version)
160 raise exceptions.InvalidVersionError(version)
162 return version
161 return version
163
162
164 @classmethod
163 @classmethod
165 def _create_table_version(cls, engine, repository, version):
164 def _create_table_version(cls, engine, repository, version):
166 """
165 """
167 Creates the versioning table in a database.
166 Creates the versioning table in a database.
168
167
169 :raises: :exc:`DatabaseAlreadyControlledError`
168 :raises: :exc:`DatabaseAlreadyControlledError`
170 """
169 """
171 # Create tables
170 # Create tables
172 tname = repository.version_table
171 tname = repository.version_table
173 meta = MetaData(engine)
172 meta = MetaData(engine)
174
173
175 table = Table(
174 table = Table(
176 tname, meta,
175 tname, meta,
177 Column('repository_id', String(250), primary_key=True),
176 Column('repository_id', String(250), primary_key=True),
178 Column('repository_path', Text),
177 Column('repository_path', Text),
179 Column('version', Integer), )
178 Column('version', Integer), )
180
179
181 # there can be multiple repositories/schemas in the same db
180 # there can be multiple repositories/schemas in the same db
182 if not table.exists():
181 if not table.exists():
183 table.create()
182 table.create()
184
183
185 # test for existing repository_id
184 # test for existing repository_id
186 s = table.select(table.c.repository_id == bindparam("repository_id"))
185 s = table.select(table.c.repository_id == bindparam("repository_id"))
187 result = engine.execute(s, repository_id=repository.id)
186 result = engine.execute(s, repository_id=repository.id)
188 if result.fetchone():
187 if result.fetchone():
189 raise exceptions.DatabaseAlreadyControlledError
188 raise exceptions.DatabaseAlreadyControlledError
190
189
191 # Insert data
190 # Insert data
192 engine.execute(table.insert().values(
191 engine.execute(table.insert().values(
193 repository_id=repository.id,
192 repository_id=repository.id,
194 repository_path=repository.path,
193 repository_path=repository.path,
195 version=int(version)))
194 version=int(version)))
196 return table
195 return table
197
196
198 @classmethod
197 @classmethod
199 def compare_model_to_db(cls, engine, model, repository):
198 def compare_model_to_db(cls, engine, model, repository):
200 """
199 """
201 Compare the current model against the current database.
200 Compare the current model against the current database.
202 """
201 """
203 if isinstance(repository, compat.string_types):
202 if isinstance(repository, str):
204 repository = Repository(repository)
203 repository = Repository(repository)
205 model = load_model(model)
204 model = load_model(model)
206
205
207 diff = schemadiff.getDiffOfModelAgainstDatabase(
206 diff = schemadiff.getDiffOfModelAgainstDatabase(
208 model, engine, excludeTables=[repository.version_table])
207 model, engine, excludeTables=[repository.version_table])
209 return diff
208 return diff
210
209
211 @classmethod
210 @classmethod
212 def create_model(cls, engine, repository, declarative=False):
211 def create_model(cls, engine, repository, declarative=False):
213 """
212 """
214 Dump the current database as a Python model.
213 Dump the current database as a Python model.
215 """
214 """
216 if isinstance(repository, compat.string_types):
215 if isinstance(repository, str):
217 repository = Repository(repository)
216 repository = Repository(repository)
218
217
219 diff = schemadiff.getDiffOfModelAgainstDatabase(
218 diff = schemadiff.getDiffOfModelAgainstDatabase(
220 MetaData(), engine, excludeTables=[repository.version_table]
219 MetaData(), engine, excludeTables=[repository.version_table]
221 )
220 )
222 return genmodel.ModelGenerator(diff, engine, declarative).genBDefinition()
221 return genmodel.ModelGenerator(diff, engine, declarative).genBDefinition()
@@ -1,160 +1,159 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3
3
4 import shutil
4 import shutil
5 import warnings
5 import warnings
6 import logging
6 import logging
7 import inspect
7 import inspect
8 from StringIO import StringIO
8 from StringIO import StringIO
9
9
10 from pyramid import compat
11 from rhodecode.lib.dbmigrate import migrate
10 from rhodecode.lib.dbmigrate import migrate
12 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
11 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
13 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
12 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
13 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
15 from rhodecode.lib.dbmigrate.migrate.versioning.script import base
14 from rhodecode.lib.dbmigrate.migrate.versioning.script import base
16 from rhodecode.lib.dbmigrate.migrate.versioning.util import import_path, load_model, with_engine
15 from rhodecode.lib.dbmigrate.migrate.versioning.util import import_path, load_model, with_engine
17 from rhodecode.lib.dbmigrate.migrate.exceptions import MigrateDeprecationWarning, InvalidScriptError, ScriptError
16 from rhodecode.lib.dbmigrate.migrate.exceptions import MigrateDeprecationWarning, InvalidScriptError, ScriptError
18
17
19 log = logging.getLogger(__name__)
18 log = logging.getLogger(__name__)
20 __all__ = ['PythonScript']
19 __all__ = ['PythonScript']
21
20
22
21
23 class PythonScript(base.BaseScript):
22 class PythonScript(base.BaseScript):
24 """Base for Python scripts"""
23 """Base for Python scripts"""
25
24
26 @classmethod
25 @classmethod
27 def create(cls, path, **opts):
26 def create(cls, path, **opts):
28 """Create an empty migration script at specified path
27 """Create an empty migration script at specified path
29
28
30 :returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
29 :returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
31 cls.require_notfound(path)
30 cls.require_notfound(path)
32
31
33 src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None))
32 src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None))
34 shutil.copy(src, path)
33 shutil.copy(src, path)
35
34
36 return cls(path)
35 return cls(path)
37
36
38 @classmethod
37 @classmethod
39 def make_update_script_for_model(cls, engine, oldmodel,
38 def make_update_script_for_model(cls, engine, oldmodel,
40 model, repository, **opts):
39 model, repository, **opts):
41 """Create a migration script based on difference between two SA models.
40 """Create a migration script based on difference between two SA models.
42
41
43 :param repository: path to migrate repository
42 :param repository: path to migrate repository
44 :param oldmodel: dotted.module.name:SAClass or SAClass object
43 :param oldmodel: dotted.module.name:SAClass or SAClass object
45 :param model: dotted.module.name:SAClass or SAClass object
44 :param model: dotted.module.name:SAClass or SAClass object
46 :param engine: SQLAlchemy engine
45 :param engine: SQLAlchemy engine
47 :type repository: string or :class:`Repository instance <migrate.versioning.repository.Repository>`
46 :type repository: string or :class:`Repository instance <migrate.versioning.repository.Repository>`
48 :type oldmodel: string or Class
47 :type oldmodel: string or Class
49 :type model: string or Class
48 :type model: string or Class
50 :type engine: Engine instance
49 :type engine: Engine instance
51 :returns: Upgrade / Downgrade script
50 :returns: Upgrade / Downgrade script
52 :rtype: string
51 :rtype: string
53 """
52 """
54
53
55 if isinstance(repository, compat.string_types):
54 if isinstance(repository, str):
56 # oh dear, an import cycle!
55 # oh dear, an import cycle!
57 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
56 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
58 repository = Repository(repository)
57 repository = Repository(repository)
59
58
60 oldmodel = load_model(oldmodel)
59 oldmodel = load_model(oldmodel)
61 model = load_model(model)
60 model = load_model(model)
62
61
63 # Compute differences.
62 # Compute differences.
64 diff = schemadiff.getDiffOfModelAgainstModel(
63 diff = schemadiff.getDiffOfModelAgainstModel(
65 model,
64 model,
66 oldmodel,
65 oldmodel,
67 excludeTables=[repository.version_table])
66 excludeTables=[repository.version_table])
68 # TODO: diff can be False (there is no difference?)
67 # TODO: diff can be False (there is no difference?)
69 decls, upgradeCommands, downgradeCommands = \
68 decls, upgradeCommands, downgradeCommands = \
70 genmodel.ModelGenerator(diff,engine).genB2AMigration()
69 genmodel.ModelGenerator(diff,engine).genB2AMigration()
71
70
72 # Store differences into file.
71 # Store differences into file.
73 src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
72 src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
74 with open(src) as f:
73 with open(src) as f:
75 contents = f.read()
74 contents = f.read()
76
75
77 # generate source
76 # generate source
78 search = 'def upgrade(migrate_engine):'
77 search = 'def upgrade(migrate_engine):'
79 contents = contents.replace(search, '\n\n'.join((decls, search)), 1)
78 contents = contents.replace(search, '\n\n'.join((decls, search)), 1)
80 if upgradeCommands:
79 if upgradeCommands:
81 contents = contents.replace(' pass', upgradeCommands, 1)
80 contents = contents.replace(' pass', upgradeCommands, 1)
82 if downgradeCommands:
81 if downgradeCommands:
83 contents = contents.replace(' pass', downgradeCommands, 1)
82 contents = contents.replace(' pass', downgradeCommands, 1)
84 return contents
83 return contents
85
84
86 @classmethod
85 @classmethod
87 def verify_module(cls, path):
86 def verify_module(cls, path):
88 """Ensure path is a valid script
87 """Ensure path is a valid script
89
88
90 :param path: Script location
89 :param path: Script location
91 :type path: string
90 :type path: string
92 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
91 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
93 :returns: Python module
92 :returns: Python module
94 """
93 """
95 # Try to import and get the upgrade() func
94 # Try to import and get the upgrade() func
96 module = import_path(path)
95 module = import_path(path)
97 try:
96 try:
98 assert callable(module.upgrade)
97 assert callable(module.upgrade)
99 except Exception as e:
98 except Exception as e:
100 raise InvalidScriptError(path + ': %s' % str(e))
99 raise InvalidScriptError(path + ': %s' % str(e))
101 return module
100 return module
102
101
103 def preview_sql(self, url, step, **args):
102 def preview_sql(self, url, step, **args):
104 """Mocks SQLAlchemy Engine to store all executed calls in a string
103 """Mocks SQLAlchemy Engine to store all executed calls in a string
105 and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
104 and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
106
105
107 :returns: SQL file
106 :returns: SQL file
108 """
107 """
109 buf = StringIO()
108 buf = StringIO()
110 args['engine_arg_strategy'] = 'mock'
109 args['engine_arg_strategy'] = 'mock'
111 args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p)
110 args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p)
112
111
113 @with_engine
112 @with_engine
114 def go(url, step, **kw):
113 def go(url, step, **kw):
115 engine = kw.pop('engine')
114 engine = kw.pop('engine')
116 self.run(engine, step)
115 self.run(engine, step)
117 return buf.getvalue()
116 return buf.getvalue()
118
117
119 return go(url, step, **args)
118 return go(url, step, **args)
120
119
121 def run(self, engine, step):
120 def run(self, engine, step):
122 """Core method of Script file.
121 """Core method of Script file.
123 Exectues :func:`update` or :func:`downgrade` functions
122 Exectues :func:`update` or :func:`downgrade` functions
124
123
125 :param engine: SQLAlchemy Engine
124 :param engine: SQLAlchemy Engine
126 :param step: Operation to run
125 :param step: Operation to run
127 :type engine: string
126 :type engine: string
128 :type step: int
127 :type step: int
129 """
128 """
130 if step > 0:
129 if step > 0:
131 op = 'upgrade'
130 op = 'upgrade'
132 elif step < 0:
131 elif step < 0:
133 op = 'downgrade'
132 op = 'downgrade'
134 else:
133 else:
135 raise ScriptError("%d is not a valid step" % step)
134 raise ScriptError("%d is not a valid step" % step)
136
135
137 funcname = base.operations[op]
136 funcname = base.operations[op]
138 script_func = self._func(funcname)
137 script_func = self._func(funcname)
139
138
140 # check for old way of using engine
139 # check for old way of using engine
141 if not inspect.getargspec(script_func)[0]:
140 if not inspect.getargspec(script_func)[0]:
142 raise TypeError("upgrade/downgrade functions must accept engine"
141 raise TypeError("upgrade/downgrade functions must accept engine"
143 " parameter (since version 0.5.4)")
142 " parameter (since version 0.5.4)")
144
143
145 script_func(engine)
144 script_func(engine)
146
145
147 @property
146 @property
148 def module(self):
147 def module(self):
149 """Calls :meth:`migrate.versioning.script.py.verify_module`
148 """Calls :meth:`migrate.versioning.script.py.verify_module`
150 and returns it.
149 and returns it.
151 """
150 """
152 if not hasattr(self, '_module'):
151 if not hasattr(self, '_module'):
153 self._module = self.verify_module(self.path)
152 self._module = self.verify_module(self.path)
154 return self._module
153 return self._module
155
154
156 def _func(self, funcname):
155 def _func(self, funcname):
157 if not hasattr(self.module, funcname):
156 if not hasattr(self.module, funcname):
158 msg = "Function '%s' is not defined in this script"
157 msg = "Function '%s' is not defined in this script"
159 raise ScriptError(msg % funcname)
158 raise ScriptError(msg % funcname)
160 return getattr(self.module, funcname)
159 return getattr(self.module, funcname)
@@ -1,181 +1,180 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3 """.. currentmodule:: migrate.versioning.util"""
3 """.. currentmodule:: migrate.versioning.util"""
4
4
5 import warnings
5 import warnings
6 import logging
6 import logging
7 from decorator import decorator
7 from decorator import decorator
8 from pkg_resources import EntryPoint
8 from pkg_resources import EntryPoint
9
9
10 from sqlalchemy import create_engine
10 from sqlalchemy import create_engine
11 from sqlalchemy.engine import Engine
11 from sqlalchemy.engine import Engine
12 from sqlalchemy.pool import StaticPool
12 from sqlalchemy.pool import StaticPool
13
13
14 from pyramid import compat
15 from rhodecode.lib.dbmigrate.migrate import exceptions
14 from rhodecode.lib.dbmigrate.migrate import exceptions
16 from rhodecode.lib.dbmigrate.migrate.versioning.util.keyedinstance import KeyedInstance
15 from rhodecode.lib.dbmigrate.migrate.versioning.util.keyedinstance import KeyedInstance
17 from rhodecode.lib.dbmigrate.migrate.versioning.util.importpath import import_path
16 from rhodecode.lib.dbmigrate.migrate.versioning.util.importpath import import_path
18
17
19
18
20 log = logging.getLogger(__name__)
19 log = logging.getLogger(__name__)
21
20
22
21
23 def load_model(dotted_name):
22 def load_model(dotted_name):
24 """Import module and use module-level variable".
23 """Import module and use module-level variable".
25
24
26 :param dotted_name: path to model in form of string: ``some.python.module:Class``
25 :param dotted_name: path to model in form of string: ``some.python.module:Class``
27
26
28 .. versionchanged:: 0.5.4
27 .. versionchanged:: 0.5.4
29
28
30 """
29 """
31 if isinstance(dotted_name, compat.string_types):
30 if isinstance(dotted_name, str):
32 if ':' not in dotted_name:
31 if ':' not in dotted_name:
33 # backwards compatibility
32 # backwards compatibility
34 warnings.warn('model should be in form of module.model:User '
33 warnings.warn('model should be in form of module.model:User '
35 'and not module.model.User', exceptions.MigrateDeprecationWarning)
34 'and not module.model.User', exceptions.MigrateDeprecationWarning)
36 dotted_name = ':'.join(dotted_name.rsplit('.', 1))
35 dotted_name = ':'.join(dotted_name.rsplit('.', 1))
37 return EntryPoint.parse('x=%s' % dotted_name).load(False)
36 return EntryPoint.parse('x=%s' % dotted_name).load(False)
38 else:
37 else:
39 # Assume it's already loaded.
38 # Assume it's already loaded.
40 return dotted_name
39 return dotted_name
41
40
42 def asbool(obj):
41 def asbool(obj):
43 """Do everything to use object as bool"""
42 """Do everything to use object as bool"""
44 if isinstance(obj, compat.string_types):
43 if isinstance(obj, str):
45 obj = obj.strip().lower()
44 obj = obj.strip().lower()
46 if obj in ['true', 'yes', 'on', 'y', 't', '1']:
45 if obj in ['true', 'yes', 'on', 'y', 't', '1']:
47 return True
46 return True
48 elif obj in ['false', 'no', 'off', 'n', 'f', '0']:
47 elif obj in ['false', 'no', 'off', 'n', 'f', '0']:
49 return False
48 return False
50 else:
49 else:
51 raise ValueError("String is not true/false: %r" % obj)
50 raise ValueError("String is not true/false: %r" % obj)
52 if obj in (True, False):
51 if obj in (True, False):
53 return bool(obj)
52 return bool(obj)
54 else:
53 else:
55 raise ValueError("String is not true/false: %r" % obj)
54 raise ValueError("String is not true/false: %r" % obj)
56
55
57 def guess_obj_type(obj):
56 def guess_obj_type(obj):
58 """Do everything to guess object type from string
57 """Do everything to guess object type from string
59
58
60 Tries to convert to `int`, `bool` and finally returns if not succeded.
59 Tries to convert to `int`, `bool` and finally returns if not succeded.
61
60
62 .. versionadded: 0.5.4
61 .. versionadded: 0.5.4
63 """
62 """
64
63
65 result = None
64 result = None
66
65
67 try:
66 try:
68 result = int(obj)
67 result = int(obj)
69 except:
68 except:
70 pass
69 pass
71
70
72 if result is None:
71 if result is None:
73 try:
72 try:
74 result = asbool(obj)
73 result = asbool(obj)
75 except:
74 except:
76 pass
75 pass
77
76
78 if result is not None:
77 if result is not None:
79 return result
78 return result
80 else:
79 else:
81 return obj
80 return obj
82
81
83 @decorator
82 @decorator
84 def catch_known_errors(f, *a, **kw):
83 def catch_known_errors(f, *a, **kw):
85 """Decorator that catches known api errors
84 """Decorator that catches known api errors
86
85
87 .. versionadded: 0.5.4
86 .. versionadded: 0.5.4
88 """
87 """
89
88
90 try:
89 try:
91 return f(*a, **kw)
90 return f(*a, **kw)
92 except exceptions.PathFoundError as e:
91 except exceptions.PathFoundError as e:
93 raise exceptions.KnownError("The path %s already exists" % e.args[0])
92 raise exceptions.KnownError("The path %s already exists" % e.args[0])
94
93
95 def construct_engine(engine, **opts):
94 def construct_engine(engine, **opts):
96 """.. versionadded:: 0.5.4
95 """.. versionadded:: 0.5.4
97
96
98 Constructs and returns SQLAlchemy engine.
97 Constructs and returns SQLAlchemy engine.
99
98
100 Currently, there are 2 ways to pass create_engine options to :mod:`migrate.versioning.api` functions:
99 Currently, there are 2 ways to pass create_engine options to :mod:`migrate.versioning.api` functions:
101
100
102 :param engine: connection string or a existing engine
101 :param engine: connection string or a existing engine
103 :param engine_dict: python dictionary of options to pass to `create_engine`
102 :param engine_dict: python dictionary of options to pass to `create_engine`
104 :param engine_arg_*: keyword parameters to pass to `create_engine` (evaluated with :func:`migrate.versioning.util.guess_obj_type`)
103 :param engine_arg_*: keyword parameters to pass to `create_engine` (evaluated with :func:`migrate.versioning.util.guess_obj_type`)
105 :type engine_dict: dict
104 :type engine_dict: dict
106 :type engine: string or Engine instance
105 :type engine: string or Engine instance
107 :type engine_arg_*: string
106 :type engine_arg_*: string
108 :returns: SQLAlchemy Engine
107 :returns: SQLAlchemy Engine
109
108
110 .. note::
109 .. note::
111
110
112 keyword parameters override ``engine_dict`` values.
111 keyword parameters override ``engine_dict`` values.
113
112
114 """
113 """
115 if isinstance(engine, Engine):
114 if isinstance(engine, Engine):
116 return engine
115 return engine
117 elif not isinstance(engine, compat.string_types):
116 elif not isinstance(engine, str):
118 raise ValueError("you need to pass either an existing engine or a database uri")
117 raise ValueError("you need to pass either an existing engine or a database uri")
119
118
120 # get options for create_engine
119 # get options for create_engine
121 if opts.get('engine_dict') and isinstance(opts['engine_dict'], dict):
120 if opts.get('engine_dict') and isinstance(opts['engine_dict'], dict):
122 kwargs = opts['engine_dict']
121 kwargs = opts['engine_dict']
123 else:
122 else:
124 kwargs = {}
123 kwargs = {}
125
124
126 # DEPRECATED: handle echo the old way
125 # DEPRECATED: handle echo the old way
127 echo = asbool(opts.get('echo', False))
126 echo = asbool(opts.get('echo', False))
128 if echo:
127 if echo:
129 warnings.warn('echo=True parameter is deprecated, pass '
128 warnings.warn('echo=True parameter is deprecated, pass '
130 'engine_arg_echo=True or engine_dict={"echo": True}',
129 'engine_arg_echo=True or engine_dict={"echo": True}',
131 exceptions.MigrateDeprecationWarning)
130 exceptions.MigrateDeprecationWarning)
132 kwargs['echo'] = echo
131 kwargs['echo'] = echo
133
132
134 # parse keyword arguments
133 # parse keyword arguments
135 for key, value in opts.iteritems():
134 for key, value in opts.iteritems():
136 if key.startswith('engine_arg_'):
135 if key.startswith('engine_arg_'):
137 kwargs[key[11:]] = guess_obj_type(value)
136 kwargs[key[11:]] = guess_obj_type(value)
138
137
139 log.debug('Constructing engine')
138 log.debug('Constructing engine')
140 # TODO: return create_engine(engine, poolclass=StaticPool, **kwargs)
139 # TODO: return create_engine(engine, poolclass=StaticPool, **kwargs)
141 # seems like 0.5.x branch does not work with engine.dispose and staticpool
140 # seems like 0.5.x branch does not work with engine.dispose and staticpool
142 return create_engine(engine, **kwargs)
141 return create_engine(engine, **kwargs)
143
142
144 @decorator
143 @decorator
145 def with_engine(f, *a, **kw):
144 def with_engine(f, *a, **kw):
146 """Decorator for :mod:`migrate.versioning.api` functions
145 """Decorator for :mod:`migrate.versioning.api` functions
147 to safely close resources after function usage.
146 to safely close resources after function usage.
148
147
149 Passes engine parameters to :func:`construct_engine` and
148 Passes engine parameters to :func:`construct_engine` and
150 resulting parameter is available as kw['engine'].
149 resulting parameter is available as kw['engine'].
151
150
152 Engine is disposed after wrapped function is executed.
151 Engine is disposed after wrapped function is executed.
153
152
154 .. versionadded: 0.6.0
153 .. versionadded: 0.6.0
155 """
154 """
156 url = a[0]
155 url = a[0]
157 engine = construct_engine(url, **kw)
156 engine = construct_engine(url, **kw)
158
157
159 try:
158 try:
160 kw['engine'] = engine
159 kw['engine'] = engine
161 return f(*a, **kw)
160 return f(*a, **kw)
162 finally:
161 finally:
163 if isinstance(engine, Engine) and engine is not url:
162 if isinstance(engine, Engine) and engine is not url:
164 log.debug('Disposing SQLAlchemy engine %s', engine)
163 log.debug('Disposing SQLAlchemy engine %s', engine)
165 engine.dispose()
164 engine.dispose()
166
165
167
166
168 class Memoize:
167 class Memoize:
169 """Memoize(fn) - an instance which acts like fn but memoizes its arguments
168 """Memoize(fn) - an instance which acts like fn but memoizes its arguments
170 Will only work on functions with non-mutable arguments
169 Will only work on functions with non-mutable arguments
171
170
172 ActiveState Code 52201
171 ActiveState Code 52201
173 """
172 """
174 def __init__(self, fn):
173 def __init__(self, fn):
175 self.fn = fn
174 self.fn = fn
176 self.memo = {}
175 self.memo = {}
177
176
178 def __call__(self, *args):
177 def __call__(self, *args):
179 if args not in self.memo:
178 if args not in self.memo:
180 self.memo[args] = self.fn(*args)
179 self.memo[args] = self.fn(*args)
181 return self.memo[args]
180 return self.memo[args]
@@ -1,1044 +1,1043 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import logging
22 import logging
23 import datetime
23 import datetime
24 import traceback
24 import traceback
25 from datetime import date
25 from datetime import date
26
26
27 from sqlalchemy import *
27 from sqlalchemy import *
28 from sqlalchemy.ext.hybrid import hybrid_property
28 from sqlalchemy.ext.hybrid import hybrid_property
29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
29 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
30 from beaker.cache import cache_region, region_invalidate
30 from beaker.cache import cache_region, region_invalidate
31 from pyramid import compat
32
31
33 from rhodecode.lib.vcs import get_backend
32 from rhodecode.lib.vcs import get_backend
34 from rhodecode.lib.vcs.utils.helpers import get_scm
33 from rhodecode.lib.vcs.utils.helpers import get_scm
35 from rhodecode.lib.vcs.exceptions import VCSError
34 from rhodecode.lib.vcs.exceptions import VCSError
36 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
37 from rhodecode.lib.auth import generate_auth_token
36 from rhodecode.lib.auth import generate_auth_token
38 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, safe_unicode
37 from rhodecode.lib.utils2 import str2bool, safe_str, get_commit_safe, safe_unicode
39 from rhodecode.lib.exceptions import UserGroupAssignedException
38 from rhodecode.lib.exceptions import UserGroupAssignedException
40 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
41
40
42 from rhodecode.model.meta import Base, Session
41 from rhodecode.model.meta import Base, Session
43 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.lib.caching_query import FromCache
44
43
45
44
46 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
47
46
48 #==============================================================================
47 #==============================================================================
49 # BASE CLASSES
48 # BASE CLASSES
50 #==============================================================================
49 #==============================================================================
51
50
52 class ModelSerializer(json.JSONEncoder):
51 class ModelSerializer(json.JSONEncoder):
53 """
52 """
54 Simple Serializer for JSON,
53 Simple Serializer for JSON,
55
54
56 usage::
55 usage::
57
56
58 to make object customized for serialization implement a __json__
57 to make object customized for serialization implement a __json__
59 method that will return a dict for serialization into json
58 method that will return a dict for serialization into json
60
59
61 example::
60 example::
62
61
63 class Task(object):
62 class Task(object):
64
63
65 def __init__(self, name, value):
64 def __init__(self, name, value):
66 self.name = name
65 self.name = name
67 self.value = value
66 self.value = value
68
67
69 def __json__(self):
68 def __json__(self):
70 return dict(name=self.name,
69 return dict(name=self.name,
71 value=self.value)
70 value=self.value)
72
71
73 """
72 """
74
73
75 def default(self, obj):
74 def default(self, obj):
76
75
77 if hasattr(obj, '__json__'):
76 if hasattr(obj, '__json__'):
78 return obj.__json__()
77 return obj.__json__()
79 else:
78 else:
80 return json.JSONEncoder.default(self, obj)
79 return json.JSONEncoder.default(self, obj)
81
80
82 class BaseModel(object):
81 class BaseModel(object):
83 """Base Model for all classess
82 """Base Model for all classess
84
83
85 """
84 """
86
85
87 @classmethod
86 @classmethod
88 def _get_keys(cls):
87 def _get_keys(cls):
89 """return column names for this model """
88 """return column names for this model """
90 return class_mapper(cls).c.keys()
89 return class_mapper(cls).c.keys()
91
90
92 def get_dict(self):
91 def get_dict(self):
93 """return dict with keys and values corresponding
92 """return dict with keys and values corresponding
94 to this model data """
93 to this model data """
95
94
96 d = {}
95 d = {}
97 for k in self._get_keys():
96 for k in self._get_keys():
98 d[k] = getattr(self, k)
97 d[k] = getattr(self, k)
99 return d
98 return d
100
99
101 def get_appstruct(self):
100 def get_appstruct(self):
102 """return list with keys and values tupples corresponding
101 """return list with keys and values tupples corresponding
103 to this model data """
102 to this model data """
104
103
105 l = []
104 l = []
106 for k in self._get_keys():
105 for k in self._get_keys():
107 l.append((k, getattr(self, k),))
106 l.append((k, getattr(self, k),))
108 return l
107 return l
109
108
110 def populate_obj(self, populate_dict):
109 def populate_obj(self, populate_dict):
111 """populate model with data from given populate_dict"""
110 """populate model with data from given populate_dict"""
112
111
113 for k in self._get_keys():
112 for k in self._get_keys():
114 if k in populate_dict:
113 if k in populate_dict:
115 setattr(self, k, populate_dict[k])
114 setattr(self, k, populate_dict[k])
116
115
117 @classmethod
116 @classmethod
118 def query(cls):
117 def query(cls):
119 return Session.query(cls)
118 return Session.query(cls)
120
119
121 @classmethod
120 @classmethod
122 def get(cls, id_):
121 def get(cls, id_):
123 if id_:
122 if id_:
124 return cls.query().get(id_)
123 return cls.query().get(id_)
125
124
126 @classmethod
125 @classmethod
127 def getAll(cls):
126 def getAll(cls):
128 return cls.query().all()
127 return cls.query().all()
129
128
130 @classmethod
129 @classmethod
131 def delete(cls, id_):
130 def delete(cls, id_):
132 obj = cls.query().get(id_)
131 obj = cls.query().get(id_)
133 Session.delete(obj)
132 Session.delete(obj)
134 Session.commit()
133 Session.commit()
135
134
136
135
137 class RhodeCodeSetting(Base, BaseModel):
136 class RhodeCodeSetting(Base, BaseModel):
138 __tablename__ = 'rhodecode_settings'
137 __tablename__ = 'rhodecode_settings'
139 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
138 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
140 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
139 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
141 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
140 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
142 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
141 _app_settings_value = Column("app_settings_value", String(255), nullable=True, unique=None, default=None)
143
142
144 def __init__(self, k='', v=''):
143 def __init__(self, k='', v=''):
145 self.app_settings_name = k
144 self.app_settings_name = k
146 self.app_settings_value = v
145 self.app_settings_value = v
147
146
148
147
149 @validates('_app_settings_value')
148 @validates('_app_settings_value')
150 def validate_settings_value(self, key, val):
149 def validate_settings_value(self, key, val):
151 assert type(val) == unicode
150 assert type(val) == unicode
152 return val
151 return val
153
152
154 @hybrid_property
153 @hybrid_property
155 def app_settings_value(self):
154 def app_settings_value(self):
156 v = self._app_settings_value
155 v = self._app_settings_value
157 if v == 'ldap_active':
156 if v == 'ldap_active':
158 v = str2bool(v)
157 v = str2bool(v)
159 return v
158 return v
160
159
161 @app_settings_value.setter
160 @app_settings_value.setter
162 def app_settings_value(self, val):
161 def app_settings_value(self, val):
163 """
162 """
164 Setter that will always make sure we use unicode in app_settings_value
163 Setter that will always make sure we use unicode in app_settings_value
165
164
166 :param val:
165 :param val:
167 """
166 """
168 self._app_settings_value = safe_unicode(val)
167 self._app_settings_value = safe_unicode(val)
169
168
170 def __repr__(self):
169 def __repr__(self):
171 return "<%s('%s:%s')>" % (self.__class__.__name__,
170 return "<%s('%s:%s')>" % (self.__class__.__name__,
172 self.app_settings_name, self.app_settings_value)
171 self.app_settings_name, self.app_settings_value)
173
172
174
173
175 @classmethod
174 @classmethod
176 def get_by_name(cls, ldap_key):
175 def get_by_name(cls, ldap_key):
177 return cls.query()\
176 return cls.query()\
178 .filter(cls.app_settings_name == ldap_key).scalar()
177 .filter(cls.app_settings_name == ldap_key).scalar()
179
178
180 @classmethod
179 @classmethod
181 def get_app_settings(cls, cache=False):
180 def get_app_settings(cls, cache=False):
182
181
183 ret = cls.query()
182 ret = cls.query()
184
183
185 if cache:
184 if cache:
186 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
185 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
187
186
188 if not ret:
187 if not ret:
189 raise Exception('Could not get application settings !')
188 raise Exception('Could not get application settings !')
190 settings = {}
189 settings = {}
191 for each in ret:
190 for each in ret:
192 settings['rhodecode_' + each.app_settings_name] = \
191 settings['rhodecode_' + each.app_settings_name] = \
193 each.app_settings_value
192 each.app_settings_value
194
193
195 return settings
194 return settings
196
195
197 @classmethod
196 @classmethod
198 def get_ldap_settings(cls, cache=False):
197 def get_ldap_settings(cls, cache=False):
199 ret = cls.query()\
198 ret = cls.query()\
200 .filter(cls.app_settings_name.startswith('ldap_')).all()
199 .filter(cls.app_settings_name.startswith('ldap_')).all()
201 fd = {}
200 fd = {}
202 for row in ret:
201 for row in ret:
203 fd.update({row.app_settings_name:row.app_settings_value})
202 fd.update({row.app_settings_name:row.app_settings_value})
204
203
205 return fd
204 return fd
206
205
207
206
208 class RhodeCodeUi(Base, BaseModel):
207 class RhodeCodeUi(Base, BaseModel):
209 __tablename__ = 'rhodecode_ui'
208 __tablename__ = 'rhodecode_ui'
210 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
209 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
211
210
212 HOOK_REPO_SIZE = 'changegroup.repo_size'
211 HOOK_REPO_SIZE = 'changegroup.repo_size'
213 HOOK_PUSH = 'pretxnchangegroup.push_logger'
212 HOOK_PUSH = 'pretxnchangegroup.push_logger'
214 HOOK_PULL = 'preoutgoing.pull_logger'
213 HOOK_PULL = 'preoutgoing.pull_logger'
215
214
216 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
215 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
217 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
216 ui_section = Column("ui_section", String(255), nullable=True, unique=None, default=None)
218 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
217 ui_key = Column("ui_key", String(255), nullable=True, unique=None, default=None)
219 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
218 ui_value = Column("ui_value", String(255), nullable=True, unique=None, default=None)
220 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
219 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
221
220
222
221
223 @classmethod
222 @classmethod
224 def get_by_key(cls, key):
223 def get_by_key(cls, key):
225 return cls.query().filter(cls.ui_key == key)
224 return cls.query().filter(cls.ui_key == key)
226
225
227
226
228 @classmethod
227 @classmethod
229 def get_builtin_hooks(cls):
228 def get_builtin_hooks(cls):
230 q = cls.query()
229 q = cls.query()
231 q = q.filter(cls.ui_key.in_([cls.HOOK_REPO_SIZE,
230 q = q.filter(cls.ui_key.in_([cls.HOOK_REPO_SIZE,
232 cls.HOOK_PUSH, cls.HOOK_PULL]))
231 cls.HOOK_PUSH, cls.HOOK_PULL]))
233 return q.all()
232 return q.all()
234
233
235 @classmethod
234 @classmethod
236 def get_custom_hooks(cls):
235 def get_custom_hooks(cls):
237 q = cls.query()
236 q = cls.query()
238 q = q.filter(~cls.ui_key.in_([cls.HOOK_REPO_SIZE,
237 q = q.filter(~cls.ui_key.in_([cls.HOOK_REPO_SIZE,
239 cls.HOOK_PUSH, cls.HOOK_PULL]))
238 cls.HOOK_PUSH, cls.HOOK_PULL]))
240 q = q.filter(cls.ui_section == 'hooks')
239 q = q.filter(cls.ui_section == 'hooks')
241 return q.all()
240 return q.all()
242
241
243 @classmethod
242 @classmethod
244 def create_or_update_hook(cls, key, val):
243 def create_or_update_hook(cls, key, val):
245 new_ui = cls.get_by_key(key).scalar() or cls()
244 new_ui = cls.get_by_key(key).scalar() or cls()
246 new_ui.ui_section = 'hooks'
245 new_ui.ui_section = 'hooks'
247 new_ui.ui_active = True
246 new_ui.ui_active = True
248 new_ui.ui_key = key
247 new_ui.ui_key = key
249 new_ui.ui_value = val
248 new_ui.ui_value = val
250
249
251 Session.add(new_ui)
250 Session.add(new_ui)
252 Session.commit()
251 Session.commit()
253
252
254
253
255 class User(Base, BaseModel):
254 class User(Base, BaseModel):
256 __tablename__ = 'users'
255 __tablename__ = 'users'
257 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
256 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
258 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
257 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
259 username = Column("username", String(255), nullable=True, unique=None, default=None)
258 username = Column("username", String(255), nullable=True, unique=None, default=None)
260 password = Column("password", String(255), nullable=True, unique=None, default=None)
259 password = Column("password", String(255), nullable=True, unique=None, default=None)
261 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
260 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
262 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
261 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
263 name = Column("name", String(255), nullable=True, unique=None, default=None)
262 name = Column("name", String(255), nullable=True, unique=None, default=None)
264 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
263 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
265 email = Column("email", String(255), nullable=True, unique=None, default=None)
264 email = Column("email", String(255), nullable=True, unique=None, default=None)
266 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
265 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
267 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
266 ldap_dn = Column("ldap_dn", String(255), nullable=True, unique=None, default=None)
268 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
267 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
269
268
270 user_log = relationship('UserLog', cascade='all')
269 user_log = relationship('UserLog', cascade='all')
271 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
270 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
272
271
273 repositories = relationship('Repository')
272 repositories = relationship('Repository')
274 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
273 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
275 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
274 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
276
275
277 group_member = relationship('UserGroupMember', cascade='all')
276 group_member = relationship('UserGroupMember', cascade='all')
278
277
279 @property
278 @property
280 def full_contact(self):
279 def full_contact(self):
281 return '%s %s <%s>' % (self.name, self.lastname, self.email)
280 return '%s %s <%s>' % (self.name, self.lastname, self.email)
282
281
283 @property
282 @property
284 def short_contact(self):
283 def short_contact(self):
285 return '%s %s' % (self.name, self.lastname)
284 return '%s %s' % (self.name, self.lastname)
286
285
287 @property
286 @property
288 def is_admin(self):
287 def is_admin(self):
289 return self.admin
288 return self.admin
290
289
291 def __repr__(self):
290 def __repr__(self):
292 try:
291 try:
293 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
292 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
294 self.user_id, self.username)
293 self.user_id, self.username)
295 except:
294 except:
296 return self.__class__.__name__
295 return self.__class__.__name__
297
296
298 @classmethod
297 @classmethod
299 def get_by_username(cls, username, case_insensitive=False):
298 def get_by_username(cls, username, case_insensitive=False):
300 if case_insensitive:
299 if case_insensitive:
301 return Session.query(cls).filter(cls.username.ilike(username)).scalar()
300 return Session.query(cls).filter(cls.username.ilike(username)).scalar()
302 else:
301 else:
303 return Session.query(cls).filter(cls.username == username).scalar()
302 return Session.query(cls).filter(cls.username == username).scalar()
304
303
305 @classmethod
304 @classmethod
306 def get_by_auth_token(cls, auth_token):
305 def get_by_auth_token(cls, auth_token):
307 return cls.query().filter(cls.api_key == auth_token).one()
306 return cls.query().filter(cls.api_key == auth_token).one()
308
307
309 def update_lastlogin(self):
308 def update_lastlogin(self):
310 """Update user lastlogin"""
309 """Update user lastlogin"""
311
310
312 self.last_login = datetime.datetime.now()
311 self.last_login = datetime.datetime.now()
313 Session.add(self)
312 Session.add(self)
314 Session.commit()
313 Session.commit()
315 log.debug('updated user %s lastlogin', self.username)
314 log.debug('updated user %s lastlogin', self.username)
316
315
317 @classmethod
316 @classmethod
318 def create(cls, form_data):
317 def create(cls, form_data):
319 from rhodecode.lib.auth import get_crypt_password
318 from rhodecode.lib.auth import get_crypt_password
320
319
321 try:
320 try:
322 new_user = cls()
321 new_user = cls()
323 for k, v in form_data.items():
322 for k, v in form_data.items():
324 if k == 'password':
323 if k == 'password':
325 v = get_crypt_password(v)
324 v = get_crypt_password(v)
326 setattr(new_user, k, v)
325 setattr(new_user, k, v)
327
326
328 new_user.api_key = generate_auth_token(form_data['username'])
327 new_user.api_key = generate_auth_token(form_data['username'])
329 Session.add(new_user)
328 Session.add(new_user)
330 Session.commit()
329 Session.commit()
331 return new_user
330 return new_user
332 except:
331 except:
333 log.error(traceback.format_exc())
332 log.error(traceback.format_exc())
334 Session.rollback()
333 Session.rollback()
335 raise
334 raise
336
335
337 class UserLog(Base, BaseModel):
336 class UserLog(Base, BaseModel):
338 __tablename__ = 'user_logs'
337 __tablename__ = 'user_logs'
339 __table_args__ = {'extend_existing':True}
338 __table_args__ = {'extend_existing':True}
340 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
339 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
341 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
340 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
342 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
341 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
343 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
342 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
344 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
343 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
345 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
344 action = Column("action", String(1200000), nullable=True, unique=None, default=None)
346 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
345 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
347
346
348 @property
347 @property
349 def action_as_day(self):
348 def action_as_day(self):
350 return date(*self.action_date.timetuple()[:3])
349 return date(*self.action_date.timetuple()[:3])
351
350
352 user = relationship('User')
351 user = relationship('User')
353 repository = relationship('Repository')
352 repository = relationship('Repository')
354
353
355
354
356 class UserGroup(Base, BaseModel):
355 class UserGroup(Base, BaseModel):
357 __tablename__ = 'users_groups'
356 __tablename__ = 'users_groups'
358 __table_args__ = {'extend_existing':True}
357 __table_args__ = {'extend_existing':True}
359
358
360 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
359 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
361 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
360 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
362 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
361 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
363
362
364 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
363 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
365
364
366 def __repr__(self):
365 def __repr__(self):
367 return '<userGroup(%s)>' % (self.users_group_name)
366 return '<userGroup(%s)>' % (self.users_group_name)
368
367
369 @classmethod
368 @classmethod
370 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
369 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
371 if case_insensitive:
370 if case_insensitive:
372 gr = cls.query()\
371 gr = cls.query()\
373 .filter(cls.users_group_name.ilike(group_name))
372 .filter(cls.users_group_name.ilike(group_name))
374 else:
373 else:
375 gr = cls.query()\
374 gr = cls.query()\
376 .filter(cls.users_group_name == group_name)
375 .filter(cls.users_group_name == group_name)
377 if cache:
376 if cache:
378 gr = gr.options(FromCache("sql_cache_short",
377 gr = gr.options(FromCache("sql_cache_short",
379 "get_user_%s" % group_name))
378 "get_user_%s" % group_name))
380 return gr.scalar()
379 return gr.scalar()
381
380
382 @classmethod
381 @classmethod
383 def get(cls, users_group_id, cache=False):
382 def get(cls, users_group_id, cache=False):
384 users_group = cls.query()
383 users_group = cls.query()
385 if cache:
384 if cache:
386 users_group = users_group.options(FromCache("sql_cache_short",
385 users_group = users_group.options(FromCache("sql_cache_short",
387 "get_users_group_%s" % users_group_id))
386 "get_users_group_%s" % users_group_id))
388 return users_group.get(users_group_id)
387 return users_group.get(users_group_id)
389
388
390 @classmethod
389 @classmethod
391 def create(cls, form_data):
390 def create(cls, form_data):
392 try:
391 try:
393 new_user_group = cls()
392 new_user_group = cls()
394 for k, v in form_data.items():
393 for k, v in form_data.items():
395 setattr(new_user_group, k, v)
394 setattr(new_user_group, k, v)
396
395
397 Session.add(new_user_group)
396 Session.add(new_user_group)
398 Session.commit()
397 Session.commit()
399 return new_user_group
398 return new_user_group
400 except:
399 except:
401 log.error(traceback.format_exc())
400 log.error(traceback.format_exc())
402 Session.rollback()
401 Session.rollback()
403 raise
402 raise
404
403
405 @classmethod
404 @classmethod
406 def update(cls, users_group_id, form_data):
405 def update(cls, users_group_id, form_data):
407
406
408 try:
407 try:
409 users_group = cls.get(users_group_id, cache=False)
408 users_group = cls.get(users_group_id, cache=False)
410
409
411 for k, v in form_data.items():
410 for k, v in form_data.items():
412 if k == 'users_group_members':
411 if k == 'users_group_members':
413 users_group.members = []
412 users_group.members = []
414 Session.flush()
413 Session.flush()
415 members_list = []
414 members_list = []
416 if v:
415 if v:
417 v = [v] if isinstance(v, compat.string_types) else v
416 v = [v] if isinstance(v, str) else v
418 for u_id in set(v):
417 for u_id in set(v):
419 member = UserGroupMember(users_group_id, u_id)
418 member = UserGroupMember(users_group_id, u_id)
420 members_list.append(member)
419 members_list.append(member)
421 setattr(users_group, 'members', members_list)
420 setattr(users_group, 'members', members_list)
422 setattr(users_group, k, v)
421 setattr(users_group, k, v)
423
422
424 Session.add(users_group)
423 Session.add(users_group)
425 Session.commit()
424 Session.commit()
426 except:
425 except:
427 log.error(traceback.format_exc())
426 log.error(traceback.format_exc())
428 Session.rollback()
427 Session.rollback()
429 raise
428 raise
430
429
431 @classmethod
430 @classmethod
432 def delete(cls, user_group_id):
431 def delete(cls, user_group_id):
433 try:
432 try:
434
433
435 # check if this group is not assigned to repo
434 # check if this group is not assigned to repo
436 assigned_groups = UserGroupRepoToPerm.query()\
435 assigned_groups = UserGroupRepoToPerm.query()\
437 .filter(UserGroupRepoToPerm.users_group_id ==
436 .filter(UserGroupRepoToPerm.users_group_id ==
438 user_group_id).all()
437 user_group_id).all()
439
438
440 if assigned_groups:
439 if assigned_groups:
441 raise UserGroupAssignedException(
440 raise UserGroupAssignedException(
442 'UserGroup assigned to %s' % assigned_groups)
441 'UserGroup assigned to %s' % assigned_groups)
443
442
444 users_group = cls.get(user_group_id, cache=False)
443 users_group = cls.get(user_group_id, cache=False)
445 Session.delete(users_group)
444 Session.delete(users_group)
446 Session.commit()
445 Session.commit()
447 except:
446 except:
448 log.error(traceback.format_exc())
447 log.error(traceback.format_exc())
449 Session.rollback()
448 Session.rollback()
450 raise
449 raise
451
450
452 class UserGroupMember(Base, BaseModel):
451 class UserGroupMember(Base, BaseModel):
453 __tablename__ = 'users_groups_members'
452 __tablename__ = 'users_groups_members'
454 __table_args__ = {'extend_existing':True}
453 __table_args__ = {'extend_existing':True}
455
454
456 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
455 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
457 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
456 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
458 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
457 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
459
458
460 user = relationship('User', lazy='joined')
459 user = relationship('User', lazy='joined')
461 users_group = relationship('UserGroup')
460 users_group = relationship('UserGroup')
462
461
463 def __init__(self, gr_id='', u_id=''):
462 def __init__(self, gr_id='', u_id=''):
464 self.users_group_id = gr_id
463 self.users_group_id = gr_id
465 self.user_id = u_id
464 self.user_id = u_id
466
465
467 @staticmethod
466 @staticmethod
468 def add_user_to_group(group, user):
467 def add_user_to_group(group, user):
469 ugm = UserGroupMember()
468 ugm = UserGroupMember()
470 ugm.users_group = group
469 ugm.users_group = group
471 ugm.user = user
470 ugm.user = user
472 Session.add(ugm)
471 Session.add(ugm)
473 Session.commit()
472 Session.commit()
474 return ugm
473 return ugm
475
474
476 class Repository(Base, BaseModel):
475 class Repository(Base, BaseModel):
477 __tablename__ = 'repositories'
476 __tablename__ = 'repositories'
478 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
477 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
479
478
480 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
479 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
481 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
480 repo_name = Column("repo_name", String(255), nullable=False, unique=True, default=None)
482 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
481 clone_uri = Column("clone_uri", String(255), nullable=True, unique=False, default=None)
483 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default='hg')
482 repo_type = Column("repo_type", String(255), nullable=False, unique=False, default='hg')
484 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
483 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
485 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
484 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
486 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
485 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
487 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
486 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
488 description = Column("description", String(10000), nullable=True, unique=None, default=None)
487 description = Column("description", String(10000), nullable=True, unique=None, default=None)
489 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
488 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
490
489
491 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
490 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
492 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
491 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
493
492
494
493
495 user = relationship('User')
494 user = relationship('User')
496 fork = relationship('Repository', remote_side=repo_id)
495 fork = relationship('Repository', remote_side=repo_id)
497 group = relationship('RepoGroup')
496 group = relationship('RepoGroup')
498 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
497 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
499 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
498 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
500 stats = relationship('Statistics', cascade='all', uselist=False)
499 stats = relationship('Statistics', cascade='all', uselist=False)
501
500
502 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
501 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
503
502
504 logs = relationship('UserLog', cascade='all')
503 logs = relationship('UserLog', cascade='all')
505
504
506 def __repr__(self):
505 def __repr__(self):
507 return "<%s('%s:%s')>" % (self.__class__.__name__,
506 return "<%s('%s:%s')>" % (self.__class__.__name__,
508 self.repo_id, self.repo_name)
507 self.repo_id, self.repo_name)
509
508
510 @classmethod
509 @classmethod
511 def url_sep(cls):
510 def url_sep(cls):
512 return '/'
511 return '/'
513
512
514 @classmethod
513 @classmethod
515 def get_by_repo_name(cls, repo_name):
514 def get_by_repo_name(cls, repo_name):
516 q = Session.query(cls).filter(cls.repo_name == repo_name)
515 q = Session.query(cls).filter(cls.repo_name == repo_name)
517 q = q.options(joinedload(Repository.fork))\
516 q = q.options(joinedload(Repository.fork))\
518 .options(joinedload(Repository.user))\
517 .options(joinedload(Repository.user))\
519 .options(joinedload(Repository.group))
518 .options(joinedload(Repository.group))
520 return q.one()
519 return q.one()
521
520
522 @classmethod
521 @classmethod
523 def get_repo_forks(cls, repo_id):
522 def get_repo_forks(cls, repo_id):
524 return cls.query().filter(Repository.fork_id == repo_id)
523 return cls.query().filter(Repository.fork_id == repo_id)
525
524
526 @classmethod
525 @classmethod
527 def base_path(cls):
526 def base_path(cls):
528 """
527 """
529 Returns base path when all repos are stored
528 Returns base path when all repos are stored
530
529
531 :param cls:
530 :param cls:
532 """
531 """
533 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
532 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
534 cls.url_sep())
533 cls.url_sep())
535 q.options(FromCache("sql_cache_short", "repository_repo_path"))
534 q.options(FromCache("sql_cache_short", "repository_repo_path"))
536 return q.one().ui_value
535 return q.one().ui_value
537
536
538 @property
537 @property
539 def just_name(self):
538 def just_name(self):
540 return self.repo_name.split(Repository.url_sep())[-1]
539 return self.repo_name.split(Repository.url_sep())[-1]
541
540
542 @property
541 @property
543 def groups_with_parents(self):
542 def groups_with_parents(self):
544 groups = []
543 groups = []
545 if self.group is None:
544 if self.group is None:
546 return groups
545 return groups
547
546
548 cur_gr = self.group
547 cur_gr = self.group
549 groups.insert(0, cur_gr)
548 groups.insert(0, cur_gr)
550 while 1:
549 while 1:
551 gr = getattr(cur_gr, 'parent_group', None)
550 gr = getattr(cur_gr, 'parent_group', None)
552 cur_gr = cur_gr.parent_group
551 cur_gr = cur_gr.parent_group
553 if gr is None:
552 if gr is None:
554 break
553 break
555 groups.insert(0, gr)
554 groups.insert(0, gr)
556
555
557 return groups
556 return groups
558
557
559 @property
558 @property
560 def groups_and_repo(self):
559 def groups_and_repo(self):
561 return self.groups_with_parents, self.just_name
560 return self.groups_with_parents, self.just_name
562
561
563 @LazyProperty
562 @LazyProperty
564 def repo_path(self):
563 def repo_path(self):
565 """
564 """
566 Returns base full path for that repository means where it actually
565 Returns base full path for that repository means where it actually
567 exists on a filesystem
566 exists on a filesystem
568 """
567 """
569 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
568 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
570 Repository.url_sep())
569 Repository.url_sep())
571 q.options(FromCache("sql_cache_short", "repository_repo_path"))
570 q.options(FromCache("sql_cache_short", "repository_repo_path"))
572 return q.one().ui_value
571 return q.one().ui_value
573
572
574 @property
573 @property
575 def repo_full_path(self):
574 def repo_full_path(self):
576 p = [self.repo_path]
575 p = [self.repo_path]
577 # we need to split the name by / since this is how we store the
576 # we need to split the name by / since this is how we store the
578 # names in the database, but that eventually needs to be converted
577 # names in the database, but that eventually needs to be converted
579 # into a valid system path
578 # into a valid system path
580 p += self.repo_name.split(Repository.url_sep())
579 p += self.repo_name.split(Repository.url_sep())
581 return os.path.join(*p)
580 return os.path.join(*p)
582
581
583 def get_new_name(self, repo_name):
582 def get_new_name(self, repo_name):
584 """
583 """
585 returns new full repository name based on assigned group and new new
584 returns new full repository name based on assigned group and new new
586
585
587 :param group_name:
586 :param group_name:
588 """
587 """
589 path_prefix = self.group.full_path_splitted if self.group else []
588 path_prefix = self.group.full_path_splitted if self.group else []
590 return Repository.url_sep().join(path_prefix + [repo_name])
589 return Repository.url_sep().join(path_prefix + [repo_name])
591
590
592 @property
591 @property
593 def _config(self):
592 def _config(self):
594 """
593 """
595 Returns db based config object.
594 Returns db based config object.
596 """
595 """
597 from rhodecode.lib.utils import make_db_config
596 from rhodecode.lib.utils import make_db_config
598 return make_db_config(clear_session=False)
597 return make_db_config(clear_session=False)
599
598
600 @classmethod
599 @classmethod
601 def is_valid(cls, repo_name):
600 def is_valid(cls, repo_name):
602 """
601 """
603 returns True if given repo name is a valid filesystem repository
602 returns True if given repo name is a valid filesystem repository
604
603
605 :param cls:
604 :param cls:
606 :param repo_name:
605 :param repo_name:
607 """
606 """
608 from rhodecode.lib.utils import is_valid_repo
607 from rhodecode.lib.utils import is_valid_repo
609
608
610 return is_valid_repo(repo_name, cls.base_path())
609 return is_valid_repo(repo_name, cls.base_path())
611
610
612
611
613 #==========================================================================
612 #==========================================================================
614 # SCM PROPERTIES
613 # SCM PROPERTIES
615 #==========================================================================
614 #==========================================================================
616
615
617 def get_commit(self, rev):
616 def get_commit(self, rev):
618 return get_commit_safe(self.scm_instance, rev)
617 return get_commit_safe(self.scm_instance, rev)
619
618
620 @property
619 @property
621 def tip(self):
620 def tip(self):
622 return self.get_commit('tip')
621 return self.get_commit('tip')
623
622
624 @property
623 @property
625 def author(self):
624 def author(self):
626 return self.tip.author
625 return self.tip.author
627
626
628 @property
627 @property
629 def last_change(self):
628 def last_change(self):
630 return self.scm_instance.last_change
629 return self.scm_instance.last_change
631
630
632 #==========================================================================
631 #==========================================================================
633 # SCM CACHE INSTANCE
632 # SCM CACHE INSTANCE
634 #==========================================================================
633 #==========================================================================
635
634
636 @property
635 @property
637 def invalidate(self):
636 def invalidate(self):
638 return CacheInvalidation.invalidate(self.repo_name)
637 return CacheInvalidation.invalidate(self.repo_name)
639
638
640 def set_invalidate(self):
639 def set_invalidate(self):
641 """
640 """
642 set a cache for invalidation for this instance
641 set a cache for invalidation for this instance
643 """
642 """
644 CacheInvalidation.set_invalidate(self.repo_name)
643 CacheInvalidation.set_invalidate(self.repo_name)
645
644
646 @LazyProperty
645 @LazyProperty
647 def scm_instance(self):
646 def scm_instance(self):
648 return self.__get_instance()
647 return self.__get_instance()
649
648
650 @property
649 @property
651 def scm_instance_cached(self):
650 def scm_instance_cached(self):
652 return self.__get_instance()
651 return self.__get_instance()
653
652
654 def __get_instance(self):
653 def __get_instance(self):
655
654
656 repo_full_path = self.repo_full_path
655 repo_full_path = self.repo_full_path
657
656
658 try:
657 try:
659 alias = get_scm(repo_full_path)[0]
658 alias = get_scm(repo_full_path)[0]
660 log.debug('Creating instance of %s repository', alias)
659 log.debug('Creating instance of %s repository', alias)
661 backend = get_backend(alias)
660 backend = get_backend(alias)
662 except VCSError:
661 except VCSError:
663 log.error(traceback.format_exc())
662 log.error(traceback.format_exc())
664 log.error('Perhaps this repository is in db and not in '
663 log.error('Perhaps this repository is in db and not in '
665 'filesystem run rescan repositories with '
664 'filesystem run rescan repositories with '
666 '"destroy old data " option from admin panel')
665 '"destroy old data " option from admin panel')
667 return
666 return
668
667
669 if alias == 'hg':
668 if alias == 'hg':
670
669
671 repo = backend(safe_str(repo_full_path), create=False,
670 repo = backend(safe_str(repo_full_path), create=False,
672 config=self._config)
671 config=self._config)
673
672
674 else:
673 else:
675 repo = backend(repo_full_path, create=False)
674 repo = backend(repo_full_path, create=False)
676
675
677 return repo
676 return repo
678
677
679
678
680 class Group(Base, BaseModel):
679 class Group(Base, BaseModel):
681 __tablename__ = 'groups'
680 __tablename__ = 'groups'
682 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
681 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
683 {'extend_existing':True},)
682 {'extend_existing':True},)
684 __mapper_args__ = {'order_by':'group_name'}
683 __mapper_args__ = {'order_by':'group_name'}
685
684
686 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
685 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
687 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
686 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
688 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
687 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
689 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
688 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
690
689
691 parent_group = relationship('Group', remote_side=group_id)
690 parent_group = relationship('Group', remote_side=group_id)
692
691
693 def __init__(self, group_name='', parent_group=None):
692 def __init__(self, group_name='', parent_group=None):
694 self.group_name = group_name
693 self.group_name = group_name
695 self.parent_group = parent_group
694 self.parent_group = parent_group
696
695
697 def __repr__(self):
696 def __repr__(self):
698 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
697 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
699 self.group_name)
698 self.group_name)
700
699
701 @classmethod
700 @classmethod
702 def url_sep(cls):
701 def url_sep(cls):
703 return '/'
702 return '/'
704
703
705 @classmethod
704 @classmethod
706 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
705 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
707 if case_insensitive:
706 if case_insensitive:
708 gr = cls.query()\
707 gr = cls.query()\
709 .filter(cls.group_name.ilike(group_name))
708 .filter(cls.group_name.ilike(group_name))
710 else:
709 else:
711 gr = cls.query()\
710 gr = cls.query()\
712 .filter(cls.group_name == group_name)
711 .filter(cls.group_name == group_name)
713 if cache:
712 if cache:
714 gr = gr.options(FromCache("sql_cache_short",
713 gr = gr.options(FromCache("sql_cache_short",
715 "get_group_%s" % group_name))
714 "get_group_%s" % group_name))
716 return gr.scalar()
715 return gr.scalar()
717
716
718 @property
717 @property
719 def parents(self):
718 def parents(self):
720 parents_recursion_limit = 5
719 parents_recursion_limit = 5
721 groups = []
720 groups = []
722 if self.parent_group is None:
721 if self.parent_group is None:
723 return groups
722 return groups
724 cur_gr = self.parent_group
723 cur_gr = self.parent_group
725 groups.insert(0, cur_gr)
724 groups.insert(0, cur_gr)
726 cnt = 0
725 cnt = 0
727 while 1:
726 while 1:
728 cnt += 1
727 cnt += 1
729 gr = getattr(cur_gr, 'parent_group', None)
728 gr = getattr(cur_gr, 'parent_group', None)
730 cur_gr = cur_gr.parent_group
729 cur_gr = cur_gr.parent_group
731 if gr is None:
730 if gr is None:
732 break
731 break
733 if cnt == parents_recursion_limit:
732 if cnt == parents_recursion_limit:
734 # this will prevent accidental infinit loops
733 # this will prevent accidental infinit loops
735 log.error('group nested more than %s',
734 log.error('group nested more than %s',
736 parents_recursion_limit)
735 parents_recursion_limit)
737 break
736 break
738
737
739 groups.insert(0, gr)
738 groups.insert(0, gr)
740 return groups
739 return groups
741
740
742 @property
741 @property
743 def children(self):
742 def children(self):
744 return Group.query().filter(Group.parent_group == self)
743 return Group.query().filter(Group.parent_group == self)
745
744
746 @property
745 @property
747 def name(self):
746 def name(self):
748 return self.group_name.split(Group.url_sep())[-1]
747 return self.group_name.split(Group.url_sep())[-1]
749
748
750 @property
749 @property
751 def full_path(self):
750 def full_path(self):
752 return self.group_name
751 return self.group_name
753
752
754 @property
753 @property
755 def full_path_splitted(self):
754 def full_path_splitted(self):
756 return self.group_name.split(Group.url_sep())
755 return self.group_name.split(Group.url_sep())
757
756
758 @property
757 @property
759 def repositories(self):
758 def repositories(self):
760 return Repository.query().filter(Repository.group == self)
759 return Repository.query().filter(Repository.group == self)
761
760
762 @property
761 @property
763 def repositories_recursive_count(self):
762 def repositories_recursive_count(self):
764 cnt = self.repositories.count()
763 cnt = self.repositories.count()
765
764
766 def children_count(group):
765 def children_count(group):
767 cnt = 0
766 cnt = 0
768 for child in group.children:
767 for child in group.children:
769 cnt += child.repositories.count()
768 cnt += child.repositories.count()
770 cnt += children_count(child)
769 cnt += children_count(child)
771 return cnt
770 return cnt
772
771
773 return cnt + children_count(self)
772 return cnt + children_count(self)
774
773
775
774
776 def get_new_name(self, group_name):
775 def get_new_name(self, group_name):
777 """
776 """
778 returns new full group name based on parent and new name
777 returns new full group name based on parent and new name
779
778
780 :param group_name:
779 :param group_name:
781 """
780 """
782 path_prefix = (self.parent_group.full_path_splitted if
781 path_prefix = (self.parent_group.full_path_splitted if
783 self.parent_group else [])
782 self.parent_group else [])
784 return Group.url_sep().join(path_prefix + [group_name])
783 return Group.url_sep().join(path_prefix + [group_name])
785
784
786
785
787 class Permission(Base, BaseModel):
786 class Permission(Base, BaseModel):
788 __tablename__ = 'permissions'
787 __tablename__ = 'permissions'
789 __table_args__ = {'extend_existing':True}
788 __table_args__ = {'extend_existing':True}
790 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
789 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
791 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
790 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
792 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
791 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
793
792
794 def __repr__(self):
793 def __repr__(self):
795 return "<%s('%s:%s')>" % (self.__class__.__name__,
794 return "<%s('%s:%s')>" % (self.__class__.__name__,
796 self.permission_id, self.permission_name)
795 self.permission_id, self.permission_name)
797
796
798 @classmethod
797 @classmethod
799 def get_by_key(cls, key):
798 def get_by_key(cls, key):
800 return cls.query().filter(cls.permission_name == key).scalar()
799 return cls.query().filter(cls.permission_name == key).scalar()
801
800
802 class UserRepoToPerm(Base, BaseModel):
801 class UserRepoToPerm(Base, BaseModel):
803 __tablename__ = 'repo_to_perm'
802 __tablename__ = 'repo_to_perm'
804 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
803 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
805 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
804 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
806 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
805 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
807 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
806 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
808 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
807 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
809
808
810 user = relationship('User')
809 user = relationship('User')
811 permission = relationship('Permission')
810 permission = relationship('Permission')
812 repository = relationship('Repository')
811 repository = relationship('Repository')
813
812
814 class UserToPerm(Base, BaseModel):
813 class UserToPerm(Base, BaseModel):
815 __tablename__ = 'user_to_perm'
814 __tablename__ = 'user_to_perm'
816 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
815 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
817 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
816 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
818 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
817 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
819 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
818 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
820
819
821 user = relationship('User')
820 user = relationship('User')
822 permission = relationship('Permission')
821 permission = relationship('Permission')
823
822
824 @classmethod
823 @classmethod
825 def has_perm(cls, user_id, perm):
824 def has_perm(cls, user_id, perm):
826 if not isinstance(perm, Permission):
825 if not isinstance(perm, Permission):
827 raise Exception('perm needs to be an instance of Permission class')
826 raise Exception('perm needs to be an instance of Permission class')
828
827
829 return cls.query().filter(cls.user_id == user_id)\
828 return cls.query().filter(cls.user_id == user_id)\
830 .filter(cls.permission == perm).scalar() is not None
829 .filter(cls.permission == perm).scalar() is not None
831
830
832 @classmethod
831 @classmethod
833 def grant_perm(cls, user_id, perm):
832 def grant_perm(cls, user_id, perm):
834 if not isinstance(perm, Permission):
833 if not isinstance(perm, Permission):
835 raise Exception('perm needs to be an instance of Permission class')
834 raise Exception('perm needs to be an instance of Permission class')
836
835
837 new = cls()
836 new = cls()
838 new.user_id = user_id
837 new.user_id = user_id
839 new.permission = perm
838 new.permission = perm
840 try:
839 try:
841 Session.add(new)
840 Session.add(new)
842 Session.commit()
841 Session.commit()
843 except:
842 except:
844 Session.rollback()
843 Session.rollback()
845
844
846
845
847 @classmethod
846 @classmethod
848 def revoke_perm(cls, user_id, perm):
847 def revoke_perm(cls, user_id, perm):
849 if not isinstance(perm, Permission):
848 if not isinstance(perm, Permission):
850 raise Exception('perm needs to be an instance of Permission class')
849 raise Exception('perm needs to be an instance of Permission class')
851
850
852 try:
851 try:
853 cls.query().filter(cls.user_id == user_id) \
852 cls.query().filter(cls.user_id == user_id) \
854 .filter(cls.permission == perm).delete()
853 .filter(cls.permission == perm).delete()
855 Session.commit()
854 Session.commit()
856 except:
855 except:
857 Session.rollback()
856 Session.rollback()
858
857
859 class UserGroupRepoToPerm(Base, BaseModel):
858 class UserGroupRepoToPerm(Base, BaseModel):
860 __tablename__ = 'users_group_repo_to_perm'
859 __tablename__ = 'users_group_repo_to_perm'
861 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
860 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
862 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
861 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
863 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
862 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
864 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
863 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
865 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
864 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
866
865
867 users_group = relationship('UserGroup')
866 users_group = relationship('UserGroup')
868 permission = relationship('Permission')
867 permission = relationship('Permission')
869 repository = relationship('Repository')
868 repository = relationship('Repository')
870
869
871 def __repr__(self):
870 def __repr__(self):
872 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
871 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
873
872
874 class UserGroupToPerm(Base, BaseModel):
873 class UserGroupToPerm(Base, BaseModel):
875 __tablename__ = 'users_group_to_perm'
874 __tablename__ = 'users_group_to_perm'
876 __table_args__ = {'extend_existing':True}
875 __table_args__ = {'extend_existing':True}
877 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
876 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
878 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
877 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
879 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
878 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
880
879
881 users_group = relationship('UserGroup')
880 users_group = relationship('UserGroup')
882 permission = relationship('Permission')
881 permission = relationship('Permission')
883
882
884
883
885 @classmethod
884 @classmethod
886 def has_perm(cls, users_group_id, perm):
885 def has_perm(cls, users_group_id, perm):
887 if not isinstance(perm, Permission):
886 if not isinstance(perm, Permission):
888 raise Exception('perm needs to be an instance of Permission class')
887 raise Exception('perm needs to be an instance of Permission class')
889
888
890 return cls.query().filter(cls.users_group_id ==
889 return cls.query().filter(cls.users_group_id ==
891 users_group_id)\
890 users_group_id)\
892 .filter(cls.permission == perm)\
891 .filter(cls.permission == perm)\
893 .scalar() is not None
892 .scalar() is not None
894
893
895 @classmethod
894 @classmethod
896 def grant_perm(cls, users_group_id, perm):
895 def grant_perm(cls, users_group_id, perm):
897 if not isinstance(perm, Permission):
896 if not isinstance(perm, Permission):
898 raise Exception('perm needs to be an instance of Permission class')
897 raise Exception('perm needs to be an instance of Permission class')
899
898
900 new = cls()
899 new = cls()
901 new.users_group_id = users_group_id
900 new.users_group_id = users_group_id
902 new.permission = perm
901 new.permission = perm
903 try:
902 try:
904 Session.add(new)
903 Session.add(new)
905 Session.commit()
904 Session.commit()
906 except:
905 except:
907 Session.rollback()
906 Session.rollback()
908
907
909
908
910 @classmethod
909 @classmethod
911 def revoke_perm(cls, users_group_id, perm):
910 def revoke_perm(cls, users_group_id, perm):
912 if not isinstance(perm, Permission):
911 if not isinstance(perm, Permission):
913 raise Exception('perm needs to be an instance of Permission class')
912 raise Exception('perm needs to be an instance of Permission class')
914
913
915 try:
914 try:
916 cls.query().filter(cls.users_group_id == users_group_id) \
915 cls.query().filter(cls.users_group_id == users_group_id) \
917 .filter(cls.permission == perm).delete()
916 .filter(cls.permission == perm).delete()
918 Session.commit()
917 Session.commit()
919 except:
918 except:
920 Session.rollback()
919 Session.rollback()
921
920
922
921
923 class UserRepoGroupToPerm(Base, BaseModel):
922 class UserRepoGroupToPerm(Base, BaseModel):
924 __tablename__ = 'group_to_perm'
923 __tablename__ = 'group_to_perm'
925 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
924 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
926
925
927 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
926 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
928 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
927 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
929 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
928 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
930 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
929 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
931
930
932 user = relationship('User')
931 user = relationship('User')
933 permission = relationship('Permission')
932 permission = relationship('Permission')
934 group = relationship('RepoGroup')
933 group = relationship('RepoGroup')
935
934
936 class Statistics(Base, BaseModel):
935 class Statistics(Base, BaseModel):
937 __tablename__ = 'statistics'
936 __tablename__ = 'statistics'
938 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
937 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
939 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
938 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
940 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
939 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
941 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
940 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
942 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
941 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
943 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
942 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
944 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
943 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
945
944
946 repository = relationship('Repository', single_parent=True)
945 repository = relationship('Repository', single_parent=True)
947
946
948 class UserFollowing(Base, BaseModel):
947 class UserFollowing(Base, BaseModel):
949 __tablename__ = 'user_followings'
948 __tablename__ = 'user_followings'
950 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
949 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
951 UniqueConstraint('user_id', 'follows_user_id')
950 UniqueConstraint('user_id', 'follows_user_id')
952 , {'extend_existing':True})
951 , {'extend_existing':True})
953
952
954 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
953 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
955 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
954 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
956 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
955 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
957 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
956 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
958 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
957 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
959
958
960 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
959 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
961
960
962 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
961 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
963 follows_repository = relationship('Repository', order_by='Repository.repo_name')
962 follows_repository = relationship('Repository', order_by='Repository.repo_name')
964
963
965
964
966 @classmethod
965 @classmethod
967 def get_repo_followers(cls, repo_id):
966 def get_repo_followers(cls, repo_id):
968 return cls.query().filter(cls.follows_repo_id == repo_id)
967 return cls.query().filter(cls.follows_repo_id == repo_id)
969
968
970 class CacheInvalidation(Base, BaseModel):
969 class CacheInvalidation(Base, BaseModel):
971 __tablename__ = 'cache_invalidation'
970 __tablename__ = 'cache_invalidation'
972 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
971 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
973 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
974 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
973 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
975 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
974 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
976 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
975 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
977
976
978
977
979 def __init__(self, cache_key, cache_args=''):
978 def __init__(self, cache_key, cache_args=''):
980 self.cache_key = cache_key
979 self.cache_key = cache_key
981 self.cache_args = cache_args
980 self.cache_args = cache_args
982 self.cache_active = False
981 self.cache_active = False
983
982
984 def __repr__(self):
983 def __repr__(self):
985 return "<%s('%s:%s')>" % (self.__class__.__name__,
984 return "<%s('%s:%s')>" % (self.__class__.__name__,
986 self.cache_id, self.cache_key)
985 self.cache_id, self.cache_key)
987
986
988 @classmethod
987 @classmethod
989 def invalidate(cls, key):
988 def invalidate(cls, key):
990 """
989 """
991 Returns Invalidation object if this given key should be invalidated
990 Returns Invalidation object if this given key should be invalidated
992 None otherwise. `cache_active = False` means that this cache
991 None otherwise. `cache_active = False` means that this cache
993 state is not valid and needs to be invalidated
992 state is not valid and needs to be invalidated
994
993
995 :param key:
994 :param key:
996 """
995 """
997 return cls.query()\
996 return cls.query()\
998 .filter(CacheInvalidation.cache_key == key)\
997 .filter(CacheInvalidation.cache_key == key)\
999 .filter(CacheInvalidation.cache_active == False)\
998 .filter(CacheInvalidation.cache_active == False)\
1000 .scalar()
999 .scalar()
1001
1000
1002 @classmethod
1001 @classmethod
1003 def set_invalidate(cls, key):
1002 def set_invalidate(cls, key):
1004 """
1003 """
1005 Mark this Cache key for invalidation
1004 Mark this Cache key for invalidation
1006
1005
1007 :param key:
1006 :param key:
1008 """
1007 """
1009
1008
1010 log.debug('marking %s for invalidation', key)
1009 log.debug('marking %s for invalidation', key)
1011 inv_obj = Session.query(cls)\
1010 inv_obj = Session.query(cls)\
1012 .filter(cls.cache_key == key).scalar()
1011 .filter(cls.cache_key == key).scalar()
1013 if inv_obj:
1012 if inv_obj:
1014 inv_obj.cache_active = False
1013 inv_obj.cache_active = False
1015 else:
1014 else:
1016 log.debug('cache key not found in invalidation db -> creating one')
1015 log.debug('cache key not found in invalidation db -> creating one')
1017 inv_obj = CacheInvalidation(key)
1016 inv_obj = CacheInvalidation(key)
1018
1017
1019 try:
1018 try:
1020 Session.add(inv_obj)
1019 Session.add(inv_obj)
1021 Session.commit()
1020 Session.commit()
1022 except Exception:
1021 except Exception:
1023 log.error(traceback.format_exc())
1022 log.error(traceback.format_exc())
1024 Session.rollback()
1023 Session.rollback()
1025
1024
1026 @classmethod
1025 @classmethod
1027 def set_valid(cls, key):
1026 def set_valid(cls, key):
1028 """
1027 """
1029 Mark this cache key as active and currently cached
1028 Mark this cache key as active and currently cached
1030
1029
1031 :param key:
1030 :param key:
1032 """
1031 """
1033 inv_obj = Session.query(CacheInvalidation)\
1032 inv_obj = Session.query(CacheInvalidation)\
1034 .filter(CacheInvalidation.cache_key == key).scalar()
1033 .filter(CacheInvalidation.cache_key == key).scalar()
1035 inv_obj.cache_active = True
1034 inv_obj.cache_active = True
1036 Session.add(inv_obj)
1035 Session.add(inv_obj)
1037 Session.commit()
1036 Session.commit()
1038
1037
1039 class DbMigrateVersion(Base, BaseModel):
1038 class DbMigrateVersion(Base, BaseModel):
1040 __tablename__ = 'db_migrate_version'
1039 __tablename__ = 'db_migrate_version'
1041 __table_args__ = {'extend_existing':True}
1040 __table_args__ = {'extend_existing':True}
1042 repository_id = Column('repository_id', String(250), primary_key=True)
1041 repository_id = Column('repository_id', String(250), primary_key=True)
1043 repository_path = Column('repository_path', Text)
1042 repository_path = Column('repository_path', Text)
1044 version = Column('version', Integer)
1043 version = Column('version', Integer)
@@ -1,4333 +1,4332 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37 from sqlalchemy import (
37 from sqlalchemy import (
38 or_, and_, not_, func, TypeDecorator, event,
38 or_, and_, not_, func, TypeDecorator, event,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType)
41 Text, Float, PickleType)
42 from sqlalchemy.sql.expression import true, false
42 from sqlalchemy.sql.expression import true, false
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.orm import (
44 from sqlalchemy.orm import (
45 relationship, joinedload, class_mapper, validates, aliased)
45 relationship, joinedload, class_mapper, validates, aliased)
46 from sqlalchemy.ext.declarative import declared_attr
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.dialects.mysql import LONGTEXT
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from beaker.cache import cache_region
50 from beaker.cache import cache_region
51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from pyramid import compat
53 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
54
53
55 from rhodecode.translation import _
54 from rhodecode.translation import _
56 from rhodecode.lib.vcs import get_vcs_instance
55 from rhodecode.lib.vcs import get_vcs_instance
57 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
56 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
58 from rhodecode.lib.utils2 import (
57 from rhodecode.lib.utils2 import (
59 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
58 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re, StrictAttributeDict, cleaned_uri)
60 glob2re, StrictAttributeDict, cleaned_uri)
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
61 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
63 JsonRaw
62 JsonRaw
64 from rhodecode.lib.ext_json import json
63 from rhodecode.lib.ext_json import json
65 from rhodecode.lib.caching_query import FromCache
64 from rhodecode.lib.caching_query import FromCache
66 from rhodecode.lib.encrypt import AESCipher
65 from rhodecode.lib.encrypt import AESCipher
67
66
68 from rhodecode.model.meta import Base, Session
67 from rhodecode.model.meta import Base, Session
69
68
70 URL_SEP = '/'
69 URL_SEP = '/'
71 log = logging.getLogger(__name__)
70 log = logging.getLogger(__name__)
72
71
73 # =============================================================================
72 # =============================================================================
74 # BASE CLASSES
73 # BASE CLASSES
75 # =============================================================================
74 # =============================================================================
76
75
77 # this is propagated from .ini file rhodecode.encrypted_values.secret or
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
78 # beaker.session.secret if first is not set.
77 # beaker.session.secret if first is not set.
79 # and initialized at environment.py
78 # and initialized at environment.py
80 ENCRYPTION_KEY = None
79 ENCRYPTION_KEY = None
81
80
82 # used to sort permissions by types, '#' used here is not allowed to be in
81 # used to sort permissions by types, '#' used here is not allowed to be in
83 # usernames, and it's very early in sorted string.printable table.
82 # usernames, and it's very early in sorted string.printable table.
84 PERMISSION_TYPE_SORT = {
83 PERMISSION_TYPE_SORT = {
85 'admin': '####',
84 'admin': '####',
86 'write': '###',
85 'write': '###',
87 'read': '##',
86 'read': '##',
88 'none': '#',
87 'none': '#',
89 }
88 }
90
89
91
90
92 def display_user_sort(obj):
91 def display_user_sort(obj):
93 """
92 """
94 Sort function used to sort permissions in .permissions() function of
93 Sort function used to sort permissions in .permissions() function of
95 Repository, RepoGroup, UserGroup. Also it put the default user in front
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
96 of all other resources
95 of all other resources
97 """
96 """
98
97
99 if obj.username == User.DEFAULT_USER:
98 if obj.username == User.DEFAULT_USER:
100 return '#####'
99 return '#####'
101 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
102 return prefix + obj.username
101 return prefix + obj.username
103
102
104
103
105 def display_user_group_sort(obj):
104 def display_user_group_sort(obj):
106 """
105 """
107 Sort function used to sort permissions in .permissions() function of
106 Sort function used to sort permissions in .permissions() function of
108 Repository, RepoGroup, UserGroup. Also it put the default user in front
107 Repository, RepoGroup, UserGroup. Also it put the default user in front
109 of all other resources
108 of all other resources
110 """
109 """
111
110
112 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
111 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
113 return prefix + obj.users_group_name
112 return prefix + obj.users_group_name
114
113
115
114
116 def _hash_key(k):
115 def _hash_key(k):
117 return md5_safe(k)
116 return md5_safe(k)
118
117
119
118
120 def in_filter_generator(qry, items, limit=500):
119 def in_filter_generator(qry, items, limit=500):
121 """
120 """
122 Splits IN() into multiple with OR
121 Splits IN() into multiple with OR
123 e.g.::
122 e.g.::
124 cnt = Repository.query().filter(
123 cnt = Repository.query().filter(
125 or_(
124 or_(
126 *in_filter_generator(Repository.repo_id, range(100000))
125 *in_filter_generator(Repository.repo_id, range(100000))
127 )).count()
126 )).count()
128 """
127 """
129 if not items:
128 if not items:
130 # empty list will cause empty query which might cause security issues
129 # empty list will cause empty query which might cause security issues
131 # this can lead to hidden unpleasant results
130 # this can lead to hidden unpleasant results
132 items = [-1]
131 items = [-1]
133
132
134 parts = []
133 parts = []
135 for chunk in range(0, len(items), limit):
134 for chunk in range(0, len(items), limit):
136 parts.append(
135 parts.append(
137 qry.in_(items[chunk: chunk + limit])
136 qry.in_(items[chunk: chunk + limit])
138 )
137 )
139
138
140 return parts
139 return parts
141
140
142
141
143 class EncryptedTextValue(TypeDecorator):
142 class EncryptedTextValue(TypeDecorator):
144 """
143 """
145 Special column for encrypted long text data, use like::
144 Special column for encrypted long text data, use like::
146
145
147 value = Column("encrypted_value", EncryptedValue(), nullable=False)
146 value = Column("encrypted_value", EncryptedValue(), nullable=False)
148
147
149 This column is intelligent so if value is in unencrypted form it return
148 This column is intelligent so if value is in unencrypted form it return
150 unencrypted form, but on save it always encrypts
149 unencrypted form, but on save it always encrypts
151 """
150 """
152 impl = Text
151 impl = Text
153
152
154 def process_bind_param(self, value, dialect):
153 def process_bind_param(self, value, dialect):
155 if not value:
154 if not value:
156 return value
155 return value
157 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
156 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
158 # protect against double encrypting if someone manually starts
157 # protect against double encrypting if someone manually starts
159 # doing
158 # doing
160 raise ValueError('value needs to be in unencrypted format, ie. '
159 raise ValueError('value needs to be in unencrypted format, ie. '
161 'not starting with enc$aes')
160 'not starting with enc$aes')
162 return 'enc$aes_hmac$%s' % AESCipher(
161 return 'enc$aes_hmac$%s' % AESCipher(
163 ENCRYPTION_KEY, hmac=True).encrypt(value)
162 ENCRYPTION_KEY, hmac=True).encrypt(value)
164
163
165 def process_result_value(self, value, dialect):
164 def process_result_value(self, value, dialect):
166 import rhodecode
165 import rhodecode
167
166
168 if not value:
167 if not value:
169 return value
168 return value
170
169
171 parts = value.split('$', 3)
170 parts = value.split('$', 3)
172 if not len(parts) == 3:
171 if not len(parts) == 3:
173 # probably not encrypted values
172 # probably not encrypted values
174 return value
173 return value
175 else:
174 else:
176 if parts[0] != 'enc':
175 if parts[0] != 'enc':
177 # parts ok but without our header ?
176 # parts ok but without our header ?
178 return value
177 return value
179 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
178 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
180 'rhodecode.encrypted_values.strict') or True)
179 'rhodecode.encrypted_values.strict') or True)
181 # at that stage we know it's our encryption
180 # at that stage we know it's our encryption
182 if parts[1] == 'aes':
181 if parts[1] == 'aes':
183 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
182 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
184 elif parts[1] == 'aes_hmac':
183 elif parts[1] == 'aes_hmac':
185 decrypted_data = AESCipher(
184 decrypted_data = AESCipher(
186 ENCRYPTION_KEY, hmac=True,
185 ENCRYPTION_KEY, hmac=True,
187 strict_verification=enc_strict_mode).decrypt(parts[2])
186 strict_verification=enc_strict_mode).decrypt(parts[2])
188 else:
187 else:
189 raise ValueError(
188 raise ValueError(
190 'Encryption type part is wrong, must be `aes` '
189 'Encryption type part is wrong, must be `aes` '
191 'or `aes_hmac`, got `%s` instead' % (parts[1]))
190 'or `aes_hmac`, got `%s` instead' % (parts[1]))
192 return decrypted_data
191 return decrypted_data
193
192
194
193
195 class BaseModel(object):
194 class BaseModel(object):
196 """
195 """
197 Base Model for all classes
196 Base Model for all classes
198 """
197 """
199
198
200 @classmethod
199 @classmethod
201 def _get_keys(cls):
200 def _get_keys(cls):
202 """return column names for this model """
201 """return column names for this model """
203 return class_mapper(cls).c.keys()
202 return class_mapper(cls).c.keys()
204
203
205 def get_dict(self):
204 def get_dict(self):
206 """
205 """
207 return dict with keys and values corresponding
206 return dict with keys and values corresponding
208 to this model data """
207 to this model data """
209
208
210 d = {}
209 d = {}
211 for k in self._get_keys():
210 for k in self._get_keys():
212 d[k] = getattr(self, k)
211 d[k] = getattr(self, k)
213
212
214 # also use __json__() if present to get additional fields
213 # also use __json__() if present to get additional fields
215 _json_attr = getattr(self, '__json__', None)
214 _json_attr = getattr(self, '__json__', None)
216 if _json_attr:
215 if _json_attr:
217 # update with attributes from __json__
216 # update with attributes from __json__
218 if callable(_json_attr):
217 if callable(_json_attr):
219 _json_attr = _json_attr()
218 _json_attr = _json_attr()
220 for k, val in _json_attr.iteritems():
219 for k, val in _json_attr.iteritems():
221 d[k] = val
220 d[k] = val
222 return d
221 return d
223
222
224 def get_appstruct(self):
223 def get_appstruct(self):
225 """return list with keys and values tuples corresponding
224 """return list with keys and values tuples corresponding
226 to this model data """
225 to this model data """
227
226
228 lst = []
227 lst = []
229 for k in self._get_keys():
228 for k in self._get_keys():
230 lst.append((k, getattr(self, k),))
229 lst.append((k, getattr(self, k),))
231 return lst
230 return lst
232
231
233 def populate_obj(self, populate_dict):
232 def populate_obj(self, populate_dict):
234 """populate model with data from given populate_dict"""
233 """populate model with data from given populate_dict"""
235
234
236 for k in self._get_keys():
235 for k in self._get_keys():
237 if k in populate_dict:
236 if k in populate_dict:
238 setattr(self, k, populate_dict[k])
237 setattr(self, k, populate_dict[k])
239
238
240 @classmethod
239 @classmethod
241 def query(cls):
240 def query(cls):
242 return Session().query(cls)
241 return Session().query(cls)
243
242
244 @classmethod
243 @classmethod
245 def get(cls, id_):
244 def get(cls, id_):
246 if id_:
245 if id_:
247 return cls.query().get(id_)
246 return cls.query().get(id_)
248
247
249 @classmethod
248 @classmethod
250 def get_or_404(cls, id_):
249 def get_or_404(cls, id_):
251 from pyramid.httpexceptions import HTTPNotFound
250 from pyramid.httpexceptions import HTTPNotFound
252
251
253 try:
252 try:
254 id_ = int(id_)
253 id_ = int(id_)
255 except (TypeError, ValueError):
254 except (TypeError, ValueError):
256 raise HTTPNotFound()
255 raise HTTPNotFound()
257
256
258 res = cls.query().get(id_)
257 res = cls.query().get(id_)
259 if not res:
258 if not res:
260 raise HTTPNotFound()
259 raise HTTPNotFound()
261 return res
260 return res
262
261
263 @classmethod
262 @classmethod
264 def getAll(cls):
263 def getAll(cls):
265 # deprecated and left for backward compatibility
264 # deprecated and left for backward compatibility
266 return cls.get_all()
265 return cls.get_all()
267
266
268 @classmethod
267 @classmethod
269 def get_all(cls):
268 def get_all(cls):
270 return cls.query().all()
269 return cls.query().all()
271
270
272 @classmethod
271 @classmethod
273 def delete(cls, id_):
272 def delete(cls, id_):
274 obj = cls.query().get(id_)
273 obj = cls.query().get(id_)
275 Session().delete(obj)
274 Session().delete(obj)
276
275
277 @classmethod
276 @classmethod
278 def identity_cache(cls, session, attr_name, value):
277 def identity_cache(cls, session, attr_name, value):
279 exist_in_session = []
278 exist_in_session = []
280 for (item_cls, pkey), instance in session.identity_map.items():
279 for (item_cls, pkey), instance in session.identity_map.items():
281 if cls == item_cls and getattr(instance, attr_name) == value:
280 if cls == item_cls and getattr(instance, attr_name) == value:
282 exist_in_session.append(instance)
281 exist_in_session.append(instance)
283 if exist_in_session:
282 if exist_in_session:
284 if len(exist_in_session) == 1:
283 if len(exist_in_session) == 1:
285 return exist_in_session[0]
284 return exist_in_session[0]
286 log.exception(
285 log.exception(
287 'multiple objects with attr %s and '
286 'multiple objects with attr %s and '
288 'value %s found with same name: %r',
287 'value %s found with same name: %r',
289 attr_name, value, exist_in_session)
288 attr_name, value, exist_in_session)
290
289
291 def __repr__(self):
290 def __repr__(self):
292 if hasattr(self, '__unicode__'):
291 if hasattr(self, '__unicode__'):
293 # python repr needs to return str
292 # python repr needs to return str
294 try:
293 try:
295 return safe_str(self.__unicode__())
294 return safe_str(self.__unicode__())
296 except UnicodeDecodeError:
295 except UnicodeDecodeError:
297 pass
296 pass
298 return '<DB:%s>' % (self.__class__.__name__)
297 return '<DB:%s>' % (self.__class__.__name__)
299
298
300
299
301 class RhodeCodeSetting(Base, BaseModel):
300 class RhodeCodeSetting(Base, BaseModel):
302 __tablename__ = 'rhodecode_settings'
301 __tablename__ = 'rhodecode_settings'
303 __table_args__ = (
302 __table_args__ = (
304 UniqueConstraint('app_settings_name'),
303 UniqueConstraint('app_settings_name'),
305 {'extend_existing': True, 'mysql_engine': 'InnoDB',
304 {'extend_existing': True, 'mysql_engine': 'InnoDB',
306 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
305 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
307 )
306 )
308
307
309 SETTINGS_TYPES = {
308 SETTINGS_TYPES = {
310 'str': safe_str,
309 'str': safe_str,
311 'int': safe_int,
310 'int': safe_int,
312 'unicode': safe_unicode,
311 'unicode': safe_unicode,
313 'bool': str2bool,
312 'bool': str2bool,
314 'list': functools.partial(aslist, sep=',')
313 'list': functools.partial(aslist, sep=',')
315 }
314 }
316 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
315 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
317 GLOBAL_CONF_KEY = 'app_settings'
316 GLOBAL_CONF_KEY = 'app_settings'
318
317
319 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
318 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
320 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
319 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
321 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
320 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
322 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
321 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
323
322
324 def __init__(self, key='', val='', type='unicode'):
323 def __init__(self, key='', val='', type='unicode'):
325 self.app_settings_name = key
324 self.app_settings_name = key
326 self.app_settings_type = type
325 self.app_settings_type = type
327 self.app_settings_value = val
326 self.app_settings_value = val
328
327
329 @validates('_app_settings_value')
328 @validates('_app_settings_value')
330 def validate_settings_value(self, key, val):
329 def validate_settings_value(self, key, val):
331 assert type(val) == unicode
330 assert type(val) == unicode
332 return val
331 return val
333
332
334 @hybrid_property
333 @hybrid_property
335 def app_settings_value(self):
334 def app_settings_value(self):
336 v = self._app_settings_value
335 v = self._app_settings_value
337 _type = self.app_settings_type
336 _type = self.app_settings_type
338 if _type:
337 if _type:
339 _type = self.app_settings_type.split('.')[0]
338 _type = self.app_settings_type.split('.')[0]
340 # decode the encrypted value
339 # decode the encrypted value
341 if 'encrypted' in self.app_settings_type:
340 if 'encrypted' in self.app_settings_type:
342 cipher = EncryptedTextValue()
341 cipher = EncryptedTextValue()
343 v = safe_unicode(cipher.process_result_value(v, None))
342 v = safe_unicode(cipher.process_result_value(v, None))
344
343
345 converter = self.SETTINGS_TYPES.get(_type) or \
344 converter = self.SETTINGS_TYPES.get(_type) or \
346 self.SETTINGS_TYPES['unicode']
345 self.SETTINGS_TYPES['unicode']
347 return converter(v)
346 return converter(v)
348
347
349 @app_settings_value.setter
348 @app_settings_value.setter
350 def app_settings_value(self, val):
349 def app_settings_value(self, val):
351 """
350 """
352 Setter that will always make sure we use unicode in app_settings_value
351 Setter that will always make sure we use unicode in app_settings_value
353
352
354 :param val:
353 :param val:
355 """
354 """
356 val = safe_unicode(val)
355 val = safe_unicode(val)
357 # encode the encrypted value
356 # encode the encrypted value
358 if 'encrypted' in self.app_settings_type:
357 if 'encrypted' in self.app_settings_type:
359 cipher = EncryptedTextValue()
358 cipher = EncryptedTextValue()
360 val = safe_unicode(cipher.process_bind_param(val, None))
359 val = safe_unicode(cipher.process_bind_param(val, None))
361 self._app_settings_value = val
360 self._app_settings_value = val
362
361
363 @hybrid_property
362 @hybrid_property
364 def app_settings_type(self):
363 def app_settings_type(self):
365 return self._app_settings_type
364 return self._app_settings_type
366
365
367 @app_settings_type.setter
366 @app_settings_type.setter
368 def app_settings_type(self, val):
367 def app_settings_type(self, val):
369 if val.split('.')[0] not in self.SETTINGS_TYPES:
368 if val.split('.')[0] not in self.SETTINGS_TYPES:
370 raise Exception('type must be one of %s got %s'
369 raise Exception('type must be one of %s got %s'
371 % (self.SETTINGS_TYPES.keys(), val))
370 % (self.SETTINGS_TYPES.keys(), val))
372 self._app_settings_type = val
371 self._app_settings_type = val
373
372
374 def __unicode__(self):
373 def __unicode__(self):
375 return u"<%s('%s:%s[%s]')>" % (
374 return u"<%s('%s:%s[%s]')>" % (
376 self.__class__.__name__,
375 self.__class__.__name__,
377 self.app_settings_name, self.app_settings_value,
376 self.app_settings_name, self.app_settings_value,
378 self.app_settings_type
377 self.app_settings_type
379 )
378 )
380
379
381
380
382 class RhodeCodeUi(Base, BaseModel):
381 class RhodeCodeUi(Base, BaseModel):
383 __tablename__ = 'rhodecode_ui'
382 __tablename__ = 'rhodecode_ui'
384 __table_args__ = (
383 __table_args__ = (
385 UniqueConstraint('ui_key'),
384 UniqueConstraint('ui_key'),
386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 )
387 )
389
388
390 HOOK_REPO_SIZE = 'changegroup.repo_size'
389 HOOK_REPO_SIZE = 'changegroup.repo_size'
391 # HG
390 # HG
392 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
391 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
393 HOOK_PULL = 'outgoing.pull_logger'
392 HOOK_PULL = 'outgoing.pull_logger'
394 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
393 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
395 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
394 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
396 HOOK_PUSH = 'changegroup.push_logger'
395 HOOK_PUSH = 'changegroup.push_logger'
397 HOOK_PUSH_KEY = 'pushkey.key_push'
396 HOOK_PUSH_KEY = 'pushkey.key_push'
398
397
399 # TODO: johbo: Unify way how hooks are configured for git and hg,
398 # TODO: johbo: Unify way how hooks are configured for git and hg,
400 # git part is currently hardcoded.
399 # git part is currently hardcoded.
401
400
402 # SVN PATTERNS
401 # SVN PATTERNS
403 SVN_BRANCH_ID = 'vcs_svn_branch'
402 SVN_BRANCH_ID = 'vcs_svn_branch'
404 SVN_TAG_ID = 'vcs_svn_tag'
403 SVN_TAG_ID = 'vcs_svn_tag'
405
404
406 ui_id = Column(
405 ui_id = Column(
407 "ui_id", Integer(), nullable=False, unique=True, default=None,
406 "ui_id", Integer(), nullable=False, unique=True, default=None,
408 primary_key=True)
407 primary_key=True)
409 ui_section = Column(
408 ui_section = Column(
410 "ui_section", String(255), nullable=True, unique=None, default=None)
409 "ui_section", String(255), nullable=True, unique=None, default=None)
411 ui_key = Column(
410 ui_key = Column(
412 "ui_key", String(255), nullable=True, unique=None, default=None)
411 "ui_key", String(255), nullable=True, unique=None, default=None)
413 ui_value = Column(
412 ui_value = Column(
414 "ui_value", String(255), nullable=True, unique=None, default=None)
413 "ui_value", String(255), nullable=True, unique=None, default=None)
415 ui_active = Column(
414 ui_active = Column(
416 "ui_active", Boolean(), nullable=True, unique=None, default=True)
415 "ui_active", Boolean(), nullable=True, unique=None, default=True)
417
416
418 def __repr__(self):
417 def __repr__(self):
419 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
418 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
420 self.ui_key, self.ui_value)
419 self.ui_key, self.ui_value)
421
420
422
421
423 class RepoRhodeCodeSetting(Base, BaseModel):
422 class RepoRhodeCodeSetting(Base, BaseModel):
424 __tablename__ = 'repo_rhodecode_settings'
423 __tablename__ = 'repo_rhodecode_settings'
425 __table_args__ = (
424 __table_args__ = (
426 UniqueConstraint(
425 UniqueConstraint(
427 'app_settings_name', 'repository_id',
426 'app_settings_name', 'repository_id',
428 name='uq_repo_rhodecode_setting_name_repo_id'),
427 name='uq_repo_rhodecode_setting_name_repo_id'),
429 {'extend_existing': True, 'mysql_engine': 'InnoDB',
428 {'extend_existing': True, 'mysql_engine': 'InnoDB',
430 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
429 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
431 )
430 )
432
431
433 repository_id = Column(
432 repository_id = Column(
434 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
433 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
435 nullable=False)
434 nullable=False)
436 app_settings_id = Column(
435 app_settings_id = Column(
437 "app_settings_id", Integer(), nullable=False, unique=True,
436 "app_settings_id", Integer(), nullable=False, unique=True,
438 default=None, primary_key=True)
437 default=None, primary_key=True)
439 app_settings_name = Column(
438 app_settings_name = Column(
440 "app_settings_name", String(255), nullable=True, unique=None,
439 "app_settings_name", String(255), nullable=True, unique=None,
441 default=None)
440 default=None)
442 _app_settings_value = Column(
441 _app_settings_value = Column(
443 "app_settings_value", String(4096), nullable=True, unique=None,
442 "app_settings_value", String(4096), nullable=True, unique=None,
444 default=None)
443 default=None)
445 _app_settings_type = Column(
444 _app_settings_type = Column(
446 "app_settings_type", String(255), nullable=True, unique=None,
445 "app_settings_type", String(255), nullable=True, unique=None,
447 default=None)
446 default=None)
448
447
449 repository = relationship('Repository')
448 repository = relationship('Repository')
450
449
451 def __init__(self, repository_id, key='', val='', type='unicode'):
450 def __init__(self, repository_id, key='', val='', type='unicode'):
452 self.repository_id = repository_id
451 self.repository_id = repository_id
453 self.app_settings_name = key
452 self.app_settings_name = key
454 self.app_settings_type = type
453 self.app_settings_type = type
455 self.app_settings_value = val
454 self.app_settings_value = val
456
455
457 @validates('_app_settings_value')
456 @validates('_app_settings_value')
458 def validate_settings_value(self, key, val):
457 def validate_settings_value(self, key, val):
459 assert type(val) == unicode
458 assert type(val) == unicode
460 return val
459 return val
461
460
462 @hybrid_property
461 @hybrid_property
463 def app_settings_value(self):
462 def app_settings_value(self):
464 v = self._app_settings_value
463 v = self._app_settings_value
465 type_ = self.app_settings_type
464 type_ = self.app_settings_type
466 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
465 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
467 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
466 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
468 return converter(v)
467 return converter(v)
469
468
470 @app_settings_value.setter
469 @app_settings_value.setter
471 def app_settings_value(self, val):
470 def app_settings_value(self, val):
472 """
471 """
473 Setter that will always make sure we use unicode in app_settings_value
472 Setter that will always make sure we use unicode in app_settings_value
474
473
475 :param val:
474 :param val:
476 """
475 """
477 self._app_settings_value = safe_unicode(val)
476 self._app_settings_value = safe_unicode(val)
478
477
479 @hybrid_property
478 @hybrid_property
480 def app_settings_type(self):
479 def app_settings_type(self):
481 return self._app_settings_type
480 return self._app_settings_type
482
481
483 @app_settings_type.setter
482 @app_settings_type.setter
484 def app_settings_type(self, val):
483 def app_settings_type(self, val):
485 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
484 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
486 if val not in SETTINGS_TYPES:
485 if val not in SETTINGS_TYPES:
487 raise Exception('type must be one of %s got %s'
486 raise Exception('type must be one of %s got %s'
488 % (SETTINGS_TYPES.keys(), val))
487 % (SETTINGS_TYPES.keys(), val))
489 self._app_settings_type = val
488 self._app_settings_type = val
490
489
491 def __unicode__(self):
490 def __unicode__(self):
492 return u"<%s('%s:%s:%s[%s]')>" % (
491 return u"<%s('%s:%s:%s[%s]')>" % (
493 self.__class__.__name__, self.repository.repo_name,
492 self.__class__.__name__, self.repository.repo_name,
494 self.app_settings_name, self.app_settings_value,
493 self.app_settings_name, self.app_settings_value,
495 self.app_settings_type
494 self.app_settings_type
496 )
495 )
497
496
498
497
499 class RepoRhodeCodeUi(Base, BaseModel):
498 class RepoRhodeCodeUi(Base, BaseModel):
500 __tablename__ = 'repo_rhodecode_ui'
499 __tablename__ = 'repo_rhodecode_ui'
501 __table_args__ = (
500 __table_args__ = (
502 UniqueConstraint(
501 UniqueConstraint(
503 'repository_id', 'ui_section', 'ui_key',
502 'repository_id', 'ui_section', 'ui_key',
504 name='uq_repo_rhodecode_ui_repository_id_section_key'),
503 name='uq_repo_rhodecode_ui_repository_id_section_key'),
505 {'extend_existing': True, 'mysql_engine': 'InnoDB',
504 {'extend_existing': True, 'mysql_engine': 'InnoDB',
506 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
505 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
507 )
506 )
508
507
509 repository_id = Column(
508 repository_id = Column(
510 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
509 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
511 nullable=False)
510 nullable=False)
512 ui_id = Column(
511 ui_id = Column(
513 "ui_id", Integer(), nullable=False, unique=True, default=None,
512 "ui_id", Integer(), nullable=False, unique=True, default=None,
514 primary_key=True)
513 primary_key=True)
515 ui_section = Column(
514 ui_section = Column(
516 "ui_section", String(255), nullable=True, unique=None, default=None)
515 "ui_section", String(255), nullable=True, unique=None, default=None)
517 ui_key = Column(
516 ui_key = Column(
518 "ui_key", String(255), nullable=True, unique=None, default=None)
517 "ui_key", String(255), nullable=True, unique=None, default=None)
519 ui_value = Column(
518 ui_value = Column(
520 "ui_value", String(255), nullable=True, unique=None, default=None)
519 "ui_value", String(255), nullable=True, unique=None, default=None)
521 ui_active = Column(
520 ui_active = Column(
522 "ui_active", Boolean(), nullable=True, unique=None, default=True)
521 "ui_active", Boolean(), nullable=True, unique=None, default=True)
523
522
524 repository = relationship('Repository')
523 repository = relationship('Repository')
525
524
526 def __repr__(self):
525 def __repr__(self):
527 return '<%s[%s:%s]%s=>%s]>' % (
526 return '<%s[%s:%s]%s=>%s]>' % (
528 self.__class__.__name__, self.repository.repo_name,
527 self.__class__.__name__, self.repository.repo_name,
529 self.ui_section, self.ui_key, self.ui_value)
528 self.ui_section, self.ui_key, self.ui_value)
530
529
531
530
532 class User(Base, BaseModel):
531 class User(Base, BaseModel):
533 __tablename__ = 'users'
532 __tablename__ = 'users'
534 __table_args__ = (
533 __table_args__ = (
535 UniqueConstraint('username'), UniqueConstraint('email'),
534 UniqueConstraint('username'), UniqueConstraint('email'),
536 Index('u_username_idx', 'username'),
535 Index('u_username_idx', 'username'),
537 Index('u_email_idx', 'email'),
536 Index('u_email_idx', 'email'),
538 {'extend_existing': True, 'mysql_engine': 'InnoDB',
537 {'extend_existing': True, 'mysql_engine': 'InnoDB',
539 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
538 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
540 )
539 )
541 DEFAULT_USER = 'default'
540 DEFAULT_USER = 'default'
542 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
541 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
543 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
542 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
544
543
545 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
544 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
546 username = Column("username", String(255), nullable=True, unique=None, default=None)
545 username = Column("username", String(255), nullable=True, unique=None, default=None)
547 password = Column("password", String(255), nullable=True, unique=None, default=None)
546 password = Column("password", String(255), nullable=True, unique=None, default=None)
548 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
547 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
549 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
548 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
550 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
549 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
551 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
550 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
552 _email = Column("email", String(255), nullable=True, unique=None, default=None)
551 _email = Column("email", String(255), nullable=True, unique=None, default=None)
553 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
552 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
554 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
553 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
555
554
556 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
555 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
557 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
556 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
558 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
557 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
559 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
558 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
560 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
559 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
561 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
560 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
562
561
563 user_log = relationship('UserLog')
562 user_log = relationship('UserLog')
564 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
563 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
565
564
566 repositories = relationship('Repository')
565 repositories = relationship('Repository')
567 repository_groups = relationship('RepoGroup')
566 repository_groups = relationship('RepoGroup')
568 user_groups = relationship('UserGroup')
567 user_groups = relationship('UserGroup')
569
568
570 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
569 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
571 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
570 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
572
571
573 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
572 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
574 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
573 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
575 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
574 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
576
575
577 group_member = relationship('UserGroupMember', cascade='all')
576 group_member = relationship('UserGroupMember', cascade='all')
578
577
579 notifications = relationship('UserNotification', cascade='all')
578 notifications = relationship('UserNotification', cascade='all')
580 # notifications assigned to this user
579 # notifications assigned to this user
581 user_created_notifications = relationship('Notification', cascade='all')
580 user_created_notifications = relationship('Notification', cascade='all')
582 # comments created by this user
581 # comments created by this user
583 user_comments = relationship('ChangesetComment', cascade='all')
582 user_comments = relationship('ChangesetComment', cascade='all')
584 # user profile extra info
583 # user profile extra info
585 user_emails = relationship('UserEmailMap', cascade='all')
584 user_emails = relationship('UserEmailMap', cascade='all')
586 user_ip_map = relationship('UserIpMap', cascade='all')
585 user_ip_map = relationship('UserIpMap', cascade='all')
587 user_auth_tokens = relationship('UserApiKeys', cascade='all')
586 user_auth_tokens = relationship('UserApiKeys', cascade='all')
588 user_ssh_keys = relationship('UserSshKeys', cascade='all')
587 user_ssh_keys = relationship('UserSshKeys', cascade='all')
589
588
590 # gists
589 # gists
591 user_gists = relationship('Gist', cascade='all')
590 user_gists = relationship('Gist', cascade='all')
592 # user pull requests
591 # user pull requests
593 user_pull_requests = relationship('PullRequest', cascade='all')
592 user_pull_requests = relationship('PullRequest', cascade='all')
594 # external identities
593 # external identities
595 extenal_identities = relationship(
594 extenal_identities = relationship(
596 'ExternalIdentity',
595 'ExternalIdentity',
597 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
596 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
598 cascade='all')
597 cascade='all')
599 # review rules
598 # review rules
600 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
599 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
601
600
602 def __unicode__(self):
601 def __unicode__(self):
603 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
602 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
604 self.user_id, self.username)
603 self.user_id, self.username)
605
604
606 @hybrid_property
605 @hybrid_property
607 def email(self):
606 def email(self):
608 return self._email
607 return self._email
609
608
610 @email.setter
609 @email.setter
611 def email(self, val):
610 def email(self, val):
612 self._email = val.lower() if val else None
611 self._email = val.lower() if val else None
613
612
614 @hybrid_property
613 @hybrid_property
615 def first_name(self):
614 def first_name(self):
616 from rhodecode.lib import helpers as h
615 from rhodecode.lib import helpers as h
617 if self.name:
616 if self.name:
618 return h.escape(self.name)
617 return h.escape(self.name)
619 return self.name
618 return self.name
620
619
621 @hybrid_property
620 @hybrid_property
622 def last_name(self):
621 def last_name(self):
623 from rhodecode.lib import helpers as h
622 from rhodecode.lib import helpers as h
624 if self.lastname:
623 if self.lastname:
625 return h.escape(self.lastname)
624 return h.escape(self.lastname)
626 return self.lastname
625 return self.lastname
627
626
628 @hybrid_property
627 @hybrid_property
629 def api_key(self):
628 def api_key(self):
630 """
629 """
631 Fetch if exist an auth-token with role ALL connected to this user
630 Fetch if exist an auth-token with role ALL connected to this user
632 """
631 """
633 user_auth_token = UserApiKeys.query()\
632 user_auth_token = UserApiKeys.query()\
634 .filter(UserApiKeys.user_id == self.user_id)\
633 .filter(UserApiKeys.user_id == self.user_id)\
635 .filter(or_(UserApiKeys.expires == -1,
634 .filter(or_(UserApiKeys.expires == -1,
636 UserApiKeys.expires >= time.time()))\
635 UserApiKeys.expires >= time.time()))\
637 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
636 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
638 if user_auth_token:
637 if user_auth_token:
639 user_auth_token = user_auth_token.api_key
638 user_auth_token = user_auth_token.api_key
640
639
641 return user_auth_token
640 return user_auth_token
642
641
643 @api_key.setter
642 @api_key.setter
644 def api_key(self, val):
643 def api_key(self, val):
645 # don't allow to set API key this is deprecated for now
644 # don't allow to set API key this is deprecated for now
646 self._api_key = None
645 self._api_key = None
647
646
648 @property
647 @property
649 def reviewer_pull_requests(self):
648 def reviewer_pull_requests(self):
650 return PullRequestReviewers.query() \
649 return PullRequestReviewers.query() \
651 .options(joinedload(PullRequestReviewers.pull_request)) \
650 .options(joinedload(PullRequestReviewers.pull_request)) \
652 .filter(PullRequestReviewers.user_id == self.user_id) \
651 .filter(PullRequestReviewers.user_id == self.user_id) \
653 .all()
652 .all()
654
653
655 @property
654 @property
656 def firstname(self):
655 def firstname(self):
657 # alias for future
656 # alias for future
658 return self.name
657 return self.name
659
658
660 @property
659 @property
661 def emails(self):
660 def emails(self):
662 other = UserEmailMap.query()\
661 other = UserEmailMap.query()\
663 .filter(UserEmailMap.user == self) \
662 .filter(UserEmailMap.user == self) \
664 .order_by(UserEmailMap.email_id.asc()) \
663 .order_by(UserEmailMap.email_id.asc()) \
665 .all()
664 .all()
666 return [self.email] + [x.email for x in other]
665 return [self.email] + [x.email for x in other]
667
666
668 @property
667 @property
669 def auth_tokens(self):
668 def auth_tokens(self):
670 auth_tokens = self.get_auth_tokens()
669 auth_tokens = self.get_auth_tokens()
671 return [x.api_key for x in auth_tokens]
670 return [x.api_key for x in auth_tokens]
672
671
673 def get_auth_tokens(self):
672 def get_auth_tokens(self):
674 return UserApiKeys.query()\
673 return UserApiKeys.query()\
675 .filter(UserApiKeys.user == self)\
674 .filter(UserApiKeys.user == self)\
676 .order_by(UserApiKeys.user_api_key_id.asc())\
675 .order_by(UserApiKeys.user_api_key_id.asc())\
677 .all()
676 .all()
678
677
679 @property
678 @property
680 def feed_token(self):
679 def feed_token(self):
681 return self.get_feed_token()
680 return self.get_feed_token()
682
681
683 def get_feed_token(self):
682 def get_feed_token(self):
684 feed_tokens = UserApiKeys.query()\
683 feed_tokens = UserApiKeys.query()\
685 .filter(UserApiKeys.user == self)\
684 .filter(UserApiKeys.user == self)\
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
685 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
687 .all()
686 .all()
688 if feed_tokens:
687 if feed_tokens:
689 return feed_tokens[0].api_key
688 return feed_tokens[0].api_key
690 return 'NO_FEED_TOKEN_AVAILABLE'
689 return 'NO_FEED_TOKEN_AVAILABLE'
691
690
692 @classmethod
691 @classmethod
693 def get(cls, user_id, cache=False):
692 def get(cls, user_id, cache=False):
694 if not user_id:
693 if not user_id:
695 return
694 return
696
695
697 user = cls.query()
696 user = cls.query()
698 if cache:
697 if cache:
699 user = user.options(
698 user = user.options(
700 FromCache("sql_cache_short", "get_users_%s" % user_id))
699 FromCache("sql_cache_short", "get_users_%s" % user_id))
701 return user.get(user_id)
700 return user.get(user_id)
702
701
703 @classmethod
702 @classmethod
704 def extra_valid_auth_tokens(cls, user, role=None):
703 def extra_valid_auth_tokens(cls, user, role=None):
705 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
704 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
706 .filter(or_(UserApiKeys.expires == -1,
705 .filter(or_(UserApiKeys.expires == -1,
707 UserApiKeys.expires >= time.time()))
706 UserApiKeys.expires >= time.time()))
708 if role:
707 if role:
709 tokens = tokens.filter(or_(UserApiKeys.role == role,
708 tokens = tokens.filter(or_(UserApiKeys.role == role,
710 UserApiKeys.role == UserApiKeys.ROLE_ALL))
709 UserApiKeys.role == UserApiKeys.ROLE_ALL))
711 return tokens.all()
710 return tokens.all()
712
711
713 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
712 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
714 from rhodecode.lib import auth
713 from rhodecode.lib import auth
715
714
716 log.debug('Trying to authenticate user: %s via auth-token, '
715 log.debug('Trying to authenticate user: %s via auth-token, '
717 'and roles: %s', self, roles)
716 'and roles: %s', self, roles)
718
717
719 if not auth_token:
718 if not auth_token:
720 return False
719 return False
721
720
722 crypto_backend = auth.crypto_backend()
721 crypto_backend = auth.crypto_backend()
723
722
724 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
723 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
725 tokens_q = UserApiKeys.query()\
724 tokens_q = UserApiKeys.query()\
726 .filter(UserApiKeys.user_id == self.user_id)\
725 .filter(UserApiKeys.user_id == self.user_id)\
727 .filter(or_(UserApiKeys.expires == -1,
726 .filter(or_(UserApiKeys.expires == -1,
728 UserApiKeys.expires >= time.time()))
727 UserApiKeys.expires >= time.time()))
729
728
730 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
729 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
731
730
732 plain_tokens = []
731 plain_tokens = []
733 hash_tokens = []
732 hash_tokens = []
734
733
735 for token in tokens_q.all():
734 for token in tokens_q.all():
736 # verify scope first
735 # verify scope first
737 if token.repo_id:
736 if token.repo_id:
738 # token has a scope, we need to verify it
737 # token has a scope, we need to verify it
739 if scope_repo_id != token.repo_id:
738 if scope_repo_id != token.repo_id:
740 log.debug(
739 log.debug(
741 'Scope mismatch: token has a set repo scope: %s, '
740 'Scope mismatch: token has a set repo scope: %s, '
742 'and calling scope is:%s, skipping further checks',
741 'and calling scope is:%s, skipping further checks',
743 token.repo, scope_repo_id)
742 token.repo, scope_repo_id)
744 # token has a scope, and it doesn't match, skip token
743 # token has a scope, and it doesn't match, skip token
745 continue
744 continue
746
745
747 if token.api_key.startswith(crypto_backend.ENC_PREF):
746 if token.api_key.startswith(crypto_backend.ENC_PREF):
748 hash_tokens.append(token.api_key)
747 hash_tokens.append(token.api_key)
749 else:
748 else:
750 plain_tokens.append(token.api_key)
749 plain_tokens.append(token.api_key)
751
750
752 is_plain_match = auth_token in plain_tokens
751 is_plain_match = auth_token in plain_tokens
753 if is_plain_match:
752 if is_plain_match:
754 return True
753 return True
755
754
756 for hashed in hash_tokens:
755 for hashed in hash_tokens:
757 # TODO(marcink): this is expensive to calculate, but most secure
756 # TODO(marcink): this is expensive to calculate, but most secure
758 match = crypto_backend.hash_check(auth_token, hashed)
757 match = crypto_backend.hash_check(auth_token, hashed)
759 if match:
758 if match:
760 return True
759 return True
761
760
762 return False
761 return False
763
762
764 @property
763 @property
765 def ip_addresses(self):
764 def ip_addresses(self):
766 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
765 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
767 return [x.ip_addr for x in ret]
766 return [x.ip_addr for x in ret]
768
767
769 @property
768 @property
770 def username_and_name(self):
769 def username_and_name(self):
771 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
770 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
772
771
773 @property
772 @property
774 def username_or_name_or_email(self):
773 def username_or_name_or_email(self):
775 full_name = self.full_name if self.full_name is not ' ' else None
774 full_name = self.full_name if self.full_name is not ' ' else None
776 return self.username or full_name or self.email
775 return self.username or full_name or self.email
777
776
778 @property
777 @property
779 def full_name(self):
778 def full_name(self):
780 return '%s %s' % (self.first_name, self.last_name)
779 return '%s %s' % (self.first_name, self.last_name)
781
780
782 @property
781 @property
783 def full_name_or_username(self):
782 def full_name_or_username(self):
784 return ('%s %s' % (self.first_name, self.last_name)
783 return ('%s %s' % (self.first_name, self.last_name)
785 if (self.first_name and self.last_name) else self.username)
784 if (self.first_name and self.last_name) else self.username)
786
785
787 @property
786 @property
788 def full_contact(self):
787 def full_contact(self):
789 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
788 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
790
789
791 @property
790 @property
792 def short_contact(self):
791 def short_contact(self):
793 return '%s %s' % (self.first_name, self.last_name)
792 return '%s %s' % (self.first_name, self.last_name)
794
793
795 @property
794 @property
796 def is_admin(self):
795 def is_admin(self):
797 return self.admin
796 return self.admin
798
797
799 def AuthUser(self, **kwargs):
798 def AuthUser(self, **kwargs):
800 """
799 """
801 Returns instance of AuthUser for this user
800 Returns instance of AuthUser for this user
802 """
801 """
803 from rhodecode.lib.auth import AuthUser
802 from rhodecode.lib.auth import AuthUser
804 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
803 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
805
804
806 @hybrid_property
805 @hybrid_property
807 def user_data(self):
806 def user_data(self):
808 if not self._user_data:
807 if not self._user_data:
809 return {}
808 return {}
810
809
811 try:
810 try:
812 return json.loads(self._user_data)
811 return json.loads(self._user_data)
813 except TypeError:
812 except TypeError:
814 return {}
813 return {}
815
814
816 @user_data.setter
815 @user_data.setter
817 def user_data(self, val):
816 def user_data(self, val):
818 if not isinstance(val, dict):
817 if not isinstance(val, dict):
819 raise Exception('user_data must be dict, got %s' % type(val))
818 raise Exception('user_data must be dict, got %s' % type(val))
820 try:
819 try:
821 self._user_data = json.dumps(val)
820 self._user_data = json.dumps(val)
822 except Exception:
821 except Exception:
823 log.error(traceback.format_exc())
822 log.error(traceback.format_exc())
824
823
825 @classmethod
824 @classmethod
826 def get_by_username(cls, username, case_insensitive=False,
825 def get_by_username(cls, username, case_insensitive=False,
827 cache=False, identity_cache=False):
826 cache=False, identity_cache=False):
828 session = Session()
827 session = Session()
829
828
830 if case_insensitive:
829 if case_insensitive:
831 q = cls.query().filter(
830 q = cls.query().filter(
832 func.lower(cls.username) == func.lower(username))
831 func.lower(cls.username) == func.lower(username))
833 else:
832 else:
834 q = cls.query().filter(cls.username == username)
833 q = cls.query().filter(cls.username == username)
835
834
836 if cache:
835 if cache:
837 if identity_cache:
836 if identity_cache:
838 val = cls.identity_cache(session, 'username', username)
837 val = cls.identity_cache(session, 'username', username)
839 if val:
838 if val:
840 return val
839 return val
841 else:
840 else:
842 cache_key = "get_user_by_name_%s" % _hash_key(username)
841 cache_key = "get_user_by_name_%s" % _hash_key(username)
843 q = q.options(
842 q = q.options(
844 FromCache("sql_cache_short", cache_key))
843 FromCache("sql_cache_short", cache_key))
845
844
846 return q.scalar()
845 return q.scalar()
847
846
848 @classmethod
847 @classmethod
849 def get_by_auth_token(cls, auth_token, cache=False):
848 def get_by_auth_token(cls, auth_token, cache=False):
850 q = UserApiKeys.query()\
849 q = UserApiKeys.query()\
851 .filter(UserApiKeys.api_key == auth_token)\
850 .filter(UserApiKeys.api_key == auth_token)\
852 .filter(or_(UserApiKeys.expires == -1,
851 .filter(or_(UserApiKeys.expires == -1,
853 UserApiKeys.expires >= time.time()))
852 UserApiKeys.expires >= time.time()))
854 if cache:
853 if cache:
855 q = q.options(
854 q = q.options(
856 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
855 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
857
856
858 match = q.first()
857 match = q.first()
859 if match:
858 if match:
860 return match.user
859 return match.user
861
860
862 @classmethod
861 @classmethod
863 def get_by_email(cls, email, case_insensitive=False, cache=False):
862 def get_by_email(cls, email, case_insensitive=False, cache=False):
864
863
865 if case_insensitive:
864 if case_insensitive:
866 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
865 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
867
866
868 else:
867 else:
869 q = cls.query().filter(cls.email == email)
868 q = cls.query().filter(cls.email == email)
870
869
871 email_key = _hash_key(email)
870 email_key = _hash_key(email)
872 if cache:
871 if cache:
873 q = q.options(
872 q = q.options(
874 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
873 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
875
874
876 ret = q.scalar()
875 ret = q.scalar()
877 if ret is None:
876 if ret is None:
878 q = UserEmailMap.query()
877 q = UserEmailMap.query()
879 # try fetching in alternate email map
878 # try fetching in alternate email map
880 if case_insensitive:
879 if case_insensitive:
881 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
880 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
882 else:
881 else:
883 q = q.filter(UserEmailMap.email == email)
882 q = q.filter(UserEmailMap.email == email)
884 q = q.options(joinedload(UserEmailMap.user))
883 q = q.options(joinedload(UserEmailMap.user))
885 if cache:
884 if cache:
886 q = q.options(
885 q = q.options(
887 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
886 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
888 ret = getattr(q.scalar(), 'user', None)
887 ret = getattr(q.scalar(), 'user', None)
889
888
890 return ret
889 return ret
891
890
892 @classmethod
891 @classmethod
893 def get_from_cs_author(cls, author):
892 def get_from_cs_author(cls, author):
894 """
893 """
895 Tries to get User objects out of commit author string
894 Tries to get User objects out of commit author string
896
895
897 :param author:
896 :param author:
898 """
897 """
899 from rhodecode.lib.helpers import email, author_name
898 from rhodecode.lib.helpers import email, author_name
900 # Valid email in the attribute passed, see if they're in the system
899 # Valid email in the attribute passed, see if they're in the system
901 _email = email(author)
900 _email = email(author)
902 if _email:
901 if _email:
903 user = cls.get_by_email(_email, case_insensitive=True)
902 user = cls.get_by_email(_email, case_insensitive=True)
904 if user:
903 if user:
905 return user
904 return user
906 # Maybe we can match by username?
905 # Maybe we can match by username?
907 _author = author_name(author)
906 _author = author_name(author)
908 user = cls.get_by_username(_author, case_insensitive=True)
907 user = cls.get_by_username(_author, case_insensitive=True)
909 if user:
908 if user:
910 return user
909 return user
911
910
912 def update_userdata(self, **kwargs):
911 def update_userdata(self, **kwargs):
913 usr = self
912 usr = self
914 old = usr.user_data
913 old = usr.user_data
915 old.update(**kwargs)
914 old.update(**kwargs)
916 usr.user_data = old
915 usr.user_data = old
917 Session().add(usr)
916 Session().add(usr)
918 log.debug('updated userdata with ', kwargs)
917 log.debug('updated userdata with ', kwargs)
919
918
920 def update_lastlogin(self):
919 def update_lastlogin(self):
921 """Update user lastlogin"""
920 """Update user lastlogin"""
922 self.last_login = datetime.datetime.now()
921 self.last_login = datetime.datetime.now()
923 Session().add(self)
922 Session().add(self)
924 log.debug('updated user %s lastlogin', self.username)
923 log.debug('updated user %s lastlogin', self.username)
925
924
926 def update_lastactivity(self):
925 def update_lastactivity(self):
927 """Update user lastactivity"""
926 """Update user lastactivity"""
928 self.last_activity = datetime.datetime.now()
927 self.last_activity = datetime.datetime.now()
929 Session().add(self)
928 Session().add(self)
930 log.debug('updated user `%s` last activity', self.username)
929 log.debug('updated user `%s` last activity', self.username)
931
930
932 def update_password(self, new_password):
931 def update_password(self, new_password):
933 from rhodecode.lib.auth import get_crypt_password
932 from rhodecode.lib.auth import get_crypt_password
934
933
935 self.password = get_crypt_password(new_password)
934 self.password = get_crypt_password(new_password)
936 Session().add(self)
935 Session().add(self)
937
936
938 @classmethod
937 @classmethod
939 def get_first_super_admin(cls):
938 def get_first_super_admin(cls):
940 user = User.query().filter(User.admin == true()).first()
939 user = User.query().filter(User.admin == true()).first()
941 if user is None:
940 if user is None:
942 raise Exception('FATAL: Missing administrative account!')
941 raise Exception('FATAL: Missing administrative account!')
943 return user
942 return user
944
943
945 @classmethod
944 @classmethod
946 def get_all_super_admins(cls):
945 def get_all_super_admins(cls):
947 """
946 """
948 Returns all admin accounts sorted by username
947 Returns all admin accounts sorted by username
949 """
948 """
950 return User.query().filter(User.admin == true())\
949 return User.query().filter(User.admin == true())\
951 .order_by(User.username.asc()).all()
950 .order_by(User.username.asc()).all()
952
951
953 @classmethod
952 @classmethod
954 def get_default_user(cls, cache=False, refresh=False):
953 def get_default_user(cls, cache=False, refresh=False):
955 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
954 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
956 if user is None:
955 if user is None:
957 raise Exception('FATAL: Missing default account!')
956 raise Exception('FATAL: Missing default account!')
958 if refresh:
957 if refresh:
959 # The default user might be based on outdated state which
958 # The default user might be based on outdated state which
960 # has been loaded from the cache.
959 # has been loaded from the cache.
961 # A call to refresh() ensures that the
960 # A call to refresh() ensures that the
962 # latest state from the database is used.
961 # latest state from the database is used.
963 Session().refresh(user)
962 Session().refresh(user)
964 return user
963 return user
965
964
966 def _get_default_perms(self, user, suffix=''):
965 def _get_default_perms(self, user, suffix=''):
967 from rhodecode.model.permission import PermissionModel
966 from rhodecode.model.permission import PermissionModel
968 return PermissionModel().get_default_perms(user.user_perms, suffix)
967 return PermissionModel().get_default_perms(user.user_perms, suffix)
969
968
970 def get_default_perms(self, suffix=''):
969 def get_default_perms(self, suffix=''):
971 return self._get_default_perms(self, suffix)
970 return self._get_default_perms(self, suffix)
972
971
973 def get_api_data(self, include_secrets=False, details='full'):
972 def get_api_data(self, include_secrets=False, details='full'):
974 """
973 """
975 Common function for generating user related data for API
974 Common function for generating user related data for API
976
975
977 :param include_secrets: By default secrets in the API data will be replaced
976 :param include_secrets: By default secrets in the API data will be replaced
978 by a placeholder value to prevent exposing this data by accident. In case
977 by a placeholder value to prevent exposing this data by accident. In case
979 this data shall be exposed, set this flag to ``True``.
978 this data shall be exposed, set this flag to ``True``.
980
979
981 :param details: details can be 'basic|full' basic gives only a subset of
980 :param details: details can be 'basic|full' basic gives only a subset of
982 the available user information that includes user_id, name and emails.
981 the available user information that includes user_id, name and emails.
983 """
982 """
984 user = self
983 user = self
985 user_data = self.user_data
984 user_data = self.user_data
986 data = {
985 data = {
987 'user_id': user.user_id,
986 'user_id': user.user_id,
988 'username': user.username,
987 'username': user.username,
989 'firstname': user.name,
988 'firstname': user.name,
990 'lastname': user.lastname,
989 'lastname': user.lastname,
991 'email': user.email,
990 'email': user.email,
992 'emails': user.emails,
991 'emails': user.emails,
993 }
992 }
994 if details == 'basic':
993 if details == 'basic':
995 return data
994 return data
996
995
997 auth_token_length = 40
996 auth_token_length = 40
998 auth_token_replacement = '*' * auth_token_length
997 auth_token_replacement = '*' * auth_token_length
999
998
1000 extras = {
999 extras = {
1001 'auth_tokens': [auth_token_replacement],
1000 'auth_tokens': [auth_token_replacement],
1002 'active': user.active,
1001 'active': user.active,
1003 'admin': user.admin,
1002 'admin': user.admin,
1004 'extern_type': user.extern_type,
1003 'extern_type': user.extern_type,
1005 'extern_name': user.extern_name,
1004 'extern_name': user.extern_name,
1006 'last_login': user.last_login,
1005 'last_login': user.last_login,
1007 'last_activity': user.last_activity,
1006 'last_activity': user.last_activity,
1008 'ip_addresses': user.ip_addresses,
1007 'ip_addresses': user.ip_addresses,
1009 'language': user_data.get('language')
1008 'language': user_data.get('language')
1010 }
1009 }
1011 data.update(extras)
1010 data.update(extras)
1012
1011
1013 if include_secrets:
1012 if include_secrets:
1014 data['auth_tokens'] = user.auth_tokens
1013 data['auth_tokens'] = user.auth_tokens
1015 return data
1014 return data
1016
1015
1017 def __json__(self):
1016 def __json__(self):
1018 data = {
1017 data = {
1019 'full_name': self.full_name,
1018 'full_name': self.full_name,
1020 'full_name_or_username': self.full_name_or_username,
1019 'full_name_or_username': self.full_name_or_username,
1021 'short_contact': self.short_contact,
1020 'short_contact': self.short_contact,
1022 'full_contact': self.full_contact,
1021 'full_contact': self.full_contact,
1023 }
1022 }
1024 data.update(self.get_api_data())
1023 data.update(self.get_api_data())
1025 return data
1024 return data
1026
1025
1027
1026
1028 class UserApiKeys(Base, BaseModel):
1027 class UserApiKeys(Base, BaseModel):
1029 __tablename__ = 'user_api_keys'
1028 __tablename__ = 'user_api_keys'
1030 __table_args__ = (
1029 __table_args__ = (
1031 Index('uak_api_key_idx', 'api_key', unique=True),
1030 Index('uak_api_key_idx', 'api_key', unique=True),
1032 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1031 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1033 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1032 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1034 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1033 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1035 )
1034 )
1036 __mapper_args__ = {}
1035 __mapper_args__ = {}
1037
1036
1038 # ApiKey role
1037 # ApiKey role
1039 ROLE_ALL = 'token_role_all'
1038 ROLE_ALL = 'token_role_all'
1040 ROLE_HTTP = 'token_role_http'
1039 ROLE_HTTP = 'token_role_http'
1041 ROLE_VCS = 'token_role_vcs'
1040 ROLE_VCS = 'token_role_vcs'
1042 ROLE_API = 'token_role_api'
1041 ROLE_API = 'token_role_api'
1043 ROLE_FEED = 'token_role_feed'
1042 ROLE_FEED = 'token_role_feed'
1044 ROLE_PASSWORD_RESET = 'token_password_reset'
1043 ROLE_PASSWORD_RESET = 'token_password_reset'
1045
1044
1046 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1045 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1047
1046
1048 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1047 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1048 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 api_key = Column("api_key", String(255), nullable=False, unique=True)
1049 api_key = Column("api_key", String(255), nullable=False, unique=True)
1051 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1050 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1052 expires = Column('expires', Float(53), nullable=False)
1051 expires = Column('expires', Float(53), nullable=False)
1053 role = Column('role', String(255), nullable=True)
1052 role = Column('role', String(255), nullable=True)
1054 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1053 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1055
1054
1056 # scope columns
1055 # scope columns
1057 repo_id = Column(
1056 repo_id = Column(
1058 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1057 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1059 nullable=True, unique=None, default=None)
1058 nullable=True, unique=None, default=None)
1060 repo = relationship('Repository', lazy='joined')
1059 repo = relationship('Repository', lazy='joined')
1061
1060
1062 repo_group_id = Column(
1061 repo_group_id = Column(
1063 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1062 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1064 nullable=True, unique=None, default=None)
1063 nullable=True, unique=None, default=None)
1065 repo_group = relationship('RepoGroup', lazy='joined')
1064 repo_group = relationship('RepoGroup', lazy='joined')
1066
1065
1067 user = relationship('User', lazy='joined')
1066 user = relationship('User', lazy='joined')
1068
1067
1069 def __unicode__(self):
1068 def __unicode__(self):
1070 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1069 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1071
1070
1072 def __json__(self):
1071 def __json__(self):
1073 data = {
1072 data = {
1074 'auth_token': self.api_key,
1073 'auth_token': self.api_key,
1075 'role': self.role,
1074 'role': self.role,
1076 'scope': self.scope_humanized,
1075 'scope': self.scope_humanized,
1077 'expired': self.expired
1076 'expired': self.expired
1078 }
1077 }
1079 return data
1078 return data
1080
1079
1081 def get_api_data(self, include_secrets=False):
1080 def get_api_data(self, include_secrets=False):
1082 data = self.__json__()
1081 data = self.__json__()
1083 if include_secrets:
1082 if include_secrets:
1084 return data
1083 return data
1085 else:
1084 else:
1086 data['auth_token'] = self.token_obfuscated
1085 data['auth_token'] = self.token_obfuscated
1087 return data
1086 return data
1088
1087
1089 @hybrid_property
1088 @hybrid_property
1090 def description_safe(self):
1089 def description_safe(self):
1091 from rhodecode.lib import helpers as h
1090 from rhodecode.lib import helpers as h
1092 return h.escape(self.description)
1091 return h.escape(self.description)
1093
1092
1094 @property
1093 @property
1095 def expired(self):
1094 def expired(self):
1096 if self.expires == -1:
1095 if self.expires == -1:
1097 return False
1096 return False
1098 return time.time() > self.expires
1097 return time.time() > self.expires
1099
1098
1100 @classmethod
1099 @classmethod
1101 def _get_role_name(cls, role):
1100 def _get_role_name(cls, role):
1102 return {
1101 return {
1103 cls.ROLE_ALL: _('all'),
1102 cls.ROLE_ALL: _('all'),
1104 cls.ROLE_HTTP: _('http/web interface'),
1103 cls.ROLE_HTTP: _('http/web interface'),
1105 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1104 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1106 cls.ROLE_API: _('api calls'),
1105 cls.ROLE_API: _('api calls'),
1107 cls.ROLE_FEED: _('feed access'),
1106 cls.ROLE_FEED: _('feed access'),
1108 }.get(role, role)
1107 }.get(role, role)
1109
1108
1110 @property
1109 @property
1111 def role_humanized(self):
1110 def role_humanized(self):
1112 return self._get_role_name(self.role)
1111 return self._get_role_name(self.role)
1113
1112
1114 def _get_scope(self):
1113 def _get_scope(self):
1115 if self.repo:
1114 if self.repo:
1116 return repr(self.repo)
1115 return repr(self.repo)
1117 if self.repo_group:
1116 if self.repo_group:
1118 return repr(self.repo_group) + ' (recursive)'
1117 return repr(self.repo_group) + ' (recursive)'
1119 return 'global'
1118 return 'global'
1120
1119
1121 @property
1120 @property
1122 def scope_humanized(self):
1121 def scope_humanized(self):
1123 return self._get_scope()
1122 return self._get_scope()
1124
1123
1125 @property
1124 @property
1126 def token_obfuscated(self):
1125 def token_obfuscated(self):
1127 if self.api_key:
1126 if self.api_key:
1128 return self.api_key[:4] + "****"
1127 return self.api_key[:4] + "****"
1129
1128
1130
1129
1131 class UserEmailMap(Base, BaseModel):
1130 class UserEmailMap(Base, BaseModel):
1132 __tablename__ = 'user_email_map'
1131 __tablename__ = 'user_email_map'
1133 __table_args__ = (
1132 __table_args__ = (
1134 Index('uem_email_idx', 'email'),
1133 Index('uem_email_idx', 'email'),
1135 UniqueConstraint('email'),
1134 UniqueConstraint('email'),
1136 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1135 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1137 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1136 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1138 )
1137 )
1139 __mapper_args__ = {}
1138 __mapper_args__ = {}
1140
1139
1141 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1140 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1142 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1141 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1143 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1142 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1144 user = relationship('User', lazy='joined')
1143 user = relationship('User', lazy='joined')
1145
1144
1146 @validates('_email')
1145 @validates('_email')
1147 def validate_email(self, key, email):
1146 def validate_email(self, key, email):
1148 # check if this email is not main one
1147 # check if this email is not main one
1149 main_email = Session().query(User).filter(User.email == email).scalar()
1148 main_email = Session().query(User).filter(User.email == email).scalar()
1150 if main_email is not None:
1149 if main_email is not None:
1151 raise AttributeError('email %s is present is user table' % email)
1150 raise AttributeError('email %s is present is user table' % email)
1152 return email
1151 return email
1153
1152
1154 @hybrid_property
1153 @hybrid_property
1155 def email(self):
1154 def email(self):
1156 return self._email
1155 return self._email
1157
1156
1158 @email.setter
1157 @email.setter
1159 def email(self, val):
1158 def email(self, val):
1160 self._email = val.lower() if val else None
1159 self._email = val.lower() if val else None
1161
1160
1162
1161
1163 class UserIpMap(Base, BaseModel):
1162 class UserIpMap(Base, BaseModel):
1164 __tablename__ = 'user_ip_map'
1163 __tablename__ = 'user_ip_map'
1165 __table_args__ = (
1164 __table_args__ = (
1166 UniqueConstraint('user_id', 'ip_addr'),
1165 UniqueConstraint('user_id', 'ip_addr'),
1167 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1166 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1168 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1167 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1169 )
1168 )
1170 __mapper_args__ = {}
1169 __mapper_args__ = {}
1171
1170
1172 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1171 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1173 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1172 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1174 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1173 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1175 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1174 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1176 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1175 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1177 user = relationship('User', lazy='joined')
1176 user = relationship('User', lazy='joined')
1178
1177
1179 @hybrid_property
1178 @hybrid_property
1180 def description_safe(self):
1179 def description_safe(self):
1181 from rhodecode.lib import helpers as h
1180 from rhodecode.lib import helpers as h
1182 return h.escape(self.description)
1181 return h.escape(self.description)
1183
1182
1184 @classmethod
1183 @classmethod
1185 def _get_ip_range(cls, ip_addr):
1184 def _get_ip_range(cls, ip_addr):
1186 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1185 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1187 return [str(net.network_address), str(net.broadcast_address)]
1186 return [str(net.network_address), str(net.broadcast_address)]
1188
1187
1189 def __json__(self):
1188 def __json__(self):
1190 return {
1189 return {
1191 'ip_addr': self.ip_addr,
1190 'ip_addr': self.ip_addr,
1192 'ip_range': self._get_ip_range(self.ip_addr),
1191 'ip_range': self._get_ip_range(self.ip_addr),
1193 }
1192 }
1194
1193
1195 def __unicode__(self):
1194 def __unicode__(self):
1196 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1195 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1197 self.user_id, self.ip_addr)
1196 self.user_id, self.ip_addr)
1198
1197
1199
1198
1200 class UserSshKeys(Base, BaseModel):
1199 class UserSshKeys(Base, BaseModel):
1201 __tablename__ = 'user_ssh_keys'
1200 __tablename__ = 'user_ssh_keys'
1202 __table_args__ = (
1201 __table_args__ = (
1203 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1202 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1204
1203
1205 UniqueConstraint('ssh_key_fingerprint'),
1204 UniqueConstraint('ssh_key_fingerprint'),
1206
1205
1207 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1208 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1207 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1209 )
1208 )
1210 __mapper_args__ = {}
1209 __mapper_args__ = {}
1211
1210
1212 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1213 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1212 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1214 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1213 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1215
1214
1216 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1215 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1217
1216
1218 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1219 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1218 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1220 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1219 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1221
1220
1222 user = relationship('User', lazy='joined')
1221 user = relationship('User', lazy='joined')
1223
1222
1224 def __json__(self):
1223 def __json__(self):
1225 data = {
1224 data = {
1226 'ssh_fingerprint': self.ssh_key_fingerprint,
1225 'ssh_fingerprint': self.ssh_key_fingerprint,
1227 'description': self.description,
1226 'description': self.description,
1228 'created_on': self.created_on
1227 'created_on': self.created_on
1229 }
1228 }
1230 return data
1229 return data
1231
1230
1232 def get_api_data(self):
1231 def get_api_data(self):
1233 data = self.__json__()
1232 data = self.__json__()
1234 return data
1233 return data
1235
1234
1236
1235
1237 class UserLog(Base, BaseModel):
1236 class UserLog(Base, BaseModel):
1238 __tablename__ = 'user_logs'
1237 __tablename__ = 'user_logs'
1239 __table_args__ = (
1238 __table_args__ = (
1240 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1239 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1241 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1240 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1242 )
1241 )
1243 VERSION_1 = 'v1'
1242 VERSION_1 = 'v1'
1244 VERSION_2 = 'v2'
1243 VERSION_2 = 'v2'
1245 VERSIONS = [VERSION_1, VERSION_2]
1244 VERSIONS = [VERSION_1, VERSION_2]
1246
1245
1247 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1246 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1248 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1247 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1249 username = Column("username", String(255), nullable=True, unique=None, default=None)
1248 username = Column("username", String(255), nullable=True, unique=None, default=None)
1250 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1249 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1251 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1250 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1252 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1251 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1253 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1252 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1254 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1253 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1255
1254
1256 version = Column("version", String(255), nullable=True, default=VERSION_1)
1255 version = Column("version", String(255), nullable=True, default=VERSION_1)
1257 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1256 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1258 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1257 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1259
1258
1260 def __unicode__(self):
1259 def __unicode__(self):
1261 return u"<%s('id:%s:%s')>" % (
1260 return u"<%s('id:%s:%s')>" % (
1262 self.__class__.__name__, self.repository_name, self.action)
1261 self.__class__.__name__, self.repository_name, self.action)
1263
1262
1264 def __json__(self):
1263 def __json__(self):
1265 return {
1264 return {
1266 'user_id': self.user_id,
1265 'user_id': self.user_id,
1267 'username': self.username,
1266 'username': self.username,
1268 'repository_id': self.repository_id,
1267 'repository_id': self.repository_id,
1269 'repository_name': self.repository_name,
1268 'repository_name': self.repository_name,
1270 'user_ip': self.user_ip,
1269 'user_ip': self.user_ip,
1271 'action_date': self.action_date,
1270 'action_date': self.action_date,
1272 'action': self.action,
1271 'action': self.action,
1273 }
1272 }
1274
1273
1275 @hybrid_property
1274 @hybrid_property
1276 def entry_id(self):
1275 def entry_id(self):
1277 return self.user_log_id
1276 return self.user_log_id
1278
1277
1279 @property
1278 @property
1280 def action_as_day(self):
1279 def action_as_day(self):
1281 return datetime.date(*self.action_date.timetuple()[:3])
1280 return datetime.date(*self.action_date.timetuple()[:3])
1282
1281
1283 user = relationship('User')
1282 user = relationship('User')
1284 repository = relationship('Repository', cascade='')
1283 repository = relationship('Repository', cascade='')
1285
1284
1286
1285
1287 class UserGroup(Base, BaseModel):
1286 class UserGroup(Base, BaseModel):
1288 __tablename__ = 'users_groups'
1287 __tablename__ = 'users_groups'
1289 __table_args__ = (
1288 __table_args__ = (
1290 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1289 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1291 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1290 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1292 )
1291 )
1293
1292
1294 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1293 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1295 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1294 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1296 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1295 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1297 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1296 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1298 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1297 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1299 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1298 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1300 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1299 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1301 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1300 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1302
1301
1303 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1302 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1304 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1303 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1305 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1304 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1306 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1305 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1307 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1306 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1308 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1307 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1309
1308
1310 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1309 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1311 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1310 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1312
1311
1313 @classmethod
1312 @classmethod
1314 def _load_group_data(cls, column):
1313 def _load_group_data(cls, column):
1315 if not column:
1314 if not column:
1316 return {}
1315 return {}
1317
1316
1318 try:
1317 try:
1319 return json.loads(column) or {}
1318 return json.loads(column) or {}
1320 except TypeError:
1319 except TypeError:
1321 return {}
1320 return {}
1322
1321
1323 @hybrid_property
1322 @hybrid_property
1324 def description_safe(self):
1323 def description_safe(self):
1325 from rhodecode.lib import helpers as h
1324 from rhodecode.lib import helpers as h
1326 return h.escape(self.description)
1325 return h.escape(self.description)
1327
1326
1328 @hybrid_property
1327 @hybrid_property
1329 def group_data(self):
1328 def group_data(self):
1330 return self._load_group_data(self._group_data)
1329 return self._load_group_data(self._group_data)
1331
1330
1332 @group_data.expression
1331 @group_data.expression
1333 def group_data(self, **kwargs):
1332 def group_data(self, **kwargs):
1334 return self._group_data
1333 return self._group_data
1335
1334
1336 @group_data.setter
1335 @group_data.setter
1337 def group_data(self, val):
1336 def group_data(self, val):
1338 try:
1337 try:
1339 self._group_data = json.dumps(val)
1338 self._group_data = json.dumps(val)
1340 except Exception:
1339 except Exception:
1341 log.error(traceback.format_exc())
1340 log.error(traceback.format_exc())
1342
1341
1343 def __unicode__(self):
1342 def __unicode__(self):
1344 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1343 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1345 self.users_group_id,
1344 self.users_group_id,
1346 self.users_group_name)
1345 self.users_group_name)
1347
1346
1348 @classmethod
1347 @classmethod
1349 def get_by_group_name(cls, group_name, cache=False,
1348 def get_by_group_name(cls, group_name, cache=False,
1350 case_insensitive=False):
1349 case_insensitive=False):
1351 if case_insensitive:
1350 if case_insensitive:
1352 q = cls.query().filter(func.lower(cls.users_group_name) ==
1351 q = cls.query().filter(func.lower(cls.users_group_name) ==
1353 func.lower(group_name))
1352 func.lower(group_name))
1354
1353
1355 else:
1354 else:
1356 q = cls.query().filter(cls.users_group_name == group_name)
1355 q = cls.query().filter(cls.users_group_name == group_name)
1357 if cache:
1356 if cache:
1358 q = q.options(
1357 q = q.options(
1359 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1358 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1360 return q.scalar()
1359 return q.scalar()
1361
1360
1362 @classmethod
1361 @classmethod
1363 def get(cls, user_group_id, cache=False):
1362 def get(cls, user_group_id, cache=False):
1364 if not user_group_id:
1363 if not user_group_id:
1365 return
1364 return
1366
1365
1367 user_group = cls.query()
1366 user_group = cls.query()
1368 if cache:
1367 if cache:
1369 user_group = user_group.options(
1368 user_group = user_group.options(
1370 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1369 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1371 return user_group.get(user_group_id)
1370 return user_group.get(user_group_id)
1372
1371
1373 def permissions(self, with_admins=True, with_owner=True):
1372 def permissions(self, with_admins=True, with_owner=True):
1374 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1373 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1375 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1374 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1376 joinedload(UserUserGroupToPerm.user),
1375 joinedload(UserUserGroupToPerm.user),
1377 joinedload(UserUserGroupToPerm.permission),)
1376 joinedload(UserUserGroupToPerm.permission),)
1378
1377
1379 # get owners and admins and permissions. We do a trick of re-writing
1378 # get owners and admins and permissions. We do a trick of re-writing
1380 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1379 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1381 # has a global reference and changing one object propagates to all
1380 # has a global reference and changing one object propagates to all
1382 # others. This means if admin is also an owner admin_row that change
1381 # others. This means if admin is also an owner admin_row that change
1383 # would propagate to both objects
1382 # would propagate to both objects
1384 perm_rows = []
1383 perm_rows = []
1385 for _usr in q.all():
1384 for _usr in q.all():
1386 usr = AttributeDict(_usr.user.get_dict())
1385 usr = AttributeDict(_usr.user.get_dict())
1387 usr.permission = _usr.permission.permission_name
1386 usr.permission = _usr.permission.permission_name
1388 perm_rows.append(usr)
1387 perm_rows.append(usr)
1389
1388
1390 # filter the perm rows by 'default' first and then sort them by
1389 # filter the perm rows by 'default' first and then sort them by
1391 # admin,write,read,none permissions sorted again alphabetically in
1390 # admin,write,read,none permissions sorted again alphabetically in
1392 # each group
1391 # each group
1393 perm_rows = sorted(perm_rows, key=display_user_sort)
1392 perm_rows = sorted(perm_rows, key=display_user_sort)
1394
1393
1395 _admin_perm = 'usergroup.admin'
1394 _admin_perm = 'usergroup.admin'
1396 owner_row = []
1395 owner_row = []
1397 if with_owner:
1396 if with_owner:
1398 usr = AttributeDict(self.user.get_dict())
1397 usr = AttributeDict(self.user.get_dict())
1399 usr.owner_row = True
1398 usr.owner_row = True
1400 usr.permission = _admin_perm
1399 usr.permission = _admin_perm
1401 owner_row.append(usr)
1400 owner_row.append(usr)
1402
1401
1403 super_admin_rows = []
1402 super_admin_rows = []
1404 if with_admins:
1403 if with_admins:
1405 for usr in User.get_all_super_admins():
1404 for usr in User.get_all_super_admins():
1406 # if this admin is also owner, don't double the record
1405 # if this admin is also owner, don't double the record
1407 if usr.user_id == owner_row[0].user_id:
1406 if usr.user_id == owner_row[0].user_id:
1408 owner_row[0].admin_row = True
1407 owner_row[0].admin_row = True
1409 else:
1408 else:
1410 usr = AttributeDict(usr.get_dict())
1409 usr = AttributeDict(usr.get_dict())
1411 usr.admin_row = True
1410 usr.admin_row = True
1412 usr.permission = _admin_perm
1411 usr.permission = _admin_perm
1413 super_admin_rows.append(usr)
1412 super_admin_rows.append(usr)
1414
1413
1415 return super_admin_rows + owner_row + perm_rows
1414 return super_admin_rows + owner_row + perm_rows
1416
1415
1417 def permission_user_groups(self):
1416 def permission_user_groups(self):
1418 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1417 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1419 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1418 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1420 joinedload(UserGroupUserGroupToPerm.target_user_group),
1419 joinedload(UserGroupUserGroupToPerm.target_user_group),
1421 joinedload(UserGroupUserGroupToPerm.permission),)
1420 joinedload(UserGroupUserGroupToPerm.permission),)
1422
1421
1423 perm_rows = []
1422 perm_rows = []
1424 for _user_group in q.all():
1423 for _user_group in q.all():
1425 usr = AttributeDict(_user_group.user_group.get_dict())
1424 usr = AttributeDict(_user_group.user_group.get_dict())
1426 usr.permission = _user_group.permission.permission_name
1425 usr.permission = _user_group.permission.permission_name
1427 perm_rows.append(usr)
1426 perm_rows.append(usr)
1428
1427
1429 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1428 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1430 return perm_rows
1429 return perm_rows
1431
1430
1432 def _get_default_perms(self, user_group, suffix=''):
1431 def _get_default_perms(self, user_group, suffix=''):
1433 from rhodecode.model.permission import PermissionModel
1432 from rhodecode.model.permission import PermissionModel
1434 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1433 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1435
1434
1436 def get_default_perms(self, suffix=''):
1435 def get_default_perms(self, suffix=''):
1437 return self._get_default_perms(self, suffix)
1436 return self._get_default_perms(self, suffix)
1438
1437
1439 def get_api_data(self, with_group_members=True, include_secrets=False):
1438 def get_api_data(self, with_group_members=True, include_secrets=False):
1440 """
1439 """
1441 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1440 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1442 basically forwarded.
1441 basically forwarded.
1443
1442
1444 """
1443 """
1445 user_group = self
1444 user_group = self
1446 data = {
1445 data = {
1447 'users_group_id': user_group.users_group_id,
1446 'users_group_id': user_group.users_group_id,
1448 'group_name': user_group.users_group_name,
1447 'group_name': user_group.users_group_name,
1449 'group_description': user_group.user_group_description,
1448 'group_description': user_group.user_group_description,
1450 'active': user_group.users_group_active,
1449 'active': user_group.users_group_active,
1451 'owner': user_group.user.username,
1450 'owner': user_group.user.username,
1452 'owner_email': user_group.user.email,
1451 'owner_email': user_group.user.email,
1453 }
1452 }
1454
1453
1455 if with_group_members:
1454 if with_group_members:
1456 users = []
1455 users = []
1457 for user in user_group.members:
1456 for user in user_group.members:
1458 user = user.user
1457 user = user.user
1459 users.append(user.get_api_data(include_secrets=include_secrets))
1458 users.append(user.get_api_data(include_secrets=include_secrets))
1460 data['users'] = users
1459 data['users'] = users
1461
1460
1462 return data
1461 return data
1463
1462
1464
1463
1465 class UserGroupMember(Base, BaseModel):
1464 class UserGroupMember(Base, BaseModel):
1466 __tablename__ = 'users_groups_members'
1465 __tablename__ = 'users_groups_members'
1467 __table_args__ = (
1466 __table_args__ = (
1468 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1467 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1469 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1468 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1470 )
1469 )
1471
1470
1472 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1471 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1473 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1472 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1474 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1473 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1475
1474
1476 user = relationship('User', lazy='joined')
1475 user = relationship('User', lazy='joined')
1477 users_group = relationship('UserGroup')
1476 users_group = relationship('UserGroup')
1478
1477
1479 def __init__(self, gr_id='', u_id=''):
1478 def __init__(self, gr_id='', u_id=''):
1480 self.users_group_id = gr_id
1479 self.users_group_id = gr_id
1481 self.user_id = u_id
1480 self.user_id = u_id
1482
1481
1483
1482
1484 class RepositoryField(Base, BaseModel):
1483 class RepositoryField(Base, BaseModel):
1485 __tablename__ = 'repositories_fields'
1484 __tablename__ = 'repositories_fields'
1486 __table_args__ = (
1485 __table_args__ = (
1487 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1486 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1488 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1487 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1489 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1488 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1490 )
1489 )
1491 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1490 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1492
1491
1493 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1492 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1494 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1493 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1495 field_key = Column("field_key", String(250))
1494 field_key = Column("field_key", String(250))
1496 field_label = Column("field_label", String(1024), nullable=False)
1495 field_label = Column("field_label", String(1024), nullable=False)
1497 field_value = Column("field_value", String(10000), nullable=False)
1496 field_value = Column("field_value", String(10000), nullable=False)
1498 field_desc = Column("field_desc", String(1024), nullable=False)
1497 field_desc = Column("field_desc", String(1024), nullable=False)
1499 field_type = Column("field_type", String(255), nullable=False, unique=None)
1498 field_type = Column("field_type", String(255), nullable=False, unique=None)
1500 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1499 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1501
1500
1502 repository = relationship('Repository')
1501 repository = relationship('Repository')
1503
1502
1504 @property
1503 @property
1505 def field_key_prefixed(self):
1504 def field_key_prefixed(self):
1506 return 'ex_%s' % self.field_key
1505 return 'ex_%s' % self.field_key
1507
1506
1508 @classmethod
1507 @classmethod
1509 def un_prefix_key(cls, key):
1508 def un_prefix_key(cls, key):
1510 if key.startswith(cls.PREFIX):
1509 if key.startswith(cls.PREFIX):
1511 return key[len(cls.PREFIX):]
1510 return key[len(cls.PREFIX):]
1512 return key
1511 return key
1513
1512
1514 @classmethod
1513 @classmethod
1515 def get_by_key_name(cls, key, repo):
1514 def get_by_key_name(cls, key, repo):
1516 row = cls.query()\
1515 row = cls.query()\
1517 .filter(cls.repository == repo)\
1516 .filter(cls.repository == repo)\
1518 .filter(cls.field_key == key).scalar()
1517 .filter(cls.field_key == key).scalar()
1519 return row
1518 return row
1520
1519
1521
1520
1522 class Repository(Base, BaseModel):
1521 class Repository(Base, BaseModel):
1523 __tablename__ = 'repositories'
1522 __tablename__ = 'repositories'
1524 __table_args__ = (
1523 __table_args__ = (
1525 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1524 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1526 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1525 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1527 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1526 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1528 )
1527 )
1529 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1528 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1530 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1529 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1531
1530
1532 STATE_CREATED = 'repo_state_created'
1531 STATE_CREATED = 'repo_state_created'
1533 STATE_PENDING = 'repo_state_pending'
1532 STATE_PENDING = 'repo_state_pending'
1534 STATE_ERROR = 'repo_state_error'
1533 STATE_ERROR = 'repo_state_error'
1535
1534
1536 LOCK_AUTOMATIC = 'lock_auto'
1535 LOCK_AUTOMATIC = 'lock_auto'
1537 LOCK_API = 'lock_api'
1536 LOCK_API = 'lock_api'
1538 LOCK_WEB = 'lock_web'
1537 LOCK_WEB = 'lock_web'
1539 LOCK_PULL = 'lock_pull'
1538 LOCK_PULL = 'lock_pull'
1540
1539
1541 NAME_SEP = URL_SEP
1540 NAME_SEP = URL_SEP
1542
1541
1543 repo_id = Column(
1542 repo_id = Column(
1544 "repo_id", Integer(), nullable=False, unique=True, default=None,
1543 "repo_id", Integer(), nullable=False, unique=True, default=None,
1545 primary_key=True)
1544 primary_key=True)
1546 _repo_name = Column(
1545 _repo_name = Column(
1547 "repo_name", Text(), nullable=False, default=None)
1546 "repo_name", Text(), nullable=False, default=None)
1548 _repo_name_hash = Column(
1547 _repo_name_hash = Column(
1549 "repo_name_hash", String(255), nullable=False, unique=True)
1548 "repo_name_hash", String(255), nullable=False, unique=True)
1550 repo_state = Column("repo_state", String(255), nullable=True)
1549 repo_state = Column("repo_state", String(255), nullable=True)
1551
1550
1552 clone_uri = Column(
1551 clone_uri = Column(
1553 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1552 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1554 default=None)
1553 default=None)
1555 repo_type = Column(
1554 repo_type = Column(
1556 "repo_type", String(255), nullable=False, unique=False, default=None)
1555 "repo_type", String(255), nullable=False, unique=False, default=None)
1557 user_id = Column(
1556 user_id = Column(
1558 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1557 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1559 unique=False, default=None)
1558 unique=False, default=None)
1560 private = Column(
1559 private = Column(
1561 "private", Boolean(), nullable=True, unique=None, default=None)
1560 "private", Boolean(), nullable=True, unique=None, default=None)
1562 enable_statistics = Column(
1561 enable_statistics = Column(
1563 "statistics", Boolean(), nullable=True, unique=None, default=True)
1562 "statistics", Boolean(), nullable=True, unique=None, default=True)
1564 enable_downloads = Column(
1563 enable_downloads = Column(
1565 "downloads", Boolean(), nullable=True, unique=None, default=True)
1564 "downloads", Boolean(), nullable=True, unique=None, default=True)
1566 description = Column(
1565 description = Column(
1567 "description", String(10000), nullable=True, unique=None, default=None)
1566 "description", String(10000), nullable=True, unique=None, default=None)
1568 created_on = Column(
1567 created_on = Column(
1569 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1568 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1570 default=datetime.datetime.now)
1569 default=datetime.datetime.now)
1571 updated_on = Column(
1570 updated_on = Column(
1572 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1571 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1573 default=datetime.datetime.now)
1572 default=datetime.datetime.now)
1574 _landing_revision = Column(
1573 _landing_revision = Column(
1575 "landing_revision", String(255), nullable=False, unique=False,
1574 "landing_revision", String(255), nullable=False, unique=False,
1576 default=None)
1575 default=None)
1577 enable_locking = Column(
1576 enable_locking = Column(
1578 "enable_locking", Boolean(), nullable=False, unique=None,
1577 "enable_locking", Boolean(), nullable=False, unique=None,
1579 default=False)
1578 default=False)
1580 _locked = Column(
1579 _locked = Column(
1581 "locked", String(255), nullable=True, unique=False, default=None)
1580 "locked", String(255), nullable=True, unique=False, default=None)
1582 _changeset_cache = Column(
1581 _changeset_cache = Column(
1583 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1582 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1584
1583
1585 fork_id = Column(
1584 fork_id = Column(
1586 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1585 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1587 nullable=True, unique=False, default=None)
1586 nullable=True, unique=False, default=None)
1588 group_id = Column(
1587 group_id = Column(
1589 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1588 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1590 unique=False, default=None)
1589 unique=False, default=None)
1591
1590
1592 user = relationship('User', lazy='joined')
1591 user = relationship('User', lazy='joined')
1593 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1592 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1594 group = relationship('RepoGroup', lazy='joined')
1593 group = relationship('RepoGroup', lazy='joined')
1595 repo_to_perm = relationship(
1594 repo_to_perm = relationship(
1596 'UserRepoToPerm', cascade='all',
1595 'UserRepoToPerm', cascade='all',
1597 order_by='UserRepoToPerm.repo_to_perm_id')
1596 order_by='UserRepoToPerm.repo_to_perm_id')
1598 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1597 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1599 stats = relationship('Statistics', cascade='all', uselist=False)
1598 stats = relationship('Statistics', cascade='all', uselist=False)
1600
1599
1601 followers = relationship(
1600 followers = relationship(
1602 'UserFollowing',
1601 'UserFollowing',
1603 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1602 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1604 cascade='all')
1603 cascade='all')
1605 extra_fields = relationship(
1604 extra_fields = relationship(
1606 'RepositoryField', cascade="all, delete, delete-orphan")
1605 'RepositoryField', cascade="all, delete, delete-orphan")
1607 logs = relationship('UserLog')
1606 logs = relationship('UserLog')
1608 comments = relationship(
1607 comments = relationship(
1609 'ChangesetComment', cascade="all, delete, delete-orphan")
1608 'ChangesetComment', cascade="all, delete, delete-orphan")
1610 pull_requests_source = relationship(
1609 pull_requests_source = relationship(
1611 'PullRequest',
1610 'PullRequest',
1612 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1611 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1613 cascade="all, delete, delete-orphan")
1612 cascade="all, delete, delete-orphan")
1614 pull_requests_target = relationship(
1613 pull_requests_target = relationship(
1615 'PullRequest',
1614 'PullRequest',
1616 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1615 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1617 cascade="all, delete, delete-orphan")
1616 cascade="all, delete, delete-orphan")
1618 ui = relationship('RepoRhodeCodeUi', cascade="all")
1617 ui = relationship('RepoRhodeCodeUi', cascade="all")
1619 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1618 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1620 integrations = relationship('Integration',
1619 integrations = relationship('Integration',
1621 cascade="all, delete, delete-orphan")
1620 cascade="all, delete, delete-orphan")
1622
1621
1623 def __unicode__(self):
1622 def __unicode__(self):
1624 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1623 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1625 safe_unicode(self.repo_name))
1624 safe_unicode(self.repo_name))
1626
1625
1627 @hybrid_property
1626 @hybrid_property
1628 def description_safe(self):
1627 def description_safe(self):
1629 from rhodecode.lib import helpers as h
1628 from rhodecode.lib import helpers as h
1630 return h.escape(self.description)
1629 return h.escape(self.description)
1631
1630
1632 @hybrid_property
1631 @hybrid_property
1633 def landing_rev(self):
1632 def landing_rev(self):
1634 # always should return [rev_type, rev]
1633 # always should return [rev_type, rev]
1635 if self._landing_revision:
1634 if self._landing_revision:
1636 _rev_info = self._landing_revision.split(':')
1635 _rev_info = self._landing_revision.split(':')
1637 if len(_rev_info) < 2:
1636 if len(_rev_info) < 2:
1638 _rev_info.insert(0, 'rev')
1637 _rev_info.insert(0, 'rev')
1639 return [_rev_info[0], _rev_info[1]]
1638 return [_rev_info[0], _rev_info[1]]
1640 return [None, None]
1639 return [None, None]
1641
1640
1642 @landing_rev.setter
1641 @landing_rev.setter
1643 def landing_rev(self, val):
1642 def landing_rev(self, val):
1644 if ':' not in val:
1643 if ':' not in val:
1645 raise ValueError('value must be delimited with `:` and consist '
1644 raise ValueError('value must be delimited with `:` and consist '
1646 'of <rev_type>:<rev>, got %s instead' % val)
1645 'of <rev_type>:<rev>, got %s instead' % val)
1647 self._landing_revision = val
1646 self._landing_revision = val
1648
1647
1649 @hybrid_property
1648 @hybrid_property
1650 def locked(self):
1649 def locked(self):
1651 if self._locked:
1650 if self._locked:
1652 user_id, timelocked, reason = self._locked.split(':')
1651 user_id, timelocked, reason = self._locked.split(':')
1653 lock_values = int(user_id), timelocked, reason
1652 lock_values = int(user_id), timelocked, reason
1654 else:
1653 else:
1655 lock_values = [None, None, None]
1654 lock_values = [None, None, None]
1656 return lock_values
1655 return lock_values
1657
1656
1658 @locked.setter
1657 @locked.setter
1659 def locked(self, val):
1658 def locked(self, val):
1660 if val and isinstance(val, (list, tuple)):
1659 if val and isinstance(val, (list, tuple)):
1661 self._locked = ':'.join(map(str, val))
1660 self._locked = ':'.join(map(str, val))
1662 else:
1661 else:
1663 self._locked = None
1662 self._locked = None
1664
1663
1665 @hybrid_property
1664 @hybrid_property
1666 def changeset_cache(self):
1665 def changeset_cache(self):
1667 from rhodecode.lib.vcs.backends.base import EmptyCommit
1666 from rhodecode.lib.vcs.backends.base import EmptyCommit
1668 dummy = EmptyCommit().__json__()
1667 dummy = EmptyCommit().__json__()
1669 if not self._changeset_cache:
1668 if not self._changeset_cache:
1670 return dummy
1669 return dummy
1671 try:
1670 try:
1672 return json.loads(self._changeset_cache)
1671 return json.loads(self._changeset_cache)
1673 except TypeError:
1672 except TypeError:
1674 return dummy
1673 return dummy
1675 except Exception:
1674 except Exception:
1676 log.error(traceback.format_exc())
1675 log.error(traceback.format_exc())
1677 return dummy
1676 return dummy
1678
1677
1679 @changeset_cache.setter
1678 @changeset_cache.setter
1680 def changeset_cache(self, val):
1679 def changeset_cache(self, val):
1681 try:
1680 try:
1682 self._changeset_cache = json.dumps(val)
1681 self._changeset_cache = json.dumps(val)
1683 except Exception:
1682 except Exception:
1684 log.error(traceback.format_exc())
1683 log.error(traceback.format_exc())
1685
1684
1686 @hybrid_property
1685 @hybrid_property
1687 def repo_name(self):
1686 def repo_name(self):
1688 return self._repo_name
1687 return self._repo_name
1689
1688
1690 @repo_name.setter
1689 @repo_name.setter
1691 def repo_name(self, value):
1690 def repo_name(self, value):
1692 self._repo_name = value
1691 self._repo_name = value
1693 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1692 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1694
1693
1695 @classmethod
1694 @classmethod
1696 def normalize_repo_name(cls, repo_name):
1695 def normalize_repo_name(cls, repo_name):
1697 """
1696 """
1698 Normalizes os specific repo_name to the format internally stored inside
1697 Normalizes os specific repo_name to the format internally stored inside
1699 database using URL_SEP
1698 database using URL_SEP
1700
1699
1701 :param cls:
1700 :param cls:
1702 :param repo_name:
1701 :param repo_name:
1703 """
1702 """
1704 return cls.NAME_SEP.join(repo_name.split(os.sep))
1703 return cls.NAME_SEP.join(repo_name.split(os.sep))
1705
1704
1706 @classmethod
1705 @classmethod
1707 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1706 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1708 session = Session()
1707 session = Session()
1709 q = session.query(cls).filter(cls.repo_name == repo_name)
1708 q = session.query(cls).filter(cls.repo_name == repo_name)
1710
1709
1711 if cache:
1710 if cache:
1712 if identity_cache:
1711 if identity_cache:
1713 val = cls.identity_cache(session, 'repo_name', repo_name)
1712 val = cls.identity_cache(session, 'repo_name', repo_name)
1714 if val:
1713 if val:
1715 return val
1714 return val
1716 else:
1715 else:
1717 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1716 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1718 q = q.options(
1717 q = q.options(
1719 FromCache("sql_cache_short", cache_key))
1718 FromCache("sql_cache_short", cache_key))
1720
1719
1721 return q.scalar()
1720 return q.scalar()
1722
1721
1723 @classmethod
1722 @classmethod
1724 def get_by_full_path(cls, repo_full_path):
1723 def get_by_full_path(cls, repo_full_path):
1725 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1724 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1726 repo_name = cls.normalize_repo_name(repo_name)
1725 repo_name = cls.normalize_repo_name(repo_name)
1727 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1726 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1728
1727
1729 @classmethod
1728 @classmethod
1730 def get_repo_forks(cls, repo_id):
1729 def get_repo_forks(cls, repo_id):
1731 return cls.query().filter(Repository.fork_id == repo_id)
1730 return cls.query().filter(Repository.fork_id == repo_id)
1732
1731
1733 @classmethod
1732 @classmethod
1734 def base_path(cls):
1733 def base_path(cls):
1735 """
1734 """
1736 Returns base path when all repos are stored
1735 Returns base path when all repos are stored
1737
1736
1738 :param cls:
1737 :param cls:
1739 """
1738 """
1740 q = Session().query(RhodeCodeUi)\
1739 q = Session().query(RhodeCodeUi)\
1741 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1740 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1742 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1741 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1743 return q.one().ui_value
1742 return q.one().ui_value
1744
1743
1745 @classmethod
1744 @classmethod
1746 def is_valid(cls, repo_name):
1745 def is_valid(cls, repo_name):
1747 """
1746 """
1748 returns True if given repo name is a valid filesystem repository
1747 returns True if given repo name is a valid filesystem repository
1749
1748
1750 :param cls:
1749 :param cls:
1751 :param repo_name:
1750 :param repo_name:
1752 """
1751 """
1753 from rhodecode.lib.utils import is_valid_repo
1752 from rhodecode.lib.utils import is_valid_repo
1754
1753
1755 return is_valid_repo(repo_name, cls.base_path())
1754 return is_valid_repo(repo_name, cls.base_path())
1756
1755
1757 @classmethod
1756 @classmethod
1758 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1757 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1759 case_insensitive=True):
1758 case_insensitive=True):
1760 q = Repository.query()
1759 q = Repository.query()
1761
1760
1762 if not isinstance(user_id, Optional):
1761 if not isinstance(user_id, Optional):
1763 q = q.filter(Repository.user_id == user_id)
1762 q = q.filter(Repository.user_id == user_id)
1764
1763
1765 if not isinstance(group_id, Optional):
1764 if not isinstance(group_id, Optional):
1766 q = q.filter(Repository.group_id == group_id)
1765 q = q.filter(Repository.group_id == group_id)
1767
1766
1768 if case_insensitive:
1767 if case_insensitive:
1769 q = q.order_by(func.lower(Repository.repo_name))
1768 q = q.order_by(func.lower(Repository.repo_name))
1770 else:
1769 else:
1771 q = q.order_by(Repository.repo_name)
1770 q = q.order_by(Repository.repo_name)
1772 return q.all()
1771 return q.all()
1773
1772
1774 @property
1773 @property
1775 def forks(self):
1774 def forks(self):
1776 """
1775 """
1777 Return forks of this repo
1776 Return forks of this repo
1778 """
1777 """
1779 return Repository.get_repo_forks(self.repo_id)
1778 return Repository.get_repo_forks(self.repo_id)
1780
1779
1781 @property
1780 @property
1782 def parent(self):
1781 def parent(self):
1783 """
1782 """
1784 Returns fork parent
1783 Returns fork parent
1785 """
1784 """
1786 return self.fork
1785 return self.fork
1787
1786
1788 @property
1787 @property
1789 def just_name(self):
1788 def just_name(self):
1790 return self.repo_name.split(self.NAME_SEP)[-1]
1789 return self.repo_name.split(self.NAME_SEP)[-1]
1791
1790
1792 @property
1791 @property
1793 def groups_with_parents(self):
1792 def groups_with_parents(self):
1794 groups = []
1793 groups = []
1795 if self.group is None:
1794 if self.group is None:
1796 return groups
1795 return groups
1797
1796
1798 cur_gr = self.group
1797 cur_gr = self.group
1799 groups.insert(0, cur_gr)
1798 groups.insert(0, cur_gr)
1800 while 1:
1799 while 1:
1801 gr = getattr(cur_gr, 'parent_group', None)
1800 gr = getattr(cur_gr, 'parent_group', None)
1802 cur_gr = cur_gr.parent_group
1801 cur_gr = cur_gr.parent_group
1803 if gr is None:
1802 if gr is None:
1804 break
1803 break
1805 groups.insert(0, gr)
1804 groups.insert(0, gr)
1806
1805
1807 return groups
1806 return groups
1808
1807
1809 @property
1808 @property
1810 def groups_and_repo(self):
1809 def groups_and_repo(self):
1811 return self.groups_with_parents, self
1810 return self.groups_with_parents, self
1812
1811
1813 @LazyProperty
1812 @LazyProperty
1814 def repo_path(self):
1813 def repo_path(self):
1815 """
1814 """
1816 Returns base full path for that repository means where it actually
1815 Returns base full path for that repository means where it actually
1817 exists on a filesystem
1816 exists on a filesystem
1818 """
1817 """
1819 q = Session().query(RhodeCodeUi).filter(
1818 q = Session().query(RhodeCodeUi).filter(
1820 RhodeCodeUi.ui_key == self.NAME_SEP)
1819 RhodeCodeUi.ui_key == self.NAME_SEP)
1821 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1820 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1822 return q.one().ui_value
1821 return q.one().ui_value
1823
1822
1824 @property
1823 @property
1825 def repo_full_path(self):
1824 def repo_full_path(self):
1826 p = [self.repo_path]
1825 p = [self.repo_path]
1827 # we need to split the name by / since this is how we store the
1826 # we need to split the name by / since this is how we store the
1828 # names in the database, but that eventually needs to be converted
1827 # names in the database, but that eventually needs to be converted
1829 # into a valid system path
1828 # into a valid system path
1830 p += self.repo_name.split(self.NAME_SEP)
1829 p += self.repo_name.split(self.NAME_SEP)
1831 return os.path.join(*map(safe_unicode, p))
1830 return os.path.join(*map(safe_unicode, p))
1832
1831
1833 @property
1832 @property
1834 def cache_keys(self):
1833 def cache_keys(self):
1835 """
1834 """
1836 Returns associated cache keys for that repo
1835 Returns associated cache keys for that repo
1837 """
1836 """
1838 return CacheKey.query()\
1837 return CacheKey.query()\
1839 .filter(CacheKey.cache_args == self.repo_name)\
1838 .filter(CacheKey.cache_args == self.repo_name)\
1840 .order_by(CacheKey.cache_key)\
1839 .order_by(CacheKey.cache_key)\
1841 .all()
1840 .all()
1842
1841
1843 def get_new_name(self, repo_name):
1842 def get_new_name(self, repo_name):
1844 """
1843 """
1845 returns new full repository name based on assigned group and new new
1844 returns new full repository name based on assigned group and new new
1846
1845
1847 :param group_name:
1846 :param group_name:
1848 """
1847 """
1849 path_prefix = self.group.full_path_splitted if self.group else []
1848 path_prefix = self.group.full_path_splitted if self.group else []
1850 return self.NAME_SEP.join(path_prefix + [repo_name])
1849 return self.NAME_SEP.join(path_prefix + [repo_name])
1851
1850
1852 @property
1851 @property
1853 def _config(self):
1852 def _config(self):
1854 """
1853 """
1855 Returns db based config object.
1854 Returns db based config object.
1856 """
1855 """
1857 from rhodecode.lib.utils import make_db_config
1856 from rhodecode.lib.utils import make_db_config
1858 return make_db_config(clear_session=False, repo=self)
1857 return make_db_config(clear_session=False, repo=self)
1859
1858
1860 def permissions(self, with_admins=True, with_owner=True):
1859 def permissions(self, with_admins=True, with_owner=True):
1861 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1860 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1862 q = q.options(joinedload(UserRepoToPerm.repository),
1861 q = q.options(joinedload(UserRepoToPerm.repository),
1863 joinedload(UserRepoToPerm.user),
1862 joinedload(UserRepoToPerm.user),
1864 joinedload(UserRepoToPerm.permission),)
1863 joinedload(UserRepoToPerm.permission),)
1865
1864
1866 # get owners and admins and permissions. We do a trick of re-writing
1865 # get owners and admins and permissions. We do a trick of re-writing
1867 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1866 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1868 # has a global reference and changing one object propagates to all
1867 # has a global reference and changing one object propagates to all
1869 # others. This means if admin is also an owner admin_row that change
1868 # others. This means if admin is also an owner admin_row that change
1870 # would propagate to both objects
1869 # would propagate to both objects
1871 perm_rows = []
1870 perm_rows = []
1872 for _usr in q.all():
1871 for _usr in q.all():
1873 usr = AttributeDict(_usr.user.get_dict())
1872 usr = AttributeDict(_usr.user.get_dict())
1874 usr.permission = _usr.permission.permission_name
1873 usr.permission = _usr.permission.permission_name
1875 perm_rows.append(usr)
1874 perm_rows.append(usr)
1876
1875
1877 # filter the perm rows by 'default' first and then sort them by
1876 # filter the perm rows by 'default' first and then sort them by
1878 # admin,write,read,none permissions sorted again alphabetically in
1877 # admin,write,read,none permissions sorted again alphabetically in
1879 # each group
1878 # each group
1880 perm_rows = sorted(perm_rows, key=display_user_sort)
1879 perm_rows = sorted(perm_rows, key=display_user_sort)
1881
1880
1882 _admin_perm = 'repository.admin'
1881 _admin_perm = 'repository.admin'
1883 owner_row = []
1882 owner_row = []
1884 if with_owner:
1883 if with_owner:
1885 usr = AttributeDict(self.user.get_dict())
1884 usr = AttributeDict(self.user.get_dict())
1886 usr.owner_row = True
1885 usr.owner_row = True
1887 usr.permission = _admin_perm
1886 usr.permission = _admin_perm
1888 owner_row.append(usr)
1887 owner_row.append(usr)
1889
1888
1890 super_admin_rows = []
1889 super_admin_rows = []
1891 if with_admins:
1890 if with_admins:
1892 for usr in User.get_all_super_admins():
1891 for usr in User.get_all_super_admins():
1893 # if this admin is also owner, don't double the record
1892 # if this admin is also owner, don't double the record
1894 if usr.user_id == owner_row[0].user_id:
1893 if usr.user_id == owner_row[0].user_id:
1895 owner_row[0].admin_row = True
1894 owner_row[0].admin_row = True
1896 else:
1895 else:
1897 usr = AttributeDict(usr.get_dict())
1896 usr = AttributeDict(usr.get_dict())
1898 usr.admin_row = True
1897 usr.admin_row = True
1899 usr.permission = _admin_perm
1898 usr.permission = _admin_perm
1900 super_admin_rows.append(usr)
1899 super_admin_rows.append(usr)
1901
1900
1902 return super_admin_rows + owner_row + perm_rows
1901 return super_admin_rows + owner_row + perm_rows
1903
1902
1904 def permission_user_groups(self):
1903 def permission_user_groups(self):
1905 q = UserGroupRepoToPerm.query().filter(
1904 q = UserGroupRepoToPerm.query().filter(
1906 UserGroupRepoToPerm.repository == self)
1905 UserGroupRepoToPerm.repository == self)
1907 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1906 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1908 joinedload(UserGroupRepoToPerm.users_group),
1907 joinedload(UserGroupRepoToPerm.users_group),
1909 joinedload(UserGroupRepoToPerm.permission),)
1908 joinedload(UserGroupRepoToPerm.permission),)
1910
1909
1911 perm_rows = []
1910 perm_rows = []
1912 for _user_group in q.all():
1911 for _user_group in q.all():
1913 usr = AttributeDict(_user_group.users_group.get_dict())
1912 usr = AttributeDict(_user_group.users_group.get_dict())
1914 usr.permission = _user_group.permission.permission_name
1913 usr.permission = _user_group.permission.permission_name
1915 perm_rows.append(usr)
1914 perm_rows.append(usr)
1916
1915
1917 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1916 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1918 return perm_rows
1917 return perm_rows
1919
1918
1920 def get_api_data(self, include_secrets=False):
1919 def get_api_data(self, include_secrets=False):
1921 """
1920 """
1922 Common function for generating repo api data
1921 Common function for generating repo api data
1923
1922
1924 :param include_secrets: See :meth:`User.get_api_data`.
1923 :param include_secrets: See :meth:`User.get_api_data`.
1925
1924
1926 """
1925 """
1927 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1926 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1928 # move this methods on models level.
1927 # move this methods on models level.
1929 from rhodecode.model.settings import SettingsModel
1928 from rhodecode.model.settings import SettingsModel
1930 from rhodecode.model.repo import RepoModel
1929 from rhodecode.model.repo import RepoModel
1931
1930
1932 repo = self
1931 repo = self
1933 _user_id, _time, _reason = self.locked
1932 _user_id, _time, _reason = self.locked
1934
1933
1935 data = {
1934 data = {
1936 'repo_id': repo.repo_id,
1935 'repo_id': repo.repo_id,
1937 'repo_name': repo.repo_name,
1936 'repo_name': repo.repo_name,
1938 'repo_type': repo.repo_type,
1937 'repo_type': repo.repo_type,
1939 'clone_uri': repo.clone_uri or '',
1938 'clone_uri': repo.clone_uri or '',
1940 'url': RepoModel().get_url(self),
1939 'url': RepoModel().get_url(self),
1941 'private': repo.private,
1940 'private': repo.private,
1942 'created_on': repo.created_on,
1941 'created_on': repo.created_on,
1943 'description': repo.description_safe,
1942 'description': repo.description_safe,
1944 'landing_rev': repo.landing_rev,
1943 'landing_rev': repo.landing_rev,
1945 'owner': repo.user.username,
1944 'owner': repo.user.username,
1946 'fork_of': repo.fork.repo_name if repo.fork else None,
1945 'fork_of': repo.fork.repo_name if repo.fork else None,
1947 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1946 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1948 'enable_statistics': repo.enable_statistics,
1947 'enable_statistics': repo.enable_statistics,
1949 'enable_locking': repo.enable_locking,
1948 'enable_locking': repo.enable_locking,
1950 'enable_downloads': repo.enable_downloads,
1949 'enable_downloads': repo.enable_downloads,
1951 'last_changeset': repo.changeset_cache,
1950 'last_changeset': repo.changeset_cache,
1952 'locked_by': User.get(_user_id).get_api_data(
1951 'locked_by': User.get(_user_id).get_api_data(
1953 include_secrets=include_secrets) if _user_id else None,
1952 include_secrets=include_secrets) if _user_id else None,
1954 'locked_date': time_to_datetime(_time) if _time else None,
1953 'locked_date': time_to_datetime(_time) if _time else None,
1955 'lock_reason': _reason if _reason else None,
1954 'lock_reason': _reason if _reason else None,
1956 }
1955 }
1957
1956
1958 # TODO: mikhail: should be per-repo settings here
1957 # TODO: mikhail: should be per-repo settings here
1959 rc_config = SettingsModel().get_all_settings()
1958 rc_config = SettingsModel().get_all_settings()
1960 repository_fields = str2bool(
1959 repository_fields = str2bool(
1961 rc_config.get('rhodecode_repository_fields'))
1960 rc_config.get('rhodecode_repository_fields'))
1962 if repository_fields:
1961 if repository_fields:
1963 for f in self.extra_fields:
1962 for f in self.extra_fields:
1964 data[f.field_key_prefixed] = f.field_value
1963 data[f.field_key_prefixed] = f.field_value
1965
1964
1966 return data
1965 return data
1967
1966
1968 @classmethod
1967 @classmethod
1969 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1968 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1970 if not lock_time:
1969 if not lock_time:
1971 lock_time = time.time()
1970 lock_time = time.time()
1972 if not lock_reason:
1971 if not lock_reason:
1973 lock_reason = cls.LOCK_AUTOMATIC
1972 lock_reason = cls.LOCK_AUTOMATIC
1974 repo.locked = [user_id, lock_time, lock_reason]
1973 repo.locked = [user_id, lock_time, lock_reason]
1975 Session().add(repo)
1974 Session().add(repo)
1976 Session().commit()
1975 Session().commit()
1977
1976
1978 @classmethod
1977 @classmethod
1979 def unlock(cls, repo):
1978 def unlock(cls, repo):
1980 repo.locked = None
1979 repo.locked = None
1981 Session().add(repo)
1980 Session().add(repo)
1982 Session().commit()
1981 Session().commit()
1983
1982
1984 @classmethod
1983 @classmethod
1985 def getlock(cls, repo):
1984 def getlock(cls, repo):
1986 return repo.locked
1985 return repo.locked
1987
1986
1988 def is_user_lock(self, user_id):
1987 def is_user_lock(self, user_id):
1989 if self.lock[0]:
1988 if self.lock[0]:
1990 lock_user_id = safe_int(self.lock[0])
1989 lock_user_id = safe_int(self.lock[0])
1991 user_id = safe_int(user_id)
1990 user_id = safe_int(user_id)
1992 # both are ints, and they are equal
1991 # both are ints, and they are equal
1993 return all([lock_user_id, user_id]) and lock_user_id == user_id
1992 return all([lock_user_id, user_id]) and lock_user_id == user_id
1994
1993
1995 return False
1994 return False
1996
1995
1997 def get_locking_state(self, action, user_id, only_when_enabled=True):
1996 def get_locking_state(self, action, user_id, only_when_enabled=True):
1998 """
1997 """
1999 Checks locking on this repository, if locking is enabled and lock is
1998 Checks locking on this repository, if locking is enabled and lock is
2000 present returns a tuple of make_lock, locked, locked_by.
1999 present returns a tuple of make_lock, locked, locked_by.
2001 make_lock can have 3 states None (do nothing) True, make lock
2000 make_lock can have 3 states None (do nothing) True, make lock
2002 False release lock, This value is later propagated to hooks, which
2001 False release lock, This value is later propagated to hooks, which
2003 do the locking. Think about this as signals passed to hooks what to do.
2002 do the locking. Think about this as signals passed to hooks what to do.
2004
2003
2005 """
2004 """
2006 # TODO: johbo: This is part of the business logic and should be moved
2005 # TODO: johbo: This is part of the business logic and should be moved
2007 # into the RepositoryModel.
2006 # into the RepositoryModel.
2008
2007
2009 if action not in ('push', 'pull'):
2008 if action not in ('push', 'pull'):
2010 raise ValueError("Invalid action value: %s" % repr(action))
2009 raise ValueError("Invalid action value: %s" % repr(action))
2011
2010
2012 # defines if locked error should be thrown to user
2011 # defines if locked error should be thrown to user
2013 currently_locked = False
2012 currently_locked = False
2014 # defines if new lock should be made, tri-state
2013 # defines if new lock should be made, tri-state
2015 make_lock = None
2014 make_lock = None
2016 repo = self
2015 repo = self
2017 user = User.get(user_id)
2016 user = User.get(user_id)
2018
2017
2019 lock_info = repo.locked
2018 lock_info = repo.locked
2020
2019
2021 if repo and (repo.enable_locking or not only_when_enabled):
2020 if repo and (repo.enable_locking or not only_when_enabled):
2022 if action == 'push':
2021 if action == 'push':
2023 # check if it's already locked !, if it is compare users
2022 # check if it's already locked !, if it is compare users
2024 locked_by_user_id = lock_info[0]
2023 locked_by_user_id = lock_info[0]
2025 if user.user_id == locked_by_user_id:
2024 if user.user_id == locked_by_user_id:
2026 log.debug(
2025 log.debug(
2027 'Got `push` action from user %s, now unlocking', user)
2026 'Got `push` action from user %s, now unlocking', user)
2028 # unlock if we have push from user who locked
2027 # unlock if we have push from user who locked
2029 make_lock = False
2028 make_lock = False
2030 else:
2029 else:
2031 # we're not the same user who locked, ban with
2030 # we're not the same user who locked, ban with
2032 # code defined in settings (default is 423 HTTP Locked) !
2031 # code defined in settings (default is 423 HTTP Locked) !
2033 log.debug('Repo %s is currently locked by %s', repo, user)
2032 log.debug('Repo %s is currently locked by %s', repo, user)
2034 currently_locked = True
2033 currently_locked = True
2035 elif action == 'pull':
2034 elif action == 'pull':
2036 # [0] user [1] date
2035 # [0] user [1] date
2037 if lock_info[0] and lock_info[1]:
2036 if lock_info[0] and lock_info[1]:
2038 log.debug('Repo %s is currently locked by %s', repo, user)
2037 log.debug('Repo %s is currently locked by %s', repo, user)
2039 currently_locked = True
2038 currently_locked = True
2040 else:
2039 else:
2041 log.debug('Setting lock on repo %s by %s', repo, user)
2040 log.debug('Setting lock on repo %s by %s', repo, user)
2042 make_lock = True
2041 make_lock = True
2043
2042
2044 else:
2043 else:
2045 log.debug('Repository %s do not have locking enabled', repo)
2044 log.debug('Repository %s do not have locking enabled', repo)
2046
2045
2047 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2046 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2048 make_lock, currently_locked, lock_info)
2047 make_lock, currently_locked, lock_info)
2049
2048
2050 from rhodecode.lib.auth import HasRepoPermissionAny
2049 from rhodecode.lib.auth import HasRepoPermissionAny
2051 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2050 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2052 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2051 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2053 # if we don't have at least write permission we cannot make a lock
2052 # if we don't have at least write permission we cannot make a lock
2054 log.debug('lock state reset back to FALSE due to lack '
2053 log.debug('lock state reset back to FALSE due to lack '
2055 'of at least read permission')
2054 'of at least read permission')
2056 make_lock = False
2055 make_lock = False
2057
2056
2058 return make_lock, currently_locked, lock_info
2057 return make_lock, currently_locked, lock_info
2059
2058
2060 @property
2059 @property
2061 def last_db_change(self):
2060 def last_db_change(self):
2062 return self.updated_on
2061 return self.updated_on
2063
2062
2064 @property
2063 @property
2065 def clone_uri_hidden(self):
2064 def clone_uri_hidden(self):
2066 clone_uri = self.clone_uri
2065 clone_uri = self.clone_uri
2067 if clone_uri:
2066 if clone_uri:
2068 import urlobject
2067 import urlobject
2069 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2068 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2070 if url_obj.password:
2069 if url_obj.password:
2071 clone_uri = url_obj.with_password('*****')
2070 clone_uri = url_obj.with_password('*****')
2072 return clone_uri
2071 return clone_uri
2073
2072
2074 def clone_url(self, **override):
2073 def clone_url(self, **override):
2075 from rhodecode.model.settings import SettingsModel
2074 from rhodecode.model.settings import SettingsModel
2076
2075
2077 uri_tmpl = None
2076 uri_tmpl = None
2078 if 'with_id' in override:
2077 if 'with_id' in override:
2079 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2078 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2080 del override['with_id']
2079 del override['with_id']
2081
2080
2082 if 'uri_tmpl' in override:
2081 if 'uri_tmpl' in override:
2083 uri_tmpl = override['uri_tmpl']
2082 uri_tmpl = override['uri_tmpl']
2084 del override['uri_tmpl']
2083 del override['uri_tmpl']
2085
2084
2086 # we didn't override our tmpl from **overrides
2085 # we didn't override our tmpl from **overrides
2087 if not uri_tmpl:
2086 if not uri_tmpl:
2088 rc_config = SettingsModel().get_all_settings(cache=True)
2087 rc_config = SettingsModel().get_all_settings(cache=True)
2089 uri_tmpl = rc_config.get(
2088 uri_tmpl = rc_config.get(
2090 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2089 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2091
2090
2092 request = get_current_request()
2091 request = get_current_request()
2093 return get_clone_url(request=request,
2092 return get_clone_url(request=request,
2094 uri_tmpl=uri_tmpl,
2093 uri_tmpl=uri_tmpl,
2095 repo_name=self.repo_name,
2094 repo_name=self.repo_name,
2096 repo_id=self.repo_id, **override)
2095 repo_id=self.repo_id, **override)
2097
2096
2098 def set_state(self, state):
2097 def set_state(self, state):
2099 self.repo_state = state
2098 self.repo_state = state
2100 Session().add(self)
2099 Session().add(self)
2101 #==========================================================================
2100 #==========================================================================
2102 # SCM PROPERTIES
2101 # SCM PROPERTIES
2103 #==========================================================================
2102 #==========================================================================
2104
2103
2105 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2104 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2106 return get_commit_safe(
2105 return get_commit_safe(
2107 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2106 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2108
2107
2109 def get_changeset(self, rev=None, pre_load=None):
2108 def get_changeset(self, rev=None, pre_load=None):
2110 warnings.warn("Use get_commit", DeprecationWarning)
2109 warnings.warn("Use get_commit", DeprecationWarning)
2111 commit_id = None
2110 commit_id = None
2112 commit_idx = None
2111 commit_idx = None
2113 if isinstance(rev, compat.string_types):
2112 if isinstance(rev, str):
2114 commit_id = rev
2113 commit_id = rev
2115 else:
2114 else:
2116 commit_idx = rev
2115 commit_idx = rev
2117 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2116 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2118 pre_load=pre_load)
2117 pre_load=pre_load)
2119
2118
2120 def get_landing_commit(self):
2119 def get_landing_commit(self):
2121 """
2120 """
2122 Returns landing commit, or if that doesn't exist returns the tip
2121 Returns landing commit, or if that doesn't exist returns the tip
2123 """
2122 """
2124 _rev_type, _rev = self.landing_rev
2123 _rev_type, _rev = self.landing_rev
2125 commit = self.get_commit(_rev)
2124 commit = self.get_commit(_rev)
2126 if isinstance(commit, EmptyCommit):
2125 if isinstance(commit, EmptyCommit):
2127 return self.get_commit()
2126 return self.get_commit()
2128 return commit
2127 return commit
2129
2128
2130 def update_commit_cache(self, cs_cache=None, config=None):
2129 def update_commit_cache(self, cs_cache=None, config=None):
2131 """
2130 """
2132 Update cache of last changeset for repository, keys should be::
2131 Update cache of last changeset for repository, keys should be::
2133
2132
2134 short_id
2133 short_id
2135 raw_id
2134 raw_id
2136 revision
2135 revision
2137 parents
2136 parents
2138 message
2137 message
2139 date
2138 date
2140 author
2139 author
2141
2140
2142 :param cs_cache:
2141 :param cs_cache:
2143 """
2142 """
2144 from rhodecode.lib.vcs.backends.base import BaseChangeset
2143 from rhodecode.lib.vcs.backends.base import BaseChangeset
2145 if cs_cache is None:
2144 if cs_cache is None:
2146 # use no-cache version here
2145 # use no-cache version here
2147 scm_repo = self.scm_instance(cache=False, config=config)
2146 scm_repo = self.scm_instance(cache=False, config=config)
2148 if scm_repo:
2147 if scm_repo:
2149 cs_cache = scm_repo.get_commit(
2148 cs_cache = scm_repo.get_commit(
2150 pre_load=["author", "date", "message", "parents"])
2149 pre_load=["author", "date", "message", "parents"])
2151 else:
2150 else:
2152 cs_cache = EmptyCommit()
2151 cs_cache = EmptyCommit()
2153
2152
2154 if isinstance(cs_cache, BaseChangeset):
2153 if isinstance(cs_cache, BaseChangeset):
2155 cs_cache = cs_cache.__json__()
2154 cs_cache = cs_cache.__json__()
2156
2155
2157 def is_outdated(new_cs_cache):
2156 def is_outdated(new_cs_cache):
2158 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2157 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2159 new_cs_cache['revision'] != self.changeset_cache['revision']):
2158 new_cs_cache['revision'] != self.changeset_cache['revision']):
2160 return True
2159 return True
2161 return False
2160 return False
2162
2161
2163 # check if we have maybe already latest cached revision
2162 # check if we have maybe already latest cached revision
2164 if is_outdated(cs_cache) or not self.changeset_cache:
2163 if is_outdated(cs_cache) or not self.changeset_cache:
2165 _default = datetime.datetime.fromtimestamp(0)
2164 _default = datetime.datetime.fromtimestamp(0)
2166 last_change = cs_cache.get('date') or _default
2165 last_change = cs_cache.get('date') or _default
2167 log.debug('updated repo %s with new commit cache %s',
2166 log.debug('updated repo %s with new commit cache %s',
2168 self.repo_name, cs_cache)
2167 self.repo_name, cs_cache)
2169 self.updated_on = last_change
2168 self.updated_on = last_change
2170 self.changeset_cache = cs_cache
2169 self.changeset_cache = cs_cache
2171 Session().add(self)
2170 Session().add(self)
2172 Session().commit()
2171 Session().commit()
2173 else:
2172 else:
2174 log.debug('Skipping update_commit_cache for repo:`%s` '
2173 log.debug('Skipping update_commit_cache for repo:`%s` '
2175 'commit already with latest changes', self.repo_name)
2174 'commit already with latest changes', self.repo_name)
2176
2175
2177 @property
2176 @property
2178 def tip(self):
2177 def tip(self):
2179 return self.get_commit('tip')
2178 return self.get_commit('tip')
2180
2179
2181 @property
2180 @property
2182 def author(self):
2181 def author(self):
2183 return self.tip.author
2182 return self.tip.author
2184
2183
2185 @property
2184 @property
2186 def last_change(self):
2185 def last_change(self):
2187 return self.scm_instance().last_change
2186 return self.scm_instance().last_change
2188
2187
2189 def get_comments(self, revisions=None):
2188 def get_comments(self, revisions=None):
2190 """
2189 """
2191 Returns comments for this repository grouped by revisions
2190 Returns comments for this repository grouped by revisions
2192
2191
2193 :param revisions: filter query by revisions only
2192 :param revisions: filter query by revisions only
2194 """
2193 """
2195 cmts = ChangesetComment.query()\
2194 cmts = ChangesetComment.query()\
2196 .filter(ChangesetComment.repo == self)
2195 .filter(ChangesetComment.repo == self)
2197 if revisions:
2196 if revisions:
2198 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2197 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2199 grouped = collections.defaultdict(list)
2198 grouped = collections.defaultdict(list)
2200 for cmt in cmts.all():
2199 for cmt in cmts.all():
2201 grouped[cmt.revision].append(cmt)
2200 grouped[cmt.revision].append(cmt)
2202 return grouped
2201 return grouped
2203
2202
2204 def statuses(self, revisions=None):
2203 def statuses(self, revisions=None):
2205 """
2204 """
2206 Returns statuses for this repository
2205 Returns statuses for this repository
2207
2206
2208 :param revisions: list of revisions to get statuses for
2207 :param revisions: list of revisions to get statuses for
2209 """
2208 """
2210 statuses = ChangesetStatus.query()\
2209 statuses = ChangesetStatus.query()\
2211 .filter(ChangesetStatus.repo == self)\
2210 .filter(ChangesetStatus.repo == self)\
2212 .filter(ChangesetStatus.version == 0)
2211 .filter(ChangesetStatus.version == 0)
2213
2212
2214 if revisions:
2213 if revisions:
2215 # Try doing the filtering in chunks to avoid hitting limits
2214 # Try doing the filtering in chunks to avoid hitting limits
2216 size = 500
2215 size = 500
2217 status_results = []
2216 status_results = []
2218 for chunk in range(0, len(revisions), size):
2217 for chunk in range(0, len(revisions), size):
2219 status_results += statuses.filter(
2218 status_results += statuses.filter(
2220 ChangesetStatus.revision.in_(
2219 ChangesetStatus.revision.in_(
2221 revisions[chunk: chunk+size])
2220 revisions[chunk: chunk+size])
2222 ).all()
2221 ).all()
2223 else:
2222 else:
2224 status_results = statuses.all()
2223 status_results = statuses.all()
2225
2224
2226 grouped = {}
2225 grouped = {}
2227
2226
2228 # maybe we have open new pullrequest without a status?
2227 # maybe we have open new pullrequest without a status?
2229 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2228 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2230 status_lbl = ChangesetStatus.get_status_lbl(stat)
2229 status_lbl = ChangesetStatus.get_status_lbl(stat)
2231 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2230 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2232 for rev in pr.revisions:
2231 for rev in pr.revisions:
2233 pr_id = pr.pull_request_id
2232 pr_id = pr.pull_request_id
2234 pr_repo = pr.target_repo.repo_name
2233 pr_repo = pr.target_repo.repo_name
2235 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2234 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2236
2235
2237 for stat in status_results:
2236 for stat in status_results:
2238 pr_id = pr_repo = None
2237 pr_id = pr_repo = None
2239 if stat.pull_request:
2238 if stat.pull_request:
2240 pr_id = stat.pull_request.pull_request_id
2239 pr_id = stat.pull_request.pull_request_id
2241 pr_repo = stat.pull_request.target_repo.repo_name
2240 pr_repo = stat.pull_request.target_repo.repo_name
2242 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2241 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2243 pr_id, pr_repo]
2242 pr_id, pr_repo]
2244 return grouped
2243 return grouped
2245
2244
2246 # ==========================================================================
2245 # ==========================================================================
2247 # SCM CACHE INSTANCE
2246 # SCM CACHE INSTANCE
2248 # ==========================================================================
2247 # ==========================================================================
2249
2248
2250 def scm_instance(self, **kwargs):
2249 def scm_instance(self, **kwargs):
2251 import rhodecode
2250 import rhodecode
2252
2251
2253 # Passing a config will not hit the cache currently only used
2252 # Passing a config will not hit the cache currently only used
2254 # for repo2dbmapper
2253 # for repo2dbmapper
2255 config = kwargs.pop('config', None)
2254 config = kwargs.pop('config', None)
2256 cache = kwargs.pop('cache', None)
2255 cache = kwargs.pop('cache', None)
2257 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2256 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2258 # if cache is NOT defined use default global, else we have a full
2257 # if cache is NOT defined use default global, else we have a full
2259 # control over cache behaviour
2258 # control over cache behaviour
2260 if cache is None and full_cache and not config:
2259 if cache is None and full_cache and not config:
2261 return self._get_instance_cached()
2260 return self._get_instance_cached()
2262 return self._get_instance(cache=bool(cache), config=config)
2261 return self._get_instance(cache=bool(cache), config=config)
2263
2262
2264 def _get_instance_cached(self):
2263 def _get_instance_cached(self):
2265 self._get_instance()
2264 self._get_instance()
2266
2265
2267 def _get_instance(self, cache=True, config=None):
2266 def _get_instance(self, cache=True, config=None):
2268 config = config or self._config
2267 config = config or self._config
2269 custom_wire = {
2268 custom_wire = {
2270 'cache': cache # controls the vcs.remote cache
2269 'cache': cache # controls the vcs.remote cache
2271 }
2270 }
2272 repo = get_vcs_instance(
2271 repo = get_vcs_instance(
2273 repo_path=safe_str(self.repo_full_path),
2272 repo_path=safe_str(self.repo_full_path),
2274 config=config,
2273 config=config,
2275 with_wire=custom_wire,
2274 with_wire=custom_wire,
2276 create=False,
2275 create=False,
2277 _vcs_alias=self.repo_type)
2276 _vcs_alias=self.repo_type)
2278
2277
2279 return repo
2278 return repo
2280
2279
2281 def __json__(self):
2280 def __json__(self):
2282 return {'landing_rev': self.landing_rev}
2281 return {'landing_rev': self.landing_rev}
2283
2282
2284 def get_dict(self):
2283 def get_dict(self):
2285
2284
2286 # Since we transformed `repo_name` to a hybrid property, we need to
2285 # Since we transformed `repo_name` to a hybrid property, we need to
2287 # keep compatibility with the code which uses `repo_name` field.
2286 # keep compatibility with the code which uses `repo_name` field.
2288
2287
2289 result = super(Repository, self).get_dict()
2288 result = super(Repository, self).get_dict()
2290 result['repo_name'] = result.pop('_repo_name', None)
2289 result['repo_name'] = result.pop('_repo_name', None)
2291 return result
2290 return result
2292
2291
2293
2292
2294 class RepoGroup(Base, BaseModel):
2293 class RepoGroup(Base, BaseModel):
2295 __tablename__ = 'groups'
2294 __tablename__ = 'groups'
2296 __table_args__ = (
2295 __table_args__ = (
2297 UniqueConstraint('group_name', 'group_parent_id'),
2296 UniqueConstraint('group_name', 'group_parent_id'),
2298 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2297 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2299 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2298 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2300 )
2299 )
2301 __mapper_args__ = {'order_by': 'group_name'}
2300 __mapper_args__ = {'order_by': 'group_name'}
2302
2301
2303 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2302 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2304
2303
2305 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2304 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2306 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2305 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2307 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2306 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2308 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2307 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2309 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2308 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2310 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2309 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2311 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2310 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2312 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2311 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2313 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2312 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2314
2313
2315 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2314 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2316 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2315 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2317 parent_group = relationship('RepoGroup', remote_side=group_id)
2316 parent_group = relationship('RepoGroup', remote_side=group_id)
2318 user = relationship('User')
2317 user = relationship('User')
2319 integrations = relationship('Integration',
2318 integrations = relationship('Integration',
2320 cascade="all, delete, delete-orphan")
2319 cascade="all, delete, delete-orphan")
2321
2320
2322 def __init__(self, group_name='', parent_group=None):
2321 def __init__(self, group_name='', parent_group=None):
2323 self.group_name = group_name
2322 self.group_name = group_name
2324 self.parent_group = parent_group
2323 self.parent_group = parent_group
2325
2324
2326 def __unicode__(self):
2325 def __unicode__(self):
2327 return u"<%s('id:%s:%s')>" % (
2326 return u"<%s('id:%s:%s')>" % (
2328 self.__class__.__name__, self.group_id, self.group_name)
2327 self.__class__.__name__, self.group_id, self.group_name)
2329
2328
2330 @hybrid_property
2329 @hybrid_property
2331 def description_safe(self):
2330 def description_safe(self):
2332 from rhodecode.lib import helpers as h
2331 from rhodecode.lib import helpers as h
2333 return h.escape(self.group_description)
2332 return h.escape(self.group_description)
2334
2333
2335 @classmethod
2334 @classmethod
2336 def _generate_choice(cls, repo_group):
2335 def _generate_choice(cls, repo_group):
2337 from webhelpers2.html import literal as _literal
2336 from webhelpers2.html import literal as _literal
2338 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2337 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2339 return repo_group.group_id, _name(repo_group.full_path_splitted)
2338 return repo_group.group_id, _name(repo_group.full_path_splitted)
2340
2339
2341 @classmethod
2340 @classmethod
2342 def groups_choices(cls, groups=None, show_empty_group=True):
2341 def groups_choices(cls, groups=None, show_empty_group=True):
2343 if not groups:
2342 if not groups:
2344 groups = cls.query().all()
2343 groups = cls.query().all()
2345
2344
2346 repo_groups = []
2345 repo_groups = []
2347 if show_empty_group:
2346 if show_empty_group:
2348 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2347 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2349
2348
2350 repo_groups.extend([cls._generate_choice(x) for x in groups])
2349 repo_groups.extend([cls._generate_choice(x) for x in groups])
2351
2350
2352 repo_groups = sorted(
2351 repo_groups = sorted(
2353 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2352 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2354 return repo_groups
2353 return repo_groups
2355
2354
2356 @classmethod
2355 @classmethod
2357 def url_sep(cls):
2356 def url_sep(cls):
2358 return URL_SEP
2357 return URL_SEP
2359
2358
2360 @classmethod
2359 @classmethod
2361 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2360 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2362 if case_insensitive:
2361 if case_insensitive:
2363 gr = cls.query().filter(func.lower(cls.group_name)
2362 gr = cls.query().filter(func.lower(cls.group_name)
2364 == func.lower(group_name))
2363 == func.lower(group_name))
2365 else:
2364 else:
2366 gr = cls.query().filter(cls.group_name == group_name)
2365 gr = cls.query().filter(cls.group_name == group_name)
2367 if cache:
2366 if cache:
2368 name_key = _hash_key(group_name)
2367 name_key = _hash_key(group_name)
2369 gr = gr.options(
2368 gr = gr.options(
2370 FromCache("sql_cache_short", "get_group_%s" % name_key))
2369 FromCache("sql_cache_short", "get_group_%s" % name_key))
2371 return gr.scalar()
2370 return gr.scalar()
2372
2371
2373 @classmethod
2372 @classmethod
2374 def get_user_personal_repo_group(cls, user_id):
2373 def get_user_personal_repo_group(cls, user_id):
2375 user = User.get(user_id)
2374 user = User.get(user_id)
2376 if user.username == User.DEFAULT_USER:
2375 if user.username == User.DEFAULT_USER:
2377 return None
2376 return None
2378
2377
2379 return cls.query()\
2378 return cls.query()\
2380 .filter(cls.personal == true()) \
2379 .filter(cls.personal == true()) \
2381 .filter(cls.user == user).scalar()
2380 .filter(cls.user == user).scalar()
2382
2381
2383 @classmethod
2382 @classmethod
2384 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2383 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2385 case_insensitive=True):
2384 case_insensitive=True):
2386 q = RepoGroup.query()
2385 q = RepoGroup.query()
2387
2386
2388 if not isinstance(user_id, Optional):
2387 if not isinstance(user_id, Optional):
2389 q = q.filter(RepoGroup.user_id == user_id)
2388 q = q.filter(RepoGroup.user_id == user_id)
2390
2389
2391 if not isinstance(group_id, Optional):
2390 if not isinstance(group_id, Optional):
2392 q = q.filter(RepoGroup.group_parent_id == group_id)
2391 q = q.filter(RepoGroup.group_parent_id == group_id)
2393
2392
2394 if case_insensitive:
2393 if case_insensitive:
2395 q = q.order_by(func.lower(RepoGroup.group_name))
2394 q = q.order_by(func.lower(RepoGroup.group_name))
2396 else:
2395 else:
2397 q = q.order_by(RepoGroup.group_name)
2396 q = q.order_by(RepoGroup.group_name)
2398 return q.all()
2397 return q.all()
2399
2398
2400 @property
2399 @property
2401 def parents(self):
2400 def parents(self):
2402 parents_recursion_limit = 10
2401 parents_recursion_limit = 10
2403 groups = []
2402 groups = []
2404 if self.parent_group is None:
2403 if self.parent_group is None:
2405 return groups
2404 return groups
2406 cur_gr = self.parent_group
2405 cur_gr = self.parent_group
2407 groups.insert(0, cur_gr)
2406 groups.insert(0, cur_gr)
2408 cnt = 0
2407 cnt = 0
2409 while 1:
2408 while 1:
2410 cnt += 1
2409 cnt += 1
2411 gr = getattr(cur_gr, 'parent_group', None)
2410 gr = getattr(cur_gr, 'parent_group', None)
2412 cur_gr = cur_gr.parent_group
2411 cur_gr = cur_gr.parent_group
2413 if gr is None:
2412 if gr is None:
2414 break
2413 break
2415 if cnt == parents_recursion_limit:
2414 if cnt == parents_recursion_limit:
2416 # this will prevent accidental infinit loops
2415 # this will prevent accidental infinit loops
2417 log.error('more than %s parents found for group %s, stopping '
2416 log.error('more than %s parents found for group %s, stopping '
2418 'recursive parent fetching', parents_recursion_limit, self)
2417 'recursive parent fetching', parents_recursion_limit, self)
2419 break
2418 break
2420
2419
2421 groups.insert(0, gr)
2420 groups.insert(0, gr)
2422 return groups
2421 return groups
2423
2422
2424 @property
2423 @property
2425 def last_db_change(self):
2424 def last_db_change(self):
2426 return self.updated_on
2425 return self.updated_on
2427
2426
2428 @property
2427 @property
2429 def children(self):
2428 def children(self):
2430 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2429 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2431
2430
2432 @property
2431 @property
2433 def name(self):
2432 def name(self):
2434 return self.group_name.split(RepoGroup.url_sep())[-1]
2433 return self.group_name.split(RepoGroup.url_sep())[-1]
2435
2434
2436 @property
2435 @property
2437 def full_path(self):
2436 def full_path(self):
2438 return self.group_name
2437 return self.group_name
2439
2438
2440 @property
2439 @property
2441 def full_path_splitted(self):
2440 def full_path_splitted(self):
2442 return self.group_name.split(RepoGroup.url_sep())
2441 return self.group_name.split(RepoGroup.url_sep())
2443
2442
2444 @property
2443 @property
2445 def repositories(self):
2444 def repositories(self):
2446 return Repository.query()\
2445 return Repository.query()\
2447 .filter(Repository.group == self)\
2446 .filter(Repository.group == self)\
2448 .order_by(Repository.repo_name)
2447 .order_by(Repository.repo_name)
2449
2448
2450 @property
2449 @property
2451 def repositories_recursive_count(self):
2450 def repositories_recursive_count(self):
2452 cnt = self.repositories.count()
2451 cnt = self.repositories.count()
2453
2452
2454 def children_count(group):
2453 def children_count(group):
2455 cnt = 0
2454 cnt = 0
2456 for child in group.children:
2455 for child in group.children:
2457 cnt += child.repositories.count()
2456 cnt += child.repositories.count()
2458 cnt += children_count(child)
2457 cnt += children_count(child)
2459 return cnt
2458 return cnt
2460
2459
2461 return cnt + children_count(self)
2460 return cnt + children_count(self)
2462
2461
2463 def _recursive_objects(self, include_repos=True):
2462 def _recursive_objects(self, include_repos=True):
2464 all_ = []
2463 all_ = []
2465
2464
2466 def _get_members(root_gr):
2465 def _get_members(root_gr):
2467 if include_repos:
2466 if include_repos:
2468 for r in root_gr.repositories:
2467 for r in root_gr.repositories:
2469 all_.append(r)
2468 all_.append(r)
2470 childs = root_gr.children.all()
2469 childs = root_gr.children.all()
2471 if childs:
2470 if childs:
2472 for gr in childs:
2471 for gr in childs:
2473 all_.append(gr)
2472 all_.append(gr)
2474 _get_members(gr)
2473 _get_members(gr)
2475
2474
2476 _get_members(self)
2475 _get_members(self)
2477 return [self] + all_
2476 return [self] + all_
2478
2477
2479 def recursive_groups_and_repos(self):
2478 def recursive_groups_and_repos(self):
2480 """
2479 """
2481 Recursive return all groups, with repositories in those groups
2480 Recursive return all groups, with repositories in those groups
2482 """
2481 """
2483 return self._recursive_objects()
2482 return self._recursive_objects()
2484
2483
2485 def recursive_groups(self):
2484 def recursive_groups(self):
2486 """
2485 """
2487 Returns all children groups for this group including children of children
2486 Returns all children groups for this group including children of children
2488 """
2487 """
2489 return self._recursive_objects(include_repos=False)
2488 return self._recursive_objects(include_repos=False)
2490
2489
2491 def get_new_name(self, group_name):
2490 def get_new_name(self, group_name):
2492 """
2491 """
2493 returns new full group name based on parent and new name
2492 returns new full group name based on parent and new name
2494
2493
2495 :param group_name:
2494 :param group_name:
2496 """
2495 """
2497 path_prefix = (self.parent_group.full_path_splitted if
2496 path_prefix = (self.parent_group.full_path_splitted if
2498 self.parent_group else [])
2497 self.parent_group else [])
2499 return RepoGroup.url_sep().join(path_prefix + [group_name])
2498 return RepoGroup.url_sep().join(path_prefix + [group_name])
2500
2499
2501 def permissions(self, with_admins=True, with_owner=True):
2500 def permissions(self, with_admins=True, with_owner=True):
2502 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2501 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2503 q = q.options(joinedload(UserRepoGroupToPerm.group),
2502 q = q.options(joinedload(UserRepoGroupToPerm.group),
2504 joinedload(UserRepoGroupToPerm.user),
2503 joinedload(UserRepoGroupToPerm.user),
2505 joinedload(UserRepoGroupToPerm.permission),)
2504 joinedload(UserRepoGroupToPerm.permission),)
2506
2505
2507 # get owners and admins and permissions. We do a trick of re-writing
2506 # get owners and admins and permissions. We do a trick of re-writing
2508 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2507 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2509 # has a global reference and changing one object propagates to all
2508 # has a global reference and changing one object propagates to all
2510 # others. This means if admin is also an owner admin_row that change
2509 # others. This means if admin is also an owner admin_row that change
2511 # would propagate to both objects
2510 # would propagate to both objects
2512 perm_rows = []
2511 perm_rows = []
2513 for _usr in q.all():
2512 for _usr in q.all():
2514 usr = AttributeDict(_usr.user.get_dict())
2513 usr = AttributeDict(_usr.user.get_dict())
2515 usr.permission = _usr.permission.permission_name
2514 usr.permission = _usr.permission.permission_name
2516 perm_rows.append(usr)
2515 perm_rows.append(usr)
2517
2516
2518 # filter the perm rows by 'default' first and then sort them by
2517 # filter the perm rows by 'default' first and then sort them by
2519 # admin,write,read,none permissions sorted again alphabetically in
2518 # admin,write,read,none permissions sorted again alphabetically in
2520 # each group
2519 # each group
2521 perm_rows = sorted(perm_rows, key=display_user_sort)
2520 perm_rows = sorted(perm_rows, key=display_user_sort)
2522
2521
2523 _admin_perm = 'group.admin'
2522 _admin_perm = 'group.admin'
2524 owner_row = []
2523 owner_row = []
2525 if with_owner:
2524 if with_owner:
2526 usr = AttributeDict(self.user.get_dict())
2525 usr = AttributeDict(self.user.get_dict())
2527 usr.owner_row = True
2526 usr.owner_row = True
2528 usr.permission = _admin_perm
2527 usr.permission = _admin_perm
2529 owner_row.append(usr)
2528 owner_row.append(usr)
2530
2529
2531 super_admin_rows = []
2530 super_admin_rows = []
2532 if with_admins:
2531 if with_admins:
2533 for usr in User.get_all_super_admins():
2532 for usr in User.get_all_super_admins():
2534 # if this admin is also owner, don't double the record
2533 # if this admin is also owner, don't double the record
2535 if usr.user_id == owner_row[0].user_id:
2534 if usr.user_id == owner_row[0].user_id:
2536 owner_row[0].admin_row = True
2535 owner_row[0].admin_row = True
2537 else:
2536 else:
2538 usr = AttributeDict(usr.get_dict())
2537 usr = AttributeDict(usr.get_dict())
2539 usr.admin_row = True
2538 usr.admin_row = True
2540 usr.permission = _admin_perm
2539 usr.permission = _admin_perm
2541 super_admin_rows.append(usr)
2540 super_admin_rows.append(usr)
2542
2541
2543 return super_admin_rows + owner_row + perm_rows
2542 return super_admin_rows + owner_row + perm_rows
2544
2543
2545 def permission_user_groups(self):
2544 def permission_user_groups(self):
2546 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2545 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2547 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2546 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2548 joinedload(UserGroupRepoGroupToPerm.users_group),
2547 joinedload(UserGroupRepoGroupToPerm.users_group),
2549 joinedload(UserGroupRepoGroupToPerm.permission),)
2548 joinedload(UserGroupRepoGroupToPerm.permission),)
2550
2549
2551 perm_rows = []
2550 perm_rows = []
2552 for _user_group in q.all():
2551 for _user_group in q.all():
2553 usr = AttributeDict(_user_group.users_group.get_dict())
2552 usr = AttributeDict(_user_group.users_group.get_dict())
2554 usr.permission = _user_group.permission.permission_name
2553 usr.permission = _user_group.permission.permission_name
2555 perm_rows.append(usr)
2554 perm_rows.append(usr)
2556
2555
2557 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2556 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2558 return perm_rows
2557 return perm_rows
2559
2558
2560 def get_api_data(self):
2559 def get_api_data(self):
2561 """
2560 """
2562 Common function for generating api data
2561 Common function for generating api data
2563
2562
2564 """
2563 """
2565 group = self
2564 group = self
2566 data = {
2565 data = {
2567 'group_id': group.group_id,
2566 'group_id': group.group_id,
2568 'group_name': group.group_name,
2567 'group_name': group.group_name,
2569 'group_description': group.description_safe,
2568 'group_description': group.description_safe,
2570 'parent_group': group.parent_group.group_name if group.parent_group else None,
2569 'parent_group': group.parent_group.group_name if group.parent_group else None,
2571 'repositories': [x.repo_name for x in group.repositories],
2570 'repositories': [x.repo_name for x in group.repositories],
2572 'owner': group.user.username,
2571 'owner': group.user.username,
2573 }
2572 }
2574 return data
2573 return data
2575
2574
2576
2575
2577 class Permission(Base, BaseModel):
2576 class Permission(Base, BaseModel):
2578 __tablename__ = 'permissions'
2577 __tablename__ = 'permissions'
2579 __table_args__ = (
2578 __table_args__ = (
2580 Index('p_perm_name_idx', 'permission_name'),
2579 Index('p_perm_name_idx', 'permission_name'),
2581 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2582 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2581 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2583 )
2582 )
2584 PERMS = [
2583 PERMS = [
2585 ('hg.admin', _('RhodeCode Super Administrator')),
2584 ('hg.admin', _('RhodeCode Super Administrator')),
2586
2585
2587 ('repository.none', _('Repository no access')),
2586 ('repository.none', _('Repository no access')),
2588 ('repository.read', _('Repository read access')),
2587 ('repository.read', _('Repository read access')),
2589 ('repository.write', _('Repository write access')),
2588 ('repository.write', _('Repository write access')),
2590 ('repository.admin', _('Repository admin access')),
2589 ('repository.admin', _('Repository admin access')),
2591
2590
2592 ('group.none', _('Repository group no access')),
2591 ('group.none', _('Repository group no access')),
2593 ('group.read', _('Repository group read access')),
2592 ('group.read', _('Repository group read access')),
2594 ('group.write', _('Repository group write access')),
2593 ('group.write', _('Repository group write access')),
2595 ('group.admin', _('Repository group admin access')),
2594 ('group.admin', _('Repository group admin access')),
2596
2595
2597 ('usergroup.none', _('User group no access')),
2596 ('usergroup.none', _('User group no access')),
2598 ('usergroup.read', _('User group read access')),
2597 ('usergroup.read', _('User group read access')),
2599 ('usergroup.write', _('User group write access')),
2598 ('usergroup.write', _('User group write access')),
2600 ('usergroup.admin', _('User group admin access')),
2599 ('usergroup.admin', _('User group admin access')),
2601
2600
2602 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2601 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2603 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2602 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2604
2603
2605 ('hg.usergroup.create.false', _('User Group creation disabled')),
2604 ('hg.usergroup.create.false', _('User Group creation disabled')),
2606 ('hg.usergroup.create.true', _('User Group creation enabled')),
2605 ('hg.usergroup.create.true', _('User Group creation enabled')),
2607
2606
2608 ('hg.create.none', _('Repository creation disabled')),
2607 ('hg.create.none', _('Repository creation disabled')),
2609 ('hg.create.repository', _('Repository creation enabled')),
2608 ('hg.create.repository', _('Repository creation enabled')),
2610 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2609 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2611 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2610 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2612
2611
2613 ('hg.fork.none', _('Repository forking disabled')),
2612 ('hg.fork.none', _('Repository forking disabled')),
2614 ('hg.fork.repository', _('Repository forking enabled')),
2613 ('hg.fork.repository', _('Repository forking enabled')),
2615
2614
2616 ('hg.register.none', _('Registration disabled')),
2615 ('hg.register.none', _('Registration disabled')),
2617 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2616 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2618 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2617 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2619
2618
2620 ('hg.password_reset.enabled', _('Password reset enabled')),
2619 ('hg.password_reset.enabled', _('Password reset enabled')),
2621 ('hg.password_reset.hidden', _('Password reset hidden')),
2620 ('hg.password_reset.hidden', _('Password reset hidden')),
2622 ('hg.password_reset.disabled', _('Password reset disabled')),
2621 ('hg.password_reset.disabled', _('Password reset disabled')),
2623
2622
2624 ('hg.extern_activate.manual', _('Manual activation of external account')),
2623 ('hg.extern_activate.manual', _('Manual activation of external account')),
2625 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2624 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2626
2625
2627 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2626 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2628 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2627 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2629 ]
2628 ]
2630
2629
2631 # definition of system default permissions for DEFAULT user
2630 # definition of system default permissions for DEFAULT user
2632 DEFAULT_USER_PERMISSIONS = [
2631 DEFAULT_USER_PERMISSIONS = [
2633 'repository.read',
2632 'repository.read',
2634 'group.read',
2633 'group.read',
2635 'usergroup.read',
2634 'usergroup.read',
2636 'hg.create.repository',
2635 'hg.create.repository',
2637 'hg.repogroup.create.false',
2636 'hg.repogroup.create.false',
2638 'hg.usergroup.create.false',
2637 'hg.usergroup.create.false',
2639 'hg.create.write_on_repogroup.true',
2638 'hg.create.write_on_repogroup.true',
2640 'hg.fork.repository',
2639 'hg.fork.repository',
2641 'hg.register.manual_activate',
2640 'hg.register.manual_activate',
2642 'hg.password_reset.enabled',
2641 'hg.password_reset.enabled',
2643 'hg.extern_activate.auto',
2642 'hg.extern_activate.auto',
2644 'hg.inherit_default_perms.true',
2643 'hg.inherit_default_perms.true',
2645 ]
2644 ]
2646
2645
2647 # defines which permissions are more important higher the more important
2646 # defines which permissions are more important higher the more important
2648 # Weight defines which permissions are more important.
2647 # Weight defines which permissions are more important.
2649 # The higher number the more important.
2648 # The higher number the more important.
2650 PERM_WEIGHTS = {
2649 PERM_WEIGHTS = {
2651 'repository.none': 0,
2650 'repository.none': 0,
2652 'repository.read': 1,
2651 'repository.read': 1,
2653 'repository.write': 3,
2652 'repository.write': 3,
2654 'repository.admin': 4,
2653 'repository.admin': 4,
2655
2654
2656 'group.none': 0,
2655 'group.none': 0,
2657 'group.read': 1,
2656 'group.read': 1,
2658 'group.write': 3,
2657 'group.write': 3,
2659 'group.admin': 4,
2658 'group.admin': 4,
2660
2659
2661 'usergroup.none': 0,
2660 'usergroup.none': 0,
2662 'usergroup.read': 1,
2661 'usergroup.read': 1,
2663 'usergroup.write': 3,
2662 'usergroup.write': 3,
2664 'usergroup.admin': 4,
2663 'usergroup.admin': 4,
2665
2664
2666 'hg.repogroup.create.false': 0,
2665 'hg.repogroup.create.false': 0,
2667 'hg.repogroup.create.true': 1,
2666 'hg.repogroup.create.true': 1,
2668
2667
2669 'hg.usergroup.create.false': 0,
2668 'hg.usergroup.create.false': 0,
2670 'hg.usergroup.create.true': 1,
2669 'hg.usergroup.create.true': 1,
2671
2670
2672 'hg.fork.none': 0,
2671 'hg.fork.none': 0,
2673 'hg.fork.repository': 1,
2672 'hg.fork.repository': 1,
2674 'hg.create.none': 0,
2673 'hg.create.none': 0,
2675 'hg.create.repository': 1
2674 'hg.create.repository': 1
2676 }
2675 }
2677
2676
2678 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2677 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2679 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2678 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2680 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2679 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2681
2680
2682 def __unicode__(self):
2681 def __unicode__(self):
2683 return u"<%s('%s:%s')>" % (
2682 return u"<%s('%s:%s')>" % (
2684 self.__class__.__name__, self.permission_id, self.permission_name
2683 self.__class__.__name__, self.permission_id, self.permission_name
2685 )
2684 )
2686
2685
2687 @classmethod
2686 @classmethod
2688 def get_by_key(cls, key):
2687 def get_by_key(cls, key):
2689 return cls.query().filter(cls.permission_name == key).scalar()
2688 return cls.query().filter(cls.permission_name == key).scalar()
2690
2689
2691 @classmethod
2690 @classmethod
2692 def get_default_repo_perms(cls, user_id, repo_id=None):
2691 def get_default_repo_perms(cls, user_id, repo_id=None):
2693 q = Session().query(UserRepoToPerm, Repository, Permission)\
2692 q = Session().query(UserRepoToPerm, Repository, Permission)\
2694 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2693 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2695 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2694 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2696 .filter(UserRepoToPerm.user_id == user_id)
2695 .filter(UserRepoToPerm.user_id == user_id)
2697 if repo_id:
2696 if repo_id:
2698 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2697 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2699 return q.all()
2698 return q.all()
2700
2699
2701 @classmethod
2700 @classmethod
2702 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2701 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2703 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2702 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2704 .join(
2703 .join(
2705 Permission,
2704 Permission,
2706 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2705 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2707 .join(
2706 .join(
2708 Repository,
2707 Repository,
2709 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2708 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2710 .join(
2709 .join(
2711 UserGroup,
2710 UserGroup,
2712 UserGroupRepoToPerm.users_group_id ==
2711 UserGroupRepoToPerm.users_group_id ==
2713 UserGroup.users_group_id)\
2712 UserGroup.users_group_id)\
2714 .join(
2713 .join(
2715 UserGroupMember,
2714 UserGroupMember,
2716 UserGroupRepoToPerm.users_group_id ==
2715 UserGroupRepoToPerm.users_group_id ==
2717 UserGroupMember.users_group_id)\
2716 UserGroupMember.users_group_id)\
2718 .filter(
2717 .filter(
2719 UserGroupMember.user_id == user_id,
2718 UserGroupMember.user_id == user_id,
2720 UserGroup.users_group_active == true())
2719 UserGroup.users_group_active == true())
2721 if repo_id:
2720 if repo_id:
2722 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2721 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2723 return q.all()
2722 return q.all()
2724
2723
2725 @classmethod
2724 @classmethod
2726 def get_default_group_perms(cls, user_id, repo_group_id=None):
2725 def get_default_group_perms(cls, user_id, repo_group_id=None):
2727 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2726 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2728 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2727 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2729 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2728 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2730 .filter(UserRepoGroupToPerm.user_id == user_id)
2729 .filter(UserRepoGroupToPerm.user_id == user_id)
2731 if repo_group_id:
2730 if repo_group_id:
2732 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2731 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2733 return q.all()
2732 return q.all()
2734
2733
2735 @classmethod
2734 @classmethod
2736 def get_default_group_perms_from_user_group(
2735 def get_default_group_perms_from_user_group(
2737 cls, user_id, repo_group_id=None):
2736 cls, user_id, repo_group_id=None):
2738 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2737 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2739 .join(
2738 .join(
2740 Permission,
2739 Permission,
2741 UserGroupRepoGroupToPerm.permission_id ==
2740 UserGroupRepoGroupToPerm.permission_id ==
2742 Permission.permission_id)\
2741 Permission.permission_id)\
2743 .join(
2742 .join(
2744 RepoGroup,
2743 RepoGroup,
2745 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2744 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2746 .join(
2745 .join(
2747 UserGroup,
2746 UserGroup,
2748 UserGroupRepoGroupToPerm.users_group_id ==
2747 UserGroupRepoGroupToPerm.users_group_id ==
2749 UserGroup.users_group_id)\
2748 UserGroup.users_group_id)\
2750 .join(
2749 .join(
2751 UserGroupMember,
2750 UserGroupMember,
2752 UserGroupRepoGroupToPerm.users_group_id ==
2751 UserGroupRepoGroupToPerm.users_group_id ==
2753 UserGroupMember.users_group_id)\
2752 UserGroupMember.users_group_id)\
2754 .filter(
2753 .filter(
2755 UserGroupMember.user_id == user_id,
2754 UserGroupMember.user_id == user_id,
2756 UserGroup.users_group_active == true())
2755 UserGroup.users_group_active == true())
2757 if repo_group_id:
2756 if repo_group_id:
2758 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2757 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2759 return q.all()
2758 return q.all()
2760
2759
2761 @classmethod
2760 @classmethod
2762 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2761 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2763 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2762 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2764 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2763 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2765 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2764 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2766 .filter(UserUserGroupToPerm.user_id == user_id)
2765 .filter(UserUserGroupToPerm.user_id == user_id)
2767 if user_group_id:
2766 if user_group_id:
2768 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2767 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2769 return q.all()
2768 return q.all()
2770
2769
2771 @classmethod
2770 @classmethod
2772 def get_default_user_group_perms_from_user_group(
2771 def get_default_user_group_perms_from_user_group(
2773 cls, user_id, user_group_id=None):
2772 cls, user_id, user_group_id=None):
2774 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2773 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2775 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2774 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2776 .join(
2775 .join(
2777 Permission,
2776 Permission,
2778 UserGroupUserGroupToPerm.permission_id ==
2777 UserGroupUserGroupToPerm.permission_id ==
2779 Permission.permission_id)\
2778 Permission.permission_id)\
2780 .join(
2779 .join(
2781 TargetUserGroup,
2780 TargetUserGroup,
2782 UserGroupUserGroupToPerm.target_user_group_id ==
2781 UserGroupUserGroupToPerm.target_user_group_id ==
2783 TargetUserGroup.users_group_id)\
2782 TargetUserGroup.users_group_id)\
2784 .join(
2783 .join(
2785 UserGroup,
2784 UserGroup,
2786 UserGroupUserGroupToPerm.user_group_id ==
2785 UserGroupUserGroupToPerm.user_group_id ==
2787 UserGroup.users_group_id)\
2786 UserGroup.users_group_id)\
2788 .join(
2787 .join(
2789 UserGroupMember,
2788 UserGroupMember,
2790 UserGroupUserGroupToPerm.user_group_id ==
2789 UserGroupUserGroupToPerm.user_group_id ==
2791 UserGroupMember.users_group_id)\
2790 UserGroupMember.users_group_id)\
2792 .filter(
2791 .filter(
2793 UserGroupMember.user_id == user_id,
2792 UserGroupMember.user_id == user_id,
2794 UserGroup.users_group_active == true())
2793 UserGroup.users_group_active == true())
2795 if user_group_id:
2794 if user_group_id:
2796 q = q.filter(
2795 q = q.filter(
2797 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2796 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2798
2797
2799 return q.all()
2798 return q.all()
2800
2799
2801
2800
2802 class UserRepoToPerm(Base, BaseModel):
2801 class UserRepoToPerm(Base, BaseModel):
2803 __tablename__ = 'repo_to_perm'
2802 __tablename__ = 'repo_to_perm'
2804 __table_args__ = (
2803 __table_args__ = (
2805 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2804 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2806 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2805 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2807 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2806 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2808 )
2807 )
2809 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2808 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2810 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2809 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2811 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2810 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2812 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2811 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2813
2812
2814 user = relationship('User')
2813 user = relationship('User')
2815 repository = relationship('Repository')
2814 repository = relationship('Repository')
2816 permission = relationship('Permission')
2815 permission = relationship('Permission')
2817
2816
2818 @classmethod
2817 @classmethod
2819 def create(cls, user, repository, permission):
2818 def create(cls, user, repository, permission):
2820 n = cls()
2819 n = cls()
2821 n.user = user
2820 n.user = user
2822 n.repository = repository
2821 n.repository = repository
2823 n.permission = permission
2822 n.permission = permission
2824 Session().add(n)
2823 Session().add(n)
2825 return n
2824 return n
2826
2825
2827 def __unicode__(self):
2826 def __unicode__(self):
2828 return u'<%s => %s >' % (self.user, self.repository)
2827 return u'<%s => %s >' % (self.user, self.repository)
2829
2828
2830
2829
2831 class UserUserGroupToPerm(Base, BaseModel):
2830 class UserUserGroupToPerm(Base, BaseModel):
2832 __tablename__ = 'user_user_group_to_perm'
2831 __tablename__ = 'user_user_group_to_perm'
2833 __table_args__ = (
2832 __table_args__ = (
2834 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2833 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2835 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2834 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2836 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2835 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2837 )
2836 )
2838 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2837 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2839 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2838 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2840 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2839 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2841 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2840 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2842
2841
2843 user = relationship('User')
2842 user = relationship('User')
2844 user_group = relationship('UserGroup')
2843 user_group = relationship('UserGroup')
2845 permission = relationship('Permission')
2844 permission = relationship('Permission')
2846
2845
2847 @classmethod
2846 @classmethod
2848 def create(cls, user, user_group, permission):
2847 def create(cls, user, user_group, permission):
2849 n = cls()
2848 n = cls()
2850 n.user = user
2849 n.user = user
2851 n.user_group = user_group
2850 n.user_group = user_group
2852 n.permission = permission
2851 n.permission = permission
2853 Session().add(n)
2852 Session().add(n)
2854 return n
2853 return n
2855
2854
2856 def __unicode__(self):
2855 def __unicode__(self):
2857 return u'<%s => %s >' % (self.user, self.user_group)
2856 return u'<%s => %s >' % (self.user, self.user_group)
2858
2857
2859
2858
2860 class UserToPerm(Base, BaseModel):
2859 class UserToPerm(Base, BaseModel):
2861 __tablename__ = 'user_to_perm'
2860 __tablename__ = 'user_to_perm'
2862 __table_args__ = (
2861 __table_args__ = (
2863 UniqueConstraint('user_id', 'permission_id'),
2862 UniqueConstraint('user_id', 'permission_id'),
2864 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2863 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2865 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2864 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2866 )
2865 )
2867 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2866 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2868 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2867 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2869 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2868 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2870
2869
2871 user = relationship('User')
2870 user = relationship('User')
2872 permission = relationship('Permission', lazy='joined')
2871 permission = relationship('Permission', lazy='joined')
2873
2872
2874 def __unicode__(self):
2873 def __unicode__(self):
2875 return u'<%s => %s >' % (self.user, self.permission)
2874 return u'<%s => %s >' % (self.user, self.permission)
2876
2875
2877
2876
2878 class UserGroupRepoToPerm(Base, BaseModel):
2877 class UserGroupRepoToPerm(Base, BaseModel):
2879 __tablename__ = 'users_group_repo_to_perm'
2878 __tablename__ = 'users_group_repo_to_perm'
2880 __table_args__ = (
2879 __table_args__ = (
2881 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2880 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2882 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2883 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2884 )
2883 )
2885 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2884 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2886 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2885 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2887 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2886 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2888 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2887 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2889
2888
2890 users_group = relationship('UserGroup')
2889 users_group = relationship('UserGroup')
2891 permission = relationship('Permission')
2890 permission = relationship('Permission')
2892 repository = relationship('Repository')
2891 repository = relationship('Repository')
2893
2892
2894 @classmethod
2893 @classmethod
2895 def create(cls, users_group, repository, permission):
2894 def create(cls, users_group, repository, permission):
2896 n = cls()
2895 n = cls()
2897 n.users_group = users_group
2896 n.users_group = users_group
2898 n.repository = repository
2897 n.repository = repository
2899 n.permission = permission
2898 n.permission = permission
2900 Session().add(n)
2899 Session().add(n)
2901 return n
2900 return n
2902
2901
2903 def __unicode__(self):
2902 def __unicode__(self):
2904 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2903 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2905
2904
2906
2905
2907 class UserGroupUserGroupToPerm(Base, BaseModel):
2906 class UserGroupUserGroupToPerm(Base, BaseModel):
2908 __tablename__ = 'user_group_user_group_to_perm'
2907 __tablename__ = 'user_group_user_group_to_perm'
2909 __table_args__ = (
2908 __table_args__ = (
2910 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2909 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2911 CheckConstraint('target_user_group_id != user_group_id'),
2910 CheckConstraint('target_user_group_id != user_group_id'),
2912 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2911 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2913 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2912 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2914 )
2913 )
2915 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2914 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2916 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2915 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2917 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2916 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2918 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2917 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2919
2918
2920 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2919 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2921 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2920 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2922 permission = relationship('Permission')
2921 permission = relationship('Permission')
2923
2922
2924 @classmethod
2923 @classmethod
2925 def create(cls, target_user_group, user_group, permission):
2924 def create(cls, target_user_group, user_group, permission):
2926 n = cls()
2925 n = cls()
2927 n.target_user_group = target_user_group
2926 n.target_user_group = target_user_group
2928 n.user_group = user_group
2927 n.user_group = user_group
2929 n.permission = permission
2928 n.permission = permission
2930 Session().add(n)
2929 Session().add(n)
2931 return n
2930 return n
2932
2931
2933 def __unicode__(self):
2932 def __unicode__(self):
2934 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2933 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2935
2934
2936
2935
2937 class UserGroupToPerm(Base, BaseModel):
2936 class UserGroupToPerm(Base, BaseModel):
2938 __tablename__ = 'users_group_to_perm'
2937 __tablename__ = 'users_group_to_perm'
2939 __table_args__ = (
2938 __table_args__ = (
2940 UniqueConstraint('users_group_id', 'permission_id',),
2939 UniqueConstraint('users_group_id', 'permission_id',),
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2940 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2941 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 )
2942 )
2944 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2943 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2945 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2944 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2946 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2945 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2947
2946
2948 users_group = relationship('UserGroup')
2947 users_group = relationship('UserGroup')
2949 permission = relationship('Permission')
2948 permission = relationship('Permission')
2950
2949
2951
2950
2952 class UserRepoGroupToPerm(Base, BaseModel):
2951 class UserRepoGroupToPerm(Base, BaseModel):
2953 __tablename__ = 'user_repo_group_to_perm'
2952 __tablename__ = 'user_repo_group_to_perm'
2954 __table_args__ = (
2953 __table_args__ = (
2955 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2954 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2956 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2955 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2957 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2956 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2958 )
2957 )
2959
2958
2960 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2959 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2961 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2960 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2962 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2961 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2963 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2962 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2964
2963
2965 user = relationship('User')
2964 user = relationship('User')
2966 group = relationship('RepoGroup')
2965 group = relationship('RepoGroup')
2967 permission = relationship('Permission')
2966 permission = relationship('Permission')
2968
2967
2969 @classmethod
2968 @classmethod
2970 def create(cls, user, repository_group, permission):
2969 def create(cls, user, repository_group, permission):
2971 n = cls()
2970 n = cls()
2972 n.user = user
2971 n.user = user
2973 n.group = repository_group
2972 n.group = repository_group
2974 n.permission = permission
2973 n.permission = permission
2975 Session().add(n)
2974 Session().add(n)
2976 return n
2975 return n
2977
2976
2978
2977
2979 class UserGroupRepoGroupToPerm(Base, BaseModel):
2978 class UserGroupRepoGroupToPerm(Base, BaseModel):
2980 __tablename__ = 'users_group_repo_group_to_perm'
2979 __tablename__ = 'users_group_repo_group_to_perm'
2981 __table_args__ = (
2980 __table_args__ = (
2982 UniqueConstraint('users_group_id', 'group_id'),
2981 UniqueConstraint('users_group_id', 'group_id'),
2983 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2982 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2984 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2983 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2985 )
2984 )
2986
2985
2987 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2986 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2988 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2987 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2989 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2988 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2990 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2989 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2991
2990
2992 users_group = relationship('UserGroup')
2991 users_group = relationship('UserGroup')
2993 permission = relationship('Permission')
2992 permission = relationship('Permission')
2994 group = relationship('RepoGroup')
2993 group = relationship('RepoGroup')
2995
2994
2996 @classmethod
2995 @classmethod
2997 def create(cls, user_group, repository_group, permission):
2996 def create(cls, user_group, repository_group, permission):
2998 n = cls()
2997 n = cls()
2999 n.users_group = user_group
2998 n.users_group = user_group
3000 n.group = repository_group
2999 n.group = repository_group
3001 n.permission = permission
3000 n.permission = permission
3002 Session().add(n)
3001 Session().add(n)
3003 return n
3002 return n
3004
3003
3005 def __unicode__(self):
3004 def __unicode__(self):
3006 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3005 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3007
3006
3008
3007
3009 class Statistics(Base, BaseModel):
3008 class Statistics(Base, BaseModel):
3010 __tablename__ = 'statistics'
3009 __tablename__ = 'statistics'
3011 __table_args__ = (
3010 __table_args__ = (
3012 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3011 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3013 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3012 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3014 )
3013 )
3015 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3014 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3016 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3015 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3017 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3016 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3018 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3017 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3019 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3018 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3020 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3019 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3021
3020
3022 repository = relationship('Repository', single_parent=True)
3021 repository = relationship('Repository', single_parent=True)
3023
3022
3024
3023
3025 class UserFollowing(Base, BaseModel):
3024 class UserFollowing(Base, BaseModel):
3026 __tablename__ = 'user_followings'
3025 __tablename__ = 'user_followings'
3027 __table_args__ = (
3026 __table_args__ = (
3028 UniqueConstraint('user_id', 'follows_repository_id'),
3027 UniqueConstraint('user_id', 'follows_repository_id'),
3029 UniqueConstraint('user_id', 'follows_user_id'),
3028 UniqueConstraint('user_id', 'follows_user_id'),
3030 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3029 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3031 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3030 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3032 )
3031 )
3033
3032
3034 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3033 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3036 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3035 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3037 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3036 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3038 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3037 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3039
3038
3040 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3039 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3041
3040
3042 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3041 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3043 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3042 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3044
3043
3045 @classmethod
3044 @classmethod
3046 def get_repo_followers(cls, repo_id):
3045 def get_repo_followers(cls, repo_id):
3047 return cls.query().filter(cls.follows_repo_id == repo_id)
3046 return cls.query().filter(cls.follows_repo_id == repo_id)
3048
3047
3049
3048
3050 class CacheKey(Base, BaseModel):
3049 class CacheKey(Base, BaseModel):
3051 __tablename__ = 'cache_invalidation'
3050 __tablename__ = 'cache_invalidation'
3052 __table_args__ = (
3051 __table_args__ = (
3053 UniqueConstraint('cache_key'),
3052 UniqueConstraint('cache_key'),
3054 Index('key_idx', 'cache_key'),
3053 Index('key_idx', 'cache_key'),
3055 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3054 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3056 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3055 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3057 )
3056 )
3058 CACHE_TYPE_ATOM = 'ATOM'
3057 CACHE_TYPE_ATOM = 'ATOM'
3059 CACHE_TYPE_RSS = 'RSS'
3058 CACHE_TYPE_RSS = 'RSS'
3060 CACHE_TYPE_README = 'README'
3059 CACHE_TYPE_README = 'README'
3061
3060
3062 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3061 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3063 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3062 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3064 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3063 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3065 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3064 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3066
3065
3067 def __init__(self, cache_key, cache_args=''):
3066 def __init__(self, cache_key, cache_args=''):
3068 self.cache_key = cache_key
3067 self.cache_key = cache_key
3069 self.cache_args = cache_args
3068 self.cache_args = cache_args
3070 self.cache_active = False
3069 self.cache_active = False
3071
3070
3072 def __unicode__(self):
3071 def __unicode__(self):
3073 return u"<%s('%s:%s[%s]')>" % (
3072 return u"<%s('%s:%s[%s]')>" % (
3074 self.__class__.__name__,
3073 self.__class__.__name__,
3075 self.cache_id, self.cache_key, self.cache_active)
3074 self.cache_id, self.cache_key, self.cache_active)
3076
3075
3077 def _cache_key_partition(self):
3076 def _cache_key_partition(self):
3078 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3077 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3079 return prefix, repo_name, suffix
3078 return prefix, repo_name, suffix
3080
3079
3081 def get_prefix(self):
3080 def get_prefix(self):
3082 """
3081 """
3083 Try to extract prefix from existing cache key. The key could consist
3082 Try to extract prefix from existing cache key. The key could consist
3084 of prefix, repo_name, suffix
3083 of prefix, repo_name, suffix
3085 """
3084 """
3086 # this returns prefix, repo_name, suffix
3085 # this returns prefix, repo_name, suffix
3087 return self._cache_key_partition()[0]
3086 return self._cache_key_partition()[0]
3088
3087
3089 def get_suffix(self):
3088 def get_suffix(self):
3090 """
3089 """
3091 get suffix that might have been used in _get_cache_key to
3090 get suffix that might have been used in _get_cache_key to
3092 generate self.cache_key. Only used for informational purposes
3091 generate self.cache_key. Only used for informational purposes
3093 in repo_edit.mako.
3092 in repo_edit.mako.
3094 """
3093 """
3095 # prefix, repo_name, suffix
3094 # prefix, repo_name, suffix
3096 return self._cache_key_partition()[2]
3095 return self._cache_key_partition()[2]
3097
3096
3098 @classmethod
3097 @classmethod
3099 def delete_all_cache(cls):
3098 def delete_all_cache(cls):
3100 """
3099 """
3101 Delete all cache keys from database.
3100 Delete all cache keys from database.
3102 Should only be run when all instances are down and all entries
3101 Should only be run when all instances are down and all entries
3103 thus stale.
3102 thus stale.
3104 """
3103 """
3105 cls.query().delete()
3104 cls.query().delete()
3106 Session().commit()
3105 Session().commit()
3107
3106
3108 @classmethod
3107 @classmethod
3109 def get_cache_key(cls, repo_name, cache_type):
3108 def get_cache_key(cls, repo_name, cache_type):
3110 """
3109 """
3111
3110
3112 Generate a cache key for this process of RhodeCode instance.
3111 Generate a cache key for this process of RhodeCode instance.
3113 Prefix most likely will be process id or maybe explicitly set
3112 Prefix most likely will be process id or maybe explicitly set
3114 instance_id from .ini file.
3113 instance_id from .ini file.
3115 """
3114 """
3116 import rhodecode
3115 import rhodecode
3117 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3116 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3118
3117
3119 repo_as_unicode = safe_unicode(repo_name)
3118 repo_as_unicode = safe_unicode(repo_name)
3120 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3119 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3121 if cache_type else repo_as_unicode
3120 if cache_type else repo_as_unicode
3122
3121
3123 return u'{}{}'.format(prefix, key)
3122 return u'{}{}'.format(prefix, key)
3124
3123
3125 @classmethod
3124 @classmethod
3126 def set_invalidate(cls, repo_name, delete=False):
3125 def set_invalidate(cls, repo_name, delete=False):
3127 """
3126 """
3128 Mark all caches of a repo as invalid in the database.
3127 Mark all caches of a repo as invalid in the database.
3129 """
3128 """
3130
3129
3131 try:
3130 try:
3132 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3131 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3133 if delete:
3132 if delete:
3134 log.debug('cache objects deleted for repo %s',
3133 log.debug('cache objects deleted for repo %s',
3135 safe_str(repo_name))
3134 safe_str(repo_name))
3136 qry.delete()
3135 qry.delete()
3137 else:
3136 else:
3138 log.debug('cache objects marked as invalid for repo %s',
3137 log.debug('cache objects marked as invalid for repo %s',
3139 safe_str(repo_name))
3138 safe_str(repo_name))
3140 qry.update({"cache_active": False})
3139 qry.update({"cache_active": False})
3141
3140
3142 Session().commit()
3141 Session().commit()
3143 except Exception:
3142 except Exception:
3144 log.exception(
3143 log.exception(
3145 'Cache key invalidation failed for repository %s',
3144 'Cache key invalidation failed for repository %s',
3146 safe_str(repo_name))
3145 safe_str(repo_name))
3147 Session().rollback()
3146 Session().rollback()
3148
3147
3149 @classmethod
3148 @classmethod
3150 def get_active_cache(cls, cache_key):
3149 def get_active_cache(cls, cache_key):
3151 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3150 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3152 if inv_obj:
3151 if inv_obj:
3153 return inv_obj
3152 return inv_obj
3154 return None
3153 return None
3155
3154
3156
3155
3157 class ChangesetComment(Base, BaseModel):
3156 class ChangesetComment(Base, BaseModel):
3158 __tablename__ = 'changeset_comments'
3157 __tablename__ = 'changeset_comments'
3159 __table_args__ = (
3158 __table_args__ = (
3160 Index('cc_revision_idx', 'revision'),
3159 Index('cc_revision_idx', 'revision'),
3161 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3160 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3162 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3161 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3163 )
3162 )
3164
3163
3165 COMMENT_OUTDATED = u'comment_outdated'
3164 COMMENT_OUTDATED = u'comment_outdated'
3166 COMMENT_TYPE_NOTE = u'note'
3165 COMMENT_TYPE_NOTE = u'note'
3167 COMMENT_TYPE_TODO = u'todo'
3166 COMMENT_TYPE_TODO = u'todo'
3168 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3167 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3169
3168
3170 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3169 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3171 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3170 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3172 revision = Column('revision', String(40), nullable=True)
3171 revision = Column('revision', String(40), nullable=True)
3173 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3172 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3174 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3173 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3175 line_no = Column('line_no', Unicode(10), nullable=True)
3174 line_no = Column('line_no', Unicode(10), nullable=True)
3176 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3175 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3177 f_path = Column('f_path', Unicode(1000), nullable=True)
3176 f_path = Column('f_path', Unicode(1000), nullable=True)
3178 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3177 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3179 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3178 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3180 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3179 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3181 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3180 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3182 renderer = Column('renderer', Unicode(64), nullable=True)
3181 renderer = Column('renderer', Unicode(64), nullable=True)
3183 display_state = Column('display_state', Unicode(128), nullable=True)
3182 display_state = Column('display_state', Unicode(128), nullable=True)
3184
3183
3185 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3184 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3186 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3185 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3187 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3186 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3188 author = relationship('User', lazy='joined')
3187 author = relationship('User', lazy='joined')
3189 repo = relationship('Repository')
3188 repo = relationship('Repository')
3190 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3189 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3191 pull_request = relationship('PullRequest', lazy='joined')
3190 pull_request = relationship('PullRequest', lazy='joined')
3192 pull_request_version = relationship('PullRequestVersion')
3191 pull_request_version = relationship('PullRequestVersion')
3193
3192
3194 @classmethod
3193 @classmethod
3195 def get_users(cls, revision=None, pull_request_id=None):
3194 def get_users(cls, revision=None, pull_request_id=None):
3196 """
3195 """
3197 Returns user associated with this ChangesetComment. ie those
3196 Returns user associated with this ChangesetComment. ie those
3198 who actually commented
3197 who actually commented
3199
3198
3200 :param cls:
3199 :param cls:
3201 :param revision:
3200 :param revision:
3202 """
3201 """
3203 q = Session().query(User)\
3202 q = Session().query(User)\
3204 .join(ChangesetComment.author)
3203 .join(ChangesetComment.author)
3205 if revision:
3204 if revision:
3206 q = q.filter(cls.revision == revision)
3205 q = q.filter(cls.revision == revision)
3207 elif pull_request_id:
3206 elif pull_request_id:
3208 q = q.filter(cls.pull_request_id == pull_request_id)
3207 q = q.filter(cls.pull_request_id == pull_request_id)
3209 return q.all()
3208 return q.all()
3210
3209
3211 @classmethod
3210 @classmethod
3212 def get_index_from_version(cls, pr_version, versions):
3211 def get_index_from_version(cls, pr_version, versions):
3213 num_versions = [x.pull_request_version_id for x in versions]
3212 num_versions = [x.pull_request_version_id for x in versions]
3214 try:
3213 try:
3215 return num_versions.index(pr_version) +1
3214 return num_versions.index(pr_version) +1
3216 except (IndexError, ValueError):
3215 except (IndexError, ValueError):
3217 return
3216 return
3218
3217
3219 @property
3218 @property
3220 def outdated(self):
3219 def outdated(self):
3221 return self.display_state == self.COMMENT_OUTDATED
3220 return self.display_state == self.COMMENT_OUTDATED
3222
3221
3223 def outdated_at_version(self, version):
3222 def outdated_at_version(self, version):
3224 """
3223 """
3225 Checks if comment is outdated for given pull request version
3224 Checks if comment is outdated for given pull request version
3226 """
3225 """
3227 return self.outdated and self.pull_request_version_id != version
3226 return self.outdated and self.pull_request_version_id != version
3228
3227
3229 def older_than_version(self, version):
3228 def older_than_version(self, version):
3230 """
3229 """
3231 Checks if comment is made from previous version than given
3230 Checks if comment is made from previous version than given
3232 """
3231 """
3233 if version is None:
3232 if version is None:
3234 return self.pull_request_version_id is not None
3233 return self.pull_request_version_id is not None
3235
3234
3236 return self.pull_request_version_id < version
3235 return self.pull_request_version_id < version
3237
3236
3238 @property
3237 @property
3239 def resolved(self):
3238 def resolved(self):
3240 return self.resolved_by[0] if self.resolved_by else None
3239 return self.resolved_by[0] if self.resolved_by else None
3241
3240
3242 @property
3241 @property
3243 def is_todo(self):
3242 def is_todo(self):
3244 return self.comment_type == self.COMMENT_TYPE_TODO
3243 return self.comment_type == self.COMMENT_TYPE_TODO
3245
3244
3246 @property
3245 @property
3247 def is_inline(self):
3246 def is_inline(self):
3248 return self.line_no and self.f_path
3247 return self.line_no and self.f_path
3249
3248
3250 def get_index_version(self, versions):
3249 def get_index_version(self, versions):
3251 return self.get_index_from_version(
3250 return self.get_index_from_version(
3252 self.pull_request_version_id, versions)
3251 self.pull_request_version_id, versions)
3253
3252
3254 def __repr__(self):
3253 def __repr__(self):
3255 if self.comment_id:
3254 if self.comment_id:
3256 return '<DB:Comment #%s>' % self.comment_id
3255 return '<DB:Comment #%s>' % self.comment_id
3257 else:
3256 else:
3258 return '<DB:Comment at %#x>' % id(self)
3257 return '<DB:Comment at %#x>' % id(self)
3259
3258
3260 def get_api_data(self):
3259 def get_api_data(self):
3261 comment = self
3260 comment = self
3262 data = {
3261 data = {
3263 'comment_id': comment.comment_id,
3262 'comment_id': comment.comment_id,
3264 'comment_type': comment.comment_type,
3263 'comment_type': comment.comment_type,
3265 'comment_text': comment.text,
3264 'comment_text': comment.text,
3266 'comment_status': comment.status_change,
3265 'comment_status': comment.status_change,
3267 'comment_f_path': comment.f_path,
3266 'comment_f_path': comment.f_path,
3268 'comment_lineno': comment.line_no,
3267 'comment_lineno': comment.line_no,
3269 'comment_author': comment.author,
3268 'comment_author': comment.author,
3270 'comment_created_on': comment.created_on
3269 'comment_created_on': comment.created_on
3271 }
3270 }
3272 return data
3271 return data
3273
3272
3274 def __json__(self):
3273 def __json__(self):
3275 data = dict()
3274 data = dict()
3276 data.update(self.get_api_data())
3275 data.update(self.get_api_data())
3277 return data
3276 return data
3278
3277
3279
3278
3280 class ChangesetStatus(Base, BaseModel):
3279 class ChangesetStatus(Base, BaseModel):
3281 __tablename__ = 'changeset_statuses'
3280 __tablename__ = 'changeset_statuses'
3282 __table_args__ = (
3281 __table_args__ = (
3283 Index('cs_revision_idx', 'revision'),
3282 Index('cs_revision_idx', 'revision'),
3284 Index('cs_version_idx', 'version'),
3283 Index('cs_version_idx', 'version'),
3285 UniqueConstraint('repo_id', 'revision', 'version'),
3284 UniqueConstraint('repo_id', 'revision', 'version'),
3286 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3285 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3287 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3286 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3288 )
3287 )
3289 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3288 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3290 STATUS_APPROVED = 'approved'
3289 STATUS_APPROVED = 'approved'
3291 STATUS_REJECTED = 'rejected'
3290 STATUS_REJECTED = 'rejected'
3292 STATUS_UNDER_REVIEW = 'under_review'
3291 STATUS_UNDER_REVIEW = 'under_review'
3293
3292
3294 STATUSES = [
3293 STATUSES = [
3295 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3294 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3296 (STATUS_APPROVED, _("Approved")),
3295 (STATUS_APPROVED, _("Approved")),
3297 (STATUS_REJECTED, _("Rejected")),
3296 (STATUS_REJECTED, _("Rejected")),
3298 (STATUS_UNDER_REVIEW, _("Under Review")),
3297 (STATUS_UNDER_REVIEW, _("Under Review")),
3299 ]
3298 ]
3300
3299
3301 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3300 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3302 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3301 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3303 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3302 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3304 revision = Column('revision', String(40), nullable=False)
3303 revision = Column('revision', String(40), nullable=False)
3305 status = Column('status', String(128), nullable=False, default=DEFAULT)
3304 status = Column('status', String(128), nullable=False, default=DEFAULT)
3306 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3305 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3307 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3306 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3308 version = Column('version', Integer(), nullable=False, default=0)
3307 version = Column('version', Integer(), nullable=False, default=0)
3309 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3308 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3310
3309
3311 author = relationship('User', lazy='joined')
3310 author = relationship('User', lazy='joined')
3312 repo = relationship('Repository')
3311 repo = relationship('Repository')
3313 comment = relationship('ChangesetComment', lazy='joined')
3312 comment = relationship('ChangesetComment', lazy='joined')
3314 pull_request = relationship('PullRequest', lazy='joined')
3313 pull_request = relationship('PullRequest', lazy='joined')
3315
3314
3316 def __unicode__(self):
3315 def __unicode__(self):
3317 return u"<%s('%s[v%s]:%s')>" % (
3316 return u"<%s('%s[v%s]:%s')>" % (
3318 self.__class__.__name__,
3317 self.__class__.__name__,
3319 self.status, self.version, self.author
3318 self.status, self.version, self.author
3320 )
3319 )
3321
3320
3322 @classmethod
3321 @classmethod
3323 def get_status_lbl(cls, value):
3322 def get_status_lbl(cls, value):
3324 return dict(cls.STATUSES).get(value)
3323 return dict(cls.STATUSES).get(value)
3325
3324
3326 @property
3325 @property
3327 def status_lbl(self):
3326 def status_lbl(self):
3328 return ChangesetStatus.get_status_lbl(self.status)
3327 return ChangesetStatus.get_status_lbl(self.status)
3329
3328
3330 def get_api_data(self):
3329 def get_api_data(self):
3331 status = self
3330 status = self
3332 data = {
3331 data = {
3333 'status_id': status.changeset_status_id,
3332 'status_id': status.changeset_status_id,
3334 'status': status.status,
3333 'status': status.status,
3335 }
3334 }
3336 return data
3335 return data
3337
3336
3338 def __json__(self):
3337 def __json__(self):
3339 data = dict()
3338 data = dict()
3340 data.update(self.get_api_data())
3339 data.update(self.get_api_data())
3341 return data
3340 return data
3342
3341
3343
3342
3344 class _PullRequestBase(BaseModel):
3343 class _PullRequestBase(BaseModel):
3345 """
3344 """
3346 Common attributes of pull request and version entries.
3345 Common attributes of pull request and version entries.
3347 """
3346 """
3348
3347
3349 # .status values
3348 # .status values
3350 STATUS_NEW = u'new'
3349 STATUS_NEW = u'new'
3351 STATUS_OPEN = u'open'
3350 STATUS_OPEN = u'open'
3352 STATUS_CLOSED = u'closed'
3351 STATUS_CLOSED = u'closed'
3353
3352
3354 title = Column('title', Unicode(255), nullable=True)
3353 title = Column('title', Unicode(255), nullable=True)
3355 description = Column(
3354 description = Column(
3356 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3355 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3357 nullable=True)
3356 nullable=True)
3358 # new/open/closed status of pull request (not approve/reject/etc)
3357 # new/open/closed status of pull request (not approve/reject/etc)
3359 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3358 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3360 created_on = Column(
3359 created_on = Column(
3361 'created_on', DateTime(timezone=False), nullable=False,
3360 'created_on', DateTime(timezone=False), nullable=False,
3362 default=datetime.datetime.now)
3361 default=datetime.datetime.now)
3363 updated_on = Column(
3362 updated_on = Column(
3364 'updated_on', DateTime(timezone=False), nullable=False,
3363 'updated_on', DateTime(timezone=False), nullable=False,
3365 default=datetime.datetime.now)
3364 default=datetime.datetime.now)
3366
3365
3367 @declared_attr
3366 @declared_attr
3368 def user_id(cls):
3367 def user_id(cls):
3369 return Column(
3368 return Column(
3370 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3369 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3371 unique=None)
3370 unique=None)
3372
3371
3373 # 500 revisions max
3372 # 500 revisions max
3374 _revisions = Column(
3373 _revisions = Column(
3375 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3374 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3376
3375
3377 @declared_attr
3376 @declared_attr
3378 def source_repo_id(cls):
3377 def source_repo_id(cls):
3379 # TODO: dan: rename column to source_repo_id
3378 # TODO: dan: rename column to source_repo_id
3380 return Column(
3379 return Column(
3381 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3380 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3382 nullable=False)
3381 nullable=False)
3383
3382
3384 source_ref = Column('org_ref', Unicode(255), nullable=False)
3383 source_ref = Column('org_ref', Unicode(255), nullable=False)
3385
3384
3386 @declared_attr
3385 @declared_attr
3387 def target_repo_id(cls):
3386 def target_repo_id(cls):
3388 # TODO: dan: rename column to target_repo_id
3387 # TODO: dan: rename column to target_repo_id
3389 return Column(
3388 return Column(
3390 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3389 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3391 nullable=False)
3390 nullable=False)
3392
3391
3393 target_ref = Column('other_ref', Unicode(255), nullable=False)
3392 target_ref = Column('other_ref', Unicode(255), nullable=False)
3394 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3393 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3395
3394
3396 # TODO: dan: rename column to last_merge_source_rev
3395 # TODO: dan: rename column to last_merge_source_rev
3397 _last_merge_source_rev = Column(
3396 _last_merge_source_rev = Column(
3398 'last_merge_org_rev', String(40), nullable=True)
3397 'last_merge_org_rev', String(40), nullable=True)
3399 # TODO: dan: rename column to last_merge_target_rev
3398 # TODO: dan: rename column to last_merge_target_rev
3400 _last_merge_target_rev = Column(
3399 _last_merge_target_rev = Column(
3401 'last_merge_other_rev', String(40), nullable=True)
3400 'last_merge_other_rev', String(40), nullable=True)
3402 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3401 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3403 merge_rev = Column('merge_rev', String(40), nullable=True)
3402 merge_rev = Column('merge_rev', String(40), nullable=True)
3404
3403
3405 reviewer_data = Column(
3404 reviewer_data = Column(
3406 'reviewer_data_json', MutationObj.as_mutable(
3405 'reviewer_data_json', MutationObj.as_mutable(
3407 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3406 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3408
3407
3409 @property
3408 @property
3410 def reviewer_data_json(self):
3409 def reviewer_data_json(self):
3411 return json.dumps(self.reviewer_data)
3410 return json.dumps(self.reviewer_data)
3412
3411
3413 @hybrid_property
3412 @hybrid_property
3414 def description_safe(self):
3413 def description_safe(self):
3415 from rhodecode.lib import helpers as h
3414 from rhodecode.lib import helpers as h
3416 return h.escape(self.description)
3415 return h.escape(self.description)
3417
3416
3418 @hybrid_property
3417 @hybrid_property
3419 def revisions(self):
3418 def revisions(self):
3420 return self._revisions.split(':') if self._revisions else []
3419 return self._revisions.split(':') if self._revisions else []
3421
3420
3422 @revisions.setter
3421 @revisions.setter
3423 def revisions(self, val):
3422 def revisions(self, val):
3424 self._revisions = ':'.join(val)
3423 self._revisions = ':'.join(val)
3425
3424
3426 @hybrid_property
3425 @hybrid_property
3427 def last_merge_status(self):
3426 def last_merge_status(self):
3428 return safe_int(self._last_merge_status)
3427 return safe_int(self._last_merge_status)
3429
3428
3430 @last_merge_status.setter
3429 @last_merge_status.setter
3431 def last_merge_status(self, val):
3430 def last_merge_status(self, val):
3432 self._last_merge_status = val
3431 self._last_merge_status = val
3433
3432
3434 @declared_attr
3433 @declared_attr
3435 def author(cls):
3434 def author(cls):
3436 return relationship('User', lazy='joined')
3435 return relationship('User', lazy='joined')
3437
3436
3438 @declared_attr
3437 @declared_attr
3439 def source_repo(cls):
3438 def source_repo(cls):
3440 return relationship(
3439 return relationship(
3441 'Repository',
3440 'Repository',
3442 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3441 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3443
3442
3444 @property
3443 @property
3445 def source_ref_parts(self):
3444 def source_ref_parts(self):
3446 return self.unicode_to_reference(self.source_ref)
3445 return self.unicode_to_reference(self.source_ref)
3447
3446
3448 @declared_attr
3447 @declared_attr
3449 def target_repo(cls):
3448 def target_repo(cls):
3450 return relationship(
3449 return relationship(
3451 'Repository',
3450 'Repository',
3452 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3451 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3453
3452
3454 @property
3453 @property
3455 def target_ref_parts(self):
3454 def target_ref_parts(self):
3456 return self.unicode_to_reference(self.target_ref)
3455 return self.unicode_to_reference(self.target_ref)
3457
3456
3458 @property
3457 @property
3459 def shadow_merge_ref(self):
3458 def shadow_merge_ref(self):
3460 return self.unicode_to_reference(self._shadow_merge_ref)
3459 return self.unicode_to_reference(self._shadow_merge_ref)
3461
3460
3462 @shadow_merge_ref.setter
3461 @shadow_merge_ref.setter
3463 def shadow_merge_ref(self, ref):
3462 def shadow_merge_ref(self, ref):
3464 self._shadow_merge_ref = self.reference_to_unicode(ref)
3463 self._shadow_merge_ref = self.reference_to_unicode(ref)
3465
3464
3466 def unicode_to_reference(self, raw):
3465 def unicode_to_reference(self, raw):
3467 """
3466 """
3468 Convert a unicode (or string) to a reference object.
3467 Convert a unicode (or string) to a reference object.
3469 If unicode evaluates to False it returns None.
3468 If unicode evaluates to False it returns None.
3470 """
3469 """
3471 if raw:
3470 if raw:
3472 refs = raw.split(':')
3471 refs = raw.split(':')
3473 return Reference(*refs)
3472 return Reference(*refs)
3474 else:
3473 else:
3475 return None
3474 return None
3476
3475
3477 def reference_to_unicode(self, ref):
3476 def reference_to_unicode(self, ref):
3478 """
3477 """
3479 Convert a reference object to unicode.
3478 Convert a reference object to unicode.
3480 If reference is None it returns None.
3479 If reference is None it returns None.
3481 """
3480 """
3482 if ref:
3481 if ref:
3483 return u':'.join(ref)
3482 return u':'.join(ref)
3484 else:
3483 else:
3485 return None
3484 return None
3486
3485
3487 def get_api_data(self, with_merge_state=True):
3486 def get_api_data(self, with_merge_state=True):
3488 from rhodecode.model.pull_request import PullRequestModel
3487 from rhodecode.model.pull_request import PullRequestModel
3489
3488
3490 pull_request = self
3489 pull_request = self
3491 if with_merge_state:
3490 if with_merge_state:
3492 merge_status = PullRequestModel().merge_status(pull_request)
3491 merge_status = PullRequestModel().merge_status(pull_request)
3493 merge_state = {
3492 merge_state = {
3494 'status': merge_status[0],
3493 'status': merge_status[0],
3495 'message': safe_unicode(merge_status[1]),
3494 'message': safe_unicode(merge_status[1]),
3496 }
3495 }
3497 else:
3496 else:
3498 merge_state = {'status': 'not_available',
3497 merge_state = {'status': 'not_available',
3499 'message': 'not_available'}
3498 'message': 'not_available'}
3500
3499
3501 merge_data = {
3500 merge_data = {
3502 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3501 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3503 'reference': (
3502 'reference': (
3504 pull_request.shadow_merge_ref._asdict()
3503 pull_request.shadow_merge_ref._asdict()
3505 if pull_request.shadow_merge_ref else None),
3504 if pull_request.shadow_merge_ref else None),
3506 }
3505 }
3507
3506
3508 data = {
3507 data = {
3509 'pull_request_id': pull_request.pull_request_id,
3508 'pull_request_id': pull_request.pull_request_id,
3510 'url': PullRequestModel().get_url(pull_request),
3509 'url': PullRequestModel().get_url(pull_request),
3511 'title': pull_request.title,
3510 'title': pull_request.title,
3512 'description': pull_request.description,
3511 'description': pull_request.description,
3513 'status': pull_request.status,
3512 'status': pull_request.status,
3514 'created_on': pull_request.created_on,
3513 'created_on': pull_request.created_on,
3515 'updated_on': pull_request.updated_on,
3514 'updated_on': pull_request.updated_on,
3516 'commit_ids': pull_request.revisions,
3515 'commit_ids': pull_request.revisions,
3517 'review_status': pull_request.calculated_review_status(),
3516 'review_status': pull_request.calculated_review_status(),
3518 'mergeable': merge_state,
3517 'mergeable': merge_state,
3519 'source': {
3518 'source': {
3520 'clone_url': pull_request.source_repo.clone_url(),
3519 'clone_url': pull_request.source_repo.clone_url(),
3521 'repository': pull_request.source_repo.repo_name,
3520 'repository': pull_request.source_repo.repo_name,
3522 'reference': {
3521 'reference': {
3523 'name': pull_request.source_ref_parts.name,
3522 'name': pull_request.source_ref_parts.name,
3524 'type': pull_request.source_ref_parts.type,
3523 'type': pull_request.source_ref_parts.type,
3525 'commit_id': pull_request.source_ref_parts.commit_id,
3524 'commit_id': pull_request.source_ref_parts.commit_id,
3526 },
3525 },
3527 },
3526 },
3528 'target': {
3527 'target': {
3529 'clone_url': pull_request.target_repo.clone_url(),
3528 'clone_url': pull_request.target_repo.clone_url(),
3530 'repository': pull_request.target_repo.repo_name,
3529 'repository': pull_request.target_repo.repo_name,
3531 'reference': {
3530 'reference': {
3532 'name': pull_request.target_ref_parts.name,
3531 'name': pull_request.target_ref_parts.name,
3533 'type': pull_request.target_ref_parts.type,
3532 'type': pull_request.target_ref_parts.type,
3534 'commit_id': pull_request.target_ref_parts.commit_id,
3533 'commit_id': pull_request.target_ref_parts.commit_id,
3535 },
3534 },
3536 },
3535 },
3537 'merge': merge_data,
3536 'merge': merge_data,
3538 'author': pull_request.author.get_api_data(include_secrets=False,
3537 'author': pull_request.author.get_api_data(include_secrets=False,
3539 details='basic'),
3538 details='basic'),
3540 'reviewers': [
3539 'reviewers': [
3541 {
3540 {
3542 'user': reviewer.get_api_data(include_secrets=False,
3541 'user': reviewer.get_api_data(include_secrets=False,
3543 details='basic'),
3542 details='basic'),
3544 'reasons': reasons,
3543 'reasons': reasons,
3545 'review_status': st[0][1].status if st else 'not_reviewed',
3544 'review_status': st[0][1].status if st else 'not_reviewed',
3546 }
3545 }
3547 for reviewer, reasons, mandatory, st in
3546 for reviewer, reasons, mandatory, st in
3548 pull_request.reviewers_statuses()
3547 pull_request.reviewers_statuses()
3549 ]
3548 ]
3550 }
3549 }
3551
3550
3552 return data
3551 return data
3553
3552
3554
3553
3555 class PullRequest(Base, _PullRequestBase):
3554 class PullRequest(Base, _PullRequestBase):
3556 __tablename__ = 'pull_requests'
3555 __tablename__ = 'pull_requests'
3557 __table_args__ = (
3556 __table_args__ = (
3558 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3559 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3560 )
3559 )
3561
3560
3562 pull_request_id = Column(
3561 pull_request_id = Column(
3563 'pull_request_id', Integer(), nullable=False, primary_key=True)
3562 'pull_request_id', Integer(), nullable=False, primary_key=True)
3564
3563
3565 def __repr__(self):
3564 def __repr__(self):
3566 if self.pull_request_id:
3565 if self.pull_request_id:
3567 return '<DB:PullRequest #%s>' % self.pull_request_id
3566 return '<DB:PullRequest #%s>' % self.pull_request_id
3568 else:
3567 else:
3569 return '<DB:PullRequest at %#x>' % id(self)
3568 return '<DB:PullRequest at %#x>' % id(self)
3570
3569
3571 reviewers = relationship('PullRequestReviewers',
3570 reviewers = relationship('PullRequestReviewers',
3572 cascade="all, delete, delete-orphan")
3571 cascade="all, delete, delete-orphan")
3573 statuses = relationship('ChangesetStatus',
3572 statuses = relationship('ChangesetStatus',
3574 cascade="all, delete, delete-orphan")
3573 cascade="all, delete, delete-orphan")
3575 comments = relationship('ChangesetComment',
3574 comments = relationship('ChangesetComment',
3576 cascade="all, delete, delete-orphan")
3575 cascade="all, delete, delete-orphan")
3577 versions = relationship('PullRequestVersion',
3576 versions = relationship('PullRequestVersion',
3578 cascade="all, delete, delete-orphan",
3577 cascade="all, delete, delete-orphan",
3579 lazy='dynamic')
3578 lazy='dynamic')
3580
3579
3581 @classmethod
3580 @classmethod
3582 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3581 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3583 internal_methods=None):
3582 internal_methods=None):
3584
3583
3585 class PullRequestDisplay(object):
3584 class PullRequestDisplay(object):
3586 """
3585 """
3587 Special object wrapper for showing PullRequest data via Versions
3586 Special object wrapper for showing PullRequest data via Versions
3588 It mimics PR object as close as possible. This is read only object
3587 It mimics PR object as close as possible. This is read only object
3589 just for display
3588 just for display
3590 """
3589 """
3591
3590
3592 def __init__(self, attrs, internal=None):
3591 def __init__(self, attrs, internal=None):
3593 self.attrs = attrs
3592 self.attrs = attrs
3594 # internal have priority over the given ones via attrs
3593 # internal have priority over the given ones via attrs
3595 self.internal = internal or ['versions']
3594 self.internal = internal or ['versions']
3596
3595
3597 def __getattr__(self, item):
3596 def __getattr__(self, item):
3598 if item in self.internal:
3597 if item in self.internal:
3599 return getattr(self, item)
3598 return getattr(self, item)
3600 try:
3599 try:
3601 return self.attrs[item]
3600 return self.attrs[item]
3602 except KeyError:
3601 except KeyError:
3603 raise AttributeError(
3602 raise AttributeError(
3604 '%s object has no attribute %s' % (self, item))
3603 '%s object has no attribute %s' % (self, item))
3605
3604
3606 def __repr__(self):
3605 def __repr__(self):
3607 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3606 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3608
3607
3609 def versions(self):
3608 def versions(self):
3610 return pull_request_obj.versions.order_by(
3609 return pull_request_obj.versions.order_by(
3611 PullRequestVersion.pull_request_version_id).all()
3610 PullRequestVersion.pull_request_version_id).all()
3612
3611
3613 def is_closed(self):
3612 def is_closed(self):
3614 return pull_request_obj.is_closed()
3613 return pull_request_obj.is_closed()
3615
3614
3616 @property
3615 @property
3617 def pull_request_version_id(self):
3616 def pull_request_version_id(self):
3618 return getattr(pull_request_obj, 'pull_request_version_id', None)
3617 return getattr(pull_request_obj, 'pull_request_version_id', None)
3619
3618
3620 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3619 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3621
3620
3622 attrs.author = StrictAttributeDict(
3621 attrs.author = StrictAttributeDict(
3623 pull_request_obj.author.get_api_data())
3622 pull_request_obj.author.get_api_data())
3624 if pull_request_obj.target_repo:
3623 if pull_request_obj.target_repo:
3625 attrs.target_repo = StrictAttributeDict(
3624 attrs.target_repo = StrictAttributeDict(
3626 pull_request_obj.target_repo.get_api_data())
3625 pull_request_obj.target_repo.get_api_data())
3627 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3626 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3628
3627
3629 if pull_request_obj.source_repo:
3628 if pull_request_obj.source_repo:
3630 attrs.source_repo = StrictAttributeDict(
3629 attrs.source_repo = StrictAttributeDict(
3631 pull_request_obj.source_repo.get_api_data())
3630 pull_request_obj.source_repo.get_api_data())
3632 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3631 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3633
3632
3634 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3633 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3635 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3634 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3636 attrs.revisions = pull_request_obj.revisions
3635 attrs.revisions = pull_request_obj.revisions
3637
3636
3638 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3637 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3639 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3638 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3640 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3639 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3641
3640
3642 return PullRequestDisplay(attrs, internal=internal_methods)
3641 return PullRequestDisplay(attrs, internal=internal_methods)
3643
3642
3644 def is_closed(self):
3643 def is_closed(self):
3645 return self.status == self.STATUS_CLOSED
3644 return self.status == self.STATUS_CLOSED
3646
3645
3647 def __json__(self):
3646 def __json__(self):
3648 return {
3647 return {
3649 'revisions': self.revisions,
3648 'revisions': self.revisions,
3650 }
3649 }
3651
3650
3652 def calculated_review_status(self):
3651 def calculated_review_status(self):
3653 from rhodecode.model.changeset_status import ChangesetStatusModel
3652 from rhodecode.model.changeset_status import ChangesetStatusModel
3654 return ChangesetStatusModel().calculated_review_status(self)
3653 return ChangesetStatusModel().calculated_review_status(self)
3655
3654
3656 def reviewers_statuses(self):
3655 def reviewers_statuses(self):
3657 from rhodecode.model.changeset_status import ChangesetStatusModel
3656 from rhodecode.model.changeset_status import ChangesetStatusModel
3658 return ChangesetStatusModel().reviewers_statuses(self)
3657 return ChangesetStatusModel().reviewers_statuses(self)
3659
3658
3660 @property
3659 @property
3661 def workspace_id(self):
3660 def workspace_id(self):
3662 from rhodecode.model.pull_request import PullRequestModel
3661 from rhodecode.model.pull_request import PullRequestModel
3663 return PullRequestModel()._workspace_id(self)
3662 return PullRequestModel()._workspace_id(self)
3664
3663
3665 def get_shadow_repo(self):
3664 def get_shadow_repo(self):
3666 workspace_id = self.workspace_id
3665 workspace_id = self.workspace_id
3667 vcs_obj = self.target_repo.scm_instance()
3666 vcs_obj = self.target_repo.scm_instance()
3668 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3667 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3669 workspace_id)
3668 workspace_id)
3670 return vcs_obj.get_shadow_instance(shadow_repository_path)
3669 return vcs_obj.get_shadow_instance(shadow_repository_path)
3671
3670
3672
3671
3673 class PullRequestVersion(Base, _PullRequestBase):
3672 class PullRequestVersion(Base, _PullRequestBase):
3674 __tablename__ = 'pull_request_versions'
3673 __tablename__ = 'pull_request_versions'
3675 __table_args__ = (
3674 __table_args__ = (
3676 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3675 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3677 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3676 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3678 )
3677 )
3679
3678
3680 pull_request_version_id = Column(
3679 pull_request_version_id = Column(
3681 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3680 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3682 pull_request_id = Column(
3681 pull_request_id = Column(
3683 'pull_request_id', Integer(),
3682 'pull_request_id', Integer(),
3684 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3683 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3685 pull_request = relationship('PullRequest')
3684 pull_request = relationship('PullRequest')
3686
3685
3687 def __repr__(self):
3686 def __repr__(self):
3688 if self.pull_request_version_id:
3687 if self.pull_request_version_id:
3689 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3688 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3690 else:
3689 else:
3691 return '<DB:PullRequestVersion at %#x>' % id(self)
3690 return '<DB:PullRequestVersion at %#x>' % id(self)
3692
3691
3693 @property
3692 @property
3694 def reviewers(self):
3693 def reviewers(self):
3695 return self.pull_request.reviewers
3694 return self.pull_request.reviewers
3696
3695
3697 @property
3696 @property
3698 def versions(self):
3697 def versions(self):
3699 return self.pull_request.versions
3698 return self.pull_request.versions
3700
3699
3701 def is_closed(self):
3700 def is_closed(self):
3702 # calculate from original
3701 # calculate from original
3703 return self.pull_request.status == self.STATUS_CLOSED
3702 return self.pull_request.status == self.STATUS_CLOSED
3704
3703
3705 def calculated_review_status(self):
3704 def calculated_review_status(self):
3706 return self.pull_request.calculated_review_status()
3705 return self.pull_request.calculated_review_status()
3707
3706
3708 def reviewers_statuses(self):
3707 def reviewers_statuses(self):
3709 return self.pull_request.reviewers_statuses()
3708 return self.pull_request.reviewers_statuses()
3710
3709
3711
3710
3712 class PullRequestReviewers(Base, BaseModel):
3711 class PullRequestReviewers(Base, BaseModel):
3713 __tablename__ = 'pull_request_reviewers'
3712 __tablename__ = 'pull_request_reviewers'
3714 __table_args__ = (
3713 __table_args__ = (
3715 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3714 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3716 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3715 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3717 )
3716 )
3718
3717
3719 @hybrid_property
3718 @hybrid_property
3720 def reasons(self):
3719 def reasons(self):
3721 if not self._reasons:
3720 if not self._reasons:
3722 return []
3721 return []
3723 return self._reasons
3722 return self._reasons
3724
3723
3725 @reasons.setter
3724 @reasons.setter
3726 def reasons(self, val):
3725 def reasons(self, val):
3727 val = val or []
3726 val = val or []
3728 if any(not isinstance(x, compat.string_types) for x in val):
3727 if any(not isinstance(x, str) for x in val):
3729 raise Exception('invalid reasons type, must be list of strings')
3728 raise Exception('invalid reasons type, must be list of strings')
3730 self._reasons = val
3729 self._reasons = val
3731
3730
3732 pull_requests_reviewers_id = Column(
3731 pull_requests_reviewers_id = Column(
3733 'pull_requests_reviewers_id', Integer(), nullable=False,
3732 'pull_requests_reviewers_id', Integer(), nullable=False,
3734 primary_key=True)
3733 primary_key=True)
3735 pull_request_id = Column(
3734 pull_request_id = Column(
3736 "pull_request_id", Integer(),
3735 "pull_request_id", Integer(),
3737 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3736 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3738 user_id = Column(
3737 user_id = Column(
3739 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3738 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3740 _reasons = Column(
3739 _reasons = Column(
3741 'reason', MutationList.as_mutable(
3740 'reason', MutationList.as_mutable(
3742 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3741 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3743 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3742 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3744 user = relationship('User')
3743 user = relationship('User')
3745 pull_request = relationship('PullRequest')
3744 pull_request = relationship('PullRequest')
3746
3745
3747
3746
3748 class Notification(Base, BaseModel):
3747 class Notification(Base, BaseModel):
3749 __tablename__ = 'notifications'
3748 __tablename__ = 'notifications'
3750 __table_args__ = (
3749 __table_args__ = (
3751 Index('notification_type_idx', 'type'),
3750 Index('notification_type_idx', 'type'),
3752 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3751 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3753 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3752 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3754 )
3753 )
3755
3754
3756 TYPE_CHANGESET_COMMENT = u'cs_comment'
3755 TYPE_CHANGESET_COMMENT = u'cs_comment'
3757 TYPE_MESSAGE = u'message'
3756 TYPE_MESSAGE = u'message'
3758 TYPE_MENTION = u'mention'
3757 TYPE_MENTION = u'mention'
3759 TYPE_REGISTRATION = u'registration'
3758 TYPE_REGISTRATION = u'registration'
3760 TYPE_PULL_REQUEST = u'pull_request'
3759 TYPE_PULL_REQUEST = u'pull_request'
3761 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3760 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3762
3761
3763 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3762 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3764 subject = Column('subject', Unicode(512), nullable=True)
3763 subject = Column('subject', Unicode(512), nullable=True)
3765 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3764 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3766 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3765 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3767 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3766 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3768 type_ = Column('type', Unicode(255))
3767 type_ = Column('type', Unicode(255))
3769
3768
3770 created_by_user = relationship('User')
3769 created_by_user = relationship('User')
3771 notifications_to_users = relationship('UserNotification', lazy='joined',
3770 notifications_to_users = relationship('UserNotification', lazy='joined',
3772 cascade="all, delete, delete-orphan")
3771 cascade="all, delete, delete-orphan")
3773
3772
3774 @property
3773 @property
3775 def recipients(self):
3774 def recipients(self):
3776 return [x.user for x in UserNotification.query()\
3775 return [x.user for x in UserNotification.query()\
3777 .filter(UserNotification.notification == self)\
3776 .filter(UserNotification.notification == self)\
3778 .order_by(UserNotification.user_id.asc()).all()]
3777 .order_by(UserNotification.user_id.asc()).all()]
3779
3778
3780 @classmethod
3779 @classmethod
3781 def create(cls, created_by, subject, body, recipients, type_=None):
3780 def create(cls, created_by, subject, body, recipients, type_=None):
3782 if type_ is None:
3781 if type_ is None:
3783 type_ = Notification.TYPE_MESSAGE
3782 type_ = Notification.TYPE_MESSAGE
3784
3783
3785 notification = cls()
3784 notification = cls()
3786 notification.created_by_user = created_by
3785 notification.created_by_user = created_by
3787 notification.subject = subject
3786 notification.subject = subject
3788 notification.body = body
3787 notification.body = body
3789 notification.type_ = type_
3788 notification.type_ = type_
3790 notification.created_on = datetime.datetime.now()
3789 notification.created_on = datetime.datetime.now()
3791
3790
3792 for u in recipients:
3791 for u in recipients:
3793 assoc = UserNotification()
3792 assoc = UserNotification()
3794 assoc.notification = notification
3793 assoc.notification = notification
3795
3794
3796 # if created_by is inside recipients mark his notification
3795 # if created_by is inside recipients mark his notification
3797 # as read
3796 # as read
3798 if u.user_id == created_by.user_id:
3797 if u.user_id == created_by.user_id:
3799 assoc.read = True
3798 assoc.read = True
3800
3799
3801 u.notifications.append(assoc)
3800 u.notifications.append(assoc)
3802 Session().add(notification)
3801 Session().add(notification)
3803
3802
3804 return notification
3803 return notification
3805
3804
3806
3805
3807 class UserNotification(Base, BaseModel):
3806 class UserNotification(Base, BaseModel):
3808 __tablename__ = 'user_to_notification'
3807 __tablename__ = 'user_to_notification'
3809 __table_args__ = (
3808 __table_args__ = (
3810 UniqueConstraint('user_id', 'notification_id'),
3809 UniqueConstraint('user_id', 'notification_id'),
3811 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3810 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3812 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3811 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3813 )
3812 )
3814 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3813 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3815 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3814 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3816 read = Column('read', Boolean, default=False)
3815 read = Column('read', Boolean, default=False)
3817 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3816 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3818
3817
3819 user = relationship('User', lazy="joined")
3818 user = relationship('User', lazy="joined")
3820 notification = relationship('Notification', lazy="joined",
3819 notification = relationship('Notification', lazy="joined",
3821 order_by=lambda: Notification.created_on.desc(),)
3820 order_by=lambda: Notification.created_on.desc(),)
3822
3821
3823 def mark_as_read(self):
3822 def mark_as_read(self):
3824 self.read = True
3823 self.read = True
3825 Session().add(self)
3824 Session().add(self)
3826
3825
3827
3826
3828 class Gist(Base, BaseModel):
3827 class Gist(Base, BaseModel):
3829 __tablename__ = 'gists'
3828 __tablename__ = 'gists'
3830 __table_args__ = (
3829 __table_args__ = (
3831 Index('g_gist_access_id_idx', 'gist_access_id'),
3830 Index('g_gist_access_id_idx', 'gist_access_id'),
3832 Index('g_created_on_idx', 'created_on'),
3831 Index('g_created_on_idx', 'created_on'),
3833 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3832 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3834 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3833 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3835 )
3834 )
3836 GIST_PUBLIC = u'public'
3835 GIST_PUBLIC = u'public'
3837 GIST_PRIVATE = u'private'
3836 GIST_PRIVATE = u'private'
3838 DEFAULT_FILENAME = u'gistfile1.txt'
3837 DEFAULT_FILENAME = u'gistfile1.txt'
3839
3838
3840 ACL_LEVEL_PUBLIC = u'acl_public'
3839 ACL_LEVEL_PUBLIC = u'acl_public'
3841 ACL_LEVEL_PRIVATE = u'acl_private'
3840 ACL_LEVEL_PRIVATE = u'acl_private'
3842
3841
3843 gist_id = Column('gist_id', Integer(), primary_key=True)
3842 gist_id = Column('gist_id', Integer(), primary_key=True)
3844 gist_access_id = Column('gist_access_id', Unicode(250))
3843 gist_access_id = Column('gist_access_id', Unicode(250))
3845 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3844 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3846 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3845 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3847 gist_expires = Column('gist_expires', Float(53), nullable=False)
3846 gist_expires = Column('gist_expires', Float(53), nullable=False)
3848 gist_type = Column('gist_type', Unicode(128), nullable=False)
3847 gist_type = Column('gist_type', Unicode(128), nullable=False)
3849 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3848 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3850 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3849 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3851 acl_level = Column('acl_level', Unicode(128), nullable=True)
3850 acl_level = Column('acl_level', Unicode(128), nullable=True)
3852
3851
3853 owner = relationship('User')
3852 owner = relationship('User')
3854
3853
3855 def __repr__(self):
3854 def __repr__(self):
3856 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3855 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3857
3856
3858 @hybrid_property
3857 @hybrid_property
3859 def description_safe(self):
3858 def description_safe(self):
3860 from rhodecode.lib import helpers as h
3859 from rhodecode.lib import helpers as h
3861 return h.escape(self.gist_description)
3860 return h.escape(self.gist_description)
3862
3861
3863 @classmethod
3862 @classmethod
3864 def get_or_404(cls, id_):
3863 def get_or_404(cls, id_):
3865 from pyramid.httpexceptions import HTTPNotFound
3864 from pyramid.httpexceptions import HTTPNotFound
3866
3865
3867 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3866 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3868 if not res:
3867 if not res:
3869 raise HTTPNotFound()
3868 raise HTTPNotFound()
3870 return res
3869 return res
3871
3870
3872 @classmethod
3871 @classmethod
3873 def get_by_access_id(cls, gist_access_id):
3872 def get_by_access_id(cls, gist_access_id):
3874 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3873 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3875
3874
3876 def gist_url(self):
3875 def gist_url(self):
3877 from rhodecode.model.gist import GistModel
3876 from rhodecode.model.gist import GistModel
3878 return GistModel().get_url(self)
3877 return GistModel().get_url(self)
3879
3878
3880 @classmethod
3879 @classmethod
3881 def base_path(cls):
3880 def base_path(cls):
3882 """
3881 """
3883 Returns base path when all gists are stored
3882 Returns base path when all gists are stored
3884
3883
3885 :param cls:
3884 :param cls:
3886 """
3885 """
3887 from rhodecode.model.gist import GIST_STORE_LOC
3886 from rhodecode.model.gist import GIST_STORE_LOC
3888 q = Session().query(RhodeCodeUi)\
3887 q = Session().query(RhodeCodeUi)\
3889 .filter(RhodeCodeUi.ui_key == URL_SEP)
3888 .filter(RhodeCodeUi.ui_key == URL_SEP)
3890 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3889 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3891 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3890 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3892
3891
3893 def get_api_data(self):
3892 def get_api_data(self):
3894 """
3893 """
3895 Common function for generating gist related data for API
3894 Common function for generating gist related data for API
3896 """
3895 """
3897 gist = self
3896 gist = self
3898 data = {
3897 data = {
3899 'gist_id': gist.gist_id,
3898 'gist_id': gist.gist_id,
3900 'type': gist.gist_type,
3899 'type': gist.gist_type,
3901 'access_id': gist.gist_access_id,
3900 'access_id': gist.gist_access_id,
3902 'description': gist.gist_description,
3901 'description': gist.gist_description,
3903 'url': gist.gist_url(),
3902 'url': gist.gist_url(),
3904 'expires': gist.gist_expires,
3903 'expires': gist.gist_expires,
3905 'created_on': gist.created_on,
3904 'created_on': gist.created_on,
3906 'modified_at': gist.modified_at,
3905 'modified_at': gist.modified_at,
3907 'content': None,
3906 'content': None,
3908 'acl_level': gist.acl_level,
3907 'acl_level': gist.acl_level,
3909 }
3908 }
3910 return data
3909 return data
3911
3910
3912 def __json__(self):
3911 def __json__(self):
3913 data = dict(
3912 data = dict(
3914 )
3913 )
3915 data.update(self.get_api_data())
3914 data.update(self.get_api_data())
3916 return data
3915 return data
3917 # SCM functions
3916 # SCM functions
3918
3917
3919 def scm_instance(self, **kwargs):
3918 def scm_instance(self, **kwargs):
3920 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3919 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3921 return get_vcs_instance(
3920 return get_vcs_instance(
3922 repo_path=safe_str(full_repo_path), create=False)
3921 repo_path=safe_str(full_repo_path), create=False)
3923
3922
3924
3923
3925 class ExternalIdentity(Base, BaseModel):
3924 class ExternalIdentity(Base, BaseModel):
3926 __tablename__ = 'external_identities'
3925 __tablename__ = 'external_identities'
3927 __table_args__ = (
3926 __table_args__ = (
3928 Index('local_user_id_idx', 'local_user_id'),
3927 Index('local_user_id_idx', 'local_user_id'),
3929 Index('external_id_idx', 'external_id'),
3928 Index('external_id_idx', 'external_id'),
3930 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3929 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3931 'mysql_charset': 'utf8'})
3930 'mysql_charset': 'utf8'})
3932
3931
3933 external_id = Column('external_id', Unicode(255), default=u'',
3932 external_id = Column('external_id', Unicode(255), default=u'',
3934 primary_key=True)
3933 primary_key=True)
3935 external_username = Column('external_username', Unicode(1024), default=u'')
3934 external_username = Column('external_username', Unicode(1024), default=u'')
3936 local_user_id = Column('local_user_id', Integer(),
3935 local_user_id = Column('local_user_id', Integer(),
3937 ForeignKey('users.user_id'), primary_key=True)
3936 ForeignKey('users.user_id'), primary_key=True)
3938 provider_name = Column('provider_name', Unicode(255), default=u'',
3937 provider_name = Column('provider_name', Unicode(255), default=u'',
3939 primary_key=True)
3938 primary_key=True)
3940 access_token = Column('access_token', String(1024), default=u'')
3939 access_token = Column('access_token', String(1024), default=u'')
3941 alt_token = Column('alt_token', String(1024), default=u'')
3940 alt_token = Column('alt_token', String(1024), default=u'')
3942 token_secret = Column('token_secret', String(1024), default=u'')
3941 token_secret = Column('token_secret', String(1024), default=u'')
3943
3942
3944 @classmethod
3943 @classmethod
3945 def by_external_id_and_provider(cls, external_id, provider_name,
3944 def by_external_id_and_provider(cls, external_id, provider_name,
3946 local_user_id=None):
3945 local_user_id=None):
3947 """
3946 """
3948 Returns ExternalIdentity instance based on search params
3947 Returns ExternalIdentity instance based on search params
3949
3948
3950 :param external_id:
3949 :param external_id:
3951 :param provider_name:
3950 :param provider_name:
3952 :return: ExternalIdentity
3951 :return: ExternalIdentity
3953 """
3952 """
3954 query = cls.query()
3953 query = cls.query()
3955 query = query.filter(cls.external_id == external_id)
3954 query = query.filter(cls.external_id == external_id)
3956 query = query.filter(cls.provider_name == provider_name)
3955 query = query.filter(cls.provider_name == provider_name)
3957 if local_user_id:
3956 if local_user_id:
3958 query = query.filter(cls.local_user_id == local_user_id)
3957 query = query.filter(cls.local_user_id == local_user_id)
3959 return query.first()
3958 return query.first()
3960
3959
3961 @classmethod
3960 @classmethod
3962 def user_by_external_id_and_provider(cls, external_id, provider_name):
3961 def user_by_external_id_and_provider(cls, external_id, provider_name):
3963 """
3962 """
3964 Returns User instance based on search params
3963 Returns User instance based on search params
3965
3964
3966 :param external_id:
3965 :param external_id:
3967 :param provider_name:
3966 :param provider_name:
3968 :return: User
3967 :return: User
3969 """
3968 """
3970 query = User.query()
3969 query = User.query()
3971 query = query.filter(cls.external_id == external_id)
3970 query = query.filter(cls.external_id == external_id)
3972 query = query.filter(cls.provider_name == provider_name)
3971 query = query.filter(cls.provider_name == provider_name)
3973 query = query.filter(User.user_id == cls.local_user_id)
3972 query = query.filter(User.user_id == cls.local_user_id)
3974 return query.first()
3973 return query.first()
3975
3974
3976 @classmethod
3975 @classmethod
3977 def by_local_user_id(cls, local_user_id):
3976 def by_local_user_id(cls, local_user_id):
3978 """
3977 """
3979 Returns all tokens for user
3978 Returns all tokens for user
3980
3979
3981 :param local_user_id:
3980 :param local_user_id:
3982 :return: ExternalIdentity
3981 :return: ExternalIdentity
3983 """
3982 """
3984 query = cls.query()
3983 query = cls.query()
3985 query = query.filter(cls.local_user_id == local_user_id)
3984 query = query.filter(cls.local_user_id == local_user_id)
3986 return query
3985 return query
3987
3986
3988
3987
3989 class Integration(Base, BaseModel):
3988 class Integration(Base, BaseModel):
3990 __tablename__ = 'integrations'
3989 __tablename__ = 'integrations'
3991 __table_args__ = (
3990 __table_args__ = (
3992 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3991 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3993 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3992 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3994 )
3993 )
3995
3994
3996 integration_id = Column('integration_id', Integer(), primary_key=True)
3995 integration_id = Column('integration_id', Integer(), primary_key=True)
3997 integration_type = Column('integration_type', String(255))
3996 integration_type = Column('integration_type', String(255))
3998 enabled = Column('enabled', Boolean(), nullable=False)
3997 enabled = Column('enabled', Boolean(), nullable=False)
3999 name = Column('name', String(255), nullable=False)
3998 name = Column('name', String(255), nullable=False)
4000 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3999 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4001 default=False)
4000 default=False)
4002
4001
4003 settings = Column(
4002 settings = Column(
4004 'settings_json', MutationObj.as_mutable(
4003 'settings_json', MutationObj.as_mutable(
4005 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4004 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4006 repo_id = Column(
4005 repo_id = Column(
4007 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4006 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4008 nullable=True, unique=None, default=None)
4007 nullable=True, unique=None, default=None)
4009 repo = relationship('Repository', lazy='joined')
4008 repo = relationship('Repository', lazy='joined')
4010
4009
4011 repo_group_id = Column(
4010 repo_group_id = Column(
4012 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4011 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4013 nullable=True, unique=None, default=None)
4012 nullable=True, unique=None, default=None)
4014 repo_group = relationship('RepoGroup', lazy='joined')
4013 repo_group = relationship('RepoGroup', lazy='joined')
4015
4014
4016 @property
4015 @property
4017 def scope(self):
4016 def scope(self):
4018 if self.repo:
4017 if self.repo:
4019 return repr(self.repo)
4018 return repr(self.repo)
4020 if self.repo_group:
4019 if self.repo_group:
4021 if self.child_repos_only:
4020 if self.child_repos_only:
4022 return repr(self.repo_group) + ' (child repos only)'
4021 return repr(self.repo_group) + ' (child repos only)'
4023 else:
4022 else:
4024 return repr(self.repo_group) + ' (recursive)'
4023 return repr(self.repo_group) + ' (recursive)'
4025 if self.child_repos_only:
4024 if self.child_repos_only:
4026 return 'root_repos'
4025 return 'root_repos'
4027 return 'global'
4026 return 'global'
4028
4027
4029 def __repr__(self):
4028 def __repr__(self):
4030 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4029 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4031
4030
4032
4031
4033 class RepoReviewRuleUser(Base, BaseModel):
4032 class RepoReviewRuleUser(Base, BaseModel):
4034 __tablename__ = 'repo_review_rules_users'
4033 __tablename__ = 'repo_review_rules_users'
4035 __table_args__ = (
4034 __table_args__ = (
4036 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4035 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4037 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4036 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4038 )
4037 )
4039 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4038 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4040 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4039 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4041 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4040 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4042 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4041 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4043 user = relationship('User')
4042 user = relationship('User')
4044
4043
4045 def rule_data(self):
4044 def rule_data(self):
4046 return {
4045 return {
4047 'mandatory': self.mandatory
4046 'mandatory': self.mandatory
4048 }
4047 }
4049
4048
4050
4049
4051 class RepoReviewRuleUserGroup(Base, BaseModel):
4050 class RepoReviewRuleUserGroup(Base, BaseModel):
4052 __tablename__ = 'repo_review_rules_users_groups'
4051 __tablename__ = 'repo_review_rules_users_groups'
4053 __table_args__ = (
4052 __table_args__ = (
4054 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4053 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4055 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4054 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4056 )
4055 )
4057 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4056 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4058 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4057 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4059 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4058 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4060 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4059 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4061 users_group = relationship('UserGroup')
4060 users_group = relationship('UserGroup')
4062
4061
4063 def rule_data(self):
4062 def rule_data(self):
4064 return {
4063 return {
4065 'mandatory': self.mandatory
4064 'mandatory': self.mandatory
4066 }
4065 }
4067
4066
4068
4067
4069 class RepoReviewRule(Base, BaseModel):
4068 class RepoReviewRule(Base, BaseModel):
4070 __tablename__ = 'repo_review_rules'
4069 __tablename__ = 'repo_review_rules'
4071 __table_args__ = (
4070 __table_args__ = (
4072 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4071 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4073 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4072 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
4074 )
4073 )
4075
4074
4076 repo_review_rule_id = Column(
4075 repo_review_rule_id = Column(
4077 'repo_review_rule_id', Integer(), primary_key=True)
4076 'repo_review_rule_id', Integer(), primary_key=True)
4078 repo_id = Column(
4077 repo_id = Column(
4079 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4078 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4080 repo = relationship('Repository', backref='review_rules')
4079 repo = relationship('Repository', backref='review_rules')
4081
4080
4082 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4081 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4083 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4082 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4084
4083
4085 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4084 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4086 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4085 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4087 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4086 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4088 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4087 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4089
4088
4090 rule_users = relationship('RepoReviewRuleUser')
4089 rule_users = relationship('RepoReviewRuleUser')
4091 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4090 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4092
4091
4093 @hybrid_property
4092 @hybrid_property
4094 def branch_pattern(self):
4093 def branch_pattern(self):
4095 return self._branch_pattern or '*'
4094 return self._branch_pattern or '*'
4096
4095
4097 def _validate_glob(self, value):
4096 def _validate_glob(self, value):
4098 re.compile('^' + glob2re(value) + '$')
4097 re.compile('^' + glob2re(value) + '$')
4099
4098
4100 @branch_pattern.setter
4099 @branch_pattern.setter
4101 def branch_pattern(self, value):
4100 def branch_pattern(self, value):
4102 self._validate_glob(value)
4101 self._validate_glob(value)
4103 self._branch_pattern = value or '*'
4102 self._branch_pattern = value or '*'
4104
4103
4105 @hybrid_property
4104 @hybrid_property
4106 def file_pattern(self):
4105 def file_pattern(self):
4107 return self._file_pattern or '*'
4106 return self._file_pattern or '*'
4108
4107
4109 @file_pattern.setter
4108 @file_pattern.setter
4110 def file_pattern(self, value):
4109 def file_pattern(self, value):
4111 self._validate_glob(value)
4110 self._validate_glob(value)
4112 self._file_pattern = value or '*'
4111 self._file_pattern = value or '*'
4113
4112
4114 def matches(self, branch, files_changed):
4113 def matches(self, branch, files_changed):
4115 """
4114 """
4116 Check if this review rule matches a branch/files in a pull request
4115 Check if this review rule matches a branch/files in a pull request
4117
4116
4118 :param branch: branch name for the commit
4117 :param branch: branch name for the commit
4119 :param files_changed: list of file paths changed in the pull request
4118 :param files_changed: list of file paths changed in the pull request
4120 """
4119 """
4121
4120
4122 branch = branch or ''
4121 branch = branch or ''
4123 files_changed = files_changed or []
4122 files_changed = files_changed or []
4124
4123
4125 branch_matches = True
4124 branch_matches = True
4126 if branch:
4125 if branch:
4127 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4126 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4128 branch_matches = bool(branch_regex.search(branch))
4127 branch_matches = bool(branch_regex.search(branch))
4129
4128
4130 files_matches = True
4129 files_matches = True
4131 if self.file_pattern != '*':
4130 if self.file_pattern != '*':
4132 files_matches = False
4131 files_matches = False
4133 file_regex = re.compile(glob2re(self.file_pattern))
4132 file_regex = re.compile(glob2re(self.file_pattern))
4134 for filename in files_changed:
4133 for filename in files_changed:
4135 if file_regex.search(filename):
4134 if file_regex.search(filename):
4136 files_matches = True
4135 files_matches = True
4137 break
4136 break
4138
4137
4139 return branch_matches and files_matches
4138 return branch_matches and files_matches
4140
4139
4141 @property
4140 @property
4142 def review_users(self):
4141 def review_users(self):
4143 """ Returns the users which this rule applies to """
4142 """ Returns the users which this rule applies to """
4144
4143
4145 users = collections.OrderedDict()
4144 users = collections.OrderedDict()
4146
4145
4147 for rule_user in self.rule_users:
4146 for rule_user in self.rule_users:
4148 if rule_user.user.active:
4147 if rule_user.user.active:
4149 if rule_user.user not in users:
4148 if rule_user.user not in users:
4150 users[rule_user.user.username] = {
4149 users[rule_user.user.username] = {
4151 'user': rule_user.user,
4150 'user': rule_user.user,
4152 'source': 'user',
4151 'source': 'user',
4153 'source_data': {},
4152 'source_data': {},
4154 'data': rule_user.rule_data()
4153 'data': rule_user.rule_data()
4155 }
4154 }
4156
4155
4157 for rule_user_group in self.rule_user_groups:
4156 for rule_user_group in self.rule_user_groups:
4158 source_data = {
4157 source_data = {
4159 'name': rule_user_group.users_group.users_group_name,
4158 'name': rule_user_group.users_group.users_group_name,
4160 'members': len(rule_user_group.users_group.members)
4159 'members': len(rule_user_group.users_group.members)
4161 }
4160 }
4162 for member in rule_user_group.users_group.members:
4161 for member in rule_user_group.users_group.members:
4163 if member.user.active:
4162 if member.user.active:
4164 users[member.user.username] = {
4163 users[member.user.username] = {
4165 'user': member.user,
4164 'user': member.user,
4166 'source': 'user_group',
4165 'source': 'user_group',
4167 'source_data': source_data,
4166 'source_data': source_data,
4168 'data': rule_user_group.rule_data()
4167 'data': rule_user_group.rule_data()
4169 }
4168 }
4170
4169
4171 return users
4170 return users
4172
4171
4173 def __repr__(self):
4172 def __repr__(self):
4174 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4173 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4175 self.repo_review_rule_id, self.repo)
4174 self.repo_review_rule_id, self.repo)
4176
4175
4177
4176
4178 class ScheduleEntry(Base, BaseModel):
4177 class ScheduleEntry(Base, BaseModel):
4179 __tablename__ = 'schedule_entries'
4178 __tablename__ = 'schedule_entries'
4180 __table_args__ = (
4179 __table_args__ = (
4181 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4180 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4182 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4181 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4183 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4184 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4185 )
4184 )
4186 schedule_types = ['crontab', 'timedelta', 'integer']
4185 schedule_types = ['crontab', 'timedelta', 'integer']
4187 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4186 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4188
4187
4189 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4188 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4190 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4189 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4191 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4190 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4192
4191
4193 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4192 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4194 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4193 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4195
4194
4196 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4195 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4197 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4196 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4198
4197
4199 # task
4198 # task
4200 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4199 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4201 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4200 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4202 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4201 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4203 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4202 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4204
4203
4205 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4204 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4206 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4205 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4207
4206
4208 @hybrid_property
4207 @hybrid_property
4209 def schedule_type(self):
4208 def schedule_type(self):
4210 return self._schedule_type
4209 return self._schedule_type
4211
4210
4212 @schedule_type.setter
4211 @schedule_type.setter
4213 def schedule_type(self, val):
4212 def schedule_type(self, val):
4214 if val not in self.schedule_types:
4213 if val not in self.schedule_types:
4215 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4214 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4216 val, self.schedule_type))
4215 val, self.schedule_type))
4217
4216
4218 self._schedule_type = val
4217 self._schedule_type = val
4219
4218
4220 @classmethod
4219 @classmethod
4221 def get_uid(cls, obj):
4220 def get_uid(cls, obj):
4222 args = obj.task_args
4221 args = obj.task_args
4223 kwargs = obj.task_kwargs
4222 kwargs = obj.task_kwargs
4224 if isinstance(args, JsonRaw):
4223 if isinstance(args, JsonRaw):
4225 try:
4224 try:
4226 args = json.loads(args)
4225 args = json.loads(args)
4227 except ValueError:
4226 except ValueError:
4228 args = tuple()
4227 args = tuple()
4229
4228
4230 if isinstance(kwargs, JsonRaw):
4229 if isinstance(kwargs, JsonRaw):
4231 try:
4230 try:
4232 kwargs = json.loads(kwargs)
4231 kwargs = json.loads(kwargs)
4233 except ValueError:
4232 except ValueError:
4234 kwargs = dict()
4233 kwargs = dict()
4235
4234
4236 dot_notation = obj.task_dot_notation
4235 dot_notation = obj.task_dot_notation
4237 val = '.'.join(map(safe_str, [
4236 val = '.'.join(map(safe_str, [
4238 sorted(dot_notation), args, sorted(kwargs.items())]))
4237 sorted(dot_notation), args, sorted(kwargs.items())]))
4239 return hashlib.sha1(val).hexdigest()
4238 return hashlib.sha1(val).hexdigest()
4240
4239
4241 @classmethod
4240 @classmethod
4242 def get_by_schedule_name(cls, schedule_name):
4241 def get_by_schedule_name(cls, schedule_name):
4243 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4242 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4244
4243
4245 @classmethod
4244 @classmethod
4246 def get_by_schedule_id(cls, schedule_id):
4245 def get_by_schedule_id(cls, schedule_id):
4247 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4246 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4248
4247
4249 @property
4248 @property
4250 def task(self):
4249 def task(self):
4251 return self.task_dot_notation
4250 return self.task_dot_notation
4252
4251
4253 @property
4252 @property
4254 def schedule(self):
4253 def schedule(self):
4255 from rhodecode.lib.celerylib.utils import raw_2_schedule
4254 from rhodecode.lib.celerylib.utils import raw_2_schedule
4256 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4255 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4257 return schedule
4256 return schedule
4258
4257
4259 @property
4258 @property
4260 def args(self):
4259 def args(self):
4261 try:
4260 try:
4262 return list(self.task_args or [])
4261 return list(self.task_args or [])
4263 except ValueError:
4262 except ValueError:
4264 return list()
4263 return list()
4265
4264
4266 @property
4265 @property
4267 def kwargs(self):
4266 def kwargs(self):
4268 try:
4267 try:
4269 return dict(self.task_kwargs or {})
4268 return dict(self.task_kwargs or {})
4270 except ValueError:
4269 except ValueError:
4271 return dict()
4270 return dict()
4272
4271
4273 def _as_raw(self, val):
4272 def _as_raw(self, val):
4274 if hasattr(val, 'de_coerce'):
4273 if hasattr(val, 'de_coerce'):
4275 val = val.de_coerce()
4274 val = val.de_coerce()
4276 if val:
4275 if val:
4277 val = json.dumps(val)
4276 val = json.dumps(val)
4278
4277
4279 return val
4278 return val
4280
4279
4281 @property
4280 @property
4282 def schedule_definition_raw(self):
4281 def schedule_definition_raw(self):
4283 return self._as_raw(self.schedule_definition)
4282 return self._as_raw(self.schedule_definition)
4284
4283
4285 @property
4284 @property
4286 def args_raw(self):
4285 def args_raw(self):
4287 return self._as_raw(self.task_args)
4286 return self._as_raw(self.task_args)
4288
4287
4289 @property
4288 @property
4290 def kwargs_raw(self):
4289 def kwargs_raw(self):
4291 return self._as_raw(self.task_kwargs)
4290 return self._as_raw(self.task_kwargs)
4292
4291
4293 def __repr__(self):
4292 def __repr__(self):
4294 return '<DB:ScheduleEntry({}:{})>'.format(
4293 return '<DB:ScheduleEntry({}:{})>'.format(
4295 self.schedule_entry_id, self.schedule_name)
4294 self.schedule_entry_id, self.schedule_name)
4296
4295
4297
4296
4298 @event.listens_for(ScheduleEntry, 'before_update')
4297 @event.listens_for(ScheduleEntry, 'before_update')
4299 def update_task_uid(mapper, connection, target):
4298 def update_task_uid(mapper, connection, target):
4300 target.task_uid = ScheduleEntry.get_uid(target)
4299 target.task_uid = ScheduleEntry.get_uid(target)
4301
4300
4302
4301
4303 @event.listens_for(ScheduleEntry, 'before_insert')
4302 @event.listens_for(ScheduleEntry, 'before_insert')
4304 def set_task_uid(mapper, connection, target):
4303 def set_task_uid(mapper, connection, target):
4305 target.task_uid = ScheduleEntry.get_uid(target)
4304 target.task_uid = ScheduleEntry.get_uid(target)
4306
4305
4307
4306
4308 class DbMigrateVersion(Base, BaseModel):
4307 class DbMigrateVersion(Base, BaseModel):
4309 __tablename__ = 'db_migrate_version'
4308 __tablename__ = 'db_migrate_version'
4310 __table_args__ = (
4309 __table_args__ = (
4311 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4310 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4312 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4311 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4313 )
4312 )
4314 repository_id = Column('repository_id', String(250), primary_key=True)
4313 repository_id = Column('repository_id', String(250), primary_key=True)
4315 repository_path = Column('repository_path', Text)
4314 repository_path = Column('repository_path', Text)
4316 version = Column('version', Integer)
4315 version = Column('version', Integer)
4317
4316
4318
4317
4319 class DbSession(Base, BaseModel):
4318 class DbSession(Base, BaseModel):
4320 __tablename__ = 'db_session'
4319 __tablename__ = 'db_session'
4321 __table_args__ = (
4320 __table_args__ = (
4322 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4321 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4323 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4322 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4324 )
4323 )
4325
4324
4326 def __repr__(self):
4325 def __repr__(self):
4327 return '<DB:DbSession({})>'.format(self.id)
4326 return '<DB:DbSession({})>'.format(self.id)
4328
4327
4329 id = Column('id', Integer())
4328 id = Column('id', Integer())
4330 namespace = Column('namespace', String(255), primary_key=True)
4329 namespace = Column('namespace', String(255), primary_key=True)
4331 accessed = Column('accessed', DateTime, nullable=False)
4330 accessed = Column('accessed', DateTime, nullable=False)
4332 created = Column('created', DateTime, nullable=False)
4331 created = Column('created', DateTime, nullable=False)
4333 data = Column('data', PickleType, nullable=False)
4332 data = Column('data', PickleType, nullable=False)
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
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