##// END OF EJS Templates
utils: drop make_ui parameter clear_session - this utility function should never mess with sessions...
Mads Kiilerich -
r7878:a1115795 default
parent child Browse files
Show More
@@ -1,683 +1,681 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.utils
15 kallithea.lib.utils
16 ~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~
17
17
18 Utilities library for Kallithea
18 Utilities library for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 18, 2010
22 :created_on: Apr 18, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import datetime
28 import datetime
29 import logging
29 import logging
30 import os
30 import os
31 import re
31 import re
32 import sys
32 import sys
33 import traceback
33 import traceback
34 from distutils.version import StrictVersion
34 from distutils.version import StrictVersion
35
35
36 import beaker
36 import beaker
37 from beaker.cache import _cache_decorate
37 from beaker.cache import _cache_decorate
38 from tg.i18n import ugettext as _
38 from tg.i18n import ugettext as _
39
39
40 from kallithea.lib.exceptions import HgsubversionImportError
40 from kallithea.lib.exceptions import HgsubversionImportError
41 from kallithea.lib.utils2 import get_current_authuser, safe_str, safe_unicode
41 from kallithea.lib.utils2 import get_current_authuser, safe_str, safe_unicode
42 from kallithea.lib.vcs.exceptions import VCSError
42 from kallithea.lib.vcs.exceptions import VCSError
43 from kallithea.lib.vcs.utils.fakemod import create_module
43 from kallithea.lib.vcs.utils.fakemod import create_module
44 from kallithea.lib.vcs.utils.helpers import get_scm
44 from kallithea.lib.vcs.utils.helpers import get_scm
45 from kallithea.lib.vcs.utils.hgcompat import config, ui
45 from kallithea.lib.vcs.utils.hgcompat import config, ui
46 from kallithea.model import meta
46 from kallithea.model import meta
47 from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog
47 from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog
48 from kallithea.model.repo_group import RepoGroupModel
48 from kallithea.model.repo_group import RepoGroupModel
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
53 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
54
54
55
55
56 #==============================================================================
56 #==============================================================================
57 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
57 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
58 #==============================================================================
58 #==============================================================================
59 def get_repo_slug(request):
59 def get_repo_slug(request):
60 _repo = request.environ['pylons.routes_dict'].get('repo_name')
60 _repo = request.environ['pylons.routes_dict'].get('repo_name')
61 if _repo:
61 if _repo:
62 _repo = _repo.rstrip('/')
62 _repo = _repo.rstrip('/')
63 return _repo
63 return _repo
64
64
65
65
66 def get_repo_group_slug(request):
66 def get_repo_group_slug(request):
67 _group = request.environ['pylons.routes_dict'].get('group_name')
67 _group = request.environ['pylons.routes_dict'].get('group_name')
68 if _group:
68 if _group:
69 _group = _group.rstrip('/')
69 _group = _group.rstrip('/')
70 return _group
70 return _group
71
71
72
72
73 def get_user_group_slug(request):
73 def get_user_group_slug(request):
74 _group = request.environ['pylons.routes_dict'].get('id')
74 _group = request.environ['pylons.routes_dict'].get('id')
75 _group = UserGroup.get(_group)
75 _group = UserGroup.get(_group)
76 if _group:
76 if _group:
77 return _group.users_group_name
77 return _group.users_group_name
78 return None
78 return None
79
79
80
80
81 def _get_permanent_id(s):
81 def _get_permanent_id(s):
82 """Helper for decoding stable URLs with repo ID. For a string like '_123'
82 """Helper for decoding stable URLs with repo ID. For a string like '_123'
83 return 123.
83 return 123.
84 """
84 """
85 by_id_match = re.match(r'^_(\d+)$', s)
85 by_id_match = re.match(r'^_(\d+)$', s)
86 if by_id_match is None:
86 if by_id_match is None:
87 return None
87 return None
88 return int(by_id_match.group(1))
88 return int(by_id_match.group(1))
89
89
90
90
91 def fix_repo_id_name(path):
91 def fix_repo_id_name(path):
92 """
92 """
93 Rewrite repo_name for _<ID> permanent URLs.
93 Rewrite repo_name for _<ID> permanent URLs.
94
94
95 Given a path, if the first path element is like _<ID>, return the path with
95 Given a path, if the first path element is like _<ID>, return the path with
96 this part expanded to the corresponding full repo name, else return the
96 this part expanded to the corresponding full repo name, else return the
97 provided path.
97 provided path.
98 """
98 """
99 first, rest = path, ''
99 first, rest = path, ''
100 if '/' in path:
100 if '/' in path:
101 first, rest_ = path.split('/', 1)
101 first, rest_ = path.split('/', 1)
102 rest = '/' + rest_
102 rest = '/' + rest_
103 repo_id = _get_permanent_id(first)
103 repo_id = _get_permanent_id(first)
104 if repo_id is not None:
104 if repo_id is not None:
105 from kallithea.model.db import Repository
105 from kallithea.model.db import Repository
106 repo = Repository.get(repo_id)
106 repo = Repository.get(repo_id)
107 if repo is not None:
107 if repo is not None:
108 return repo.repo_name + rest
108 return repo.repo_name + rest
109 return path
109 return path
110
110
111
111
112 def action_logger(user, action, repo, ipaddr='', commit=False):
112 def action_logger(user, action, repo, ipaddr='', commit=False):
113 """
113 """
114 Action logger for various actions made by users
114 Action logger for various actions made by users
115
115
116 :param user: user that made this action, can be a unique username string or
116 :param user: user that made this action, can be a unique username string or
117 object containing user_id attribute
117 object containing user_id attribute
118 :param action: action to log, should be on of predefined unique actions for
118 :param action: action to log, should be on of predefined unique actions for
119 easy translations
119 easy translations
120 :param repo: string name of repository or object containing repo_id,
120 :param repo: string name of repository or object containing repo_id,
121 that action was made on
121 that action was made on
122 :param ipaddr: optional IP address from what the action was made
122 :param ipaddr: optional IP address from what the action was made
123
123
124 """
124 """
125
125
126 # if we don't get explicit IP address try to get one from registered user
126 # if we don't get explicit IP address try to get one from registered user
127 # in tmpl context var
127 # in tmpl context var
128 if not ipaddr:
128 if not ipaddr:
129 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
129 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
130
130
131 if getattr(user, 'user_id', None):
131 if getattr(user, 'user_id', None):
132 user_obj = User.get(user.user_id)
132 user_obj = User.get(user.user_id)
133 elif isinstance(user, basestring):
133 elif isinstance(user, basestring):
134 user_obj = User.get_by_username(user)
134 user_obj = User.get_by_username(user)
135 else:
135 else:
136 raise Exception('You have to provide a user object or a username')
136 raise Exception('You have to provide a user object or a username')
137
137
138 if getattr(repo, 'repo_id', None):
138 if getattr(repo, 'repo_id', None):
139 repo_obj = Repository.get(repo.repo_id)
139 repo_obj = Repository.get(repo.repo_id)
140 repo_name = repo_obj.repo_name
140 repo_name = repo_obj.repo_name
141 elif isinstance(repo, basestring):
141 elif isinstance(repo, basestring):
142 repo_name = repo.lstrip('/')
142 repo_name = repo.lstrip('/')
143 repo_obj = Repository.get_by_repo_name(repo_name)
143 repo_obj = Repository.get_by_repo_name(repo_name)
144 else:
144 else:
145 repo_obj = None
145 repo_obj = None
146 repo_name = u''
146 repo_name = u''
147
147
148 user_log = UserLog()
148 user_log = UserLog()
149 user_log.user_id = user_obj.user_id
149 user_log.user_id = user_obj.user_id
150 user_log.username = user_obj.username
150 user_log.username = user_obj.username
151 user_log.action = safe_unicode(action)
151 user_log.action = safe_unicode(action)
152
152
153 user_log.repository = repo_obj
153 user_log.repository = repo_obj
154 user_log.repository_name = repo_name
154 user_log.repository_name = repo_name
155
155
156 user_log.action_date = datetime.datetime.now()
156 user_log.action_date = datetime.datetime.now()
157 user_log.user_ip = ipaddr
157 user_log.user_ip = ipaddr
158 meta.Session().add(user_log)
158 meta.Session().add(user_log)
159
159
160 log.info('Logging action:%s on %s by user:%s ip:%s',
160 log.info('Logging action:%s on %s by user:%s ip:%s',
161 action, safe_unicode(repo), user_obj, ipaddr)
161 action, safe_unicode(repo), user_obj, ipaddr)
162 if commit:
162 if commit:
163 meta.Session().commit()
163 meta.Session().commit()
164
164
165
165
166 def get_filesystem_repos(path):
166 def get_filesystem_repos(path):
167 """
167 """
168 Scans given path for repos and return (name,(type,path)) tuple
168 Scans given path for repos and return (name,(type,path)) tuple
169
169
170 :param path: path to scan for repositories
170 :param path: path to scan for repositories
171 :param recursive: recursive search and return names with subdirs in front
171 :param recursive: recursive search and return names with subdirs in front
172 """
172 """
173
173
174 # remove ending slash for better results
174 # remove ending slash for better results
175 path = safe_str(path.rstrip(os.sep))
175 path = safe_str(path.rstrip(os.sep))
176 log.debug('now scanning in %s', path)
176 log.debug('now scanning in %s', path)
177
177
178 def isdir(*n):
178 def isdir(*n):
179 return os.path.isdir(os.path.join(*n))
179 return os.path.isdir(os.path.join(*n))
180
180
181 for root, dirs, _files in os.walk(path):
181 for root, dirs, _files in os.walk(path):
182 recurse_dirs = []
182 recurse_dirs = []
183 for subdir in dirs:
183 for subdir in dirs:
184 # skip removed repos
184 # skip removed repos
185 if REMOVED_REPO_PAT.match(subdir):
185 if REMOVED_REPO_PAT.match(subdir):
186 continue
186 continue
187
187
188 # skip .<something> dirs TODO: rly? then we should prevent creating them ...
188 # skip .<something> dirs TODO: rly? then we should prevent creating them ...
189 if subdir.startswith('.'):
189 if subdir.startswith('.'):
190 continue
190 continue
191
191
192 cur_path = os.path.join(root, subdir)
192 cur_path = os.path.join(root, subdir)
193 if isdir(cur_path, '.git'):
193 if isdir(cur_path, '.git'):
194 log.warning('ignoring non-bare Git repo: %s', cur_path)
194 log.warning('ignoring non-bare Git repo: %s', cur_path)
195 continue
195 continue
196
196
197 if (isdir(cur_path, '.hg') or
197 if (isdir(cur_path, '.hg') or
198 isdir(cur_path, '.svn') or
198 isdir(cur_path, '.svn') or
199 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or
199 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or
200 os.path.isfile(os.path.join(cur_path, 'packed-refs')))):
200 os.path.isfile(os.path.join(cur_path, 'packed-refs')))):
201
201
202 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
202 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
203 log.warning('ignoring repo path without access: %s', cur_path)
203 log.warning('ignoring repo path without access: %s', cur_path)
204 continue
204 continue
205
205
206 if not os.access(cur_path, os.W_OK):
206 if not os.access(cur_path, os.W_OK):
207 log.warning('repo path without write access: %s', cur_path)
207 log.warning('repo path without write access: %s', cur_path)
208
208
209 try:
209 try:
210 scm_info = get_scm(cur_path)
210 scm_info = get_scm(cur_path)
211 assert cur_path.startswith(path)
211 assert cur_path.startswith(path)
212 repo_path = cur_path[len(path) + 1:]
212 repo_path = cur_path[len(path) + 1:]
213 yield repo_path, scm_info
213 yield repo_path, scm_info
214 continue # no recursion
214 continue # no recursion
215 except VCSError:
215 except VCSError:
216 # We should perhaps ignore such broken repos, but especially
216 # We should perhaps ignore such broken repos, but especially
217 # the bare git detection is unreliable so we dive into it
217 # the bare git detection is unreliable so we dive into it
218 pass
218 pass
219
219
220 recurse_dirs.append(subdir)
220 recurse_dirs.append(subdir)
221
221
222 dirs[:] = recurse_dirs
222 dirs[:] = recurse_dirs
223
223
224
224
225 def is_valid_repo_uri(repo_type, url, ui):
225 def is_valid_repo_uri(repo_type, url, ui):
226 """Check if the url seems like a valid remote repo location - raise an Exception if any problems"""
226 """Check if the url seems like a valid remote repo location - raise an Exception if any problems"""
227 if repo_type == 'hg':
227 if repo_type == 'hg':
228 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
228 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
229 if url.startswith('http') or url.startswith('ssh'):
229 if url.startswith('http') or url.startswith('ssh'):
230 # initially check if it's at least the proper URL
230 # initially check if it's at least the proper URL
231 # or does it pass basic auth
231 # or does it pass basic auth
232 MercurialRepository._check_url(url, ui)
232 MercurialRepository._check_url(url, ui)
233 elif url.startswith('svn+http'):
233 elif url.startswith('svn+http'):
234 try:
234 try:
235 from hgsubversion.svnrepo import svnremoterepo
235 from hgsubversion.svnrepo import svnremoterepo
236 except ImportError:
236 except ImportError:
237 raise HgsubversionImportError(_('Unable to activate hgsubversion support. '
237 raise HgsubversionImportError(_('Unable to activate hgsubversion support. '
238 'The "hgsubversion" library is missing'))
238 'The "hgsubversion" library is missing'))
239 svnremoterepo(ui, url).svn.uuid
239 svnremoterepo(ui, url).svn.uuid
240 elif url.startswith('git+http'):
240 elif url.startswith('git+http'):
241 raise NotImplementedError()
241 raise NotImplementedError()
242 else:
242 else:
243 raise Exception('URI %s not allowed' % (url,))
243 raise Exception('URI %s not allowed' % (url,))
244
244
245 elif repo_type == 'git':
245 elif repo_type == 'git':
246 from kallithea.lib.vcs.backends.git.repository import GitRepository
246 from kallithea.lib.vcs.backends.git.repository import GitRepository
247 if url.startswith('http') or url.startswith('git'):
247 if url.startswith('http') or url.startswith('git'):
248 # initially check if it's at least the proper URL
248 # initially check if it's at least the proper URL
249 # or does it pass basic auth
249 # or does it pass basic auth
250 GitRepository._check_url(url)
250 GitRepository._check_url(url)
251 elif url.startswith('svn+http'):
251 elif url.startswith('svn+http'):
252 raise NotImplementedError()
252 raise NotImplementedError()
253 elif url.startswith('hg+http'):
253 elif url.startswith('hg+http'):
254 raise NotImplementedError()
254 raise NotImplementedError()
255 else:
255 else:
256 raise Exception('URI %s not allowed' % (url))
256 raise Exception('URI %s not allowed' % (url))
257
257
258
258
259 def is_valid_repo(repo_name, base_path, scm=None):
259 def is_valid_repo(repo_name, base_path, scm=None):
260 """
260 """
261 Returns True if given path is a valid repository False otherwise.
261 Returns True if given path is a valid repository False otherwise.
262 If scm param is given also compare if given scm is the same as expected
262 If scm param is given also compare if given scm is the same as expected
263 from scm parameter
263 from scm parameter
264
264
265 :param repo_name:
265 :param repo_name:
266 :param base_path:
266 :param base_path:
267 :param scm:
267 :param scm:
268
268
269 :return True: if given path is a valid repository
269 :return True: if given path is a valid repository
270 """
270 """
271 # TODO: paranoid security checks?
271 # TODO: paranoid security checks?
272 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
272 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
273
273
274 try:
274 try:
275 scm_ = get_scm(full_path)
275 scm_ = get_scm(full_path)
276 if scm:
276 if scm:
277 return scm_[0] == scm
277 return scm_[0] == scm
278 return True
278 return True
279 except VCSError:
279 except VCSError:
280 return False
280 return False
281
281
282
282
283 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
283 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
284 """
284 """
285 Returns True if given path is a repository group False otherwise
285 Returns True if given path is a repository group False otherwise
286
286
287 :param repo_name:
287 :param repo_name:
288 :param base_path:
288 :param base_path:
289 """
289 """
290 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
290 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
291
291
292 # check if it's not a repo
292 # check if it's not a repo
293 if is_valid_repo(repo_group_name, base_path):
293 if is_valid_repo(repo_group_name, base_path):
294 return False
294 return False
295
295
296 try:
296 try:
297 # we need to check bare git repos at higher level
297 # we need to check bare git repos at higher level
298 # since we might match branches/hooks/info/objects or possible
298 # since we might match branches/hooks/info/objects or possible
299 # other things inside bare git repo
299 # other things inside bare git repo
300 get_scm(os.path.dirname(full_path))
300 get_scm(os.path.dirname(full_path))
301 return False
301 return False
302 except VCSError:
302 except VCSError:
303 pass
303 pass
304
304
305 # check if it's a valid path
305 # check if it's a valid path
306 if skip_path_check or os.path.isdir(full_path):
306 if skip_path_check or os.path.isdir(full_path):
307 return True
307 return True
308
308
309 return False
309 return False
310
310
311
311
312 # propagated from mercurial documentation
312 # propagated from mercurial documentation
313 ui_sections = ['alias', 'auth',
313 ui_sections = ['alias', 'auth',
314 'decode/encode', 'defaults',
314 'decode/encode', 'defaults',
315 'diff', 'email',
315 'diff', 'email',
316 'extensions', 'format',
316 'extensions', 'format',
317 'merge-patterns', 'merge-tools',
317 'merge-patterns', 'merge-tools',
318 'hooks', 'http_proxy',
318 'hooks', 'http_proxy',
319 'smtp', 'patch',
319 'smtp', 'patch',
320 'paths', 'profiling',
320 'paths', 'profiling',
321 'server', 'trusted',
321 'server', 'trusted',
322 'ui', 'web', ]
322 'ui', 'web', ]
323
323
324
324
325 def make_ui(repo_path=None, clear_session=True):
325 def make_ui(repo_path=None):
326 """
326 """
327 Create an Mercurial 'ui' object based on database Ui settings, possibly
327 Create an Mercurial 'ui' object based on database Ui settings, possibly
328 augmenting with content from a hgrc file.
328 augmenting with content from a hgrc file.
329 """
329 """
330 baseui = ui.ui()
330 baseui = ui.ui()
331
331
332 # clean the baseui object
332 # clean the baseui object
333 baseui._ocfg = config.config()
333 baseui._ocfg = config.config()
334 baseui._ucfg = config.config()
334 baseui._ucfg = config.config()
335 baseui._tcfg = config.config()
335 baseui._tcfg = config.config()
336
336
337 sa = meta.Session()
337 sa = meta.Session()
338 for ui_ in sa.query(Ui).all():
338 for ui_ in sa.query(Ui).all():
339 if ui_.ui_active:
339 if ui_.ui_active:
340 ui_val = '' if ui_.ui_value is None else safe_str(ui_.ui_value)
340 ui_val = '' if ui_.ui_value is None else safe_str(ui_.ui_value)
341 log.debug('config from db: [%s] %s=%r', ui_.ui_section,
341 log.debug('config from db: [%s] %s=%r', ui_.ui_section,
342 ui_.ui_key, ui_val)
342 ui_.ui_key, ui_val)
343 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
343 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
344 ui_val)
344 ui_val)
345 if clear_session:
346 meta.Session.remove()
347
345
348 # force set push_ssl requirement to False, Kallithea handles that
346 # force set push_ssl requirement to False, Kallithea handles that
349 baseui.setconfig('web', 'push_ssl', False)
347 baseui.setconfig('web', 'push_ssl', False)
350 baseui.setconfig('web', 'allow_push', '*')
348 baseui.setconfig('web', 'allow_push', '*')
351 # prevent interactive questions for ssh password / passphrase
349 # prevent interactive questions for ssh password / passphrase
352 ssh = baseui.config('ui', 'ssh', default='ssh')
350 ssh = baseui.config('ui', 'ssh', default='ssh')
353 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
351 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
354 # push / pull hooks
352 # push / pull hooks
355 baseui.setconfig('hooks', 'changegroup.kallithea_log_push_action', 'python:kallithea.lib.hooks.log_push_action')
353 baseui.setconfig('hooks', 'changegroup.kallithea_log_push_action', 'python:kallithea.lib.hooks.log_push_action')
356 baseui.setconfig('hooks', 'outgoing.kallithea_log_pull_action', 'python:kallithea.lib.hooks.log_pull_action')
354 baseui.setconfig('hooks', 'outgoing.kallithea_log_pull_action', 'python:kallithea.lib.hooks.log_pull_action')
357
355
358 if repo_path is not None:
356 if repo_path is not None:
359 hgrc_path = os.path.join(repo_path, '.hg', 'hgrc')
357 hgrc_path = os.path.join(repo_path, '.hg', 'hgrc')
360 if os.path.isfile(hgrc_path):
358 if os.path.isfile(hgrc_path):
361 log.debug('reading hgrc from %s', hgrc_path)
359 log.debug('reading hgrc from %s', hgrc_path)
362 cfg = config.config()
360 cfg = config.config()
363 cfg.read(hgrc_path)
361 cfg.read(hgrc_path)
364 for section in ui_sections:
362 for section in ui_sections:
365 for k, v in cfg.items(section):
363 for k, v in cfg.items(section):
366 log.debug('config from file: [%s] %s=%s', section, k, v)
364 log.debug('config from file: [%s] %s=%s', section, k, v)
367 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
365 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
368 else:
366 else:
369 log.debug('hgrc file is not present at %s, skipping...', hgrc_path)
367 log.debug('hgrc file is not present at %s, skipping...', hgrc_path)
370
368
371 return baseui
369 return baseui
372
370
373
371
374 def set_app_settings(config):
372 def set_app_settings(config):
375 """
373 """
376 Updates app config with new settings from database
374 Updates app config with new settings from database
377
375
378 :param config:
376 :param config:
379 """
377 """
380 hgsettings = Setting.get_app_settings()
378 hgsettings = Setting.get_app_settings()
381
379
382 for k, v in hgsettings.items():
380 for k, v in hgsettings.items():
383 config[k] = v
381 config[k] = v
384
382
385
383
386 def set_vcs_config(config):
384 def set_vcs_config(config):
387 """
385 """
388 Patch VCS config with some Kallithea specific stuff
386 Patch VCS config with some Kallithea specific stuff
389
387
390 :param config: kallithea.CONFIG
388 :param config: kallithea.CONFIG
391 """
389 """
392 from kallithea.lib.vcs import conf
390 from kallithea.lib.vcs import conf
393 from kallithea.lib.utils2 import aslist
391 from kallithea.lib.utils2 import aslist
394 conf.settings.BACKENDS = {
392 conf.settings.BACKENDS = {
395 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
393 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
396 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
394 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
397 }
395 }
398
396
399 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
397 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
400 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
398 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
401 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
399 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
402 'utf-8'), sep=',')
400 'utf-8'), sep=',')
403
401
404
402
405 def set_indexer_config(config):
403 def set_indexer_config(config):
406 """
404 """
407 Update Whoosh index mapping
405 Update Whoosh index mapping
408
406
409 :param config: kallithea.CONFIG
407 :param config: kallithea.CONFIG
410 """
408 """
411 from kallithea.config import conf
409 from kallithea.config import conf
412
410
413 log.debug('adding extra into INDEX_EXTENSIONS')
411 log.debug('adding extra into INDEX_EXTENSIONS')
414 conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', '')))
412 conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', '')))
415
413
416 log.debug('adding extra into INDEX_FILENAMES')
414 log.debug('adding extra into INDEX_FILENAMES')
417 conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', '')))
415 conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', '')))
418
416
419
417
420 def map_groups(path):
418 def map_groups(path):
421 """
419 """
422 Given a full path to a repository, create all nested groups that this
420 Given a full path to a repository, create all nested groups that this
423 repo is inside. This function creates parent-child relationships between
421 repo is inside. This function creates parent-child relationships between
424 groups and creates default perms for all new groups.
422 groups and creates default perms for all new groups.
425
423
426 :param paths: full path to repository
424 :param paths: full path to repository
427 """
425 """
428 sa = meta.Session()
426 sa = meta.Session()
429 groups = path.split(Repository.url_sep())
427 groups = path.split(Repository.url_sep())
430 parent = None
428 parent = None
431 group = None
429 group = None
432
430
433 # last element is repo in nested groups structure
431 # last element is repo in nested groups structure
434 groups = groups[:-1]
432 groups = groups[:-1]
435 rgm = RepoGroupModel()
433 rgm = RepoGroupModel()
436 owner = User.get_first_admin()
434 owner = User.get_first_admin()
437 for lvl, group_name in enumerate(groups):
435 for lvl, group_name in enumerate(groups):
438 group_name = u'/'.join(groups[:lvl] + [group_name])
436 group_name = u'/'.join(groups[:lvl] + [group_name])
439 group = RepoGroup.get_by_group_name(group_name)
437 group = RepoGroup.get_by_group_name(group_name)
440 desc = '%s group' % group_name
438 desc = '%s group' % group_name
441
439
442 # skip folders that are now removed repos
440 # skip folders that are now removed repos
443 if REMOVED_REPO_PAT.match(group_name):
441 if REMOVED_REPO_PAT.match(group_name):
444 break
442 break
445
443
446 if group is None:
444 if group is None:
447 log.debug('creating group level: %s group_name: %s',
445 log.debug('creating group level: %s group_name: %s',
448 lvl, group_name)
446 lvl, group_name)
449 group = RepoGroup(group_name, parent)
447 group = RepoGroup(group_name, parent)
450 group.group_description = desc
448 group.group_description = desc
451 group.owner = owner
449 group.owner = owner
452 sa.add(group)
450 sa.add(group)
453 rgm._create_default_perms(group)
451 rgm._create_default_perms(group)
454 sa.flush()
452 sa.flush()
455
453
456 parent = group
454 parent = group
457 return group
455 return group
458
456
459
457
460 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
458 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
461 install_git_hooks=False, user=None, overwrite_git_hooks=False):
459 install_git_hooks=False, user=None, overwrite_git_hooks=False):
462 """
460 """
463 maps all repos given in initial_repo_list, non existing repositories
461 maps all repos given in initial_repo_list, non existing repositories
464 are created, if remove_obsolete is True it also check for db entries
462 are created, if remove_obsolete is True it also check for db entries
465 that are not in initial_repo_list and removes them.
463 that are not in initial_repo_list and removes them.
466
464
467 :param initial_repo_list: list of repositories found by scanning methods
465 :param initial_repo_list: list of repositories found by scanning methods
468 :param remove_obsolete: check for obsolete entries in database
466 :param remove_obsolete: check for obsolete entries in database
469 :param install_git_hooks: if this is True, also check and install git hook
467 :param install_git_hooks: if this is True, also check and install git hook
470 for a repo if missing
468 for a repo if missing
471 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
469 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
472 that may be encountered (even if user-deployed)
470 that may be encountered (even if user-deployed)
473 """
471 """
474 from kallithea.model.repo import RepoModel
472 from kallithea.model.repo import RepoModel
475 from kallithea.model.scm import ScmModel
473 from kallithea.model.scm import ScmModel
476 sa = meta.Session()
474 sa = meta.Session()
477 repo_model = RepoModel()
475 repo_model = RepoModel()
478 if user is None:
476 if user is None:
479 user = User.get_first_admin()
477 user = User.get_first_admin()
480 added = []
478 added = []
481
479
482 # creation defaults
480 # creation defaults
483 defs = Setting.get_default_repo_settings(strip_prefix=True)
481 defs = Setting.get_default_repo_settings(strip_prefix=True)
484 enable_statistics = defs.get('repo_enable_statistics')
482 enable_statistics = defs.get('repo_enable_statistics')
485 enable_downloads = defs.get('repo_enable_downloads')
483 enable_downloads = defs.get('repo_enable_downloads')
486 private = defs.get('repo_private')
484 private = defs.get('repo_private')
487
485
488 for name, repo in initial_repo_list.items():
486 for name, repo in initial_repo_list.items():
489 group = map_groups(name)
487 group = map_groups(name)
490 unicode_name = safe_unicode(name)
488 unicode_name = safe_unicode(name)
491 db_repo = repo_model.get_by_repo_name(unicode_name)
489 db_repo = repo_model.get_by_repo_name(unicode_name)
492 # found repo that is on filesystem not in Kallithea database
490 # found repo that is on filesystem not in Kallithea database
493 if not db_repo:
491 if not db_repo:
494 log.info('repository %s not found, creating now', name)
492 log.info('repository %s not found, creating now', name)
495 added.append(name)
493 added.append(name)
496 desc = (repo.description
494 desc = (repo.description
497 if repo.description != 'unknown'
495 if repo.description != 'unknown'
498 else '%s repository' % name)
496 else '%s repository' % name)
499
497
500 new_repo = repo_model._create_repo(
498 new_repo = repo_model._create_repo(
501 repo_name=name,
499 repo_name=name,
502 repo_type=repo.alias,
500 repo_type=repo.alias,
503 description=desc,
501 description=desc,
504 repo_group=getattr(group, 'group_id', None),
502 repo_group=getattr(group, 'group_id', None),
505 owner=user,
503 owner=user,
506 enable_downloads=enable_downloads,
504 enable_downloads=enable_downloads,
507 enable_statistics=enable_statistics,
505 enable_statistics=enable_statistics,
508 private=private,
506 private=private,
509 state=Repository.STATE_CREATED
507 state=Repository.STATE_CREATED
510 )
508 )
511 sa.commit()
509 sa.commit()
512 # we added that repo just now, and make sure it has githook
510 # we added that repo just now, and make sure it has githook
513 # installed, and updated server info
511 # installed, and updated server info
514 if new_repo.repo_type == 'git':
512 if new_repo.repo_type == 'git':
515 git_repo = new_repo.scm_instance
513 git_repo = new_repo.scm_instance
516 ScmModel().install_git_hooks(git_repo)
514 ScmModel().install_git_hooks(git_repo)
517 # update repository server-info
515 # update repository server-info
518 log.debug('Running update server info')
516 log.debug('Running update server info')
519 git_repo._update_server_info()
517 git_repo._update_server_info()
520 new_repo.update_changeset_cache()
518 new_repo.update_changeset_cache()
521 elif install_git_hooks:
519 elif install_git_hooks:
522 if db_repo.repo_type == 'git':
520 if db_repo.repo_type == 'git':
523 ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks)
521 ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks)
524
522
525 removed = []
523 removed = []
526 # remove from database those repositories that are not in the filesystem
524 # remove from database those repositories that are not in the filesystem
527 unicode_initial_repo_list = set(safe_unicode(name) for name in initial_repo_list)
525 unicode_initial_repo_list = set(safe_unicode(name) for name in initial_repo_list)
528 for repo in sa.query(Repository).all():
526 for repo in sa.query(Repository).all():
529 if repo.repo_name not in unicode_initial_repo_list:
527 if repo.repo_name not in unicode_initial_repo_list:
530 if remove_obsolete:
528 if remove_obsolete:
531 log.debug("Removing non-existing repository found in db `%s`",
529 log.debug("Removing non-existing repository found in db `%s`",
532 repo.repo_name)
530 repo.repo_name)
533 try:
531 try:
534 RepoModel().delete(repo, forks='detach', fs_remove=False)
532 RepoModel().delete(repo, forks='detach', fs_remove=False)
535 sa.commit()
533 sa.commit()
536 except Exception:
534 except Exception:
537 #don't hold further removals on error
535 #don't hold further removals on error
538 log.error(traceback.format_exc())
536 log.error(traceback.format_exc())
539 sa.rollback()
537 sa.rollback()
540 removed.append(repo.repo_name)
538 removed.append(repo.repo_name)
541 return added, removed
539 return added, removed
542
540
543
541
544 def load_rcextensions(root_path):
542 def load_rcextensions(root_path):
545 import kallithea
543 import kallithea
546 from kallithea.config import conf
544 from kallithea.config import conf
547
545
548 path = os.path.join(root_path, 'rcextensions', '__init__.py')
546 path = os.path.join(root_path, 'rcextensions', '__init__.py')
549 if os.path.isfile(path):
547 if os.path.isfile(path):
550 rcext = create_module('rc', path)
548 rcext = create_module('rc', path)
551 EXT = kallithea.EXTENSIONS = rcext
549 EXT = kallithea.EXTENSIONS = rcext
552 log.debug('Found rcextensions now loading %s...', rcext)
550 log.debug('Found rcextensions now loading %s...', rcext)
553
551
554 # Additional mappings that are not present in the pygments lexers
552 # Additional mappings that are not present in the pygments lexers
555 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
553 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
556
554
557 # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
555 # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
558
556
559 if getattr(EXT, 'INDEX_EXTENSIONS', []):
557 if getattr(EXT, 'INDEX_EXTENSIONS', []):
560 log.debug('settings custom INDEX_EXTENSIONS')
558 log.debug('settings custom INDEX_EXTENSIONS')
561 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
559 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
562
560
563 # ADDITIONAL MAPPINGS
561 # ADDITIONAL MAPPINGS
564 log.debug('adding extra into INDEX_EXTENSIONS')
562 log.debug('adding extra into INDEX_EXTENSIONS')
565 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
563 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
566
564
567 # auto check if the module is not missing any data, set to default if is
565 # auto check if the module is not missing any data, set to default if is
568 # this will help autoupdate new feature of rcext module
566 # this will help autoupdate new feature of rcext module
569 #from kallithea.config import rcextensions
567 #from kallithea.config import rcextensions
570 #for k in dir(rcextensions):
568 #for k in dir(rcextensions):
571 # if not k.startswith('_') and not hasattr(EXT, k):
569 # if not k.startswith('_') and not hasattr(EXT, k):
572 # setattr(EXT, k, getattr(rcextensions, k))
570 # setattr(EXT, k, getattr(rcextensions, k))
573
571
574
572
575 #==============================================================================
573 #==============================================================================
576 # MISC
574 # MISC
577 #==============================================================================
575 #==============================================================================
578
576
579 git_req_ver = StrictVersion('1.7.4')
577 git_req_ver = StrictVersion('1.7.4')
580
578
581 def check_git_version():
579 def check_git_version():
582 """
580 """
583 Checks what version of git is installed on the system, and raise a system exit
581 Checks what version of git is installed on the system, and raise a system exit
584 if it's too old for Kallithea to work properly.
582 if it's too old for Kallithea to work properly.
585 """
583 """
586 from kallithea import BACKENDS
584 from kallithea import BACKENDS
587 from kallithea.lib.vcs.backends.git.repository import GitRepository
585 from kallithea.lib.vcs.backends.git.repository import GitRepository
588 from kallithea.lib.vcs.conf import settings
586 from kallithea.lib.vcs.conf import settings
589
587
590 if 'git' not in BACKENDS:
588 if 'git' not in BACKENDS:
591 return None
589 return None
592
590
593 if not settings.GIT_EXECUTABLE_PATH:
591 if not settings.GIT_EXECUTABLE_PATH:
594 log.warning('No git executable configured - check "git_path" in the ini file.')
592 log.warning('No git executable configured - check "git_path" in the ini file.')
595 return None
593 return None
596
594
597 stdout, stderr = GitRepository._run_git_command(['--version'], _bare=True,
595 stdout, stderr = GitRepository._run_git_command(['--version'], _bare=True,
598 _safe=True)
596 _safe=True)
599
597
600 if stderr:
598 if stderr:
601 log.warning('Error/stderr from "%s --version": %r', settings.GIT_EXECUTABLE_PATH, stderr)
599 log.warning('Error/stderr from "%s --version": %r', settings.GIT_EXECUTABLE_PATH, stderr)
602
600
603 m = re.search(r"\d+.\d+.\d+", stdout)
601 m = re.search(r"\d+.\d+.\d+", stdout)
604 if m:
602 if m:
605 ver = StrictVersion(m.group(0))
603 ver = StrictVersion(m.group(0))
606 log.debug('Git executable: "%s", version %s (parsed from: "%s")',
604 log.debug('Git executable: "%s", version %s (parsed from: "%s")',
607 settings.GIT_EXECUTABLE_PATH, ver, stdout.strip())
605 settings.GIT_EXECUTABLE_PATH, ver, stdout.strip())
608 if ver < git_req_ver:
606 if ver < git_req_ver:
609 log.error('Kallithea detected %s version %s, which is too old '
607 log.error('Kallithea detected %s version %s, which is too old '
610 'for the system to function properly. '
608 'for the system to function properly. '
611 'Please upgrade to version %s or later. '
609 'Please upgrade to version %s or later. '
612 'If you strictly need Mercurial repositories, you can '
610 'If you strictly need Mercurial repositories, you can '
613 'clear the "git_path" setting in the ini file.',
611 'clear the "git_path" setting in the ini file.',
614 settings.GIT_EXECUTABLE_PATH, ver, git_req_ver)
612 settings.GIT_EXECUTABLE_PATH, ver, git_req_ver)
615 log.error("Terminating ...")
613 log.error("Terminating ...")
616 sys.exit(1)
614 sys.exit(1)
617 else:
615 else:
618 ver = StrictVersion('0.0.0')
616 ver = StrictVersion('0.0.0')
619 log.warning('Error finding version number in "%s --version" stdout: %r',
617 log.warning('Error finding version number in "%s --version" stdout: %r',
620 settings.GIT_EXECUTABLE_PATH, stdout.strip())
618 settings.GIT_EXECUTABLE_PATH, stdout.strip())
621
619
622 return ver
620 return ver
623
621
624
622
625 #===============================================================================
623 #===============================================================================
626 # CACHE RELATED METHODS
624 # CACHE RELATED METHODS
627 #===============================================================================
625 #===============================================================================
628
626
629 # set cache regions for beaker so celery can utilise it
627 # set cache regions for beaker so celery can utilise it
630 def setup_cache_regions(settings):
628 def setup_cache_regions(settings):
631 # Create dict with just beaker cache configs with prefix stripped
629 # Create dict with just beaker cache configs with prefix stripped
632 cache_settings = {'regions': None}
630 cache_settings = {'regions': None}
633 prefix = 'beaker.cache.'
631 prefix = 'beaker.cache.'
634 for key in settings:
632 for key in settings:
635 if key.startswith(prefix):
633 if key.startswith(prefix):
636 name = key[len(prefix):]
634 name = key[len(prefix):]
637 cache_settings[name] = settings[key]
635 cache_settings[name] = settings[key]
638 # Find all regions, apply defaults, and apply to beaker
636 # Find all regions, apply defaults, and apply to beaker
639 if cache_settings['regions']:
637 if cache_settings['regions']:
640 for region in cache_settings['regions'].split(','):
638 for region in cache_settings['regions'].split(','):
641 region = region.strip()
639 region = region.strip()
642 prefix = region + '.'
640 prefix = region + '.'
643 region_settings = {}
641 region_settings = {}
644 for key in cache_settings:
642 for key in cache_settings:
645 if key.startswith(prefix):
643 if key.startswith(prefix):
646 name = key[len(prefix):]
644 name = key[len(prefix):]
647 region_settings[name] = cache_settings[key]
645 region_settings[name] = cache_settings[key]
648 region_settings.setdefault('expire',
646 region_settings.setdefault('expire',
649 cache_settings.get('expire', '60'))
647 cache_settings.get('expire', '60'))
650 region_settings.setdefault('lock_dir',
648 region_settings.setdefault('lock_dir',
651 cache_settings.get('lock_dir'))
649 cache_settings.get('lock_dir'))
652 region_settings.setdefault('data_dir',
650 region_settings.setdefault('data_dir',
653 cache_settings.get('data_dir'))
651 cache_settings.get('data_dir'))
654 region_settings.setdefault('type',
652 region_settings.setdefault('type',
655 cache_settings.get('type', 'memory'))
653 cache_settings.get('type', 'memory'))
656 beaker.cache.cache_regions[region] = region_settings
654 beaker.cache.cache_regions[region] = region_settings
657
655
658
656
659 def conditional_cache(region, prefix, condition, func):
657 def conditional_cache(region, prefix, condition, func):
660 """
658 """
661
659
662 Conditional caching function use like::
660 Conditional caching function use like::
663 def _c(arg):
661 def _c(arg):
664 #heavy computation function
662 #heavy computation function
665 return data
663 return data
666
664
667 # depending from condition the compute is wrapped in cache or not
665 # depending from condition the compute is wrapped in cache or not
668 compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func)
666 compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func)
669 return compute(arg)
667 return compute(arg)
670
668
671 :param region: name of cache region
669 :param region: name of cache region
672 :param prefix: cache region prefix
670 :param prefix: cache region prefix
673 :param condition: condition for cache to be triggered, and return data cached
671 :param condition: condition for cache to be triggered, and return data cached
674 :param func: wrapped heavy function to compute
672 :param func: wrapped heavy function to compute
675
673
676 """
674 """
677 wrapped = func
675 wrapped = func
678 if condition:
676 if condition:
679 log.debug('conditional_cache: True, wrapping call of '
677 log.debug('conditional_cache: True, wrapping call of '
680 'func: %s into %s region cache' % (region, func))
678 'func: %s into %s region cache' % (region, func))
681 wrapped = _cache_decorate((prefix,), None, None, region)(func)
679 wrapped = _cache_decorate((prefix,), None, None, region)(func)
682
680
683 return wrapped
681 return wrapped
@@ -1,2548 +1,2548 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.db
15 kallithea.model.db
16 ~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~
17
17
18 Database Models for Kallithea
18 Database Models for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 08, 2010
22 :created_on: Apr 08, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import collections
28 import collections
29 import datetime
29 import datetime
30 import functools
30 import functools
31 import hashlib
31 import hashlib
32 import logging
32 import logging
33 import os
33 import os
34 import time
34 import time
35 import traceback
35 import traceback
36
36
37 import ipaddr
37 import ipaddr
38 import sqlalchemy
38 import sqlalchemy
39 from beaker.cache import cache_region, region_invalidate
39 from beaker.cache import cache_region, region_invalidate
40 from sqlalchemy import *
40 from sqlalchemy import *
41 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.orm import class_mapper, joinedload, relationship, validates
42 from sqlalchemy.orm import class_mapper, joinedload, relationship, validates
43 from tg.i18n import lazy_ugettext as _
43 from tg.i18n import lazy_ugettext as _
44 from webob.exc import HTTPNotFound
44 from webob.exc import HTTPNotFound
45
45
46 import kallithea
46 import kallithea
47 from kallithea.lib.caching_query import FromCache
47 from kallithea.lib.caching_query import FromCache
48 from kallithea.lib.compat import json
48 from kallithea.lib.compat import json
49 from kallithea.lib.exceptions import DefaultUserException
49 from kallithea.lib.exceptions import DefaultUserException
50 from kallithea.lib.utils2 import Optional, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_int, safe_str, safe_unicode, str2bool, urlreadable
50 from kallithea.lib.utils2 import Optional, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_int, safe_str, safe_unicode, str2bool, urlreadable
51 from kallithea.lib.vcs import get_backend
51 from kallithea.lib.vcs import get_backend
52 from kallithea.lib.vcs.backends.base import EmptyChangeset
52 from kallithea.lib.vcs.backends.base import EmptyChangeset
53 from kallithea.lib.vcs.utils.helpers import get_scm
53 from kallithea.lib.vcs.utils.helpers import get_scm
54 from kallithea.lib.vcs.utils.lazy import LazyProperty
54 from kallithea.lib.vcs.utils.lazy import LazyProperty
55 from kallithea.model.meta import Base, Session
55 from kallithea.model.meta import Base, Session
56
56
57
57
58 URL_SEP = '/'
58 URL_SEP = '/'
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61 #==============================================================================
61 #==============================================================================
62 # BASE CLASSES
62 # BASE CLASSES
63 #==============================================================================
63 #==============================================================================
64
64
65 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
65 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
66
66
67
67
68 class BaseDbModel(object):
68 class BaseDbModel(object):
69 """
69 """
70 Base Model for all classes
70 Base Model for all classes
71 """
71 """
72
72
73 @classmethod
73 @classmethod
74 def _get_keys(cls):
74 def _get_keys(cls):
75 """return column names for this model """
75 """return column names for this model """
76 return class_mapper(cls).c.keys()
76 return class_mapper(cls).c.keys()
77
77
78 def get_dict(self):
78 def get_dict(self):
79 """
79 """
80 return dict with keys and values corresponding
80 return dict with keys and values corresponding
81 to this model data """
81 to this model data """
82
82
83 d = {}
83 d = {}
84 for k in self._get_keys():
84 for k in self._get_keys():
85 d[k] = getattr(self, k)
85 d[k] = getattr(self, k)
86
86
87 # also use __json__() if present to get additional fields
87 # also use __json__() if present to get additional fields
88 _json_attr = getattr(self, '__json__', None)
88 _json_attr = getattr(self, '__json__', None)
89 if _json_attr:
89 if _json_attr:
90 # update with attributes from __json__
90 # update with attributes from __json__
91 if callable(_json_attr):
91 if callable(_json_attr):
92 _json_attr = _json_attr()
92 _json_attr = _json_attr()
93 for k, val in _json_attr.iteritems():
93 for k, val in _json_attr.iteritems():
94 d[k] = val
94 d[k] = val
95 return d
95 return d
96
96
97 def get_appstruct(self):
97 def get_appstruct(self):
98 """return list with keys and values tuples corresponding
98 """return list with keys and values tuples corresponding
99 to this model data """
99 to this model data """
100
100
101 return [
101 return [
102 (k, getattr(self, k))
102 (k, getattr(self, k))
103 for k in self._get_keys()
103 for k in self._get_keys()
104 ]
104 ]
105
105
106 def populate_obj(self, populate_dict):
106 def populate_obj(self, populate_dict):
107 """populate model with data from given populate_dict"""
107 """populate model with data from given populate_dict"""
108
108
109 for k in self._get_keys():
109 for k in self._get_keys():
110 if k in populate_dict:
110 if k in populate_dict:
111 setattr(self, k, populate_dict[k])
111 setattr(self, k, populate_dict[k])
112
112
113 @classmethod
113 @classmethod
114 def query(cls):
114 def query(cls):
115 return Session().query(cls)
115 return Session().query(cls)
116
116
117 @classmethod
117 @classmethod
118 def get(cls, id_):
118 def get(cls, id_):
119 if id_:
119 if id_:
120 return cls.query().get(id_)
120 return cls.query().get(id_)
121
121
122 @classmethod
122 @classmethod
123 def guess_instance(cls, value, callback=None):
123 def guess_instance(cls, value, callback=None):
124 """Haphazardly attempt to convert `value` to a `cls` instance.
124 """Haphazardly attempt to convert `value` to a `cls` instance.
125
125
126 If `value` is None or already a `cls` instance, return it. If `value`
126 If `value` is None or already a `cls` instance, return it. If `value`
127 is a number (or looks like one if you squint just right), assume it's
127 is a number (or looks like one if you squint just right), assume it's
128 a database primary key and let SQLAlchemy sort things out. Otherwise,
128 a database primary key and let SQLAlchemy sort things out. Otherwise,
129 fall back to resolving it using `callback` (if specified); this could
129 fall back to resolving it using `callback` (if specified); this could
130 e.g. be a function that looks up instances by name (though that won't
130 e.g. be a function that looks up instances by name (though that won't
131 work if the name begins with a digit). Otherwise, raise Exception.
131 work if the name begins with a digit). Otherwise, raise Exception.
132 """
132 """
133
133
134 if value is None:
134 if value is None:
135 return None
135 return None
136 if isinstance(value, cls):
136 if isinstance(value, cls):
137 return value
137 return value
138 if isinstance(value, (int, long)) or safe_str(value).isdigit():
138 if isinstance(value, (int, long)) or safe_str(value).isdigit():
139 return cls.get(value)
139 return cls.get(value)
140 if callback is not None:
140 if callback is not None:
141 return callback(value)
141 return callback(value)
142
142
143 raise Exception(
143 raise Exception(
144 'given object must be int, long or Instance of %s '
144 'given object must be int, long or Instance of %s '
145 'got %s, no callback provided' % (cls, type(value))
145 'got %s, no callback provided' % (cls, type(value))
146 )
146 )
147
147
148 @classmethod
148 @classmethod
149 def get_or_404(cls, id_):
149 def get_or_404(cls, id_):
150 try:
150 try:
151 id_ = int(id_)
151 id_ = int(id_)
152 except (TypeError, ValueError):
152 except (TypeError, ValueError):
153 raise HTTPNotFound
153 raise HTTPNotFound
154
154
155 res = cls.query().get(id_)
155 res = cls.query().get(id_)
156 if res is None:
156 if res is None:
157 raise HTTPNotFound
157 raise HTTPNotFound
158 return res
158 return res
159
159
160 @classmethod
160 @classmethod
161 def delete(cls, id_):
161 def delete(cls, id_):
162 obj = cls.query().get(id_)
162 obj = cls.query().get(id_)
163 Session().delete(obj)
163 Session().delete(obj)
164
164
165 def __repr__(self):
165 def __repr__(self):
166 if hasattr(self, '__unicode__'):
166 if hasattr(self, '__unicode__'):
167 # python repr needs to return str
167 # python repr needs to return str
168 try:
168 try:
169 return safe_str(self.__unicode__())
169 return safe_str(self.__unicode__())
170 except UnicodeDecodeError:
170 except UnicodeDecodeError:
171 pass
171 pass
172 return '<DB:%s>' % (self.__class__.__name__)
172 return '<DB:%s>' % (self.__class__.__name__)
173
173
174
174
175 _table_args_default_dict = {'extend_existing': True,
175 _table_args_default_dict = {'extend_existing': True,
176 'mysql_engine': 'InnoDB',
176 'mysql_engine': 'InnoDB',
177 'mysql_charset': 'utf8',
177 'mysql_charset': 'utf8',
178 'sqlite_autoincrement': True,
178 'sqlite_autoincrement': True,
179 }
179 }
180
180
181 class Setting(Base, BaseDbModel):
181 class Setting(Base, BaseDbModel):
182 __tablename__ = 'settings'
182 __tablename__ = 'settings'
183 __table_args__ = (
183 __table_args__ = (
184 _table_args_default_dict,
184 _table_args_default_dict,
185 )
185 )
186
186
187 SETTINGS_TYPES = {
187 SETTINGS_TYPES = {
188 'str': safe_str,
188 'str': safe_str,
189 'int': safe_int,
189 'int': safe_int,
190 'unicode': safe_unicode,
190 'unicode': safe_unicode,
191 'bool': str2bool,
191 'bool': str2bool,
192 'list': functools.partial(aslist, sep=',')
192 'list': functools.partial(aslist, sep=',')
193 }
193 }
194 DEFAULT_UPDATE_URL = ''
194 DEFAULT_UPDATE_URL = ''
195
195
196 app_settings_id = Column(Integer(), primary_key=True)
196 app_settings_id = Column(Integer(), primary_key=True)
197 app_settings_name = Column(String(255), nullable=False, unique=True)
197 app_settings_name = Column(String(255), nullable=False, unique=True)
198 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
198 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
199 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
199 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
200
200
201 def __init__(self, key='', val='', type='unicode'):
201 def __init__(self, key='', val='', type='unicode'):
202 self.app_settings_name = key
202 self.app_settings_name = key
203 self.app_settings_value = val
203 self.app_settings_value = val
204 self.app_settings_type = type
204 self.app_settings_type = type
205
205
206 @validates('_app_settings_value')
206 @validates('_app_settings_value')
207 def validate_settings_value(self, key, val):
207 def validate_settings_value(self, key, val):
208 assert type(val) == unicode
208 assert type(val) == unicode
209 return val
209 return val
210
210
211 @hybrid_property
211 @hybrid_property
212 def app_settings_value(self):
212 def app_settings_value(self):
213 v = self._app_settings_value
213 v = self._app_settings_value
214 _type = self.app_settings_type
214 _type = self.app_settings_type
215 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
215 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
216 return converter(v)
216 return converter(v)
217
217
218 @app_settings_value.setter
218 @app_settings_value.setter
219 def app_settings_value(self, val):
219 def app_settings_value(self, val):
220 """
220 """
221 Setter that will always make sure we use unicode in app_settings_value
221 Setter that will always make sure we use unicode in app_settings_value
222
222
223 :param val:
223 :param val:
224 """
224 """
225 self._app_settings_value = safe_unicode(val)
225 self._app_settings_value = safe_unicode(val)
226
226
227 @hybrid_property
227 @hybrid_property
228 def app_settings_type(self):
228 def app_settings_type(self):
229 return self._app_settings_type
229 return self._app_settings_type
230
230
231 @app_settings_type.setter
231 @app_settings_type.setter
232 def app_settings_type(self, val):
232 def app_settings_type(self, val):
233 if val not in self.SETTINGS_TYPES:
233 if val not in self.SETTINGS_TYPES:
234 raise Exception('type must be one of %s got %s'
234 raise Exception('type must be one of %s got %s'
235 % (self.SETTINGS_TYPES.keys(), val))
235 % (self.SETTINGS_TYPES.keys(), val))
236 self._app_settings_type = val
236 self._app_settings_type = val
237
237
238 def __unicode__(self):
238 def __unicode__(self):
239 return u"<%s('%s:%s[%s]')>" % (
239 return u"<%s('%s:%s[%s]')>" % (
240 self.__class__.__name__,
240 self.__class__.__name__,
241 self.app_settings_name, self.app_settings_value, self.app_settings_type
241 self.app_settings_name, self.app_settings_value, self.app_settings_type
242 )
242 )
243
243
244 @classmethod
244 @classmethod
245 def get_by_name(cls, key):
245 def get_by_name(cls, key):
246 return cls.query() \
246 return cls.query() \
247 .filter(cls.app_settings_name == key).scalar()
247 .filter(cls.app_settings_name == key).scalar()
248
248
249 @classmethod
249 @classmethod
250 def get_by_name_or_create(cls, key, val='', type='unicode'):
250 def get_by_name_or_create(cls, key, val='', type='unicode'):
251 res = cls.get_by_name(key)
251 res = cls.get_by_name(key)
252 if res is None:
252 if res is None:
253 res = cls(key, val, type)
253 res = cls(key, val, type)
254 return res
254 return res
255
255
256 @classmethod
256 @classmethod
257 def create_or_update(cls, key, val=Optional(''), type=Optional('unicode')):
257 def create_or_update(cls, key, val=Optional(''), type=Optional('unicode')):
258 """
258 """
259 Creates or updates Kallithea setting. If updates are triggered, it will only
259 Creates or updates Kallithea setting. If updates are triggered, it will only
260 update parameters that are explicitly set. Optional instance will be skipped.
260 update parameters that are explicitly set. Optional instance will be skipped.
261
261
262 :param key:
262 :param key:
263 :param val:
263 :param val:
264 :param type:
264 :param type:
265 :return:
265 :return:
266 """
266 """
267 res = cls.get_by_name(key)
267 res = cls.get_by_name(key)
268 if res is None:
268 if res is None:
269 val = Optional.extract(val)
269 val = Optional.extract(val)
270 type = Optional.extract(type)
270 type = Optional.extract(type)
271 res = cls(key, val, type)
271 res = cls(key, val, type)
272 Session().add(res)
272 Session().add(res)
273 else:
273 else:
274 res.app_settings_name = key
274 res.app_settings_name = key
275 if not isinstance(val, Optional):
275 if not isinstance(val, Optional):
276 # update if set
276 # update if set
277 res.app_settings_value = val
277 res.app_settings_value = val
278 if not isinstance(type, Optional):
278 if not isinstance(type, Optional):
279 # update if set
279 # update if set
280 res.app_settings_type = type
280 res.app_settings_type = type
281 return res
281 return res
282
282
283 @classmethod
283 @classmethod
284 def get_app_settings(cls, cache=False):
284 def get_app_settings(cls, cache=False):
285
285
286 ret = cls.query()
286 ret = cls.query()
287
287
288 if cache:
288 if cache:
289 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
289 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
290
290
291 if ret is None:
291 if ret is None:
292 raise Exception('Could not get application settings !')
292 raise Exception('Could not get application settings !')
293 settings = {}
293 settings = {}
294 for each in ret:
294 for each in ret:
295 settings[each.app_settings_name] = \
295 settings[each.app_settings_name] = \
296 each.app_settings_value
296 each.app_settings_value
297
297
298 return settings
298 return settings
299
299
300 @classmethod
300 @classmethod
301 def get_auth_settings(cls, cache=False):
301 def get_auth_settings(cls, cache=False):
302 ret = cls.query() \
302 ret = cls.query() \
303 .filter(cls.app_settings_name.startswith('auth_')).all()
303 .filter(cls.app_settings_name.startswith('auth_')).all()
304 fd = {}
304 fd = {}
305 for row in ret:
305 for row in ret:
306 fd[row.app_settings_name] = row.app_settings_value
306 fd[row.app_settings_name] = row.app_settings_value
307 return fd
307 return fd
308
308
309 @classmethod
309 @classmethod
310 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
310 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
311 ret = cls.query() \
311 ret = cls.query() \
312 .filter(cls.app_settings_name.startswith('default_')).all()
312 .filter(cls.app_settings_name.startswith('default_')).all()
313 fd = {}
313 fd = {}
314 for row in ret:
314 for row in ret:
315 key = row.app_settings_name
315 key = row.app_settings_name
316 if strip_prefix:
316 if strip_prefix:
317 key = remove_prefix(key, prefix='default_')
317 key = remove_prefix(key, prefix='default_')
318 fd.update({key: row.app_settings_value})
318 fd.update({key: row.app_settings_value})
319
319
320 return fd
320 return fd
321
321
322 @classmethod
322 @classmethod
323 def get_server_info(cls):
323 def get_server_info(cls):
324 import pkg_resources
324 import pkg_resources
325 import platform
325 import platform
326 from kallithea.lib.utils import check_git_version
326 from kallithea.lib.utils import check_git_version
327 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
327 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
328 info = {
328 info = {
329 'modules': sorted(mods, key=lambda k: k[0].lower()),
329 'modules': sorted(mods, key=lambda k: k[0].lower()),
330 'py_version': platform.python_version(),
330 'py_version': platform.python_version(),
331 'platform': safe_unicode(platform.platform()),
331 'platform': safe_unicode(platform.platform()),
332 'kallithea_version': kallithea.__version__,
332 'kallithea_version': kallithea.__version__,
333 'git_version': safe_unicode(check_git_version()),
333 'git_version': safe_unicode(check_git_version()),
334 'git_path': kallithea.CONFIG.get('git_path')
334 'git_path': kallithea.CONFIG.get('git_path')
335 }
335 }
336 return info
336 return info
337
337
338
338
339 class Ui(Base, BaseDbModel):
339 class Ui(Base, BaseDbModel):
340 __tablename__ = 'ui'
340 __tablename__ = 'ui'
341 __table_args__ = (
341 __table_args__ = (
342 # FIXME: ui_key as key is wrong and should be removed when the corresponding
342 # FIXME: ui_key as key is wrong and should be removed when the corresponding
343 # Ui.get_by_key has been replaced by the composite key
343 # Ui.get_by_key has been replaced by the composite key
344 UniqueConstraint('ui_key'),
344 UniqueConstraint('ui_key'),
345 UniqueConstraint('ui_section', 'ui_key'),
345 UniqueConstraint('ui_section', 'ui_key'),
346 _table_args_default_dict,
346 _table_args_default_dict,
347 )
347 )
348
348
349 HOOK_UPDATE = 'changegroup.update'
349 HOOK_UPDATE = 'changegroup.update'
350 HOOK_REPO_SIZE = 'changegroup.repo_size'
350 HOOK_REPO_SIZE = 'changegroup.repo_size'
351
351
352 ui_id = Column(Integer(), primary_key=True)
352 ui_id = Column(Integer(), primary_key=True)
353 ui_section = Column(String(255), nullable=False)
353 ui_section = Column(String(255), nullable=False)
354 ui_key = Column(String(255), nullable=False)
354 ui_key = Column(String(255), nullable=False)
355 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
355 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
356 ui_active = Column(Boolean(), nullable=False, default=True)
356 ui_active = Column(Boolean(), nullable=False, default=True)
357
357
358 @classmethod
358 @classmethod
359 def get_by_key(cls, section, key):
359 def get_by_key(cls, section, key):
360 """ Return specified Ui object, or None if not found. """
360 """ Return specified Ui object, or None if not found. """
361 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
361 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
362
362
363 @classmethod
363 @classmethod
364 def get_or_create(cls, section, key):
364 def get_or_create(cls, section, key):
365 """ Return specified Ui object, creating it if necessary. """
365 """ Return specified Ui object, creating it if necessary. """
366 setting = cls.get_by_key(section, key)
366 setting = cls.get_by_key(section, key)
367 if setting is None:
367 if setting is None:
368 setting = cls(ui_section=section, ui_key=key)
368 setting = cls(ui_section=section, ui_key=key)
369 Session().add(setting)
369 Session().add(setting)
370 return setting
370 return setting
371
371
372 @classmethod
372 @classmethod
373 def get_builtin_hooks(cls):
373 def get_builtin_hooks(cls):
374 q = cls.query()
374 q = cls.query()
375 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
375 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
376 q = q.filter(cls.ui_section == 'hooks')
376 q = q.filter(cls.ui_section == 'hooks')
377 return q.all()
377 return q.all()
378
378
379 @classmethod
379 @classmethod
380 def get_custom_hooks(cls):
380 def get_custom_hooks(cls):
381 q = cls.query()
381 q = cls.query()
382 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
382 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
383 q = q.filter(cls.ui_section == 'hooks')
383 q = q.filter(cls.ui_section == 'hooks')
384 return q.all()
384 return q.all()
385
385
386 @classmethod
386 @classmethod
387 def get_repos_location(cls):
387 def get_repos_location(cls):
388 return cls.get_by_key('paths', '/').ui_value
388 return cls.get_by_key('paths', '/').ui_value
389
389
390 @classmethod
390 @classmethod
391 def create_or_update_hook(cls, key, val):
391 def create_or_update_hook(cls, key, val):
392 new_ui = cls.get_or_create('hooks', key)
392 new_ui = cls.get_or_create('hooks', key)
393 new_ui.ui_active = True
393 new_ui.ui_active = True
394 new_ui.ui_value = val
394 new_ui.ui_value = val
395
395
396 def __repr__(self):
396 def __repr__(self):
397 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
397 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
398 self.ui_key, self.ui_value)
398 self.ui_key, self.ui_value)
399
399
400
400
401 class User(Base, BaseDbModel):
401 class User(Base, BaseDbModel):
402 __tablename__ = 'users'
402 __tablename__ = 'users'
403 __table_args__ = (
403 __table_args__ = (
404 Index('u_username_idx', 'username'),
404 Index('u_username_idx', 'username'),
405 Index('u_email_idx', 'email'),
405 Index('u_email_idx', 'email'),
406 _table_args_default_dict,
406 _table_args_default_dict,
407 )
407 )
408
408
409 DEFAULT_USER = 'default'
409 DEFAULT_USER = 'default'
410 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
410 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
411 # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
411 # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
412 DEFAULT_AUTH_TYPE = 'internal'
412 DEFAULT_AUTH_TYPE = 'internal'
413
413
414 user_id = Column(Integer(), primary_key=True)
414 user_id = Column(Integer(), primary_key=True)
415 username = Column(String(255), nullable=False, unique=True)
415 username = Column(String(255), nullable=False, unique=True)
416 password = Column(String(255), nullable=False)
416 password = Column(String(255), nullable=False)
417 active = Column(Boolean(), nullable=False, default=True)
417 active = Column(Boolean(), nullable=False, default=True)
418 admin = Column(Boolean(), nullable=False, default=False)
418 admin = Column(Boolean(), nullable=False, default=False)
419 name = Column("firstname", Unicode(255), nullable=False)
419 name = Column("firstname", Unicode(255), nullable=False)
420 lastname = Column(Unicode(255), nullable=False)
420 lastname = Column(Unicode(255), nullable=False)
421 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
421 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
422 last_login = Column(DateTime(timezone=False), nullable=True)
422 last_login = Column(DateTime(timezone=False), nullable=True)
423 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
423 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
424 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
424 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
425 api_key = Column(String(255), nullable=False)
425 api_key = Column(String(255), nullable=False)
426 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
426 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
427 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
427 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
428
428
429 user_log = relationship('UserLog')
429 user_log = relationship('UserLog')
430 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
430 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
431
431
432 repositories = relationship('Repository')
432 repositories = relationship('Repository')
433 repo_groups = relationship('RepoGroup')
433 repo_groups = relationship('RepoGroup')
434 user_groups = relationship('UserGroup')
434 user_groups = relationship('UserGroup')
435 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
435 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
436 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
436 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
437
437
438 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
438 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
439 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
439 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
440
440
441 group_member = relationship('UserGroupMember', cascade='all')
441 group_member = relationship('UserGroupMember', cascade='all')
442
442
443 # comments created by this user
443 # comments created by this user
444 user_comments = relationship('ChangesetComment', cascade='all')
444 user_comments = relationship('ChangesetComment', cascade='all')
445 # extra emails for this user
445 # extra emails for this user
446 user_emails = relationship('UserEmailMap', cascade='all')
446 user_emails = relationship('UserEmailMap', cascade='all')
447 # extra API keys
447 # extra API keys
448 user_api_keys = relationship('UserApiKeys', cascade='all')
448 user_api_keys = relationship('UserApiKeys', cascade='all')
449 ssh_keys = relationship('UserSshKeys', cascade='all')
449 ssh_keys = relationship('UserSshKeys', cascade='all')
450
450
451 @hybrid_property
451 @hybrid_property
452 def email(self):
452 def email(self):
453 return self._email
453 return self._email
454
454
455 @email.setter
455 @email.setter
456 def email(self, val):
456 def email(self, val):
457 self._email = val.lower() if val else None
457 self._email = val.lower() if val else None
458
458
459 @property
459 @property
460 def firstname(self):
460 def firstname(self):
461 # alias for future
461 # alias for future
462 return self.name
462 return self.name
463
463
464 @property
464 @property
465 def emails(self):
465 def emails(self):
466 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
466 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
467 return [self.email] + [x.email for x in other]
467 return [self.email] + [x.email for x in other]
468
468
469 @property
469 @property
470 def api_keys(self):
470 def api_keys(self):
471 other = UserApiKeys.query().filter(UserApiKeys.user == self).all()
471 other = UserApiKeys.query().filter(UserApiKeys.user == self).all()
472 return [self.api_key] + [x.api_key for x in other]
472 return [self.api_key] + [x.api_key for x in other]
473
473
474 @property
474 @property
475 def ip_addresses(self):
475 def ip_addresses(self):
476 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
476 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
477 return [x.ip_addr for x in ret]
477 return [x.ip_addr for x in ret]
478
478
479 @property
479 @property
480 def full_name(self):
480 def full_name(self):
481 return '%s %s' % (self.firstname, self.lastname)
481 return '%s %s' % (self.firstname, self.lastname)
482
482
483 @property
483 @property
484 def full_name_or_username(self):
484 def full_name_or_username(self):
485 """
485 """
486 Show full name.
486 Show full name.
487 If full name is not set, fall back to username.
487 If full name is not set, fall back to username.
488 """
488 """
489 return ('%s %s' % (self.firstname, self.lastname)
489 return ('%s %s' % (self.firstname, self.lastname)
490 if (self.firstname and self.lastname) else self.username)
490 if (self.firstname and self.lastname) else self.username)
491
491
492 @property
492 @property
493 def full_name_and_username(self):
493 def full_name_and_username(self):
494 """
494 """
495 Show full name and username as 'Firstname Lastname (username)'.
495 Show full name and username as 'Firstname Lastname (username)'.
496 If full name is not set, fall back to username.
496 If full name is not set, fall back to username.
497 """
497 """
498 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
498 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
499 if (self.firstname and self.lastname) else self.username)
499 if (self.firstname and self.lastname) else self.username)
500
500
501 @property
501 @property
502 def full_contact(self):
502 def full_contact(self):
503 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
503 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
504
504
505 @property
505 @property
506 def short_contact(self):
506 def short_contact(self):
507 return '%s %s' % (self.firstname, self.lastname)
507 return '%s %s' % (self.firstname, self.lastname)
508
508
509 @property
509 @property
510 def is_admin(self):
510 def is_admin(self):
511 return self.admin
511 return self.admin
512
512
513 @hybrid_property
513 @hybrid_property
514 def is_default_user(self):
514 def is_default_user(self):
515 return self.username == User.DEFAULT_USER
515 return self.username == User.DEFAULT_USER
516
516
517 @hybrid_property
517 @hybrid_property
518 def user_data(self):
518 def user_data(self):
519 if not self._user_data:
519 if not self._user_data:
520 return {}
520 return {}
521
521
522 try:
522 try:
523 return json.loads(self._user_data)
523 return json.loads(self._user_data)
524 except TypeError:
524 except TypeError:
525 return {}
525 return {}
526
526
527 @user_data.setter
527 @user_data.setter
528 def user_data(self, val):
528 def user_data(self, val):
529 try:
529 try:
530 self._user_data = json.dumps(val)
530 self._user_data = json.dumps(val)
531 except Exception:
531 except Exception:
532 log.error(traceback.format_exc())
532 log.error(traceback.format_exc())
533
533
534 def __unicode__(self):
534 def __unicode__(self):
535 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
535 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
536 self.user_id, self.username)
536 self.user_id, self.username)
537
537
538 @classmethod
538 @classmethod
539 def guess_instance(cls, value):
539 def guess_instance(cls, value):
540 return super(User, cls).guess_instance(value, User.get_by_username)
540 return super(User, cls).guess_instance(value, User.get_by_username)
541
541
542 @classmethod
542 @classmethod
543 def get_or_404(cls, id_, allow_default=True):
543 def get_or_404(cls, id_, allow_default=True):
544 '''
544 '''
545 Overridden version of BaseDbModel.get_or_404, with an extra check on
545 Overridden version of BaseDbModel.get_or_404, with an extra check on
546 the default user.
546 the default user.
547 '''
547 '''
548 user = super(User, cls).get_or_404(id_)
548 user = super(User, cls).get_or_404(id_)
549 if not allow_default and user.is_default_user:
549 if not allow_default and user.is_default_user:
550 raise DefaultUserException()
550 raise DefaultUserException()
551 return user
551 return user
552
552
553 @classmethod
553 @classmethod
554 def get_by_username_or_email(cls, username_or_email, case_insensitive=False, cache=False):
554 def get_by_username_or_email(cls, username_or_email, case_insensitive=False, cache=False):
555 """
555 """
556 For anything that looks like an email address, look up by the email address (matching
556 For anything that looks like an email address, look up by the email address (matching
557 case insensitively).
557 case insensitively).
558 For anything else, try to look up by the user name.
558 For anything else, try to look up by the user name.
559
559
560 This assumes no normal username can have '@' symbol.
560 This assumes no normal username can have '@' symbol.
561 """
561 """
562 if '@' in username_or_email:
562 if '@' in username_or_email:
563 return User.get_by_email(username_or_email, cache=cache)
563 return User.get_by_email(username_or_email, cache=cache)
564 else:
564 else:
565 return User.get_by_username(username_or_email, case_insensitive=case_insensitive, cache=cache)
565 return User.get_by_username(username_or_email, case_insensitive=case_insensitive, cache=cache)
566
566
567 @classmethod
567 @classmethod
568 def get_by_username(cls, username, case_insensitive=False, cache=False):
568 def get_by_username(cls, username, case_insensitive=False, cache=False):
569 if case_insensitive:
569 if case_insensitive:
570 q = cls.query().filter(func.lower(cls.username) == func.lower(username))
570 q = cls.query().filter(func.lower(cls.username) == func.lower(username))
571 else:
571 else:
572 q = cls.query().filter(cls.username == username)
572 q = cls.query().filter(cls.username == username)
573
573
574 if cache:
574 if cache:
575 q = q.options(FromCache(
575 q = q.options(FromCache(
576 "sql_cache_short",
576 "sql_cache_short",
577 "get_user_%s" % _hash_key(username)
577 "get_user_%s" % _hash_key(username)
578 )
578 )
579 )
579 )
580 return q.scalar()
580 return q.scalar()
581
581
582 @classmethod
582 @classmethod
583 def get_by_api_key(cls, api_key, cache=False, fallback=True):
583 def get_by_api_key(cls, api_key, cache=False, fallback=True):
584 if len(api_key) != 40 or not api_key.isalnum():
584 if len(api_key) != 40 or not api_key.isalnum():
585 return None
585 return None
586
586
587 q = cls.query().filter(cls.api_key == api_key)
587 q = cls.query().filter(cls.api_key == api_key)
588
588
589 if cache:
589 if cache:
590 q = q.options(FromCache("sql_cache_short",
590 q = q.options(FromCache("sql_cache_short",
591 "get_api_key_%s" % api_key))
591 "get_api_key_%s" % api_key))
592 res = q.scalar()
592 res = q.scalar()
593
593
594 if fallback and not res:
594 if fallback and not res:
595 # fallback to additional keys
595 # fallback to additional keys
596 _res = UserApiKeys.query().filter_by(api_key=api_key, is_expired=False).first()
596 _res = UserApiKeys.query().filter_by(api_key=api_key, is_expired=False).first()
597 if _res:
597 if _res:
598 res = _res.user
598 res = _res.user
599 if res is None or not res.active or res.is_default_user:
599 if res is None or not res.active or res.is_default_user:
600 return None
600 return None
601 return res
601 return res
602
602
603 @classmethod
603 @classmethod
604 def get_by_email(cls, email, cache=False):
604 def get_by_email(cls, email, cache=False):
605 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
605 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
606
606
607 if cache:
607 if cache:
608 q = q.options(FromCache("sql_cache_short",
608 q = q.options(FromCache("sql_cache_short",
609 "get_email_key_%s" % email))
609 "get_email_key_%s" % email))
610
610
611 ret = q.scalar()
611 ret = q.scalar()
612 if ret is None:
612 if ret is None:
613 q = UserEmailMap.query()
613 q = UserEmailMap.query()
614 # try fetching in alternate email map
614 # try fetching in alternate email map
615 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
615 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
616 q = q.options(joinedload(UserEmailMap.user))
616 q = q.options(joinedload(UserEmailMap.user))
617 if cache:
617 if cache:
618 q = q.options(FromCache("sql_cache_short",
618 q = q.options(FromCache("sql_cache_short",
619 "get_email_map_key_%s" % email))
619 "get_email_map_key_%s" % email))
620 ret = getattr(q.scalar(), 'user', None)
620 ret = getattr(q.scalar(), 'user', None)
621
621
622 return ret
622 return ret
623
623
624 @classmethod
624 @classmethod
625 def get_from_cs_author(cls, author):
625 def get_from_cs_author(cls, author):
626 """
626 """
627 Tries to get User objects out of commit author string
627 Tries to get User objects out of commit author string
628
628
629 :param author:
629 :param author:
630 """
630 """
631 from kallithea.lib.helpers import email, author_name
631 from kallithea.lib.helpers import email, author_name
632 # Valid email in the attribute passed, see if they're in the system
632 # Valid email in the attribute passed, see if they're in the system
633 _email = email(author)
633 _email = email(author)
634 if _email:
634 if _email:
635 user = cls.get_by_email(_email)
635 user = cls.get_by_email(_email)
636 if user is not None:
636 if user is not None:
637 return user
637 return user
638 # Maybe we can match by username?
638 # Maybe we can match by username?
639 _author = author_name(author)
639 _author = author_name(author)
640 user = cls.get_by_username(_author, case_insensitive=True)
640 user = cls.get_by_username(_author, case_insensitive=True)
641 if user is not None:
641 if user is not None:
642 return user
642 return user
643
643
644 def update_lastlogin(self):
644 def update_lastlogin(self):
645 """Update user lastlogin"""
645 """Update user lastlogin"""
646 self.last_login = datetime.datetime.now()
646 self.last_login = datetime.datetime.now()
647 log.debug('updated user %s lastlogin', self.username)
647 log.debug('updated user %s lastlogin', self.username)
648
648
649 @classmethod
649 @classmethod
650 def get_first_admin(cls):
650 def get_first_admin(cls):
651 user = User.query().filter(User.admin == True).first()
651 user = User.query().filter(User.admin == True).first()
652 if user is None:
652 if user is None:
653 raise Exception('Missing administrative account!')
653 raise Exception('Missing administrative account!')
654 return user
654 return user
655
655
656 @classmethod
656 @classmethod
657 def get_default_user(cls, cache=False):
657 def get_default_user(cls, cache=False):
658 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
658 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
659 if user is None:
659 if user is None:
660 raise Exception('Missing default account!')
660 raise Exception('Missing default account!')
661 return user
661 return user
662
662
663 def get_api_data(self, details=False):
663 def get_api_data(self, details=False):
664 """
664 """
665 Common function for generating user related data for API
665 Common function for generating user related data for API
666 """
666 """
667 user = self
667 user = self
668 data = dict(
668 data = dict(
669 user_id=user.user_id,
669 user_id=user.user_id,
670 username=user.username,
670 username=user.username,
671 firstname=user.name,
671 firstname=user.name,
672 lastname=user.lastname,
672 lastname=user.lastname,
673 email=user.email,
673 email=user.email,
674 emails=user.emails,
674 emails=user.emails,
675 active=user.active,
675 active=user.active,
676 admin=user.admin,
676 admin=user.admin,
677 )
677 )
678 if details:
678 if details:
679 data.update(dict(
679 data.update(dict(
680 extern_type=user.extern_type,
680 extern_type=user.extern_type,
681 extern_name=user.extern_name,
681 extern_name=user.extern_name,
682 api_key=user.api_key,
682 api_key=user.api_key,
683 api_keys=user.api_keys,
683 api_keys=user.api_keys,
684 last_login=user.last_login,
684 last_login=user.last_login,
685 ip_addresses=user.ip_addresses
685 ip_addresses=user.ip_addresses
686 ))
686 ))
687 return data
687 return data
688
688
689 def __json__(self):
689 def __json__(self):
690 data = dict(
690 data = dict(
691 full_name=self.full_name,
691 full_name=self.full_name,
692 full_name_or_username=self.full_name_or_username,
692 full_name_or_username=self.full_name_or_username,
693 short_contact=self.short_contact,
693 short_contact=self.short_contact,
694 full_contact=self.full_contact
694 full_contact=self.full_contact
695 )
695 )
696 data.update(self.get_api_data())
696 data.update(self.get_api_data())
697 return data
697 return data
698
698
699
699
700 class UserApiKeys(Base, BaseDbModel):
700 class UserApiKeys(Base, BaseDbModel):
701 __tablename__ = 'user_api_keys'
701 __tablename__ = 'user_api_keys'
702 __table_args__ = (
702 __table_args__ = (
703 Index('uak_api_key_idx', 'api_key'),
703 Index('uak_api_key_idx', 'api_key'),
704 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
704 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
705 _table_args_default_dict,
705 _table_args_default_dict,
706 )
706 )
707
707
708 user_api_key_id = Column(Integer(), primary_key=True)
708 user_api_key_id = Column(Integer(), primary_key=True)
709 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
709 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
710 api_key = Column(String(255), nullable=False, unique=True)
710 api_key = Column(String(255), nullable=False, unique=True)
711 description = Column(UnicodeText(), nullable=False)
711 description = Column(UnicodeText(), nullable=False)
712 expires = Column(Float(53), nullable=False)
712 expires = Column(Float(53), nullable=False)
713 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
713 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
714
714
715 user = relationship('User')
715 user = relationship('User')
716
716
717 @hybrid_property
717 @hybrid_property
718 def is_expired(self):
718 def is_expired(self):
719 return (self.expires != -1) & (time.time() > self.expires)
719 return (self.expires != -1) & (time.time() > self.expires)
720
720
721
721
722 class UserEmailMap(Base, BaseDbModel):
722 class UserEmailMap(Base, BaseDbModel):
723 __tablename__ = 'user_email_map'
723 __tablename__ = 'user_email_map'
724 __table_args__ = (
724 __table_args__ = (
725 Index('uem_email_idx', 'email'),
725 Index('uem_email_idx', 'email'),
726 _table_args_default_dict,
726 _table_args_default_dict,
727 )
727 )
728
728
729 email_id = Column(Integer(), primary_key=True)
729 email_id = Column(Integer(), primary_key=True)
730 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
730 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
731 _email = Column("email", String(255), nullable=False, unique=True)
731 _email = Column("email", String(255), nullable=False, unique=True)
732 user = relationship('User')
732 user = relationship('User')
733
733
734 @validates('_email')
734 @validates('_email')
735 def validate_email(self, key, email):
735 def validate_email(self, key, email):
736 # check if this email is not main one
736 # check if this email is not main one
737 main_email = Session().query(User).filter(User.email == email).scalar()
737 main_email = Session().query(User).filter(User.email == email).scalar()
738 if main_email is not None:
738 if main_email is not None:
739 raise AttributeError('email %s is present is user table' % email)
739 raise AttributeError('email %s is present is user table' % email)
740 return email
740 return email
741
741
742 @hybrid_property
742 @hybrid_property
743 def email(self):
743 def email(self):
744 return self._email
744 return self._email
745
745
746 @email.setter
746 @email.setter
747 def email(self, val):
747 def email(self, val):
748 self._email = val.lower() if val else None
748 self._email = val.lower() if val else None
749
749
750
750
751 class UserIpMap(Base, BaseDbModel):
751 class UserIpMap(Base, BaseDbModel):
752 __tablename__ = 'user_ip_map'
752 __tablename__ = 'user_ip_map'
753 __table_args__ = (
753 __table_args__ = (
754 UniqueConstraint('user_id', 'ip_addr'),
754 UniqueConstraint('user_id', 'ip_addr'),
755 _table_args_default_dict,
755 _table_args_default_dict,
756 )
756 )
757
757
758 ip_id = Column(Integer(), primary_key=True)
758 ip_id = Column(Integer(), primary_key=True)
759 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
759 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
760 ip_addr = Column(String(255), nullable=False)
760 ip_addr = Column(String(255), nullable=False)
761 active = Column(Boolean(), nullable=False, default=True)
761 active = Column(Boolean(), nullable=False, default=True)
762 user = relationship('User')
762 user = relationship('User')
763
763
764 @classmethod
764 @classmethod
765 def _get_ip_range(cls, ip_addr):
765 def _get_ip_range(cls, ip_addr):
766 net = ipaddr.IPNetwork(address=ip_addr)
766 net = ipaddr.IPNetwork(address=ip_addr)
767 return [str(net.network), str(net.broadcast)]
767 return [str(net.network), str(net.broadcast)]
768
768
769 def __json__(self):
769 def __json__(self):
770 return dict(
770 return dict(
771 ip_addr=self.ip_addr,
771 ip_addr=self.ip_addr,
772 ip_range=self._get_ip_range(self.ip_addr)
772 ip_range=self._get_ip_range(self.ip_addr)
773 )
773 )
774
774
775 def __unicode__(self):
775 def __unicode__(self):
776 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
776 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
777 self.user_id, self.ip_addr)
777 self.user_id, self.ip_addr)
778
778
779
779
780 class UserLog(Base, BaseDbModel):
780 class UserLog(Base, BaseDbModel):
781 __tablename__ = 'user_logs'
781 __tablename__ = 'user_logs'
782 __table_args__ = (
782 __table_args__ = (
783 _table_args_default_dict,
783 _table_args_default_dict,
784 )
784 )
785
785
786 user_log_id = Column(Integer(), primary_key=True)
786 user_log_id = Column(Integer(), primary_key=True)
787 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
787 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
788 username = Column(String(255), nullable=False)
788 username = Column(String(255), nullable=False)
789 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
789 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
790 repository_name = Column(Unicode(255), nullable=False)
790 repository_name = Column(Unicode(255), nullable=False)
791 user_ip = Column(String(255), nullable=True)
791 user_ip = Column(String(255), nullable=True)
792 action = Column(UnicodeText(), nullable=False)
792 action = Column(UnicodeText(), nullable=False)
793 action_date = Column(DateTime(timezone=False), nullable=False)
793 action_date = Column(DateTime(timezone=False), nullable=False)
794
794
795 def __unicode__(self):
795 def __unicode__(self):
796 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
796 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
797 self.repository_name,
797 self.repository_name,
798 self.action)
798 self.action)
799
799
800 @property
800 @property
801 def action_as_day(self):
801 def action_as_day(self):
802 return datetime.date(*self.action_date.timetuple()[:3])
802 return datetime.date(*self.action_date.timetuple()[:3])
803
803
804 user = relationship('User')
804 user = relationship('User')
805 repository = relationship('Repository', cascade='')
805 repository = relationship('Repository', cascade='')
806
806
807
807
808 class UserGroup(Base, BaseDbModel):
808 class UserGroup(Base, BaseDbModel):
809 __tablename__ = 'users_groups'
809 __tablename__ = 'users_groups'
810 __table_args__ = (
810 __table_args__ = (
811 _table_args_default_dict,
811 _table_args_default_dict,
812 )
812 )
813
813
814 users_group_id = Column(Integer(), primary_key=True)
814 users_group_id = Column(Integer(), primary_key=True)
815 users_group_name = Column(Unicode(255), nullable=False, unique=True)
815 users_group_name = Column(Unicode(255), nullable=False, unique=True)
816 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
816 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
817 users_group_active = Column(Boolean(), nullable=False)
817 users_group_active = Column(Boolean(), nullable=False)
818 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
818 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
819 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
819 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
820 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
820 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
821
821
822 members = relationship('UserGroupMember', cascade="all, delete-orphan")
822 members = relationship('UserGroupMember', cascade="all, delete-orphan")
823 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
823 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
824 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
824 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
825 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
825 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
826 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
826 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
827 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
827 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
828
828
829 owner = relationship('User')
829 owner = relationship('User')
830
830
831 @hybrid_property
831 @hybrid_property
832 def group_data(self):
832 def group_data(self):
833 if not self._group_data:
833 if not self._group_data:
834 return {}
834 return {}
835
835
836 try:
836 try:
837 return json.loads(self._group_data)
837 return json.loads(self._group_data)
838 except TypeError:
838 except TypeError:
839 return {}
839 return {}
840
840
841 @group_data.setter
841 @group_data.setter
842 def group_data(self, val):
842 def group_data(self, val):
843 try:
843 try:
844 self._group_data = json.dumps(val)
844 self._group_data = json.dumps(val)
845 except Exception:
845 except Exception:
846 log.error(traceback.format_exc())
846 log.error(traceback.format_exc())
847
847
848 def __unicode__(self):
848 def __unicode__(self):
849 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
849 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
850 self.users_group_id,
850 self.users_group_id,
851 self.users_group_name)
851 self.users_group_name)
852
852
853 @classmethod
853 @classmethod
854 def guess_instance(cls, value):
854 def guess_instance(cls, value):
855 return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
855 return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
856
856
857 @classmethod
857 @classmethod
858 def get_by_group_name(cls, group_name, cache=False,
858 def get_by_group_name(cls, group_name, cache=False,
859 case_insensitive=False):
859 case_insensitive=False):
860 if case_insensitive:
860 if case_insensitive:
861 q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name))
861 q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name))
862 else:
862 else:
863 q = cls.query().filter(cls.users_group_name == group_name)
863 q = cls.query().filter(cls.users_group_name == group_name)
864 if cache:
864 if cache:
865 q = q.options(FromCache(
865 q = q.options(FromCache(
866 "sql_cache_short",
866 "sql_cache_short",
867 "get_group_%s" % _hash_key(group_name)
867 "get_group_%s" % _hash_key(group_name)
868 )
868 )
869 )
869 )
870 return q.scalar()
870 return q.scalar()
871
871
872 @classmethod
872 @classmethod
873 def get(cls, user_group_id, cache=False):
873 def get(cls, user_group_id, cache=False):
874 user_group = cls.query()
874 user_group = cls.query()
875 if cache:
875 if cache:
876 user_group = user_group.options(FromCache("sql_cache_short",
876 user_group = user_group.options(FromCache("sql_cache_short",
877 "get_users_group_%s" % user_group_id))
877 "get_users_group_%s" % user_group_id))
878 return user_group.get(user_group_id)
878 return user_group.get(user_group_id)
879
879
880 def get_api_data(self, with_members=True):
880 def get_api_data(self, with_members=True):
881 user_group = self
881 user_group = self
882
882
883 data = dict(
883 data = dict(
884 users_group_id=user_group.users_group_id,
884 users_group_id=user_group.users_group_id,
885 group_name=user_group.users_group_name,
885 group_name=user_group.users_group_name,
886 group_description=user_group.user_group_description,
886 group_description=user_group.user_group_description,
887 active=user_group.users_group_active,
887 active=user_group.users_group_active,
888 owner=user_group.owner.username,
888 owner=user_group.owner.username,
889 )
889 )
890 if with_members:
890 if with_members:
891 data['members'] = [
891 data['members'] = [
892 ugm.user.get_api_data()
892 ugm.user.get_api_data()
893 for ugm in user_group.members
893 for ugm in user_group.members
894 ]
894 ]
895
895
896 return data
896 return data
897
897
898
898
899 class UserGroupMember(Base, BaseDbModel):
899 class UserGroupMember(Base, BaseDbModel):
900 __tablename__ = 'users_groups_members'
900 __tablename__ = 'users_groups_members'
901 __table_args__ = (
901 __table_args__ = (
902 _table_args_default_dict,
902 _table_args_default_dict,
903 )
903 )
904
904
905 users_group_member_id = Column(Integer(), primary_key=True)
905 users_group_member_id = Column(Integer(), primary_key=True)
906 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
906 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
907 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
907 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
908
908
909 user = relationship('User')
909 user = relationship('User')
910 users_group = relationship('UserGroup')
910 users_group = relationship('UserGroup')
911
911
912 def __init__(self, gr_id='', u_id=''):
912 def __init__(self, gr_id='', u_id=''):
913 self.users_group_id = gr_id
913 self.users_group_id = gr_id
914 self.user_id = u_id
914 self.user_id = u_id
915
915
916
916
917 class RepositoryField(Base, BaseDbModel):
917 class RepositoryField(Base, BaseDbModel):
918 __tablename__ = 'repositories_fields'
918 __tablename__ = 'repositories_fields'
919 __table_args__ = (
919 __table_args__ = (
920 UniqueConstraint('repository_id', 'field_key'), # no-multi field
920 UniqueConstraint('repository_id', 'field_key'), # no-multi field
921 _table_args_default_dict,
921 _table_args_default_dict,
922 )
922 )
923
923
924 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
924 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
925
925
926 repo_field_id = Column(Integer(), primary_key=True)
926 repo_field_id = Column(Integer(), primary_key=True)
927 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
927 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
928 field_key = Column(String(250), nullable=False)
928 field_key = Column(String(250), nullable=False)
929 field_label = Column(String(1024), nullable=False)
929 field_label = Column(String(1024), nullable=False)
930 field_value = Column(String(10000), nullable=False)
930 field_value = Column(String(10000), nullable=False)
931 field_desc = Column(String(1024), nullable=False)
931 field_desc = Column(String(1024), nullable=False)
932 field_type = Column(String(255), nullable=False)
932 field_type = Column(String(255), nullable=False)
933 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
933 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
934
934
935 repository = relationship('Repository')
935 repository = relationship('Repository')
936
936
937 @property
937 @property
938 def field_key_prefixed(self):
938 def field_key_prefixed(self):
939 return 'ex_%s' % self.field_key
939 return 'ex_%s' % self.field_key
940
940
941 @classmethod
941 @classmethod
942 def un_prefix_key(cls, key):
942 def un_prefix_key(cls, key):
943 if key.startswith(cls.PREFIX):
943 if key.startswith(cls.PREFIX):
944 return key[len(cls.PREFIX):]
944 return key[len(cls.PREFIX):]
945 return key
945 return key
946
946
947 @classmethod
947 @classmethod
948 def get_by_key_name(cls, key, repo):
948 def get_by_key_name(cls, key, repo):
949 row = cls.query() \
949 row = cls.query() \
950 .filter(cls.repository == repo) \
950 .filter(cls.repository == repo) \
951 .filter(cls.field_key == key).scalar()
951 .filter(cls.field_key == key).scalar()
952 return row
952 return row
953
953
954
954
955 class Repository(Base, BaseDbModel):
955 class Repository(Base, BaseDbModel):
956 __tablename__ = 'repositories'
956 __tablename__ = 'repositories'
957 __table_args__ = (
957 __table_args__ = (
958 Index('r_repo_name_idx', 'repo_name'),
958 Index('r_repo_name_idx', 'repo_name'),
959 _table_args_default_dict,
959 _table_args_default_dict,
960 )
960 )
961
961
962 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
962 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
963 DEFAULT_CLONE_SSH = 'ssh://{system_user}@{hostname}/{repo}'
963 DEFAULT_CLONE_SSH = 'ssh://{system_user}@{hostname}/{repo}'
964
964
965 STATE_CREATED = u'repo_state_created'
965 STATE_CREATED = u'repo_state_created'
966 STATE_PENDING = u'repo_state_pending'
966 STATE_PENDING = u'repo_state_pending'
967 STATE_ERROR = u'repo_state_error'
967 STATE_ERROR = u'repo_state_error'
968
968
969 repo_id = Column(Integer(), primary_key=True)
969 repo_id = Column(Integer(), primary_key=True)
970 repo_name = Column(Unicode(255), nullable=False, unique=True)
970 repo_name = Column(Unicode(255), nullable=False, unique=True)
971 repo_state = Column(String(255), nullable=False)
971 repo_state = Column(String(255), nullable=False)
972
972
973 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
973 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
974 repo_type = Column(String(255), nullable=False) # 'hg' or 'git'
974 repo_type = Column(String(255), nullable=False) # 'hg' or 'git'
975 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
975 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
976 private = Column(Boolean(), nullable=False)
976 private = Column(Boolean(), nullable=False)
977 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
977 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
978 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
978 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
979 description = Column(Unicode(10000), nullable=False)
979 description = Column(Unicode(10000), nullable=False)
980 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
980 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
981 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
981 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
982 _landing_revision = Column("landing_revision", String(255), nullable=False)
982 _landing_revision = Column("landing_revision", String(255), nullable=False)
983 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
983 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
984
984
985 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
985 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
986 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
986 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
987
987
988 owner = relationship('User')
988 owner = relationship('User')
989 fork = relationship('Repository', remote_side=repo_id)
989 fork = relationship('Repository', remote_side=repo_id)
990 group = relationship('RepoGroup')
990 group = relationship('RepoGroup')
991 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
991 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
992 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
992 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
993 stats = relationship('Statistics', cascade='all', uselist=False)
993 stats = relationship('Statistics', cascade='all', uselist=False)
994
994
995 followers = relationship('UserFollowing',
995 followers = relationship('UserFollowing',
996 primaryjoin='UserFollowing.follows_repository_id==Repository.repo_id',
996 primaryjoin='UserFollowing.follows_repository_id==Repository.repo_id',
997 cascade='all')
997 cascade='all')
998 extra_fields = relationship('RepositoryField',
998 extra_fields = relationship('RepositoryField',
999 cascade="all, delete-orphan")
999 cascade="all, delete-orphan")
1000
1000
1001 logs = relationship('UserLog')
1001 logs = relationship('UserLog')
1002 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
1002 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
1003
1003
1004 pull_requests_org = relationship('PullRequest',
1004 pull_requests_org = relationship('PullRequest',
1005 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
1005 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
1006 cascade="all, delete-orphan")
1006 cascade="all, delete-orphan")
1007
1007
1008 pull_requests_other = relationship('PullRequest',
1008 pull_requests_other = relationship('PullRequest',
1009 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
1009 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
1010 cascade="all, delete-orphan")
1010 cascade="all, delete-orphan")
1011
1011
1012 def __unicode__(self):
1012 def __unicode__(self):
1013 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1013 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1014 safe_unicode(self.repo_name))
1014 safe_unicode(self.repo_name))
1015
1015
1016 @hybrid_property
1016 @hybrid_property
1017 def landing_rev(self):
1017 def landing_rev(self):
1018 # always should return [rev_type, rev]
1018 # always should return [rev_type, rev]
1019 if self._landing_revision:
1019 if self._landing_revision:
1020 _rev_info = self._landing_revision.split(':')
1020 _rev_info = self._landing_revision.split(':')
1021 if len(_rev_info) < 2:
1021 if len(_rev_info) < 2:
1022 _rev_info.insert(0, 'rev')
1022 _rev_info.insert(0, 'rev')
1023 return [_rev_info[0], _rev_info[1]]
1023 return [_rev_info[0], _rev_info[1]]
1024 return [None, None]
1024 return [None, None]
1025
1025
1026 @landing_rev.setter
1026 @landing_rev.setter
1027 def landing_rev(self, val):
1027 def landing_rev(self, val):
1028 if ':' not in val:
1028 if ':' not in val:
1029 raise ValueError('value must be delimited with `:` and consist '
1029 raise ValueError('value must be delimited with `:` and consist '
1030 'of <rev_type>:<rev>, got %s instead' % val)
1030 'of <rev_type>:<rev>, got %s instead' % val)
1031 self._landing_revision = val
1031 self._landing_revision = val
1032
1032
1033 @hybrid_property
1033 @hybrid_property
1034 def changeset_cache(self):
1034 def changeset_cache(self):
1035 try:
1035 try:
1036 cs_cache = json.loads(self._changeset_cache) # might raise on bad data
1036 cs_cache = json.loads(self._changeset_cache) # might raise on bad data
1037 cs_cache['raw_id'] # verify data, raise exception on error
1037 cs_cache['raw_id'] # verify data, raise exception on error
1038 return cs_cache
1038 return cs_cache
1039 except (TypeError, KeyError, ValueError):
1039 except (TypeError, KeyError, ValueError):
1040 return EmptyChangeset().__json__()
1040 return EmptyChangeset().__json__()
1041
1041
1042 @changeset_cache.setter
1042 @changeset_cache.setter
1043 def changeset_cache(self, val):
1043 def changeset_cache(self, val):
1044 try:
1044 try:
1045 self._changeset_cache = json.dumps(val)
1045 self._changeset_cache = json.dumps(val)
1046 except Exception:
1046 except Exception:
1047 log.error(traceback.format_exc())
1047 log.error(traceback.format_exc())
1048
1048
1049 @classmethod
1049 @classmethod
1050 def query(cls, sorted=False):
1050 def query(cls, sorted=False):
1051 """Add Repository-specific helpers for common query constructs.
1051 """Add Repository-specific helpers for common query constructs.
1052
1052
1053 sorted: if True, apply the default ordering (name, case insensitive).
1053 sorted: if True, apply the default ordering (name, case insensitive).
1054 """
1054 """
1055 q = super(Repository, cls).query()
1055 q = super(Repository, cls).query()
1056
1056
1057 if sorted:
1057 if sorted:
1058 q = q.order_by(func.lower(Repository.repo_name))
1058 q = q.order_by(func.lower(Repository.repo_name))
1059
1059
1060 return q
1060 return q
1061
1061
1062 @classmethod
1062 @classmethod
1063 def url_sep(cls):
1063 def url_sep(cls):
1064 return URL_SEP
1064 return URL_SEP
1065
1065
1066 @classmethod
1066 @classmethod
1067 def normalize_repo_name(cls, repo_name):
1067 def normalize_repo_name(cls, repo_name):
1068 """
1068 """
1069 Normalizes os specific repo_name to the format internally stored inside
1069 Normalizes os specific repo_name to the format internally stored inside
1070 database using URL_SEP
1070 database using URL_SEP
1071
1071
1072 :param cls:
1072 :param cls:
1073 :param repo_name:
1073 :param repo_name:
1074 """
1074 """
1075 return cls.url_sep().join(repo_name.split(os.sep))
1075 return cls.url_sep().join(repo_name.split(os.sep))
1076
1076
1077 @classmethod
1077 @classmethod
1078 def guess_instance(cls, value):
1078 def guess_instance(cls, value):
1079 return super(Repository, cls).guess_instance(value, Repository.get_by_repo_name)
1079 return super(Repository, cls).guess_instance(value, Repository.get_by_repo_name)
1080
1080
1081 @classmethod
1081 @classmethod
1082 def get_by_repo_name(cls, repo_name, case_insensitive=False):
1082 def get_by_repo_name(cls, repo_name, case_insensitive=False):
1083 """Get the repo, defaulting to database case sensitivity.
1083 """Get the repo, defaulting to database case sensitivity.
1084 case_insensitive will be slower and should only be specified if necessary."""
1084 case_insensitive will be slower and should only be specified if necessary."""
1085 if case_insensitive:
1085 if case_insensitive:
1086 q = Session().query(cls).filter(func.lower(cls.repo_name) == func.lower(repo_name))
1086 q = Session().query(cls).filter(func.lower(cls.repo_name) == func.lower(repo_name))
1087 else:
1087 else:
1088 q = Session().query(cls).filter(cls.repo_name == repo_name)
1088 q = Session().query(cls).filter(cls.repo_name == repo_name)
1089 q = q.options(joinedload(Repository.fork)) \
1089 q = q.options(joinedload(Repository.fork)) \
1090 .options(joinedload(Repository.owner)) \
1090 .options(joinedload(Repository.owner)) \
1091 .options(joinedload(Repository.group))
1091 .options(joinedload(Repository.group))
1092 return q.scalar()
1092 return q.scalar()
1093
1093
1094 @classmethod
1094 @classmethod
1095 def get_by_full_path(cls, repo_full_path):
1095 def get_by_full_path(cls, repo_full_path):
1096 base_full_path = os.path.realpath(cls.base_path())
1096 base_full_path = os.path.realpath(cls.base_path())
1097 repo_full_path = os.path.realpath(repo_full_path)
1097 repo_full_path = os.path.realpath(repo_full_path)
1098 assert repo_full_path.startswith(base_full_path + os.path.sep)
1098 assert repo_full_path.startswith(base_full_path + os.path.sep)
1099 repo_name = repo_full_path[len(base_full_path) + 1:]
1099 repo_name = repo_full_path[len(base_full_path) + 1:]
1100 repo_name = cls.normalize_repo_name(repo_name)
1100 repo_name = cls.normalize_repo_name(repo_name)
1101 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1101 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1102
1102
1103 @classmethod
1103 @classmethod
1104 def get_repo_forks(cls, repo_id):
1104 def get_repo_forks(cls, repo_id):
1105 return cls.query().filter(Repository.fork_id == repo_id)
1105 return cls.query().filter(Repository.fork_id == repo_id)
1106
1106
1107 @classmethod
1107 @classmethod
1108 def base_path(cls):
1108 def base_path(cls):
1109 """
1109 """
1110 Returns base path where all repos are stored
1110 Returns base path where all repos are stored
1111
1111
1112 :param cls:
1112 :param cls:
1113 """
1113 """
1114 q = Session().query(Ui) \
1114 q = Session().query(Ui) \
1115 .filter(Ui.ui_key == cls.url_sep())
1115 .filter(Ui.ui_key == cls.url_sep())
1116 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1116 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1117 return q.one().ui_value
1117 return q.one().ui_value
1118
1118
1119 @property
1119 @property
1120 def forks(self):
1120 def forks(self):
1121 """
1121 """
1122 Return forks of this repo
1122 Return forks of this repo
1123 """
1123 """
1124 return Repository.get_repo_forks(self.repo_id)
1124 return Repository.get_repo_forks(self.repo_id)
1125
1125
1126 @property
1126 @property
1127 def parent(self):
1127 def parent(self):
1128 """
1128 """
1129 Returns fork parent
1129 Returns fork parent
1130 """
1130 """
1131 return self.fork
1131 return self.fork
1132
1132
1133 @property
1133 @property
1134 def just_name(self):
1134 def just_name(self):
1135 return self.repo_name.split(Repository.url_sep())[-1]
1135 return self.repo_name.split(Repository.url_sep())[-1]
1136
1136
1137 @property
1137 @property
1138 def groups_with_parents(self):
1138 def groups_with_parents(self):
1139 groups = []
1139 groups = []
1140 group = self.group
1140 group = self.group
1141 while group is not None:
1141 while group is not None:
1142 groups.append(group)
1142 groups.append(group)
1143 group = group.parent_group
1143 group = group.parent_group
1144 assert group not in groups, group # avoid recursion on bad db content
1144 assert group not in groups, group # avoid recursion on bad db content
1145 groups.reverse()
1145 groups.reverse()
1146 return groups
1146 return groups
1147
1147
1148 @LazyProperty
1148 @LazyProperty
1149 def repo_path(self):
1149 def repo_path(self):
1150 """
1150 """
1151 Returns base full path for that repository means where it actually
1151 Returns base full path for that repository means where it actually
1152 exists on a filesystem
1152 exists on a filesystem
1153 """
1153 """
1154 q = Session().query(Ui).filter(Ui.ui_key ==
1154 q = Session().query(Ui).filter(Ui.ui_key ==
1155 Repository.url_sep())
1155 Repository.url_sep())
1156 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1156 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1157 return q.one().ui_value
1157 return q.one().ui_value
1158
1158
1159 @property
1159 @property
1160 def repo_full_path(self):
1160 def repo_full_path(self):
1161 p = [self.repo_path]
1161 p = [self.repo_path]
1162 # we need to split the name by / since this is how we store the
1162 # we need to split the name by / since this is how we store the
1163 # names in the database, but that eventually needs to be converted
1163 # names in the database, but that eventually needs to be converted
1164 # into a valid system path
1164 # into a valid system path
1165 p += self.repo_name.split(Repository.url_sep())
1165 p += self.repo_name.split(Repository.url_sep())
1166 return os.path.join(*map(safe_unicode, p))
1166 return os.path.join(*map(safe_unicode, p))
1167
1167
1168 @property
1168 @property
1169 def cache_keys(self):
1169 def cache_keys(self):
1170 """
1170 """
1171 Returns associated cache keys for that repo
1171 Returns associated cache keys for that repo
1172 """
1172 """
1173 return CacheInvalidation.query() \
1173 return CacheInvalidation.query() \
1174 .filter(CacheInvalidation.cache_args == self.repo_name) \
1174 .filter(CacheInvalidation.cache_args == self.repo_name) \
1175 .order_by(CacheInvalidation.cache_key) \
1175 .order_by(CacheInvalidation.cache_key) \
1176 .all()
1176 .all()
1177
1177
1178 def get_new_name(self, repo_name):
1178 def get_new_name(self, repo_name):
1179 """
1179 """
1180 returns new full repository name based on assigned group and new new
1180 returns new full repository name based on assigned group and new new
1181
1181
1182 :param group_name:
1182 :param group_name:
1183 """
1183 """
1184 path_prefix = self.group.full_path_splitted if self.group else []
1184 path_prefix = self.group.full_path_splitted if self.group else []
1185 return Repository.url_sep().join(path_prefix + [repo_name])
1185 return Repository.url_sep().join(path_prefix + [repo_name])
1186
1186
1187 @property
1187 @property
1188 def _ui(self):
1188 def _ui(self):
1189 """
1189 """
1190 Creates an db based ui object for this repository
1190 Creates an db based ui object for this repository
1191 """
1191 """
1192 from kallithea.lib.utils import make_ui
1192 from kallithea.lib.utils import make_ui
1193 return make_ui(clear_session=False)
1193 return make_ui()
1194
1194
1195 @classmethod
1195 @classmethod
1196 def is_valid(cls, repo_name):
1196 def is_valid(cls, repo_name):
1197 """
1197 """
1198 returns True if given repo name is a valid filesystem repository
1198 returns True if given repo name is a valid filesystem repository
1199
1199
1200 :param cls:
1200 :param cls:
1201 :param repo_name:
1201 :param repo_name:
1202 """
1202 """
1203 from kallithea.lib.utils import is_valid_repo
1203 from kallithea.lib.utils import is_valid_repo
1204
1204
1205 return is_valid_repo(repo_name, cls.base_path())
1205 return is_valid_repo(repo_name, cls.base_path())
1206
1206
1207 def get_api_data(self, with_revision_names=False,
1207 def get_api_data(self, with_revision_names=False,
1208 with_pullrequests=False):
1208 with_pullrequests=False):
1209 """
1209 """
1210 Common function for generating repo api data.
1210 Common function for generating repo api data.
1211 Optionally, also return tags, branches, bookmarks and PRs.
1211 Optionally, also return tags, branches, bookmarks and PRs.
1212 """
1212 """
1213 repo = self
1213 repo = self
1214 data = dict(
1214 data = dict(
1215 repo_id=repo.repo_id,
1215 repo_id=repo.repo_id,
1216 repo_name=repo.repo_name,
1216 repo_name=repo.repo_name,
1217 repo_type=repo.repo_type,
1217 repo_type=repo.repo_type,
1218 clone_uri=repo.clone_uri,
1218 clone_uri=repo.clone_uri,
1219 private=repo.private,
1219 private=repo.private,
1220 created_on=repo.created_on,
1220 created_on=repo.created_on,
1221 description=repo.description,
1221 description=repo.description,
1222 landing_rev=repo.landing_rev,
1222 landing_rev=repo.landing_rev,
1223 owner=repo.owner.username,
1223 owner=repo.owner.username,
1224 fork_of=repo.fork.repo_name if repo.fork else None,
1224 fork_of=repo.fork.repo_name if repo.fork else None,
1225 enable_statistics=repo.enable_statistics,
1225 enable_statistics=repo.enable_statistics,
1226 enable_downloads=repo.enable_downloads,
1226 enable_downloads=repo.enable_downloads,
1227 last_changeset=repo.changeset_cache,
1227 last_changeset=repo.changeset_cache,
1228 )
1228 )
1229 if with_revision_names:
1229 if with_revision_names:
1230 scm_repo = repo.scm_instance_no_cache()
1230 scm_repo = repo.scm_instance_no_cache()
1231 data.update(dict(
1231 data.update(dict(
1232 tags=scm_repo.tags,
1232 tags=scm_repo.tags,
1233 branches=scm_repo.branches,
1233 branches=scm_repo.branches,
1234 bookmarks=scm_repo.bookmarks,
1234 bookmarks=scm_repo.bookmarks,
1235 ))
1235 ))
1236 if with_pullrequests:
1236 if with_pullrequests:
1237 data['pull_requests'] = repo.pull_requests_other
1237 data['pull_requests'] = repo.pull_requests_other
1238 rc_config = Setting.get_app_settings()
1238 rc_config = Setting.get_app_settings()
1239 repository_fields = str2bool(rc_config.get('repository_fields'))
1239 repository_fields = str2bool(rc_config.get('repository_fields'))
1240 if repository_fields:
1240 if repository_fields:
1241 for f in self.extra_fields:
1241 for f in self.extra_fields:
1242 data[f.field_key_prefixed] = f.field_value
1242 data[f.field_key_prefixed] = f.field_value
1243
1243
1244 return data
1244 return data
1245
1245
1246 @property
1246 @property
1247 def last_db_change(self):
1247 def last_db_change(self):
1248 return self.updated_on
1248 return self.updated_on
1249
1249
1250 @property
1250 @property
1251 def clone_uri_hidden(self):
1251 def clone_uri_hidden(self):
1252 clone_uri = self.clone_uri
1252 clone_uri = self.clone_uri
1253 if clone_uri:
1253 if clone_uri:
1254 import urlobject
1254 import urlobject
1255 url_obj = urlobject.URLObject(self.clone_uri)
1255 url_obj = urlobject.URLObject(self.clone_uri)
1256 if url_obj.password:
1256 if url_obj.password:
1257 clone_uri = url_obj.with_password('*****')
1257 clone_uri = url_obj.with_password('*****')
1258 return clone_uri
1258 return clone_uri
1259
1259
1260 def clone_url(self, clone_uri_tmpl, with_id=False, username=None):
1260 def clone_url(self, clone_uri_tmpl, with_id=False, username=None):
1261 if '{repo}' not in clone_uri_tmpl and '_{repoid}' not in clone_uri_tmpl:
1261 if '{repo}' not in clone_uri_tmpl and '_{repoid}' not in clone_uri_tmpl:
1262 log.error("Configured clone_uri_tmpl %r has no '{repo}' or '_{repoid}' and cannot toggle to use repo id URLs", clone_uri_tmpl)
1262 log.error("Configured clone_uri_tmpl %r has no '{repo}' or '_{repoid}' and cannot toggle to use repo id URLs", clone_uri_tmpl)
1263 elif with_id:
1263 elif with_id:
1264 clone_uri_tmpl = clone_uri_tmpl.replace('{repo}', '_{repoid}')
1264 clone_uri_tmpl = clone_uri_tmpl.replace('{repo}', '_{repoid}')
1265 else:
1265 else:
1266 clone_uri_tmpl = clone_uri_tmpl.replace('_{repoid}', '{repo}')
1266 clone_uri_tmpl = clone_uri_tmpl.replace('_{repoid}', '{repo}')
1267
1267
1268 import kallithea.lib.helpers as h
1268 import kallithea.lib.helpers as h
1269 prefix_url = h.canonical_url('home')
1269 prefix_url = h.canonical_url('home')
1270
1270
1271 return get_clone_url(clone_uri_tmpl=clone_uri_tmpl,
1271 return get_clone_url(clone_uri_tmpl=clone_uri_tmpl,
1272 prefix_url=prefix_url,
1272 prefix_url=prefix_url,
1273 repo_name=self.repo_name,
1273 repo_name=self.repo_name,
1274 repo_id=self.repo_id,
1274 repo_id=self.repo_id,
1275 username=username)
1275 username=username)
1276
1276
1277 def set_state(self, state):
1277 def set_state(self, state):
1278 self.repo_state = state
1278 self.repo_state = state
1279
1279
1280 #==========================================================================
1280 #==========================================================================
1281 # SCM PROPERTIES
1281 # SCM PROPERTIES
1282 #==========================================================================
1282 #==========================================================================
1283
1283
1284 def get_changeset(self, rev=None):
1284 def get_changeset(self, rev=None):
1285 return get_changeset_safe(self.scm_instance, rev)
1285 return get_changeset_safe(self.scm_instance, rev)
1286
1286
1287 def get_landing_changeset(self):
1287 def get_landing_changeset(self):
1288 """
1288 """
1289 Returns landing changeset, or if that doesn't exist returns the tip
1289 Returns landing changeset, or if that doesn't exist returns the tip
1290 """
1290 """
1291 _rev_type, _rev = self.landing_rev
1291 _rev_type, _rev = self.landing_rev
1292 cs = self.get_changeset(_rev)
1292 cs = self.get_changeset(_rev)
1293 if isinstance(cs, EmptyChangeset):
1293 if isinstance(cs, EmptyChangeset):
1294 return self.get_changeset()
1294 return self.get_changeset()
1295 return cs
1295 return cs
1296
1296
1297 def update_changeset_cache(self, cs_cache=None):
1297 def update_changeset_cache(self, cs_cache=None):
1298 """
1298 """
1299 Update cache of last changeset for repository, keys should be::
1299 Update cache of last changeset for repository, keys should be::
1300
1300
1301 short_id
1301 short_id
1302 raw_id
1302 raw_id
1303 revision
1303 revision
1304 message
1304 message
1305 date
1305 date
1306 author
1306 author
1307
1307
1308 :param cs_cache:
1308 :param cs_cache:
1309 """
1309 """
1310 from kallithea.lib.vcs.backends.base import BaseChangeset
1310 from kallithea.lib.vcs.backends.base import BaseChangeset
1311 if cs_cache is None:
1311 if cs_cache is None:
1312 cs_cache = EmptyChangeset()
1312 cs_cache = EmptyChangeset()
1313 # use no-cache version here
1313 # use no-cache version here
1314 scm_repo = self.scm_instance_no_cache()
1314 scm_repo = self.scm_instance_no_cache()
1315 if scm_repo:
1315 if scm_repo:
1316 cs_cache = scm_repo.get_changeset()
1316 cs_cache = scm_repo.get_changeset()
1317
1317
1318 if isinstance(cs_cache, BaseChangeset):
1318 if isinstance(cs_cache, BaseChangeset):
1319 cs_cache = cs_cache.__json__()
1319 cs_cache = cs_cache.__json__()
1320
1320
1321 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1321 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1322 _default = datetime.datetime.fromtimestamp(0)
1322 _default = datetime.datetime.fromtimestamp(0)
1323 last_change = cs_cache.get('date') or _default
1323 last_change = cs_cache.get('date') or _default
1324 log.debug('updated repo %s with new cs cache %s',
1324 log.debug('updated repo %s with new cs cache %s',
1325 self.repo_name, cs_cache)
1325 self.repo_name, cs_cache)
1326 self.updated_on = last_change
1326 self.updated_on = last_change
1327 self.changeset_cache = cs_cache
1327 self.changeset_cache = cs_cache
1328 Session().commit()
1328 Session().commit()
1329 else:
1329 else:
1330 log.debug('changeset_cache for %s already up to date with %s',
1330 log.debug('changeset_cache for %s already up to date with %s',
1331 self.repo_name, cs_cache['raw_id'])
1331 self.repo_name, cs_cache['raw_id'])
1332
1332
1333 @property
1333 @property
1334 def tip(self):
1334 def tip(self):
1335 return self.get_changeset('tip')
1335 return self.get_changeset('tip')
1336
1336
1337 @property
1337 @property
1338 def author(self):
1338 def author(self):
1339 return self.tip.author
1339 return self.tip.author
1340
1340
1341 @property
1341 @property
1342 def last_change(self):
1342 def last_change(self):
1343 return self.scm_instance.last_change
1343 return self.scm_instance.last_change
1344
1344
1345 def get_comments(self, revisions=None):
1345 def get_comments(self, revisions=None):
1346 """
1346 """
1347 Returns comments for this repository grouped by revisions
1347 Returns comments for this repository grouped by revisions
1348
1348
1349 :param revisions: filter query by revisions only
1349 :param revisions: filter query by revisions only
1350 """
1350 """
1351 cmts = ChangesetComment.query() \
1351 cmts = ChangesetComment.query() \
1352 .filter(ChangesetComment.repo == self)
1352 .filter(ChangesetComment.repo == self)
1353 if revisions is not None:
1353 if revisions is not None:
1354 if not revisions:
1354 if not revisions:
1355 return {} # don't use sql 'in' on empty set
1355 return {} # don't use sql 'in' on empty set
1356 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1356 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1357 grouped = collections.defaultdict(list)
1357 grouped = collections.defaultdict(list)
1358 for cmt in cmts.all():
1358 for cmt in cmts.all():
1359 grouped[cmt.revision].append(cmt)
1359 grouped[cmt.revision].append(cmt)
1360 return grouped
1360 return grouped
1361
1361
1362 def statuses(self, revisions):
1362 def statuses(self, revisions):
1363 """
1363 """
1364 Returns statuses for this repository.
1364 Returns statuses for this repository.
1365 PRs without any votes do _not_ show up as unreviewed.
1365 PRs without any votes do _not_ show up as unreviewed.
1366
1366
1367 :param revisions: list of revisions to get statuses for
1367 :param revisions: list of revisions to get statuses for
1368 """
1368 """
1369 if not revisions:
1369 if not revisions:
1370 return {}
1370 return {}
1371
1371
1372 statuses = ChangesetStatus.query() \
1372 statuses = ChangesetStatus.query() \
1373 .filter(ChangesetStatus.repo == self) \
1373 .filter(ChangesetStatus.repo == self) \
1374 .filter(ChangesetStatus.version == 0) \
1374 .filter(ChangesetStatus.version == 0) \
1375 .filter(ChangesetStatus.revision.in_(revisions))
1375 .filter(ChangesetStatus.revision.in_(revisions))
1376
1376
1377 grouped = {}
1377 grouped = {}
1378 for stat in statuses.all():
1378 for stat in statuses.all():
1379 pr_id = pr_nice_id = pr_repo = None
1379 pr_id = pr_nice_id = pr_repo = None
1380 if stat.pull_request:
1380 if stat.pull_request:
1381 pr_id = stat.pull_request.pull_request_id
1381 pr_id = stat.pull_request.pull_request_id
1382 pr_nice_id = PullRequest.make_nice_id(pr_id)
1382 pr_nice_id = PullRequest.make_nice_id(pr_id)
1383 pr_repo = stat.pull_request.other_repo.repo_name
1383 pr_repo = stat.pull_request.other_repo.repo_name
1384 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1384 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1385 pr_id, pr_repo, pr_nice_id,
1385 pr_id, pr_repo, pr_nice_id,
1386 stat.author]
1386 stat.author]
1387 return grouped
1387 return grouped
1388
1388
1389 def _repo_size(self):
1389 def _repo_size(self):
1390 from kallithea.lib import helpers as h
1390 from kallithea.lib import helpers as h
1391 log.debug('calculating repository size...')
1391 log.debug('calculating repository size...')
1392 return h.format_byte_size(self.scm_instance.size)
1392 return h.format_byte_size(self.scm_instance.size)
1393
1393
1394 #==========================================================================
1394 #==========================================================================
1395 # SCM CACHE INSTANCE
1395 # SCM CACHE INSTANCE
1396 #==========================================================================
1396 #==========================================================================
1397
1397
1398 def set_invalidate(self):
1398 def set_invalidate(self):
1399 """
1399 """
1400 Mark caches of this repo as invalid.
1400 Mark caches of this repo as invalid.
1401 """
1401 """
1402 CacheInvalidation.set_invalidate(self.repo_name)
1402 CacheInvalidation.set_invalidate(self.repo_name)
1403
1403
1404 _scm_instance = None
1404 _scm_instance = None
1405
1405
1406 @property
1406 @property
1407 def scm_instance(self):
1407 def scm_instance(self):
1408 if self._scm_instance is None:
1408 if self._scm_instance is None:
1409 self._scm_instance = self.scm_instance_cached()
1409 self._scm_instance = self.scm_instance_cached()
1410 return self._scm_instance
1410 return self._scm_instance
1411
1411
1412 def scm_instance_cached(self, valid_cache_keys=None):
1412 def scm_instance_cached(self, valid_cache_keys=None):
1413 @cache_region('long_term', 'scm_instance_cached')
1413 @cache_region('long_term', 'scm_instance_cached')
1414 def _c(repo_name): # repo_name is just for the cache key
1414 def _c(repo_name): # repo_name is just for the cache key
1415 log.debug('Creating new %s scm_instance and populating cache', repo_name)
1415 log.debug('Creating new %s scm_instance and populating cache', repo_name)
1416 return self.scm_instance_no_cache()
1416 return self.scm_instance_no_cache()
1417 rn = self.repo_name
1417 rn = self.repo_name
1418
1418
1419 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1419 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1420 if not valid:
1420 if not valid:
1421 log.debug('Cache for %s invalidated, getting new object', rn)
1421 log.debug('Cache for %s invalidated, getting new object', rn)
1422 region_invalidate(_c, None, 'scm_instance_cached', rn)
1422 region_invalidate(_c, None, 'scm_instance_cached', rn)
1423 else:
1423 else:
1424 log.debug('Trying to get scm_instance of %s from cache', rn)
1424 log.debug('Trying to get scm_instance of %s from cache', rn)
1425 return _c(rn)
1425 return _c(rn)
1426
1426
1427 def scm_instance_no_cache(self):
1427 def scm_instance_no_cache(self):
1428 repo_full_path = safe_str(self.repo_full_path)
1428 repo_full_path = safe_str(self.repo_full_path)
1429 alias = get_scm(repo_full_path)[0]
1429 alias = get_scm(repo_full_path)[0]
1430 log.debug('Creating instance of %s repository from %s',
1430 log.debug('Creating instance of %s repository from %s',
1431 alias, self.repo_full_path)
1431 alias, self.repo_full_path)
1432 backend = get_backend(alias)
1432 backend = get_backend(alias)
1433
1433
1434 if alias == 'hg':
1434 if alias == 'hg':
1435 repo = backend(repo_full_path, create=False,
1435 repo = backend(repo_full_path, create=False,
1436 baseui=self._ui)
1436 baseui=self._ui)
1437 else:
1437 else:
1438 repo = backend(repo_full_path, create=False)
1438 repo = backend(repo_full_path, create=False)
1439
1439
1440 return repo
1440 return repo
1441
1441
1442 def __json__(self):
1442 def __json__(self):
1443 return dict(
1443 return dict(
1444 repo_id=self.repo_id,
1444 repo_id=self.repo_id,
1445 repo_name=self.repo_name,
1445 repo_name=self.repo_name,
1446 landing_rev=self.landing_rev,
1446 landing_rev=self.landing_rev,
1447 )
1447 )
1448
1448
1449
1449
1450 class RepoGroup(Base, BaseDbModel):
1450 class RepoGroup(Base, BaseDbModel):
1451 __tablename__ = 'groups'
1451 __tablename__ = 'groups'
1452 __table_args__ = (
1452 __table_args__ = (
1453 _table_args_default_dict,
1453 _table_args_default_dict,
1454 )
1454 )
1455
1455
1456 SEP = ' &raquo; '
1456 SEP = ' &raquo; '
1457
1457
1458 group_id = Column(Integer(), primary_key=True)
1458 group_id = Column(Integer(), primary_key=True)
1459 group_name = Column(Unicode(255), nullable=False, unique=True) # full path
1459 group_name = Column(Unicode(255), nullable=False, unique=True) # full path
1460 parent_group_id = Column('group_parent_id', Integer(), ForeignKey('groups.group_id'), nullable=True)
1460 parent_group_id = Column('group_parent_id', Integer(), ForeignKey('groups.group_id'), nullable=True)
1461 group_description = Column(Unicode(10000), nullable=False)
1461 group_description = Column(Unicode(10000), nullable=False)
1462 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1462 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1463 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1463 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1464
1464
1465 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1465 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1466 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1466 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1467 parent_group = relationship('RepoGroup', remote_side=group_id)
1467 parent_group = relationship('RepoGroup', remote_side=group_id)
1468 owner = relationship('User')
1468 owner = relationship('User')
1469
1469
1470 @classmethod
1470 @classmethod
1471 def query(cls, sorted=False):
1471 def query(cls, sorted=False):
1472 """Add RepoGroup-specific helpers for common query constructs.
1472 """Add RepoGroup-specific helpers for common query constructs.
1473
1473
1474 sorted: if True, apply the default ordering (name, case insensitive).
1474 sorted: if True, apply the default ordering (name, case insensitive).
1475 """
1475 """
1476 q = super(RepoGroup, cls).query()
1476 q = super(RepoGroup, cls).query()
1477
1477
1478 if sorted:
1478 if sorted:
1479 q = q.order_by(func.lower(RepoGroup.group_name))
1479 q = q.order_by(func.lower(RepoGroup.group_name))
1480
1480
1481 return q
1481 return q
1482
1482
1483 def __init__(self, group_name='', parent_group=None):
1483 def __init__(self, group_name='', parent_group=None):
1484 self.group_name = group_name
1484 self.group_name = group_name
1485 self.parent_group = parent_group
1485 self.parent_group = parent_group
1486
1486
1487 def __unicode__(self):
1487 def __unicode__(self):
1488 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1488 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1489 self.group_name)
1489 self.group_name)
1490
1490
1491 @classmethod
1491 @classmethod
1492 def _generate_choice(cls, repo_group):
1492 def _generate_choice(cls, repo_group):
1493 """Return tuple with group_id and name as html literal"""
1493 """Return tuple with group_id and name as html literal"""
1494 from webhelpers2.html import literal
1494 from webhelpers2.html import literal
1495 if repo_group is None:
1495 if repo_group is None:
1496 return (-1, u'-- %s --' % _('top level'))
1496 return (-1, u'-- %s --' % _('top level'))
1497 return repo_group.group_id, literal(cls.SEP.join(repo_group.full_path_splitted))
1497 return repo_group.group_id, literal(cls.SEP.join(repo_group.full_path_splitted))
1498
1498
1499 @classmethod
1499 @classmethod
1500 def groups_choices(cls, groups):
1500 def groups_choices(cls, groups):
1501 """Return tuples with group_id and name as html literal."""
1501 """Return tuples with group_id and name as html literal."""
1502 return sorted((cls._generate_choice(g) for g in groups),
1502 return sorted((cls._generate_choice(g) for g in groups),
1503 key=lambda c: c[1].split(cls.SEP))
1503 key=lambda c: c[1].split(cls.SEP))
1504
1504
1505 @classmethod
1505 @classmethod
1506 def url_sep(cls):
1506 def url_sep(cls):
1507 return URL_SEP
1507 return URL_SEP
1508
1508
1509 @classmethod
1509 @classmethod
1510 def guess_instance(cls, value):
1510 def guess_instance(cls, value):
1511 return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
1511 return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
1512
1512
1513 @classmethod
1513 @classmethod
1514 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1514 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1515 group_name = group_name.rstrip('/')
1515 group_name = group_name.rstrip('/')
1516 if case_insensitive:
1516 if case_insensitive:
1517 gr = cls.query() \
1517 gr = cls.query() \
1518 .filter(func.lower(cls.group_name) == func.lower(group_name))
1518 .filter(func.lower(cls.group_name) == func.lower(group_name))
1519 else:
1519 else:
1520 gr = cls.query() \
1520 gr = cls.query() \
1521 .filter(cls.group_name == group_name)
1521 .filter(cls.group_name == group_name)
1522 if cache:
1522 if cache:
1523 gr = gr.options(FromCache(
1523 gr = gr.options(FromCache(
1524 "sql_cache_short",
1524 "sql_cache_short",
1525 "get_group_%s" % _hash_key(group_name)
1525 "get_group_%s" % _hash_key(group_name)
1526 )
1526 )
1527 )
1527 )
1528 return gr.scalar()
1528 return gr.scalar()
1529
1529
1530 @property
1530 @property
1531 def parents(self):
1531 def parents(self):
1532 groups = []
1532 groups = []
1533 group = self.parent_group
1533 group = self.parent_group
1534 while group is not None:
1534 while group is not None:
1535 groups.append(group)
1535 groups.append(group)
1536 group = group.parent_group
1536 group = group.parent_group
1537 assert group not in groups, group # avoid recursion on bad db content
1537 assert group not in groups, group # avoid recursion on bad db content
1538 groups.reverse()
1538 groups.reverse()
1539 return groups
1539 return groups
1540
1540
1541 @property
1541 @property
1542 def children(self):
1542 def children(self):
1543 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1543 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1544
1544
1545 @property
1545 @property
1546 def name(self):
1546 def name(self):
1547 return self.group_name.split(RepoGroup.url_sep())[-1]
1547 return self.group_name.split(RepoGroup.url_sep())[-1]
1548
1548
1549 @property
1549 @property
1550 def full_path(self):
1550 def full_path(self):
1551 return self.group_name
1551 return self.group_name
1552
1552
1553 @property
1553 @property
1554 def full_path_splitted(self):
1554 def full_path_splitted(self):
1555 return self.group_name.split(RepoGroup.url_sep())
1555 return self.group_name.split(RepoGroup.url_sep())
1556
1556
1557 @property
1557 @property
1558 def repositories(self):
1558 def repositories(self):
1559 return Repository.query(sorted=True).filter_by(group=self)
1559 return Repository.query(sorted=True).filter_by(group=self)
1560
1560
1561 @property
1561 @property
1562 def repositories_recursive_count(self):
1562 def repositories_recursive_count(self):
1563 cnt = self.repositories.count()
1563 cnt = self.repositories.count()
1564
1564
1565 def children_count(group):
1565 def children_count(group):
1566 cnt = 0
1566 cnt = 0
1567 for child in group.children:
1567 for child in group.children:
1568 cnt += child.repositories.count()
1568 cnt += child.repositories.count()
1569 cnt += children_count(child)
1569 cnt += children_count(child)
1570 return cnt
1570 return cnt
1571
1571
1572 return cnt + children_count(self)
1572 return cnt + children_count(self)
1573
1573
1574 def _recursive_objects(self, include_repos=True):
1574 def _recursive_objects(self, include_repos=True):
1575 all_ = []
1575 all_ = []
1576
1576
1577 def _get_members(root_gr):
1577 def _get_members(root_gr):
1578 if include_repos:
1578 if include_repos:
1579 for r in root_gr.repositories:
1579 for r in root_gr.repositories:
1580 all_.append(r)
1580 all_.append(r)
1581 childs = root_gr.children.all()
1581 childs = root_gr.children.all()
1582 if childs:
1582 if childs:
1583 for gr in childs:
1583 for gr in childs:
1584 all_.append(gr)
1584 all_.append(gr)
1585 _get_members(gr)
1585 _get_members(gr)
1586
1586
1587 _get_members(self)
1587 _get_members(self)
1588 return [self] + all_
1588 return [self] + all_
1589
1589
1590 def recursive_groups_and_repos(self):
1590 def recursive_groups_and_repos(self):
1591 """
1591 """
1592 Recursive return all groups, with repositories in those groups
1592 Recursive return all groups, with repositories in those groups
1593 """
1593 """
1594 return self._recursive_objects()
1594 return self._recursive_objects()
1595
1595
1596 def recursive_groups(self):
1596 def recursive_groups(self):
1597 """
1597 """
1598 Returns all children groups for this group including children of children
1598 Returns all children groups for this group including children of children
1599 """
1599 """
1600 return self._recursive_objects(include_repos=False)
1600 return self._recursive_objects(include_repos=False)
1601
1601
1602 def get_new_name(self, group_name):
1602 def get_new_name(self, group_name):
1603 """
1603 """
1604 returns new full group name based on parent and new name
1604 returns new full group name based on parent and new name
1605
1605
1606 :param group_name:
1606 :param group_name:
1607 """
1607 """
1608 path_prefix = (self.parent_group.full_path_splitted if
1608 path_prefix = (self.parent_group.full_path_splitted if
1609 self.parent_group else [])
1609 self.parent_group else [])
1610 return RepoGroup.url_sep().join(path_prefix + [group_name])
1610 return RepoGroup.url_sep().join(path_prefix + [group_name])
1611
1611
1612 def get_api_data(self):
1612 def get_api_data(self):
1613 """
1613 """
1614 Common function for generating api data
1614 Common function for generating api data
1615
1615
1616 """
1616 """
1617 group = self
1617 group = self
1618 data = dict(
1618 data = dict(
1619 group_id=group.group_id,
1619 group_id=group.group_id,
1620 group_name=group.group_name,
1620 group_name=group.group_name,
1621 group_description=group.group_description,
1621 group_description=group.group_description,
1622 parent_group=group.parent_group.group_name if group.parent_group else None,
1622 parent_group=group.parent_group.group_name if group.parent_group else None,
1623 repositories=[x.repo_name for x in group.repositories],
1623 repositories=[x.repo_name for x in group.repositories],
1624 owner=group.owner.username
1624 owner=group.owner.username
1625 )
1625 )
1626 return data
1626 return data
1627
1627
1628
1628
1629 class Permission(Base, BaseDbModel):
1629 class Permission(Base, BaseDbModel):
1630 __tablename__ = 'permissions'
1630 __tablename__ = 'permissions'
1631 __table_args__ = (
1631 __table_args__ = (
1632 Index('p_perm_name_idx', 'permission_name'),
1632 Index('p_perm_name_idx', 'permission_name'),
1633 _table_args_default_dict,
1633 _table_args_default_dict,
1634 )
1634 )
1635
1635
1636 PERMS = (
1636 PERMS = (
1637 ('hg.admin', _('Kallithea Administrator')),
1637 ('hg.admin', _('Kallithea Administrator')),
1638
1638
1639 ('repository.none', _('Default user has no access to new repositories')),
1639 ('repository.none', _('Default user has no access to new repositories')),
1640 ('repository.read', _('Default user has read access to new repositories')),
1640 ('repository.read', _('Default user has read access to new repositories')),
1641 ('repository.write', _('Default user has write access to new repositories')),
1641 ('repository.write', _('Default user has write access to new repositories')),
1642 ('repository.admin', _('Default user has admin access to new repositories')),
1642 ('repository.admin', _('Default user has admin access to new repositories')),
1643
1643
1644 ('group.none', _('Default user has no access to new repository groups')),
1644 ('group.none', _('Default user has no access to new repository groups')),
1645 ('group.read', _('Default user has read access to new repository groups')),
1645 ('group.read', _('Default user has read access to new repository groups')),
1646 ('group.write', _('Default user has write access to new repository groups')),
1646 ('group.write', _('Default user has write access to new repository groups')),
1647 ('group.admin', _('Default user has admin access to new repository groups')),
1647 ('group.admin', _('Default user has admin access to new repository groups')),
1648
1648
1649 ('usergroup.none', _('Default user has no access to new user groups')),
1649 ('usergroup.none', _('Default user has no access to new user groups')),
1650 ('usergroup.read', _('Default user has read access to new user groups')),
1650 ('usergroup.read', _('Default user has read access to new user groups')),
1651 ('usergroup.write', _('Default user has write access to new user groups')),
1651 ('usergroup.write', _('Default user has write access to new user groups')),
1652 ('usergroup.admin', _('Default user has admin access to new user groups')),
1652 ('usergroup.admin', _('Default user has admin access to new user groups')),
1653
1653
1654 ('hg.repogroup.create.false', _('Only admins can create repository groups')),
1654 ('hg.repogroup.create.false', _('Only admins can create repository groups')),
1655 ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
1655 ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
1656
1656
1657 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1657 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1658 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1658 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1659
1659
1660 ('hg.create.none', _('Only admins can create top level repositories')),
1660 ('hg.create.none', _('Only admins can create top level repositories')),
1661 ('hg.create.repository', _('Non-admins can create top level repositories')),
1661 ('hg.create.repository', _('Non-admins can create top level repositories')),
1662
1662
1663 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
1663 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
1664 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
1664 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
1665
1665
1666 ('hg.fork.none', _('Only admins can fork repositories')),
1666 ('hg.fork.none', _('Only admins can fork repositories')),
1667 ('hg.fork.repository', _('Non-admins can fork repositories')),
1667 ('hg.fork.repository', _('Non-admins can fork repositories')),
1668
1668
1669 ('hg.register.none', _('Registration disabled')),
1669 ('hg.register.none', _('Registration disabled')),
1670 ('hg.register.manual_activate', _('User registration with manual account activation')),
1670 ('hg.register.manual_activate', _('User registration with manual account activation')),
1671 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1671 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1672
1672
1673 ('hg.extern_activate.manual', _('Manual activation of external account')),
1673 ('hg.extern_activate.manual', _('Manual activation of external account')),
1674 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1674 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1675 )
1675 )
1676
1676
1677 # definition of system default permissions for DEFAULT user
1677 # definition of system default permissions for DEFAULT user
1678 DEFAULT_USER_PERMISSIONS = (
1678 DEFAULT_USER_PERMISSIONS = (
1679 'repository.read',
1679 'repository.read',
1680 'group.read',
1680 'group.read',
1681 'usergroup.read',
1681 'usergroup.read',
1682 'hg.create.repository',
1682 'hg.create.repository',
1683 'hg.create.write_on_repogroup.true',
1683 'hg.create.write_on_repogroup.true',
1684 'hg.fork.repository',
1684 'hg.fork.repository',
1685 'hg.register.manual_activate',
1685 'hg.register.manual_activate',
1686 'hg.extern_activate.auto',
1686 'hg.extern_activate.auto',
1687 )
1687 )
1688
1688
1689 # defines which permissions are more important higher the more important
1689 # defines which permissions are more important higher the more important
1690 # Weight defines which permissions are more important.
1690 # Weight defines which permissions are more important.
1691 # The higher number the more important.
1691 # The higher number the more important.
1692 PERM_WEIGHTS = {
1692 PERM_WEIGHTS = {
1693 'repository.none': 0,
1693 'repository.none': 0,
1694 'repository.read': 1,
1694 'repository.read': 1,
1695 'repository.write': 3,
1695 'repository.write': 3,
1696 'repository.admin': 4,
1696 'repository.admin': 4,
1697
1697
1698 'group.none': 0,
1698 'group.none': 0,
1699 'group.read': 1,
1699 'group.read': 1,
1700 'group.write': 3,
1700 'group.write': 3,
1701 'group.admin': 4,
1701 'group.admin': 4,
1702
1702
1703 'usergroup.none': 0,
1703 'usergroup.none': 0,
1704 'usergroup.read': 1,
1704 'usergroup.read': 1,
1705 'usergroup.write': 3,
1705 'usergroup.write': 3,
1706 'usergroup.admin': 4,
1706 'usergroup.admin': 4,
1707
1707
1708 'hg.repogroup.create.false': 0,
1708 'hg.repogroup.create.false': 0,
1709 'hg.repogroup.create.true': 1,
1709 'hg.repogroup.create.true': 1,
1710
1710
1711 'hg.usergroup.create.false': 0,
1711 'hg.usergroup.create.false': 0,
1712 'hg.usergroup.create.true': 1,
1712 'hg.usergroup.create.true': 1,
1713
1713
1714 'hg.fork.none': 0,
1714 'hg.fork.none': 0,
1715 'hg.fork.repository': 1,
1715 'hg.fork.repository': 1,
1716
1716
1717 'hg.create.none': 0,
1717 'hg.create.none': 0,
1718 'hg.create.repository': 1,
1718 'hg.create.repository': 1,
1719
1719
1720 'hg.create.write_on_repogroup.false': 0,
1720 'hg.create.write_on_repogroup.false': 0,
1721 'hg.create.write_on_repogroup.true': 1,
1721 'hg.create.write_on_repogroup.true': 1,
1722
1722
1723 'hg.register.none': 0,
1723 'hg.register.none': 0,
1724 'hg.register.manual_activate': 1,
1724 'hg.register.manual_activate': 1,
1725 'hg.register.auto_activate': 2,
1725 'hg.register.auto_activate': 2,
1726
1726
1727 'hg.extern_activate.manual': 0,
1727 'hg.extern_activate.manual': 0,
1728 'hg.extern_activate.auto': 1,
1728 'hg.extern_activate.auto': 1,
1729 }
1729 }
1730
1730
1731 permission_id = Column(Integer(), primary_key=True)
1731 permission_id = Column(Integer(), primary_key=True)
1732 permission_name = Column(String(255), nullable=False)
1732 permission_name = Column(String(255), nullable=False)
1733
1733
1734 def __unicode__(self):
1734 def __unicode__(self):
1735 return u"<%s('%s:%s')>" % (
1735 return u"<%s('%s:%s')>" % (
1736 self.__class__.__name__, self.permission_id, self.permission_name
1736 self.__class__.__name__, self.permission_id, self.permission_name
1737 )
1737 )
1738
1738
1739 @classmethod
1739 @classmethod
1740 def guess_instance(cls, value):
1740 def guess_instance(cls, value):
1741 return super(Permission, cls).guess_instance(value, Permission.get_by_key)
1741 return super(Permission, cls).guess_instance(value, Permission.get_by_key)
1742
1742
1743 @classmethod
1743 @classmethod
1744 def get_by_key(cls, key):
1744 def get_by_key(cls, key):
1745 return cls.query().filter(cls.permission_name == key).scalar()
1745 return cls.query().filter(cls.permission_name == key).scalar()
1746
1746
1747 @classmethod
1747 @classmethod
1748 def get_default_perms(cls, default_user_id):
1748 def get_default_perms(cls, default_user_id):
1749 q = Session().query(UserRepoToPerm, Repository, cls) \
1749 q = Session().query(UserRepoToPerm, Repository, cls) \
1750 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id)) \
1750 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id)) \
1751 .join((cls, UserRepoToPerm.permission_id == cls.permission_id)) \
1751 .join((cls, UserRepoToPerm.permission_id == cls.permission_id)) \
1752 .filter(UserRepoToPerm.user_id == default_user_id)
1752 .filter(UserRepoToPerm.user_id == default_user_id)
1753
1753
1754 return q.all()
1754 return q.all()
1755
1755
1756 @classmethod
1756 @classmethod
1757 def get_default_group_perms(cls, default_user_id):
1757 def get_default_group_perms(cls, default_user_id):
1758 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls) \
1758 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls) \
1759 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id)) \
1759 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id)) \
1760 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id)) \
1760 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id)) \
1761 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1761 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1762
1762
1763 return q.all()
1763 return q.all()
1764
1764
1765 @classmethod
1765 @classmethod
1766 def get_default_user_group_perms(cls, default_user_id):
1766 def get_default_user_group_perms(cls, default_user_id):
1767 q = Session().query(UserUserGroupToPerm, UserGroup, cls) \
1767 q = Session().query(UserUserGroupToPerm, UserGroup, cls) \
1768 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id)) \
1768 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id)) \
1769 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id)) \
1769 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id)) \
1770 .filter(UserUserGroupToPerm.user_id == default_user_id)
1770 .filter(UserUserGroupToPerm.user_id == default_user_id)
1771
1771
1772 return q.all()
1772 return q.all()
1773
1773
1774
1774
1775 class UserRepoToPerm(Base, BaseDbModel):
1775 class UserRepoToPerm(Base, BaseDbModel):
1776 __tablename__ = 'repo_to_perm'
1776 __tablename__ = 'repo_to_perm'
1777 __table_args__ = (
1777 __table_args__ = (
1778 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1778 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1779 _table_args_default_dict,
1779 _table_args_default_dict,
1780 )
1780 )
1781
1781
1782 repo_to_perm_id = Column(Integer(), primary_key=True)
1782 repo_to_perm_id = Column(Integer(), primary_key=True)
1783 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1783 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1784 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1784 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1785 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1785 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1786
1786
1787 user = relationship('User')
1787 user = relationship('User')
1788 repository = relationship('Repository')
1788 repository = relationship('Repository')
1789 permission = relationship('Permission')
1789 permission = relationship('Permission')
1790
1790
1791 @classmethod
1791 @classmethod
1792 def create(cls, user, repository, permission):
1792 def create(cls, user, repository, permission):
1793 n = cls()
1793 n = cls()
1794 n.user = user
1794 n.user = user
1795 n.repository = repository
1795 n.repository = repository
1796 n.permission = permission
1796 n.permission = permission
1797 Session().add(n)
1797 Session().add(n)
1798 return n
1798 return n
1799
1799
1800 def __unicode__(self):
1800 def __unicode__(self):
1801 return u'<%s => %s >' % (self.user, self.repository)
1801 return u'<%s => %s >' % (self.user, self.repository)
1802
1802
1803
1803
1804 class UserUserGroupToPerm(Base, BaseDbModel):
1804 class UserUserGroupToPerm(Base, BaseDbModel):
1805 __tablename__ = 'user_user_group_to_perm'
1805 __tablename__ = 'user_user_group_to_perm'
1806 __table_args__ = (
1806 __table_args__ = (
1807 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1807 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1808 _table_args_default_dict,
1808 _table_args_default_dict,
1809 )
1809 )
1810
1810
1811 user_user_group_to_perm_id = Column(Integer(), primary_key=True)
1811 user_user_group_to_perm_id = Column(Integer(), primary_key=True)
1812 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1812 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1813 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1813 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1814 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1814 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1815
1815
1816 user = relationship('User')
1816 user = relationship('User')
1817 user_group = relationship('UserGroup')
1817 user_group = relationship('UserGroup')
1818 permission = relationship('Permission')
1818 permission = relationship('Permission')
1819
1819
1820 @classmethod
1820 @classmethod
1821 def create(cls, user, user_group, permission):
1821 def create(cls, user, user_group, permission):
1822 n = cls()
1822 n = cls()
1823 n.user = user
1823 n.user = user
1824 n.user_group = user_group
1824 n.user_group = user_group
1825 n.permission = permission
1825 n.permission = permission
1826 Session().add(n)
1826 Session().add(n)
1827 return n
1827 return n
1828
1828
1829 def __unicode__(self):
1829 def __unicode__(self):
1830 return u'<%s => %s >' % (self.user, self.user_group)
1830 return u'<%s => %s >' % (self.user, self.user_group)
1831
1831
1832
1832
1833 class UserToPerm(Base, BaseDbModel):
1833 class UserToPerm(Base, BaseDbModel):
1834 __tablename__ = 'user_to_perm'
1834 __tablename__ = 'user_to_perm'
1835 __table_args__ = (
1835 __table_args__ = (
1836 UniqueConstraint('user_id', 'permission_id'),
1836 UniqueConstraint('user_id', 'permission_id'),
1837 _table_args_default_dict,
1837 _table_args_default_dict,
1838 )
1838 )
1839
1839
1840 user_to_perm_id = Column(Integer(), primary_key=True)
1840 user_to_perm_id = Column(Integer(), primary_key=True)
1841 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1841 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1842 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1842 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1843
1843
1844 user = relationship('User')
1844 user = relationship('User')
1845 permission = relationship('Permission')
1845 permission = relationship('Permission')
1846
1846
1847 def __unicode__(self):
1847 def __unicode__(self):
1848 return u'<%s => %s >' % (self.user, self.permission)
1848 return u'<%s => %s >' % (self.user, self.permission)
1849
1849
1850
1850
1851 class UserGroupRepoToPerm(Base, BaseDbModel):
1851 class UserGroupRepoToPerm(Base, BaseDbModel):
1852 __tablename__ = 'users_group_repo_to_perm'
1852 __tablename__ = 'users_group_repo_to_perm'
1853 __table_args__ = (
1853 __table_args__ = (
1854 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1854 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1855 _table_args_default_dict,
1855 _table_args_default_dict,
1856 )
1856 )
1857
1857
1858 users_group_to_perm_id = Column(Integer(), primary_key=True)
1858 users_group_to_perm_id = Column(Integer(), primary_key=True)
1859 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1859 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1860 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1860 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1861 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1861 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1862
1862
1863 users_group = relationship('UserGroup')
1863 users_group = relationship('UserGroup')
1864 permission = relationship('Permission')
1864 permission = relationship('Permission')
1865 repository = relationship('Repository')
1865 repository = relationship('Repository')
1866
1866
1867 @classmethod
1867 @classmethod
1868 def create(cls, users_group, repository, permission):
1868 def create(cls, users_group, repository, permission):
1869 n = cls()
1869 n = cls()
1870 n.users_group = users_group
1870 n.users_group = users_group
1871 n.repository = repository
1871 n.repository = repository
1872 n.permission = permission
1872 n.permission = permission
1873 Session().add(n)
1873 Session().add(n)
1874 return n
1874 return n
1875
1875
1876 def __unicode__(self):
1876 def __unicode__(self):
1877 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1877 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1878
1878
1879
1879
1880 class UserGroupUserGroupToPerm(Base, BaseDbModel):
1880 class UserGroupUserGroupToPerm(Base, BaseDbModel):
1881 __tablename__ = 'user_group_user_group_to_perm'
1881 __tablename__ = 'user_group_user_group_to_perm'
1882 __table_args__ = (
1882 __table_args__ = (
1883 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1883 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1884 _table_args_default_dict,
1884 _table_args_default_dict,
1885 )
1885 )
1886
1886
1887 user_group_user_group_to_perm_id = Column(Integer(), primary_key=True)
1887 user_group_user_group_to_perm_id = Column(Integer(), primary_key=True)
1888 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1888 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1889 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1889 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1890 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1890 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1891
1891
1892 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1892 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1893 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1893 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1894 permission = relationship('Permission')
1894 permission = relationship('Permission')
1895
1895
1896 @classmethod
1896 @classmethod
1897 def create(cls, target_user_group, user_group, permission):
1897 def create(cls, target_user_group, user_group, permission):
1898 n = cls()
1898 n = cls()
1899 n.target_user_group = target_user_group
1899 n.target_user_group = target_user_group
1900 n.user_group = user_group
1900 n.user_group = user_group
1901 n.permission = permission
1901 n.permission = permission
1902 Session().add(n)
1902 Session().add(n)
1903 return n
1903 return n
1904
1904
1905 def __unicode__(self):
1905 def __unicode__(self):
1906 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1906 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1907
1907
1908
1908
1909 class UserGroupToPerm(Base, BaseDbModel):
1909 class UserGroupToPerm(Base, BaseDbModel):
1910 __tablename__ = 'users_group_to_perm'
1910 __tablename__ = 'users_group_to_perm'
1911 __table_args__ = (
1911 __table_args__ = (
1912 UniqueConstraint('users_group_id', 'permission_id',),
1912 UniqueConstraint('users_group_id', 'permission_id',),
1913 _table_args_default_dict,
1913 _table_args_default_dict,
1914 )
1914 )
1915
1915
1916 users_group_to_perm_id = Column(Integer(), primary_key=True)
1916 users_group_to_perm_id = Column(Integer(), primary_key=True)
1917 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1917 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1918 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1918 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1919
1919
1920 users_group = relationship('UserGroup')
1920 users_group = relationship('UserGroup')
1921 permission = relationship('Permission')
1921 permission = relationship('Permission')
1922
1922
1923
1923
1924 class UserRepoGroupToPerm(Base, BaseDbModel):
1924 class UserRepoGroupToPerm(Base, BaseDbModel):
1925 __tablename__ = 'user_repo_group_to_perm'
1925 __tablename__ = 'user_repo_group_to_perm'
1926 __table_args__ = (
1926 __table_args__ = (
1927 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1927 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1928 _table_args_default_dict,
1928 _table_args_default_dict,
1929 )
1929 )
1930
1930
1931 group_to_perm_id = Column(Integer(), primary_key=True)
1931 group_to_perm_id = Column(Integer(), primary_key=True)
1932 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1932 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1933 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1933 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1934 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1934 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1935
1935
1936 user = relationship('User')
1936 user = relationship('User')
1937 group = relationship('RepoGroup')
1937 group = relationship('RepoGroup')
1938 permission = relationship('Permission')
1938 permission = relationship('Permission')
1939
1939
1940 @classmethod
1940 @classmethod
1941 def create(cls, user, repository_group, permission):
1941 def create(cls, user, repository_group, permission):
1942 n = cls()
1942 n = cls()
1943 n.user = user
1943 n.user = user
1944 n.group = repository_group
1944 n.group = repository_group
1945 n.permission = permission
1945 n.permission = permission
1946 Session().add(n)
1946 Session().add(n)
1947 return n
1947 return n
1948
1948
1949
1949
1950 class UserGroupRepoGroupToPerm(Base, BaseDbModel):
1950 class UserGroupRepoGroupToPerm(Base, BaseDbModel):
1951 __tablename__ = 'users_group_repo_group_to_perm'
1951 __tablename__ = 'users_group_repo_group_to_perm'
1952 __table_args__ = (
1952 __table_args__ = (
1953 UniqueConstraint('users_group_id', 'group_id'),
1953 UniqueConstraint('users_group_id', 'group_id'),
1954 _table_args_default_dict,
1954 _table_args_default_dict,
1955 )
1955 )
1956
1956
1957 users_group_repo_group_to_perm_id = Column(Integer(), primary_key=True)
1957 users_group_repo_group_to_perm_id = Column(Integer(), primary_key=True)
1958 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1958 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1959 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1959 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1960 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1960 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1961
1961
1962 users_group = relationship('UserGroup')
1962 users_group = relationship('UserGroup')
1963 permission = relationship('Permission')
1963 permission = relationship('Permission')
1964 group = relationship('RepoGroup')
1964 group = relationship('RepoGroup')
1965
1965
1966 @classmethod
1966 @classmethod
1967 def create(cls, user_group, repository_group, permission):
1967 def create(cls, user_group, repository_group, permission):
1968 n = cls()
1968 n = cls()
1969 n.users_group = user_group
1969 n.users_group = user_group
1970 n.group = repository_group
1970 n.group = repository_group
1971 n.permission = permission
1971 n.permission = permission
1972 Session().add(n)
1972 Session().add(n)
1973 return n
1973 return n
1974
1974
1975
1975
1976 class Statistics(Base, BaseDbModel):
1976 class Statistics(Base, BaseDbModel):
1977 __tablename__ = 'statistics'
1977 __tablename__ = 'statistics'
1978 __table_args__ = (
1978 __table_args__ = (
1979 _table_args_default_dict,
1979 _table_args_default_dict,
1980 )
1980 )
1981
1981
1982 stat_id = Column(Integer(), primary_key=True)
1982 stat_id = Column(Integer(), primary_key=True)
1983 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
1983 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
1984 stat_on_revision = Column(Integer(), nullable=False)
1984 stat_on_revision = Column(Integer(), nullable=False)
1985 commit_activity = Column(LargeBinary(1000000), nullable=False) # JSON data
1985 commit_activity = Column(LargeBinary(1000000), nullable=False) # JSON data
1986 commit_activity_combined = Column(LargeBinary(), nullable=False) # JSON data
1986 commit_activity_combined = Column(LargeBinary(), nullable=False) # JSON data
1987 languages = Column(LargeBinary(1000000), nullable=False) # JSON data
1987 languages = Column(LargeBinary(1000000), nullable=False) # JSON data
1988
1988
1989 repository = relationship('Repository', single_parent=True)
1989 repository = relationship('Repository', single_parent=True)
1990
1990
1991
1991
1992 class UserFollowing(Base, BaseDbModel):
1992 class UserFollowing(Base, BaseDbModel):
1993 __tablename__ = 'user_followings'
1993 __tablename__ = 'user_followings'
1994 __table_args__ = (
1994 __table_args__ = (
1995 UniqueConstraint('user_id', 'follows_repository_id', name='uq_user_followings_user_repo'),
1995 UniqueConstraint('user_id', 'follows_repository_id', name='uq_user_followings_user_repo'),
1996 UniqueConstraint('user_id', 'follows_user_id', name='uq_user_followings_user_user'),
1996 UniqueConstraint('user_id', 'follows_user_id', name='uq_user_followings_user_user'),
1997 _table_args_default_dict,
1997 _table_args_default_dict,
1998 )
1998 )
1999
1999
2000 user_following_id = Column(Integer(), primary_key=True)
2000 user_following_id = Column(Integer(), primary_key=True)
2001 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2001 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2002 follows_repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
2002 follows_repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
2003 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
2003 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
2004 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2004 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2005
2005
2006 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2006 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2007
2007
2008 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2008 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2009 follows_repository = relationship('Repository', order_by=lambda: func.lower(Repository.repo_name))
2009 follows_repository = relationship('Repository', order_by=lambda: func.lower(Repository.repo_name))
2010
2010
2011 @classmethod
2011 @classmethod
2012 def get_repo_followers(cls, repo_id):
2012 def get_repo_followers(cls, repo_id):
2013 return cls.query().filter(cls.follows_repository_id == repo_id)
2013 return cls.query().filter(cls.follows_repository_id == repo_id)
2014
2014
2015
2015
2016 class CacheInvalidation(Base, BaseDbModel):
2016 class CacheInvalidation(Base, BaseDbModel):
2017 __tablename__ = 'cache_invalidation'
2017 __tablename__ = 'cache_invalidation'
2018 __table_args__ = (
2018 __table_args__ = (
2019 Index('key_idx', 'cache_key'),
2019 Index('key_idx', 'cache_key'),
2020 _table_args_default_dict,
2020 _table_args_default_dict,
2021 )
2021 )
2022
2022
2023 # cache_id, not used
2023 # cache_id, not used
2024 cache_id = Column(Integer(), primary_key=True)
2024 cache_id = Column(Integer(), primary_key=True)
2025 # cache_key as created by _get_cache_key
2025 # cache_key as created by _get_cache_key
2026 cache_key = Column(Unicode(255), nullable=False, unique=True)
2026 cache_key = Column(Unicode(255), nullable=False, unique=True)
2027 # cache_args is a repo_name
2027 # cache_args is a repo_name
2028 cache_args = Column(Unicode(255), nullable=False)
2028 cache_args = Column(Unicode(255), nullable=False)
2029 # instance sets cache_active True when it is caching, other instances set
2029 # instance sets cache_active True when it is caching, other instances set
2030 # cache_active to False to indicate that this cache is invalid
2030 # cache_active to False to indicate that this cache is invalid
2031 cache_active = Column(Boolean(), nullable=False, default=False)
2031 cache_active = Column(Boolean(), nullable=False, default=False)
2032
2032
2033 def __init__(self, cache_key, repo_name=''):
2033 def __init__(self, cache_key, repo_name=''):
2034 self.cache_key = cache_key
2034 self.cache_key = cache_key
2035 self.cache_args = repo_name
2035 self.cache_args = repo_name
2036 self.cache_active = False
2036 self.cache_active = False
2037
2037
2038 def __unicode__(self):
2038 def __unicode__(self):
2039 return u"<%s('%s:%s[%s]')>" % (
2039 return u"<%s('%s:%s[%s]')>" % (
2040 self.__class__.__name__,
2040 self.__class__.__name__,
2041 self.cache_id, self.cache_key, self.cache_active)
2041 self.cache_id, self.cache_key, self.cache_active)
2042
2042
2043 def _cache_key_partition(self):
2043 def _cache_key_partition(self):
2044 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2044 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2045 return prefix, repo_name, suffix
2045 return prefix, repo_name, suffix
2046
2046
2047 def get_prefix(self):
2047 def get_prefix(self):
2048 """
2048 """
2049 get prefix that might have been used in _get_cache_key to
2049 get prefix that might have been used in _get_cache_key to
2050 generate self.cache_key. Only used for informational purposes
2050 generate self.cache_key. Only used for informational purposes
2051 in repo_edit.html.
2051 in repo_edit.html.
2052 """
2052 """
2053 # prefix, repo_name, suffix
2053 # prefix, repo_name, suffix
2054 return self._cache_key_partition()[0]
2054 return self._cache_key_partition()[0]
2055
2055
2056 def get_suffix(self):
2056 def get_suffix(self):
2057 """
2057 """
2058 get suffix that might have been used in _get_cache_key to
2058 get suffix that might have been used in _get_cache_key to
2059 generate self.cache_key. Only used for informational purposes
2059 generate self.cache_key. Only used for informational purposes
2060 in repo_edit.html.
2060 in repo_edit.html.
2061 """
2061 """
2062 # prefix, repo_name, suffix
2062 # prefix, repo_name, suffix
2063 return self._cache_key_partition()[2]
2063 return self._cache_key_partition()[2]
2064
2064
2065 @classmethod
2065 @classmethod
2066 def clear_cache(cls):
2066 def clear_cache(cls):
2067 """
2067 """
2068 Delete all cache keys from database.
2068 Delete all cache keys from database.
2069 Should only be run when all instances are down and all entries thus stale.
2069 Should only be run when all instances are down and all entries thus stale.
2070 """
2070 """
2071 cls.query().delete()
2071 cls.query().delete()
2072 Session().commit()
2072 Session().commit()
2073
2073
2074 @classmethod
2074 @classmethod
2075 def _get_cache_key(cls, key):
2075 def _get_cache_key(cls, key):
2076 """
2076 """
2077 Wrapper for generating a unique cache key for this instance and "key".
2077 Wrapper for generating a unique cache key for this instance and "key".
2078 key must / will start with a repo_name which will be stored in .cache_args .
2078 key must / will start with a repo_name which will be stored in .cache_args .
2079 """
2079 """
2080 prefix = kallithea.CONFIG.get('instance_id', '')
2080 prefix = kallithea.CONFIG.get('instance_id', '')
2081 return "%s%s" % (prefix, key)
2081 return "%s%s" % (prefix, key)
2082
2082
2083 @classmethod
2083 @classmethod
2084 def set_invalidate(cls, repo_name):
2084 def set_invalidate(cls, repo_name):
2085 """
2085 """
2086 Mark all caches of a repo as invalid in the database.
2086 Mark all caches of a repo as invalid in the database.
2087 """
2087 """
2088 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
2088 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
2089 log.debug('for repo %s got %s invalidation objects',
2089 log.debug('for repo %s got %s invalidation objects',
2090 safe_str(repo_name), inv_objs)
2090 safe_str(repo_name), inv_objs)
2091
2091
2092 for inv_obj in inv_objs:
2092 for inv_obj in inv_objs:
2093 log.debug('marking %s key for invalidation based on repo_name=%s',
2093 log.debug('marking %s key for invalidation based on repo_name=%s',
2094 inv_obj, safe_str(repo_name))
2094 inv_obj, safe_str(repo_name))
2095 Session().delete(inv_obj)
2095 Session().delete(inv_obj)
2096 Session().commit()
2096 Session().commit()
2097
2097
2098 @classmethod
2098 @classmethod
2099 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
2099 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
2100 """
2100 """
2101 Mark this cache key as active and currently cached.
2101 Mark this cache key as active and currently cached.
2102 Return True if the existing cache registration still was valid.
2102 Return True if the existing cache registration still was valid.
2103 Return False to indicate that it had been invalidated and caches should be refreshed.
2103 Return False to indicate that it had been invalidated and caches should be refreshed.
2104 """
2104 """
2105
2105
2106 key = (repo_name + '_' + kind) if kind else repo_name
2106 key = (repo_name + '_' + kind) if kind else repo_name
2107 cache_key = cls._get_cache_key(key)
2107 cache_key = cls._get_cache_key(key)
2108
2108
2109 if valid_cache_keys and cache_key in valid_cache_keys:
2109 if valid_cache_keys and cache_key in valid_cache_keys:
2110 return True
2110 return True
2111
2111
2112 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2112 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2113 if inv_obj is None:
2113 if inv_obj is None:
2114 inv_obj = cls(cache_key, repo_name)
2114 inv_obj = cls(cache_key, repo_name)
2115 Session().add(inv_obj)
2115 Session().add(inv_obj)
2116 elif inv_obj.cache_active:
2116 elif inv_obj.cache_active:
2117 return True
2117 return True
2118 inv_obj.cache_active = True
2118 inv_obj.cache_active = True
2119 try:
2119 try:
2120 Session().commit()
2120 Session().commit()
2121 except sqlalchemy.exc.IntegrityError:
2121 except sqlalchemy.exc.IntegrityError:
2122 log.error('commit of CacheInvalidation failed - retrying')
2122 log.error('commit of CacheInvalidation failed - retrying')
2123 Session().rollback()
2123 Session().rollback()
2124 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2124 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2125 if inv_obj is None:
2125 if inv_obj is None:
2126 log.error('failed to create CacheInvalidation entry')
2126 log.error('failed to create CacheInvalidation entry')
2127 # TODO: fail badly?
2127 # TODO: fail badly?
2128 # else: TOCTOU - another thread added the key at the same time; no further action required
2128 # else: TOCTOU - another thread added the key at the same time; no further action required
2129 return False
2129 return False
2130
2130
2131 @classmethod
2131 @classmethod
2132 def get_valid_cache_keys(cls):
2132 def get_valid_cache_keys(cls):
2133 """
2133 """
2134 Return opaque object with information of which caches still are valid
2134 Return opaque object with information of which caches still are valid
2135 and can be used without checking for invalidation.
2135 and can be used without checking for invalidation.
2136 """
2136 """
2137 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
2137 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
2138
2138
2139
2139
2140 class ChangesetComment(Base, BaseDbModel):
2140 class ChangesetComment(Base, BaseDbModel):
2141 __tablename__ = 'changeset_comments'
2141 __tablename__ = 'changeset_comments'
2142 __table_args__ = (
2142 __table_args__ = (
2143 Index('cc_revision_idx', 'revision'),
2143 Index('cc_revision_idx', 'revision'),
2144 Index('cc_pull_request_id_idx', 'pull_request_id'),
2144 Index('cc_pull_request_id_idx', 'pull_request_id'),
2145 _table_args_default_dict,
2145 _table_args_default_dict,
2146 )
2146 )
2147
2147
2148 comment_id = Column(Integer(), primary_key=True)
2148 comment_id = Column(Integer(), primary_key=True)
2149 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2149 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2150 revision = Column(String(40), nullable=True)
2150 revision = Column(String(40), nullable=True)
2151 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2151 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2152 line_no = Column(Unicode(10), nullable=True)
2152 line_no = Column(Unicode(10), nullable=True)
2153 f_path = Column(Unicode(1000), nullable=True)
2153 f_path = Column(Unicode(1000), nullable=True)
2154 author_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2154 author_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2155 text = Column(UnicodeText(), nullable=False)
2155 text = Column(UnicodeText(), nullable=False)
2156 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2156 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2157 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2157 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2158
2158
2159 author = relationship('User')
2159 author = relationship('User')
2160 repo = relationship('Repository')
2160 repo = relationship('Repository')
2161 # status_change is frequently used directly in templates - make it a lazy
2161 # status_change is frequently used directly in templates - make it a lazy
2162 # join to avoid fetching each related ChangesetStatus on demand.
2162 # join to avoid fetching each related ChangesetStatus on demand.
2163 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
2163 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
2164 status_change = relationship('ChangesetStatus',
2164 status_change = relationship('ChangesetStatus',
2165 cascade="all, delete-orphan", lazy='joined')
2165 cascade="all, delete-orphan", lazy='joined')
2166 pull_request = relationship('PullRequest')
2166 pull_request = relationship('PullRequest')
2167
2167
2168 def url(self):
2168 def url(self):
2169 anchor = "comment-%s" % self.comment_id
2169 anchor = "comment-%s" % self.comment_id
2170 import kallithea.lib.helpers as h
2170 import kallithea.lib.helpers as h
2171 if self.revision:
2171 if self.revision:
2172 return h.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
2172 return h.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
2173 elif self.pull_request_id is not None:
2173 elif self.pull_request_id is not None:
2174 return self.pull_request.url(anchor=anchor)
2174 return self.pull_request.url(anchor=anchor)
2175
2175
2176 def __json__(self):
2176 def __json__(self):
2177 return dict(
2177 return dict(
2178 comment_id=self.comment_id,
2178 comment_id=self.comment_id,
2179 username=self.author.username,
2179 username=self.author.username,
2180 text=self.text,
2180 text=self.text,
2181 )
2181 )
2182
2182
2183 def deletable(self):
2183 def deletable(self):
2184 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
2184 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
2185
2185
2186
2186
2187 class ChangesetStatus(Base, BaseDbModel):
2187 class ChangesetStatus(Base, BaseDbModel):
2188 __tablename__ = 'changeset_statuses'
2188 __tablename__ = 'changeset_statuses'
2189 __table_args__ = (
2189 __table_args__ = (
2190 Index('cs_revision_idx', 'revision'),
2190 Index('cs_revision_idx', 'revision'),
2191 Index('cs_version_idx', 'version'),
2191 Index('cs_version_idx', 'version'),
2192 Index('cs_pull_request_id_idx', 'pull_request_id'),
2192 Index('cs_pull_request_id_idx', 'pull_request_id'),
2193 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
2193 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
2194 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
2194 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
2195 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
2195 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
2196 UniqueConstraint('repo_id', 'revision', 'version'),
2196 UniqueConstraint('repo_id', 'revision', 'version'),
2197 _table_args_default_dict,
2197 _table_args_default_dict,
2198 )
2198 )
2199
2199
2200 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2200 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2201 STATUS_APPROVED = 'approved'
2201 STATUS_APPROVED = 'approved'
2202 STATUS_REJECTED = 'rejected' # is shown as "Not approved" - TODO: change database content / scheme
2202 STATUS_REJECTED = 'rejected' # is shown as "Not approved" - TODO: change database content / scheme
2203 STATUS_UNDER_REVIEW = 'under_review'
2203 STATUS_UNDER_REVIEW = 'under_review'
2204
2204
2205 STATUSES = [
2205 STATUSES = [
2206 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
2206 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
2207 (STATUS_UNDER_REVIEW, _("Under review")),
2207 (STATUS_UNDER_REVIEW, _("Under review")),
2208 (STATUS_REJECTED, _("Not approved")),
2208 (STATUS_REJECTED, _("Not approved")),
2209 (STATUS_APPROVED, _("Approved")),
2209 (STATUS_APPROVED, _("Approved")),
2210 ]
2210 ]
2211 STATUSES_DICT = dict(STATUSES)
2211 STATUSES_DICT = dict(STATUSES)
2212
2212
2213 changeset_status_id = Column(Integer(), primary_key=True)
2213 changeset_status_id = Column(Integer(), primary_key=True)
2214 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2214 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2215 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2215 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2216 revision = Column(String(40), nullable=True)
2216 revision = Column(String(40), nullable=True)
2217 status = Column(String(128), nullable=False, default=DEFAULT)
2217 status = Column(String(128), nullable=False, default=DEFAULT)
2218 comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
2218 comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
2219 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
2219 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
2220 version = Column(Integer(), nullable=False, default=0)
2220 version = Column(Integer(), nullable=False, default=0)
2221 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2221 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2222
2222
2223 author = relationship('User')
2223 author = relationship('User')
2224 repo = relationship('Repository')
2224 repo = relationship('Repository')
2225 comment = relationship('ChangesetComment')
2225 comment = relationship('ChangesetComment')
2226 pull_request = relationship('PullRequest')
2226 pull_request = relationship('PullRequest')
2227
2227
2228 def __unicode__(self):
2228 def __unicode__(self):
2229 return u"<%s('%s:%s')>" % (
2229 return u"<%s('%s:%s')>" % (
2230 self.__class__.__name__,
2230 self.__class__.__name__,
2231 self.status, self.author
2231 self.status, self.author
2232 )
2232 )
2233
2233
2234 @classmethod
2234 @classmethod
2235 def get_status_lbl(cls, value):
2235 def get_status_lbl(cls, value):
2236 return cls.STATUSES_DICT.get(value)
2236 return cls.STATUSES_DICT.get(value)
2237
2237
2238 @property
2238 @property
2239 def status_lbl(self):
2239 def status_lbl(self):
2240 return ChangesetStatus.get_status_lbl(self.status)
2240 return ChangesetStatus.get_status_lbl(self.status)
2241
2241
2242 def __json__(self):
2242 def __json__(self):
2243 return dict(
2243 return dict(
2244 status=self.status,
2244 status=self.status,
2245 modified_at=self.modified_at.replace(microsecond=0),
2245 modified_at=self.modified_at.replace(microsecond=0),
2246 reviewer=self.author.username,
2246 reviewer=self.author.username,
2247 )
2247 )
2248
2248
2249
2249
2250 class PullRequest(Base, BaseDbModel):
2250 class PullRequest(Base, BaseDbModel):
2251 __tablename__ = 'pull_requests'
2251 __tablename__ = 'pull_requests'
2252 __table_args__ = (
2252 __table_args__ = (
2253 Index('pr_org_repo_id_idx', 'org_repo_id'),
2253 Index('pr_org_repo_id_idx', 'org_repo_id'),
2254 Index('pr_other_repo_id_idx', 'other_repo_id'),
2254 Index('pr_other_repo_id_idx', 'other_repo_id'),
2255 _table_args_default_dict,
2255 _table_args_default_dict,
2256 )
2256 )
2257
2257
2258 # values for .status
2258 # values for .status
2259 STATUS_NEW = u'new'
2259 STATUS_NEW = u'new'
2260 STATUS_CLOSED = u'closed'
2260 STATUS_CLOSED = u'closed'
2261
2261
2262 pull_request_id = Column(Integer(), primary_key=True)
2262 pull_request_id = Column(Integer(), primary_key=True)
2263 title = Column(Unicode(255), nullable=False)
2263 title = Column(Unicode(255), nullable=False)
2264 description = Column(UnicodeText(), nullable=False)
2264 description = Column(UnicodeText(), nullable=False)
2265 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2265 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2266 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2266 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2267 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2267 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2268 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2268 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2269 _revisions = Column('revisions', UnicodeText(), nullable=False)
2269 _revisions = Column('revisions', UnicodeText(), nullable=False)
2270 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2270 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2271 org_ref = Column(Unicode(255), nullable=False)
2271 org_ref = Column(Unicode(255), nullable=False)
2272 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2272 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2273 other_ref = Column(Unicode(255), nullable=False)
2273 other_ref = Column(Unicode(255), nullable=False)
2274
2274
2275 @hybrid_property
2275 @hybrid_property
2276 def revisions(self):
2276 def revisions(self):
2277 return self._revisions.split(':')
2277 return self._revisions.split(':')
2278
2278
2279 @revisions.setter
2279 @revisions.setter
2280 def revisions(self, val):
2280 def revisions(self, val):
2281 self._revisions = safe_unicode(':'.join(val))
2281 self._revisions = safe_unicode(':'.join(val))
2282
2282
2283 @property
2283 @property
2284 def org_ref_parts(self):
2284 def org_ref_parts(self):
2285 return self.org_ref.split(':')
2285 return self.org_ref.split(':')
2286
2286
2287 @property
2287 @property
2288 def other_ref_parts(self):
2288 def other_ref_parts(self):
2289 return self.other_ref.split(':')
2289 return self.other_ref.split(':')
2290
2290
2291 owner = relationship('User')
2291 owner = relationship('User')
2292 reviewers = relationship('PullRequestReviewer',
2292 reviewers = relationship('PullRequestReviewer',
2293 cascade="all, delete-orphan")
2293 cascade="all, delete-orphan")
2294 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2294 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2295 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2295 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2296 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2296 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2297 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2297 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2298 cascade="all, delete-orphan")
2298 cascade="all, delete-orphan")
2299
2299
2300 @classmethod
2300 @classmethod
2301 def query(cls, reviewer_id=None, include_closed=True, sorted=False):
2301 def query(cls, reviewer_id=None, include_closed=True, sorted=False):
2302 """Add PullRequest-specific helpers for common query constructs.
2302 """Add PullRequest-specific helpers for common query constructs.
2303
2303
2304 reviewer_id: only PRs with the specified user added as reviewer.
2304 reviewer_id: only PRs with the specified user added as reviewer.
2305
2305
2306 include_closed: if False, do not include closed PRs.
2306 include_closed: if False, do not include closed PRs.
2307
2307
2308 sorted: if True, apply the default ordering (newest first).
2308 sorted: if True, apply the default ordering (newest first).
2309 """
2309 """
2310 q = super(PullRequest, cls).query()
2310 q = super(PullRequest, cls).query()
2311
2311
2312 if reviewer_id is not None:
2312 if reviewer_id is not None:
2313 q = q.join(PullRequestReviewer).filter(PullRequestReviewer.user_id == reviewer_id)
2313 q = q.join(PullRequestReviewer).filter(PullRequestReviewer.user_id == reviewer_id)
2314
2314
2315 if not include_closed:
2315 if not include_closed:
2316 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
2316 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
2317
2317
2318 if sorted:
2318 if sorted:
2319 q = q.order_by(PullRequest.created_on.desc())
2319 q = q.order_by(PullRequest.created_on.desc())
2320
2320
2321 return q
2321 return q
2322
2322
2323 def get_reviewer_users(self):
2323 def get_reviewer_users(self):
2324 """Like .reviewers, but actually returning the users"""
2324 """Like .reviewers, but actually returning the users"""
2325 return User.query() \
2325 return User.query() \
2326 .join(PullRequestReviewer) \
2326 .join(PullRequestReviewer) \
2327 .filter(PullRequestReviewer.pull_request == self) \
2327 .filter(PullRequestReviewer.pull_request == self) \
2328 .order_by(PullRequestReviewer.pull_request_reviewers_id) \
2328 .order_by(PullRequestReviewer.pull_request_reviewers_id) \
2329 .all()
2329 .all()
2330
2330
2331 def is_closed(self):
2331 def is_closed(self):
2332 return self.status == self.STATUS_CLOSED
2332 return self.status == self.STATUS_CLOSED
2333
2333
2334 def user_review_status(self, user_id):
2334 def user_review_status(self, user_id):
2335 """Return the user's latest status votes on PR"""
2335 """Return the user's latest status votes on PR"""
2336 # note: no filtering on repo - that would be redundant
2336 # note: no filtering on repo - that would be redundant
2337 status = ChangesetStatus.query() \
2337 status = ChangesetStatus.query() \
2338 .filter(ChangesetStatus.pull_request == self) \
2338 .filter(ChangesetStatus.pull_request == self) \
2339 .filter(ChangesetStatus.user_id == user_id) \
2339 .filter(ChangesetStatus.user_id == user_id) \
2340 .order_by(ChangesetStatus.version) \
2340 .order_by(ChangesetStatus.version) \
2341 .first()
2341 .first()
2342 return str(status.status) if status else ''
2342 return str(status.status) if status else ''
2343
2343
2344 @classmethod
2344 @classmethod
2345 def make_nice_id(cls, pull_request_id):
2345 def make_nice_id(cls, pull_request_id):
2346 '''Return pull request id nicely formatted for displaying'''
2346 '''Return pull request id nicely formatted for displaying'''
2347 return '#%s' % pull_request_id
2347 return '#%s' % pull_request_id
2348
2348
2349 def nice_id(self):
2349 def nice_id(self):
2350 '''Return the id of this pull request, nicely formatted for displaying'''
2350 '''Return the id of this pull request, nicely formatted for displaying'''
2351 return self.make_nice_id(self.pull_request_id)
2351 return self.make_nice_id(self.pull_request_id)
2352
2352
2353 def get_api_data(self):
2353 def get_api_data(self):
2354 return self.__json__()
2354 return self.__json__()
2355
2355
2356 def __json__(self):
2356 def __json__(self):
2357 clone_uri_tmpl = kallithea.CONFIG.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
2357 clone_uri_tmpl = kallithea.CONFIG.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
2358 return dict(
2358 return dict(
2359 pull_request_id=self.pull_request_id,
2359 pull_request_id=self.pull_request_id,
2360 url=self.url(),
2360 url=self.url(),
2361 reviewers=self.reviewers,
2361 reviewers=self.reviewers,
2362 revisions=self.revisions,
2362 revisions=self.revisions,
2363 owner=self.owner.username,
2363 owner=self.owner.username,
2364 title=self.title,
2364 title=self.title,
2365 description=self.description,
2365 description=self.description,
2366 org_repo_url=self.org_repo.clone_url(clone_uri_tmpl=clone_uri_tmpl),
2366 org_repo_url=self.org_repo.clone_url(clone_uri_tmpl=clone_uri_tmpl),
2367 org_ref_parts=self.org_ref_parts,
2367 org_ref_parts=self.org_ref_parts,
2368 other_ref_parts=self.other_ref_parts,
2368 other_ref_parts=self.other_ref_parts,
2369 status=self.status,
2369 status=self.status,
2370 comments=self.comments,
2370 comments=self.comments,
2371 statuses=self.statuses,
2371 statuses=self.statuses,
2372 )
2372 )
2373
2373
2374 def url(self, **kwargs):
2374 def url(self, **kwargs):
2375 canonical = kwargs.pop('canonical', None)
2375 canonical = kwargs.pop('canonical', None)
2376 import kallithea.lib.helpers as h
2376 import kallithea.lib.helpers as h
2377 b = self.org_ref_parts[1]
2377 b = self.org_ref_parts[1]
2378 if b != self.other_ref_parts[1]:
2378 if b != self.other_ref_parts[1]:
2379 s = '/_/' + b
2379 s = '/_/' + b
2380 else:
2380 else:
2381 s = '/_/' + self.title
2381 s = '/_/' + self.title
2382 kwargs['extra'] = urlreadable(s)
2382 kwargs['extra'] = urlreadable(s)
2383 if canonical:
2383 if canonical:
2384 return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2384 return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2385 pull_request_id=self.pull_request_id, **kwargs)
2385 pull_request_id=self.pull_request_id, **kwargs)
2386 return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2386 return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2387 pull_request_id=self.pull_request_id, **kwargs)
2387 pull_request_id=self.pull_request_id, **kwargs)
2388
2388
2389
2389
2390 class PullRequestReviewer(Base, BaseDbModel):
2390 class PullRequestReviewer(Base, BaseDbModel):
2391 __tablename__ = 'pull_request_reviewers'
2391 __tablename__ = 'pull_request_reviewers'
2392 __table_args__ = (
2392 __table_args__ = (
2393 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2393 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2394 _table_args_default_dict,
2394 _table_args_default_dict,
2395 )
2395 )
2396
2396
2397 def __init__(self, user=None, pull_request=None):
2397 def __init__(self, user=None, pull_request=None):
2398 self.user = user
2398 self.user = user
2399 self.pull_request = pull_request
2399 self.pull_request = pull_request
2400
2400
2401 pull_request_reviewers_id = Column('pull_requests_reviewers_id', Integer(), primary_key=True)
2401 pull_request_reviewers_id = Column('pull_requests_reviewers_id', Integer(), primary_key=True)
2402 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2402 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2403 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2403 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2404
2404
2405 user = relationship('User')
2405 user = relationship('User')
2406 pull_request = relationship('PullRequest')
2406 pull_request = relationship('PullRequest')
2407
2407
2408 def __json__(self):
2408 def __json__(self):
2409 return dict(
2409 return dict(
2410 username=self.user.username if self.user else None,
2410 username=self.user.username if self.user else None,
2411 )
2411 )
2412
2412
2413
2413
2414 class Notification(object):
2414 class Notification(object):
2415 __tablename__ = 'notifications'
2415 __tablename__ = 'notifications'
2416
2416
2417 class UserNotification(object):
2417 class UserNotification(object):
2418 __tablename__ = 'user_to_notification'
2418 __tablename__ = 'user_to_notification'
2419
2419
2420
2420
2421 class Gist(Base, BaseDbModel):
2421 class Gist(Base, BaseDbModel):
2422 __tablename__ = 'gists'
2422 __tablename__ = 'gists'
2423 __table_args__ = (
2423 __table_args__ = (
2424 Index('g_gist_access_id_idx', 'gist_access_id'),
2424 Index('g_gist_access_id_idx', 'gist_access_id'),
2425 Index('g_created_on_idx', 'created_on'),
2425 Index('g_created_on_idx', 'created_on'),
2426 _table_args_default_dict,
2426 _table_args_default_dict,
2427 )
2427 )
2428
2428
2429 GIST_PUBLIC = u'public'
2429 GIST_PUBLIC = u'public'
2430 GIST_PRIVATE = u'private'
2430 GIST_PRIVATE = u'private'
2431 DEFAULT_FILENAME = u'gistfile1.txt'
2431 DEFAULT_FILENAME = u'gistfile1.txt'
2432
2432
2433 gist_id = Column(Integer(), primary_key=True)
2433 gist_id = Column(Integer(), primary_key=True)
2434 gist_access_id = Column(Unicode(250), nullable=False)
2434 gist_access_id = Column(Unicode(250), nullable=False)
2435 gist_description = Column(UnicodeText(), nullable=False)
2435 gist_description = Column(UnicodeText(), nullable=False)
2436 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2436 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2437 gist_expires = Column(Float(53), nullable=False)
2437 gist_expires = Column(Float(53), nullable=False)
2438 gist_type = Column(Unicode(128), nullable=False)
2438 gist_type = Column(Unicode(128), nullable=False)
2439 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2439 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2440 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2440 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2441
2441
2442 owner = relationship('User')
2442 owner = relationship('User')
2443
2443
2444 @hybrid_property
2444 @hybrid_property
2445 def is_expired(self):
2445 def is_expired(self):
2446 return (self.gist_expires != -1) & (time.time() > self.gist_expires)
2446 return (self.gist_expires != -1) & (time.time() > self.gist_expires)
2447
2447
2448 def __repr__(self):
2448 def __repr__(self):
2449 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
2449 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
2450
2450
2451 @classmethod
2451 @classmethod
2452 def guess_instance(cls, value):
2452 def guess_instance(cls, value):
2453 return super(Gist, cls).guess_instance(value, Gist.get_by_access_id)
2453 return super(Gist, cls).guess_instance(value, Gist.get_by_access_id)
2454
2454
2455 @classmethod
2455 @classmethod
2456 def get_or_404(cls, id_):
2456 def get_or_404(cls, id_):
2457 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2457 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2458 if res is None:
2458 if res is None:
2459 raise HTTPNotFound
2459 raise HTTPNotFound
2460 return res
2460 return res
2461
2461
2462 @classmethod
2462 @classmethod
2463 def get_by_access_id(cls, gist_access_id):
2463 def get_by_access_id(cls, gist_access_id):
2464 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2464 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2465
2465
2466 def gist_url(self):
2466 def gist_url(self):
2467 alias_url = kallithea.CONFIG.get('gist_alias_url')
2467 alias_url = kallithea.CONFIG.get('gist_alias_url')
2468 if alias_url:
2468 if alias_url:
2469 return alias_url.replace('{gistid}', self.gist_access_id)
2469 return alias_url.replace('{gistid}', self.gist_access_id)
2470
2470
2471 import kallithea.lib.helpers as h
2471 import kallithea.lib.helpers as h
2472 return h.canonical_url('gist', gist_id=self.gist_access_id)
2472 return h.canonical_url('gist', gist_id=self.gist_access_id)
2473
2473
2474 @classmethod
2474 @classmethod
2475 def base_path(cls):
2475 def base_path(cls):
2476 """
2476 """
2477 Returns base path where all gists are stored
2477 Returns base path where all gists are stored
2478
2478
2479 :param cls:
2479 :param cls:
2480 """
2480 """
2481 from kallithea.model.gist import GIST_STORE_LOC
2481 from kallithea.model.gist import GIST_STORE_LOC
2482 q = Session().query(Ui) \
2482 q = Session().query(Ui) \
2483 .filter(Ui.ui_key == URL_SEP)
2483 .filter(Ui.ui_key == URL_SEP)
2484 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2484 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2485 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2485 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2486
2486
2487 def get_api_data(self):
2487 def get_api_data(self):
2488 """
2488 """
2489 Common function for generating gist related data for API
2489 Common function for generating gist related data for API
2490 """
2490 """
2491 gist = self
2491 gist = self
2492 data = dict(
2492 data = dict(
2493 gist_id=gist.gist_id,
2493 gist_id=gist.gist_id,
2494 type=gist.gist_type,
2494 type=gist.gist_type,
2495 access_id=gist.gist_access_id,
2495 access_id=gist.gist_access_id,
2496 description=gist.gist_description,
2496 description=gist.gist_description,
2497 url=gist.gist_url(),
2497 url=gist.gist_url(),
2498 expires=gist.gist_expires,
2498 expires=gist.gist_expires,
2499 created_on=gist.created_on,
2499 created_on=gist.created_on,
2500 )
2500 )
2501 return data
2501 return data
2502
2502
2503 def __json__(self):
2503 def __json__(self):
2504 data = dict(
2504 data = dict(
2505 )
2505 )
2506 data.update(self.get_api_data())
2506 data.update(self.get_api_data())
2507 return data
2507 return data
2508 ## SCM functions
2508 ## SCM functions
2509
2509
2510 @property
2510 @property
2511 def scm_instance(self):
2511 def scm_instance(self):
2512 from kallithea.lib.vcs import get_repo
2512 from kallithea.lib.vcs import get_repo
2513 base_path = self.base_path()
2513 base_path = self.base_path()
2514 return get_repo(os.path.join(*map(safe_str,
2514 return get_repo(os.path.join(*map(safe_str,
2515 [base_path, self.gist_access_id])))
2515 [base_path, self.gist_access_id])))
2516
2516
2517
2517
2518 class UserSshKeys(Base, BaseDbModel):
2518 class UserSshKeys(Base, BaseDbModel):
2519 __tablename__ = 'user_ssh_keys'
2519 __tablename__ = 'user_ssh_keys'
2520 __table_args__ = (
2520 __table_args__ = (
2521 Index('usk_public_key_idx', 'public_key'),
2521 Index('usk_public_key_idx', 'public_key'),
2522 Index('usk_fingerprint_idx', 'fingerprint'),
2522 Index('usk_fingerprint_idx', 'fingerprint'),
2523 UniqueConstraint('fingerprint'),
2523 UniqueConstraint('fingerprint'),
2524 _table_args_default_dict
2524 _table_args_default_dict
2525 )
2525 )
2526 __mapper_args__ = {}
2526 __mapper_args__ = {}
2527
2527
2528 user_ssh_key_id = Column(Integer(), primary_key=True)
2528 user_ssh_key_id = Column(Integer(), primary_key=True)
2529 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2529 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2530 _public_key = Column('public_key', UnicodeText(), nullable=False)
2530 _public_key = Column('public_key', UnicodeText(), nullable=False)
2531 description = Column(UnicodeText(), nullable=False)
2531 description = Column(UnicodeText(), nullable=False)
2532 fingerprint = Column(String(255), nullable=False, unique=True)
2532 fingerprint = Column(String(255), nullable=False, unique=True)
2533 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2533 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2534 last_seen = Column(DateTime(timezone=False), nullable=True)
2534 last_seen = Column(DateTime(timezone=False), nullable=True)
2535
2535
2536 user = relationship('User')
2536 user = relationship('User')
2537
2537
2538 @property
2538 @property
2539 def public_key(self):
2539 def public_key(self):
2540 return self._public_key
2540 return self._public_key
2541
2541
2542 @public_key.setter
2542 @public_key.setter
2543 def public_key(self, full_key):
2543 def public_key(self, full_key):
2544 # the full public key is too long to be suitable as database key - instead,
2544 # the full public key is too long to be suitable as database key - instead,
2545 # use fingerprints similar to 'ssh-keygen -E sha256 -lf ~/.ssh/id_rsa.pub'
2545 # use fingerprints similar to 'ssh-keygen -E sha256 -lf ~/.ssh/id_rsa.pub'
2546 self._public_key = full_key
2546 self._public_key = full_key
2547 enc_key = full_key.split(" ")[1]
2547 enc_key = full_key.split(" ")[1]
2548 self.fingerprint = hashlib.sha256(enc_key.decode('base64')).digest().encode('base64').replace('\n', '').rstrip('=')
2548 self.fingerprint = hashlib.sha256(enc_key.decode('base64')).digest().encode('base64').replace('\n', '').rstrip('=')
@@ -1,720 +1,720 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.repo
15 kallithea.model.repo
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 Repository model for kallithea
18 Repository model for kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jun 5, 2010
22 :created_on: Jun 5, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26
26
27 """
27 """
28
28
29 import logging
29 import logging
30 import os
30 import os
31 import shutil
31 import shutil
32 import traceback
32 import traceback
33 from datetime import datetime
33 from datetime import datetime
34
34
35 import kallithea.lib.utils2
35 import kallithea.lib.utils2
36 from kallithea.lib import helpers as h
36 from kallithea.lib import helpers as h
37 from kallithea.lib.auth import HasRepoPermissionLevel, HasUserGroupPermissionLevel
37 from kallithea.lib.auth import HasRepoPermissionLevel, HasUserGroupPermissionLevel
38 from kallithea.lib.caching_query import FromCache
38 from kallithea.lib.caching_query import FromCache
39 from kallithea.lib.exceptions import AttachedForksError
39 from kallithea.lib.exceptions import AttachedForksError
40 from kallithea.lib.hooks import log_delete_repository
40 from kallithea.lib.hooks import log_delete_repository
41 from kallithea.lib.utils import is_valid_repo_uri, make_ui
41 from kallithea.lib.utils import is_valid_repo_uri, make_ui
42 from kallithea.lib.utils2 import LazyProperty, get_current_authuser, obfuscate_url_pw, remove_prefix, safe_str, safe_unicode
42 from kallithea.lib.utils2 import LazyProperty, get_current_authuser, obfuscate_url_pw, remove_prefix, safe_str, safe_unicode
43 from kallithea.lib.vcs.backends import get_backend
43 from kallithea.lib.vcs.backends import get_backend
44 from kallithea.model.db import (
44 from kallithea.model.db import (
45 Permission, RepoGroup, Repository, RepositoryField, Session, Statistics, Ui, User, UserGroup, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm, UserRepoToPerm)
45 Permission, RepoGroup, Repository, RepositoryField, Session, Statistics, Ui, User, UserGroup, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm, UserRepoToPerm)
46
46
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class RepoModel(object):
51 class RepoModel(object):
52
52
53 URL_SEPARATOR = Repository.url_sep()
53 URL_SEPARATOR = Repository.url_sep()
54
54
55 def _create_default_perms(self, repository, private):
55 def _create_default_perms(self, repository, private):
56 # create default permission
56 # create default permission
57 default = 'repository.read'
57 default = 'repository.read'
58 def_user = User.get_default_user()
58 def_user = User.get_default_user()
59 for p in def_user.user_perms:
59 for p in def_user.user_perms:
60 if p.permission.permission_name.startswith('repository.'):
60 if p.permission.permission_name.startswith('repository.'):
61 default = p.permission.permission_name
61 default = p.permission.permission_name
62 break
62 break
63
63
64 default_perm = 'repository.none' if private else default
64 default_perm = 'repository.none' if private else default
65
65
66 repo_to_perm = UserRepoToPerm()
66 repo_to_perm = UserRepoToPerm()
67 repo_to_perm.permission = Permission.get_by_key(default_perm)
67 repo_to_perm.permission = Permission.get_by_key(default_perm)
68
68
69 repo_to_perm.repository = repository
69 repo_to_perm.repository = repository
70 repo_to_perm.user_id = def_user.user_id
70 repo_to_perm.user_id = def_user.user_id
71 Session().add(repo_to_perm)
71 Session().add(repo_to_perm)
72
72
73 return repo_to_perm
73 return repo_to_perm
74
74
75 @LazyProperty
75 @LazyProperty
76 def repos_path(self):
76 def repos_path(self):
77 """
77 """
78 Gets the repositories root path from database
78 Gets the repositories root path from database
79 """
79 """
80
80
81 q = Ui.query().filter(Ui.ui_key == '/').one()
81 q = Ui.query().filter(Ui.ui_key == '/').one()
82 return q.ui_value
82 return q.ui_value
83
83
84 def get(self, repo_id, cache=False):
84 def get(self, repo_id, cache=False):
85 repo = Repository.query() \
85 repo = Repository.query() \
86 .filter(Repository.repo_id == repo_id)
86 .filter(Repository.repo_id == repo_id)
87
87
88 if cache:
88 if cache:
89 repo = repo.options(FromCache("sql_cache_short",
89 repo = repo.options(FromCache("sql_cache_short",
90 "get_repo_%s" % repo_id))
90 "get_repo_%s" % repo_id))
91 return repo.scalar()
91 return repo.scalar()
92
92
93 def get_repo(self, repository):
93 def get_repo(self, repository):
94 return Repository.guess_instance(repository)
94 return Repository.guess_instance(repository)
95
95
96 def get_by_repo_name(self, repo_name, cache=False):
96 def get_by_repo_name(self, repo_name, cache=False):
97 repo = Repository.query() \
97 repo = Repository.query() \
98 .filter(Repository.repo_name == repo_name)
98 .filter(Repository.repo_name == repo_name)
99
99
100 if cache:
100 if cache:
101 repo = repo.options(FromCache("sql_cache_short",
101 repo = repo.options(FromCache("sql_cache_short",
102 "get_repo_%s" % repo_name))
102 "get_repo_%s" % repo_name))
103 return repo.scalar()
103 return repo.scalar()
104
104
105 def get_all_user_repos(self, user):
105 def get_all_user_repos(self, user):
106 """
106 """
107 Gets all repositories that user have at least read access
107 Gets all repositories that user have at least read access
108
108
109 :param user:
109 :param user:
110 """
110 """
111 from kallithea.lib.auth import AuthUser
111 from kallithea.lib.auth import AuthUser
112 user = User.guess_instance(user)
112 user = User.guess_instance(user)
113 repos = AuthUser(dbuser=user).permissions['repositories']
113 repos = AuthUser(dbuser=user).permissions['repositories']
114 access_check = lambda r: r[1] in ['repository.read',
114 access_check = lambda r: r[1] in ['repository.read',
115 'repository.write',
115 'repository.write',
116 'repository.admin']
116 'repository.admin']
117 repos = [x[0] for x in filter(access_check, repos.items())]
117 repos = [x[0] for x in filter(access_check, repos.items())]
118 return Repository.query().filter(Repository.repo_name.in_(repos))
118 return Repository.query().filter(Repository.repo_name.in_(repos))
119
119
120 @classmethod
120 @classmethod
121 def _render_datatable(cls, tmpl, *args, **kwargs):
121 def _render_datatable(cls, tmpl, *args, **kwargs):
122 import kallithea
122 import kallithea
123 from tg import tmpl_context as c, request, app_globals
123 from tg import tmpl_context as c, request, app_globals
124 from tg.i18n import ugettext as _
124 from tg.i18n import ugettext as _
125
125
126 _tmpl_lookup = app_globals.mako_lookup
126 _tmpl_lookup = app_globals.mako_lookup
127 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
127 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
128
128
129 tmpl = template.get_def(tmpl)
129 tmpl = template.get_def(tmpl)
130 kwargs.update(dict(_=_, h=h, c=c, request=request))
130 kwargs.update(dict(_=_, h=h, c=c, request=request))
131 return tmpl.render(*args, **kwargs)
131 return tmpl.render(*args, **kwargs)
132
132
133 def get_repos_as_dict(self, repos_list, repo_groups_list=None,
133 def get_repos_as_dict(self, repos_list, repo_groups_list=None,
134 admin=False,
134 admin=False,
135 short_name=False):
135 short_name=False):
136 """Return repository list for use by DataTable.
136 """Return repository list for use by DataTable.
137 repos_list: list of repositories - but will be filtered for read permission.
137 repos_list: list of repositories - but will be filtered for read permission.
138 repo_groups_list: added at top of list without permission check.
138 repo_groups_list: added at top of list without permission check.
139 admin: return data for action column.
139 admin: return data for action column.
140 """
140 """
141 _render = self._render_datatable
141 _render = self._render_datatable
142 from tg import tmpl_context as c
142 from tg import tmpl_context as c
143
143
144 def repo_lnk(name, rtype, rstate, private, fork_of):
144 def repo_lnk(name, rtype, rstate, private, fork_of):
145 return _render('repo_name', name, rtype, rstate, private, fork_of,
145 return _render('repo_name', name, rtype, rstate, private, fork_of,
146 short_name=short_name)
146 short_name=short_name)
147
147
148 def last_change(last_change):
148 def last_change(last_change):
149 return _render("last_change", last_change)
149 return _render("last_change", last_change)
150
150
151 def rss_lnk(repo_name):
151 def rss_lnk(repo_name):
152 return _render("rss", repo_name)
152 return _render("rss", repo_name)
153
153
154 def atom_lnk(repo_name):
154 def atom_lnk(repo_name):
155 return _render("atom", repo_name)
155 return _render("atom", repo_name)
156
156
157 def last_rev(repo_name, cs_cache):
157 def last_rev(repo_name, cs_cache):
158 return _render('revision', repo_name, cs_cache.get('revision'),
158 return _render('revision', repo_name, cs_cache.get('revision'),
159 cs_cache.get('raw_id'), cs_cache.get('author'),
159 cs_cache.get('raw_id'), cs_cache.get('author'),
160 cs_cache.get('message'))
160 cs_cache.get('message'))
161
161
162 def desc(desc):
162 def desc(desc):
163 return h.urlify_text(desc, truncate=80, stylize=c.visual.stylify_metalabels)
163 return h.urlify_text(desc, truncate=80, stylize=c.visual.stylify_metalabels)
164
164
165 def state(repo_state):
165 def state(repo_state):
166 return _render("repo_state", repo_state)
166 return _render("repo_state", repo_state)
167
167
168 def repo_actions(repo_name):
168 def repo_actions(repo_name):
169 return _render('repo_actions', repo_name)
169 return _render('repo_actions', repo_name)
170
170
171 def owner_actions(owner_id, username):
171 def owner_actions(owner_id, username):
172 return _render('user_name', owner_id, username)
172 return _render('user_name', owner_id, username)
173
173
174 repos_data = []
174 repos_data = []
175
175
176 for gr in repo_groups_list or []:
176 for gr in repo_groups_list or []:
177 repos_data.append(dict(
177 repos_data.append(dict(
178 raw_name='\0' + gr.name, # sort before repositories
178 raw_name='\0' + gr.name, # sort before repositories
179 just_name=gr.name,
179 just_name=gr.name,
180 name=_render('group_name_html', group_name=gr.group_name, name=gr.name),
180 name=_render('group_name_html', group_name=gr.group_name, name=gr.name),
181 desc=gr.group_description))
181 desc=gr.group_description))
182
182
183 for repo in repos_list:
183 for repo in repos_list:
184 if not HasRepoPermissionLevel('read')(repo.repo_name, 'get_repos_as_dict check'):
184 if not HasRepoPermissionLevel('read')(repo.repo_name, 'get_repos_as_dict check'):
185 continue
185 continue
186 cs_cache = repo.changeset_cache
186 cs_cache = repo.changeset_cache
187 row = {
187 row = {
188 "raw_name": repo.repo_name,
188 "raw_name": repo.repo_name,
189 "just_name": repo.just_name,
189 "just_name": repo.just_name,
190 "name": repo_lnk(repo.repo_name, repo.repo_type,
190 "name": repo_lnk(repo.repo_name, repo.repo_type,
191 repo.repo_state, repo.private, repo.fork),
191 repo.repo_state, repo.private, repo.fork),
192 "last_change_iso": repo.last_db_change.isoformat(),
192 "last_change_iso": repo.last_db_change.isoformat(),
193 "last_change": last_change(repo.last_db_change),
193 "last_change": last_change(repo.last_db_change),
194 "last_changeset": last_rev(repo.repo_name, cs_cache),
194 "last_changeset": last_rev(repo.repo_name, cs_cache),
195 "last_rev_raw": cs_cache.get('revision'),
195 "last_rev_raw": cs_cache.get('revision'),
196 "desc": desc(repo.description),
196 "desc": desc(repo.description),
197 "owner": h.person(repo.owner),
197 "owner": h.person(repo.owner),
198 "state": state(repo.repo_state),
198 "state": state(repo.repo_state),
199 "rss": rss_lnk(repo.repo_name),
199 "rss": rss_lnk(repo.repo_name),
200 "atom": atom_lnk(repo.repo_name),
200 "atom": atom_lnk(repo.repo_name),
201 }
201 }
202 if admin:
202 if admin:
203 row.update({
203 row.update({
204 "action": repo_actions(repo.repo_name),
204 "action": repo_actions(repo.repo_name),
205 "owner": owner_actions(repo.owner_id,
205 "owner": owner_actions(repo.owner_id,
206 h.person(repo.owner))
206 h.person(repo.owner))
207 })
207 })
208 repos_data.append(row)
208 repos_data.append(row)
209
209
210 return {
210 return {
211 "sort": "name",
211 "sort": "name",
212 "dir": "asc",
212 "dir": "asc",
213 "records": repos_data
213 "records": repos_data
214 }
214 }
215
215
216 def _get_defaults(self, repo_name):
216 def _get_defaults(self, repo_name):
217 """
217 """
218 Gets information about repository, and returns a dict for
218 Gets information about repository, and returns a dict for
219 usage in forms
219 usage in forms
220
220
221 :param repo_name:
221 :param repo_name:
222 """
222 """
223
223
224 repo_info = Repository.get_by_repo_name(repo_name)
224 repo_info = Repository.get_by_repo_name(repo_name)
225
225
226 if repo_info is None:
226 if repo_info is None:
227 return None
227 return None
228
228
229 defaults = repo_info.get_dict()
229 defaults = repo_info.get_dict()
230 defaults['repo_name'] = repo_info.just_name
230 defaults['repo_name'] = repo_info.just_name
231 defaults['repo_group'] = repo_info.group_id
231 defaults['repo_group'] = repo_info.group_id
232
232
233 for strip, k in [(0, 'repo_type'), (1, 'repo_enable_downloads'),
233 for strip, k in [(0, 'repo_type'), (1, 'repo_enable_downloads'),
234 (1, 'repo_description'),
234 (1, 'repo_description'),
235 (1, 'repo_landing_rev'), (0, 'clone_uri'),
235 (1, 'repo_landing_rev'), (0, 'clone_uri'),
236 (1, 'repo_private'), (1, 'repo_enable_statistics')]:
236 (1, 'repo_private'), (1, 'repo_enable_statistics')]:
237 attr = k
237 attr = k
238 if strip:
238 if strip:
239 attr = remove_prefix(k, 'repo_')
239 attr = remove_prefix(k, 'repo_')
240
240
241 val = defaults[attr]
241 val = defaults[attr]
242 if k == 'repo_landing_rev':
242 if k == 'repo_landing_rev':
243 val = ':'.join(defaults[attr])
243 val = ':'.join(defaults[attr])
244 defaults[k] = val
244 defaults[k] = val
245 if k == 'clone_uri':
245 if k == 'clone_uri':
246 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
246 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
247
247
248 # fill owner
248 # fill owner
249 if repo_info.owner:
249 if repo_info.owner:
250 defaults.update({'owner': repo_info.owner.username})
250 defaults.update({'owner': repo_info.owner.username})
251 else:
251 else:
252 replacement_user = User.query().filter(User.admin ==
252 replacement_user = User.query().filter(User.admin ==
253 True).first().username
253 True).first().username
254 defaults.update({'owner': replacement_user})
254 defaults.update({'owner': replacement_user})
255
255
256 # fill repository users
256 # fill repository users
257 for p in repo_info.repo_to_perm:
257 for p in repo_info.repo_to_perm:
258 defaults.update({'u_perm_%s' % p.user.username:
258 defaults.update({'u_perm_%s' % p.user.username:
259 p.permission.permission_name})
259 p.permission.permission_name})
260
260
261 # fill repository groups
261 # fill repository groups
262 for p in repo_info.users_group_to_perm:
262 for p in repo_info.users_group_to_perm:
263 defaults.update({'g_perm_%s' % p.users_group.users_group_name:
263 defaults.update({'g_perm_%s' % p.users_group.users_group_name:
264 p.permission.permission_name})
264 p.permission.permission_name})
265
265
266 return defaults
266 return defaults
267
267
268 def update(self, repo, **kwargs):
268 def update(self, repo, **kwargs):
269 try:
269 try:
270 cur_repo = Repository.guess_instance(repo)
270 cur_repo = Repository.guess_instance(repo)
271 org_repo_name = cur_repo.repo_name
271 org_repo_name = cur_repo.repo_name
272 if 'owner' in kwargs:
272 if 'owner' in kwargs:
273 cur_repo.owner = User.get_by_username(kwargs['owner'])
273 cur_repo.owner = User.get_by_username(kwargs['owner'])
274
274
275 if 'repo_group' in kwargs:
275 if 'repo_group' in kwargs:
276 assert kwargs['repo_group'] != u'-1', kwargs # RepoForm should have converted to None
276 assert kwargs['repo_group'] != u'-1', kwargs # RepoForm should have converted to None
277 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
277 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
278 cur_repo.repo_name = cur_repo.get_new_name(cur_repo.just_name)
278 cur_repo.repo_name = cur_repo.get_new_name(cur_repo.just_name)
279 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
279 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
280 for k in ['repo_enable_downloads',
280 for k in ['repo_enable_downloads',
281 'repo_description',
281 'repo_description',
282 'repo_landing_rev',
282 'repo_landing_rev',
283 'repo_private',
283 'repo_private',
284 'repo_enable_statistics',
284 'repo_enable_statistics',
285 ]:
285 ]:
286 if k in kwargs:
286 if k in kwargs:
287 setattr(cur_repo, remove_prefix(k, 'repo_'), kwargs[k])
287 setattr(cur_repo, remove_prefix(k, 'repo_'), kwargs[k])
288 clone_uri = kwargs.get('clone_uri')
288 clone_uri = kwargs.get('clone_uri')
289 if clone_uri is not None and clone_uri != cur_repo.clone_uri_hidden:
289 if clone_uri is not None and clone_uri != cur_repo.clone_uri_hidden:
290 # clone_uri is modified - if given a value, check it is valid
290 # clone_uri is modified - if given a value, check it is valid
291 if clone_uri != '':
291 if clone_uri != '':
292 # will raise exception on error
292 # will raise exception on error
293 is_valid_repo_uri(cur_repo.repo_type, clone_uri, make_ui(clear_session=False))
293 is_valid_repo_uri(cur_repo.repo_type, clone_uri, make_ui())
294 cur_repo.clone_uri = clone_uri
294 cur_repo.clone_uri = clone_uri
295
295
296 if 'repo_name' in kwargs:
296 if 'repo_name' in kwargs:
297 repo_name = kwargs['repo_name']
297 repo_name = kwargs['repo_name']
298 if kallithea.lib.utils2.repo_name_slug(repo_name) != repo_name:
298 if kallithea.lib.utils2.repo_name_slug(repo_name) != repo_name:
299 raise Exception('invalid repo name %s' % repo_name)
299 raise Exception('invalid repo name %s' % repo_name)
300 cur_repo.repo_name = cur_repo.get_new_name(repo_name)
300 cur_repo.repo_name = cur_repo.get_new_name(repo_name)
301
301
302 # if private flag is set, reset default permission to NONE
302 # if private flag is set, reset default permission to NONE
303 if kwargs.get('repo_private'):
303 if kwargs.get('repo_private'):
304 EMPTY_PERM = 'repository.none'
304 EMPTY_PERM = 'repository.none'
305 RepoModel().grant_user_permission(
305 RepoModel().grant_user_permission(
306 repo=cur_repo, user='default', perm=EMPTY_PERM
306 repo=cur_repo, user='default', perm=EMPTY_PERM
307 )
307 )
308 # handle extra fields
308 # handle extra fields
309 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
309 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
310 kwargs):
310 kwargs):
311 k = RepositoryField.un_prefix_key(field)
311 k = RepositoryField.un_prefix_key(field)
312 ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
312 ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
313 if ex_field:
313 if ex_field:
314 ex_field.field_value = kwargs[field]
314 ex_field.field_value = kwargs[field]
315
315
316 if org_repo_name != cur_repo.repo_name:
316 if org_repo_name != cur_repo.repo_name:
317 # rename repository
317 # rename repository
318 self._rename_filesystem_repo(old=org_repo_name, new=cur_repo.repo_name)
318 self._rename_filesystem_repo(old=org_repo_name, new=cur_repo.repo_name)
319
319
320 return cur_repo
320 return cur_repo
321 except Exception:
321 except Exception:
322 log.error(traceback.format_exc())
322 log.error(traceback.format_exc())
323 raise
323 raise
324
324
325 def _create_repo(self, repo_name, repo_type, description, owner,
325 def _create_repo(self, repo_name, repo_type, description, owner,
326 private=False, clone_uri=None, repo_group=None,
326 private=False, clone_uri=None, repo_group=None,
327 landing_rev='rev:tip', fork_of=None,
327 landing_rev='rev:tip', fork_of=None,
328 copy_fork_permissions=False, enable_statistics=False,
328 copy_fork_permissions=False, enable_statistics=False,
329 enable_downloads=False,
329 enable_downloads=False,
330 copy_group_permissions=False, state=Repository.STATE_PENDING):
330 copy_group_permissions=False, state=Repository.STATE_PENDING):
331 """
331 """
332 Create repository inside database with PENDING state. This should only be
332 Create repository inside database with PENDING state. This should only be
333 executed by create() repo, with exception of importing existing repos.
333 executed by create() repo, with exception of importing existing repos.
334
334
335 """
335 """
336 from kallithea.model.scm import ScmModel
336 from kallithea.model.scm import ScmModel
337
337
338 owner = User.guess_instance(owner)
338 owner = User.guess_instance(owner)
339 fork_of = Repository.guess_instance(fork_of)
339 fork_of = Repository.guess_instance(fork_of)
340 repo_group = RepoGroup.guess_instance(repo_group)
340 repo_group = RepoGroup.guess_instance(repo_group)
341 try:
341 try:
342 repo_name = safe_unicode(repo_name)
342 repo_name = safe_unicode(repo_name)
343 description = safe_unicode(description)
343 description = safe_unicode(description)
344 # repo name is just a name of repository
344 # repo name is just a name of repository
345 # while repo_name_full is a full qualified name that is combined
345 # while repo_name_full is a full qualified name that is combined
346 # with name and path of group
346 # with name and path of group
347 repo_name_full = repo_name
347 repo_name_full = repo_name
348 repo_name = repo_name.split(self.URL_SEPARATOR)[-1]
348 repo_name = repo_name.split(self.URL_SEPARATOR)[-1]
349 if kallithea.lib.utils2.repo_name_slug(repo_name) != repo_name:
349 if kallithea.lib.utils2.repo_name_slug(repo_name) != repo_name:
350 raise Exception('invalid repo name %s' % repo_name)
350 raise Exception('invalid repo name %s' % repo_name)
351
351
352 new_repo = Repository()
352 new_repo = Repository()
353 new_repo.repo_state = state
353 new_repo.repo_state = state
354 new_repo.enable_statistics = False
354 new_repo.enable_statistics = False
355 new_repo.repo_name = repo_name_full
355 new_repo.repo_name = repo_name_full
356 new_repo.repo_type = repo_type
356 new_repo.repo_type = repo_type
357 new_repo.owner = owner
357 new_repo.owner = owner
358 new_repo.group = repo_group
358 new_repo.group = repo_group
359 new_repo.description = description or repo_name
359 new_repo.description = description or repo_name
360 new_repo.private = private
360 new_repo.private = private
361 if clone_uri:
361 if clone_uri:
362 # will raise exception on error
362 # will raise exception on error
363 is_valid_repo_uri(repo_type, clone_uri, make_ui(clear_session=False))
363 is_valid_repo_uri(repo_type, clone_uri, make_ui())
364 new_repo.clone_uri = clone_uri
364 new_repo.clone_uri = clone_uri
365 new_repo.landing_rev = landing_rev
365 new_repo.landing_rev = landing_rev
366
366
367 new_repo.enable_statistics = enable_statistics
367 new_repo.enable_statistics = enable_statistics
368 new_repo.enable_downloads = enable_downloads
368 new_repo.enable_downloads = enable_downloads
369
369
370 if fork_of:
370 if fork_of:
371 parent_repo = fork_of
371 parent_repo = fork_of
372 new_repo.fork = parent_repo
372 new_repo.fork = parent_repo
373
373
374 Session().add(new_repo)
374 Session().add(new_repo)
375
375
376 if fork_of and copy_fork_permissions:
376 if fork_of and copy_fork_permissions:
377 repo = fork_of
377 repo = fork_of
378 user_perms = UserRepoToPerm.query() \
378 user_perms = UserRepoToPerm.query() \
379 .filter(UserRepoToPerm.repository == repo).all()
379 .filter(UserRepoToPerm.repository == repo).all()
380 group_perms = UserGroupRepoToPerm.query() \
380 group_perms = UserGroupRepoToPerm.query() \
381 .filter(UserGroupRepoToPerm.repository == repo).all()
381 .filter(UserGroupRepoToPerm.repository == repo).all()
382
382
383 for perm in user_perms:
383 for perm in user_perms:
384 UserRepoToPerm.create(perm.user, new_repo, perm.permission)
384 UserRepoToPerm.create(perm.user, new_repo, perm.permission)
385
385
386 for perm in group_perms:
386 for perm in group_perms:
387 UserGroupRepoToPerm.create(perm.users_group, new_repo,
387 UserGroupRepoToPerm.create(perm.users_group, new_repo,
388 perm.permission)
388 perm.permission)
389
389
390 elif repo_group and copy_group_permissions:
390 elif repo_group and copy_group_permissions:
391
391
392 user_perms = UserRepoGroupToPerm.query() \
392 user_perms = UserRepoGroupToPerm.query() \
393 .filter(UserRepoGroupToPerm.group == repo_group).all()
393 .filter(UserRepoGroupToPerm.group == repo_group).all()
394
394
395 group_perms = UserGroupRepoGroupToPerm.query() \
395 group_perms = UserGroupRepoGroupToPerm.query() \
396 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
396 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
397
397
398 for perm in user_perms:
398 for perm in user_perms:
399 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
399 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
400 perm_obj = Permission.get_by_key(perm_name)
400 perm_obj = Permission.get_by_key(perm_name)
401 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
401 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
402
402
403 for perm in group_perms:
403 for perm in group_perms:
404 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
404 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
405 perm_obj = Permission.get_by_key(perm_name)
405 perm_obj = Permission.get_by_key(perm_name)
406 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
406 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
407
407
408 else:
408 else:
409 self._create_default_perms(new_repo, private)
409 self._create_default_perms(new_repo, private)
410
410
411 # now automatically start following this repository as owner
411 # now automatically start following this repository as owner
412 ScmModel().toggle_following_repo(new_repo.repo_id, owner.user_id)
412 ScmModel().toggle_following_repo(new_repo.repo_id, owner.user_id)
413 # we need to flush here, in order to check if database won't
413 # we need to flush here, in order to check if database won't
414 # throw any exceptions, create filesystem dirs at the very end
414 # throw any exceptions, create filesystem dirs at the very end
415 Session().flush()
415 Session().flush()
416 return new_repo
416 return new_repo
417 except Exception:
417 except Exception:
418 log.error(traceback.format_exc())
418 log.error(traceback.format_exc())
419 raise
419 raise
420
420
421 def create(self, form_data, cur_user):
421 def create(self, form_data, cur_user):
422 """
422 """
423 Create repository using celery tasks
423 Create repository using celery tasks
424
424
425 :param form_data:
425 :param form_data:
426 :param cur_user:
426 :param cur_user:
427 """
427 """
428 from kallithea.lib.celerylib import tasks
428 from kallithea.lib.celerylib import tasks
429 return tasks.create_repo(form_data, cur_user)
429 return tasks.create_repo(form_data, cur_user)
430
430
431 def _update_permissions(self, repo, perms_new=None, perms_updates=None,
431 def _update_permissions(self, repo, perms_new=None, perms_updates=None,
432 check_perms=True):
432 check_perms=True):
433 if not perms_new:
433 if not perms_new:
434 perms_new = []
434 perms_new = []
435 if not perms_updates:
435 if not perms_updates:
436 perms_updates = []
436 perms_updates = []
437
437
438 # update permissions
438 # update permissions
439 for member, perm, member_type in perms_updates:
439 for member, perm, member_type in perms_updates:
440 if member_type == 'user':
440 if member_type == 'user':
441 # this updates existing one
441 # this updates existing one
442 self.grant_user_permission(
442 self.grant_user_permission(
443 repo=repo, user=member, perm=perm
443 repo=repo, user=member, perm=perm
444 )
444 )
445 else:
445 else:
446 # check if we have permissions to alter this usergroup's access
446 # check if we have permissions to alter this usergroup's access
447 if not check_perms or HasUserGroupPermissionLevel('read')(member):
447 if not check_perms or HasUserGroupPermissionLevel('read')(member):
448 self.grant_user_group_permission(
448 self.grant_user_group_permission(
449 repo=repo, group_name=member, perm=perm
449 repo=repo, group_name=member, perm=perm
450 )
450 )
451 # set new permissions
451 # set new permissions
452 for member, perm, member_type in perms_new:
452 for member, perm, member_type in perms_new:
453 if member_type == 'user':
453 if member_type == 'user':
454 self.grant_user_permission(
454 self.grant_user_permission(
455 repo=repo, user=member, perm=perm
455 repo=repo, user=member, perm=perm
456 )
456 )
457 else:
457 else:
458 # check if we have permissions to alter this usergroup's access
458 # check if we have permissions to alter this usergroup's access
459 if not check_perms or HasUserGroupPermissionLevel('read')(member):
459 if not check_perms or HasUserGroupPermissionLevel('read')(member):
460 self.grant_user_group_permission(
460 self.grant_user_group_permission(
461 repo=repo, group_name=member, perm=perm
461 repo=repo, group_name=member, perm=perm
462 )
462 )
463
463
464 def create_fork(self, form_data, cur_user):
464 def create_fork(self, form_data, cur_user):
465 """
465 """
466 Simple wrapper into executing celery task for fork creation
466 Simple wrapper into executing celery task for fork creation
467
467
468 :param form_data:
468 :param form_data:
469 :param cur_user:
469 :param cur_user:
470 """
470 """
471 from kallithea.lib.celerylib import tasks
471 from kallithea.lib.celerylib import tasks
472 return tasks.create_repo_fork(form_data, cur_user)
472 return tasks.create_repo_fork(form_data, cur_user)
473
473
474 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
474 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
475 """
475 """
476 Delete given repository, forks parameter defines what do do with
476 Delete given repository, forks parameter defines what do do with
477 attached forks. Throws AttachedForksError if deleted repo has attached
477 attached forks. Throws AttachedForksError if deleted repo has attached
478 forks
478 forks
479
479
480 :param repo:
480 :param repo:
481 :param forks: str 'delete' or 'detach'
481 :param forks: str 'delete' or 'detach'
482 :param fs_remove: remove(archive) repo from filesystem
482 :param fs_remove: remove(archive) repo from filesystem
483 """
483 """
484 if not cur_user:
484 if not cur_user:
485 cur_user = getattr(get_current_authuser(), 'username', None)
485 cur_user = getattr(get_current_authuser(), 'username', None)
486 repo = Repository.guess_instance(repo)
486 repo = Repository.guess_instance(repo)
487 if repo is not None:
487 if repo is not None:
488 if forks == 'detach':
488 if forks == 'detach':
489 for r in repo.forks:
489 for r in repo.forks:
490 r.fork = None
490 r.fork = None
491 elif forks == 'delete':
491 elif forks == 'delete':
492 for r in repo.forks:
492 for r in repo.forks:
493 self.delete(r, forks='delete')
493 self.delete(r, forks='delete')
494 elif [f for f in repo.forks]:
494 elif [f for f in repo.forks]:
495 raise AttachedForksError()
495 raise AttachedForksError()
496
496
497 old_repo_dict = repo.get_dict()
497 old_repo_dict = repo.get_dict()
498 try:
498 try:
499 Session().delete(repo)
499 Session().delete(repo)
500 if fs_remove:
500 if fs_remove:
501 self._delete_filesystem_repo(repo)
501 self._delete_filesystem_repo(repo)
502 else:
502 else:
503 log.debug('skipping removal from filesystem')
503 log.debug('skipping removal from filesystem')
504 log_delete_repository(old_repo_dict,
504 log_delete_repository(old_repo_dict,
505 deleted_by=cur_user)
505 deleted_by=cur_user)
506 except Exception:
506 except Exception:
507 log.error(traceback.format_exc())
507 log.error(traceback.format_exc())
508 raise
508 raise
509
509
510 def grant_user_permission(self, repo, user, perm):
510 def grant_user_permission(self, repo, user, perm):
511 """
511 """
512 Grant permission for user on given repository, or update existing one
512 Grant permission for user on given repository, or update existing one
513 if found
513 if found
514
514
515 :param repo: Instance of Repository, repository_id, or repository name
515 :param repo: Instance of Repository, repository_id, or repository name
516 :param user: Instance of User, user_id or username
516 :param user: Instance of User, user_id or username
517 :param perm: Instance of Permission, or permission_name
517 :param perm: Instance of Permission, or permission_name
518 """
518 """
519 user = User.guess_instance(user)
519 user = User.guess_instance(user)
520 repo = Repository.guess_instance(repo)
520 repo = Repository.guess_instance(repo)
521 permission = Permission.guess_instance(perm)
521 permission = Permission.guess_instance(perm)
522
522
523 # check if we have that permission already
523 # check if we have that permission already
524 obj = UserRepoToPerm.query() \
524 obj = UserRepoToPerm.query() \
525 .filter(UserRepoToPerm.user == user) \
525 .filter(UserRepoToPerm.user == user) \
526 .filter(UserRepoToPerm.repository == repo) \
526 .filter(UserRepoToPerm.repository == repo) \
527 .scalar()
527 .scalar()
528 if obj is None:
528 if obj is None:
529 # create new !
529 # create new !
530 obj = UserRepoToPerm()
530 obj = UserRepoToPerm()
531 Session().add(obj)
531 Session().add(obj)
532 obj.repository = repo
532 obj.repository = repo
533 obj.user = user
533 obj.user = user
534 obj.permission = permission
534 obj.permission = permission
535 log.debug('Granted perm %s to %s on %s', perm, user, repo)
535 log.debug('Granted perm %s to %s on %s', perm, user, repo)
536 return obj
536 return obj
537
537
538 def revoke_user_permission(self, repo, user):
538 def revoke_user_permission(self, repo, user):
539 """
539 """
540 Revoke permission for user on given repository
540 Revoke permission for user on given repository
541
541
542 :param repo: Instance of Repository, repository_id, or repository name
542 :param repo: Instance of Repository, repository_id, or repository name
543 :param user: Instance of User, user_id or username
543 :param user: Instance of User, user_id or username
544 """
544 """
545
545
546 user = User.guess_instance(user)
546 user = User.guess_instance(user)
547 repo = Repository.guess_instance(repo)
547 repo = Repository.guess_instance(repo)
548
548
549 obj = UserRepoToPerm.query() \
549 obj = UserRepoToPerm.query() \
550 .filter(UserRepoToPerm.repository == repo) \
550 .filter(UserRepoToPerm.repository == repo) \
551 .filter(UserRepoToPerm.user == user) \
551 .filter(UserRepoToPerm.user == user) \
552 .scalar()
552 .scalar()
553 if obj is not None:
553 if obj is not None:
554 Session().delete(obj)
554 Session().delete(obj)
555 log.debug('Revoked perm on %s on %s', repo, user)
555 log.debug('Revoked perm on %s on %s', repo, user)
556
556
557 def grant_user_group_permission(self, repo, group_name, perm):
557 def grant_user_group_permission(self, repo, group_name, perm):
558 """
558 """
559 Grant permission for user group on given repository, or update
559 Grant permission for user group on given repository, or update
560 existing one if found
560 existing one if found
561
561
562 :param repo: Instance of Repository, repository_id, or repository name
562 :param repo: Instance of Repository, repository_id, or repository name
563 :param group_name: Instance of UserGroup, users_group_id,
563 :param group_name: Instance of UserGroup, users_group_id,
564 or user group name
564 or user group name
565 :param perm: Instance of Permission, or permission_name
565 :param perm: Instance of Permission, or permission_name
566 """
566 """
567 repo = Repository.guess_instance(repo)
567 repo = Repository.guess_instance(repo)
568 group_name = UserGroup.guess_instance(group_name)
568 group_name = UserGroup.guess_instance(group_name)
569 permission = Permission.guess_instance(perm)
569 permission = Permission.guess_instance(perm)
570
570
571 # check if we have that permission already
571 # check if we have that permission already
572 obj = UserGroupRepoToPerm.query() \
572 obj = UserGroupRepoToPerm.query() \
573 .filter(UserGroupRepoToPerm.users_group == group_name) \
573 .filter(UserGroupRepoToPerm.users_group == group_name) \
574 .filter(UserGroupRepoToPerm.repository == repo) \
574 .filter(UserGroupRepoToPerm.repository == repo) \
575 .scalar()
575 .scalar()
576
576
577 if obj is None:
577 if obj is None:
578 # create new
578 # create new
579 obj = UserGroupRepoToPerm()
579 obj = UserGroupRepoToPerm()
580 Session().add(obj)
580 Session().add(obj)
581
581
582 obj.repository = repo
582 obj.repository = repo
583 obj.users_group = group_name
583 obj.users_group = group_name
584 obj.permission = permission
584 obj.permission = permission
585 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
585 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
586 return obj
586 return obj
587
587
588 def revoke_user_group_permission(self, repo, group_name):
588 def revoke_user_group_permission(self, repo, group_name):
589 """
589 """
590 Revoke permission for user group on given repository
590 Revoke permission for user group on given repository
591
591
592 :param repo: Instance of Repository, repository_id, or repository name
592 :param repo: Instance of Repository, repository_id, or repository name
593 :param group_name: Instance of UserGroup, users_group_id,
593 :param group_name: Instance of UserGroup, users_group_id,
594 or user group name
594 or user group name
595 """
595 """
596 repo = Repository.guess_instance(repo)
596 repo = Repository.guess_instance(repo)
597 group_name = UserGroup.guess_instance(group_name)
597 group_name = UserGroup.guess_instance(group_name)
598
598
599 obj = UserGroupRepoToPerm.query() \
599 obj = UserGroupRepoToPerm.query() \
600 .filter(UserGroupRepoToPerm.repository == repo) \
600 .filter(UserGroupRepoToPerm.repository == repo) \
601 .filter(UserGroupRepoToPerm.users_group == group_name) \
601 .filter(UserGroupRepoToPerm.users_group == group_name) \
602 .scalar()
602 .scalar()
603 if obj is not None:
603 if obj is not None:
604 Session().delete(obj)
604 Session().delete(obj)
605 log.debug('Revoked perm to %s on %s', repo, group_name)
605 log.debug('Revoked perm to %s on %s', repo, group_name)
606
606
607 def delete_stats(self, repo_name):
607 def delete_stats(self, repo_name):
608 """
608 """
609 removes stats for given repo
609 removes stats for given repo
610
610
611 :param repo_name:
611 :param repo_name:
612 """
612 """
613 repo = Repository.guess_instance(repo_name)
613 repo = Repository.guess_instance(repo_name)
614 try:
614 try:
615 obj = Statistics.query() \
615 obj = Statistics.query() \
616 .filter(Statistics.repository == repo).scalar()
616 .filter(Statistics.repository == repo).scalar()
617 if obj is not None:
617 if obj is not None:
618 Session().delete(obj)
618 Session().delete(obj)
619 except Exception:
619 except Exception:
620 log.error(traceback.format_exc())
620 log.error(traceback.format_exc())
621 raise
621 raise
622
622
623 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
623 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
624 clone_uri=None, repo_store_location=None):
624 clone_uri=None, repo_store_location=None):
625 """
625 """
626 Makes repository on filesystem. Operation is group aware, meaning that it will create
626 Makes repository on filesystem. Operation is group aware, meaning that it will create
627 a repository within a group, and alter the paths accordingly to the group location.
627 a repository within a group, and alter the paths accordingly to the group location.
628
628
629 Note: clone_uri is low level and not validated - it might be a file system path used for validated cloning
629 Note: clone_uri is low level and not validated - it might be a file system path used for validated cloning
630 """
630 """
631 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
631 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
632 from kallithea.model.scm import ScmModel
632 from kallithea.model.scm import ScmModel
633
633
634 if '/' in repo_name:
634 if '/' in repo_name:
635 raise ValueError('repo_name must not contain groups got `%s`' % repo_name)
635 raise ValueError('repo_name must not contain groups got `%s`' % repo_name)
636
636
637 if isinstance(repo_group, RepoGroup):
637 if isinstance(repo_group, RepoGroup):
638 new_parent_path = os.sep.join(repo_group.full_path_splitted)
638 new_parent_path = os.sep.join(repo_group.full_path_splitted)
639 else:
639 else:
640 new_parent_path = repo_group or ''
640 new_parent_path = repo_group or ''
641
641
642 if repo_store_location:
642 if repo_store_location:
643 _paths = [repo_store_location]
643 _paths = [repo_store_location]
644 else:
644 else:
645 _paths = [self.repos_path, new_parent_path, repo_name]
645 _paths = [self.repos_path, new_parent_path, repo_name]
646 # we need to make it str for mercurial
646 # we need to make it str for mercurial
647 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
647 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
648
648
649 # check if this path is not a repository
649 # check if this path is not a repository
650 if is_valid_repo(repo_path, self.repos_path):
650 if is_valid_repo(repo_path, self.repos_path):
651 raise Exception('This path %s is a valid repository' % repo_path)
651 raise Exception('This path %s is a valid repository' % repo_path)
652
652
653 # check if this path is a group
653 # check if this path is a group
654 if is_valid_repo_group(repo_path, self.repos_path):
654 if is_valid_repo_group(repo_path, self.repos_path):
655 raise Exception('This path %s is a valid group' % repo_path)
655 raise Exception('This path %s is a valid group' % repo_path)
656
656
657 log.info('creating repo %s in %s from url: `%s`',
657 log.info('creating repo %s in %s from url: `%s`',
658 repo_name, safe_unicode(repo_path),
658 repo_name, safe_unicode(repo_path),
659 obfuscate_url_pw(clone_uri))
659 obfuscate_url_pw(clone_uri))
660
660
661 backend = get_backend(repo_type)
661 backend = get_backend(repo_type)
662
662
663 if repo_type == 'hg':
663 if repo_type == 'hg':
664 baseui = make_ui(clear_session=False)
664 baseui = make_ui()
665 # patch and reset hooks section of UI config to not run any
665 # patch and reset hooks section of UI config to not run any
666 # hooks on creating remote repo
666 # hooks on creating remote repo
667 for k, v in baseui.configitems('hooks'):
667 for k, v in baseui.configitems('hooks'):
668 baseui.setconfig('hooks', k, None)
668 baseui.setconfig('hooks', k, None)
669
669
670 repo = backend(repo_path, create=True, src_url=clone_uri, baseui=baseui)
670 repo = backend(repo_path, create=True, src_url=clone_uri, baseui=baseui)
671 elif repo_type == 'git':
671 elif repo_type == 'git':
672 repo = backend(repo_path, create=True, src_url=clone_uri, bare=True)
672 repo = backend(repo_path, create=True, src_url=clone_uri, bare=True)
673 # add kallithea hook into this repo
673 # add kallithea hook into this repo
674 ScmModel().install_git_hooks(repo=repo)
674 ScmModel().install_git_hooks(repo=repo)
675 else:
675 else:
676 raise Exception('Not supported repo_type %s expected hg/git' % repo_type)
676 raise Exception('Not supported repo_type %s expected hg/git' % repo_type)
677
677
678 log.debug('Created repo %s with %s backend',
678 log.debug('Created repo %s with %s backend',
679 safe_unicode(repo_name), safe_unicode(repo_type))
679 safe_unicode(repo_name), safe_unicode(repo_type))
680 return repo
680 return repo
681
681
682 def _rename_filesystem_repo(self, old, new):
682 def _rename_filesystem_repo(self, old, new):
683 """
683 """
684 renames repository on filesystem
684 renames repository on filesystem
685
685
686 :param old: old name
686 :param old: old name
687 :param new: new name
687 :param new: new name
688 """
688 """
689 log.info('renaming repo from %s to %s', old, new)
689 log.info('renaming repo from %s to %s', old, new)
690
690
691 old_path = safe_str(os.path.join(self.repos_path, old))
691 old_path = safe_str(os.path.join(self.repos_path, old))
692 new_path = safe_str(os.path.join(self.repos_path, new))
692 new_path = safe_str(os.path.join(self.repos_path, new))
693 if os.path.isdir(new_path):
693 if os.path.isdir(new_path):
694 raise Exception(
694 raise Exception(
695 'Was trying to rename to already existing dir %s' % new_path
695 'Was trying to rename to already existing dir %s' % new_path
696 )
696 )
697 shutil.move(old_path, new_path)
697 shutil.move(old_path, new_path)
698
698
699 def _delete_filesystem_repo(self, repo):
699 def _delete_filesystem_repo(self, repo):
700 """
700 """
701 removes repo from filesystem, the removal is actually done by
701 removes repo from filesystem, the removal is actually done by
702 renaming dir to a 'rm__*' prefix which Kallithea will skip.
702 renaming dir to a 'rm__*' prefix which Kallithea will skip.
703 It can be undeleted later by reverting the rename.
703 It can be undeleted later by reverting the rename.
704
704
705 :param repo: repo object
705 :param repo: repo object
706 """
706 """
707 rm_path = safe_str(os.path.join(self.repos_path, repo.repo_name))
707 rm_path = safe_str(os.path.join(self.repos_path, repo.repo_name))
708 log.info("Removing %s", rm_path)
708 log.info("Removing %s", rm_path)
709
709
710 _now = datetime.now()
710 _now = datetime.now()
711 _ms = str(_now.microsecond).rjust(6, '0')
711 _ms = str(_now.microsecond).rjust(6, '0')
712 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
712 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
713 repo.just_name)
713 repo.just_name)
714 if repo.group:
714 if repo.group:
715 args = repo.group.full_path_splitted + [_d]
715 args = repo.group.full_path_splitted + [_d]
716 _d = os.path.join(*args)
716 _d = os.path.join(*args)
717 if os.path.exists(rm_path):
717 if os.path.exists(rm_path):
718 shutil.move(rm_path, safe_str(os.path.join(self.repos_path, _d)))
718 shutil.move(rm_path, safe_str(os.path.join(self.repos_path, _d)))
719 else:
719 else:
720 log.error("Can't find repo to delete in %r", rm_path)
720 log.error("Can't find repo to delete in %r", rm_path)
@@ -1,804 +1,804 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Set of generic validators
15 Set of generic validators
16 """
16 """
17
17
18 import logging
18 import logging
19 import os
19 import os
20 import re
20 import re
21 from collections import defaultdict
21 from collections import defaultdict
22
22
23 import formencode
23 import formencode
24 import ipaddr
24 import ipaddr
25 import sqlalchemy
25 import sqlalchemy
26 from formencode.validators import CIDR, Bool, Email, FancyValidator, Int, IPAddress, NotEmpty, Number, OneOf, Regex, Set, String, StringBoolean, UnicodeString
26 from formencode.validators import CIDR, Bool, Email, FancyValidator, Int, IPAddress, NotEmpty, Number, OneOf, Regex, Set, String, StringBoolean, UnicodeString
27 from sqlalchemy import func
27 from sqlalchemy import func
28 from tg.i18n import ugettext as _
28 from tg.i18n import ugettext as _
29
29
30 from kallithea.config.routing import ADMIN_PREFIX
30 from kallithea.config.routing import ADMIN_PREFIX
31 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel
31 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel
32 from kallithea.lib.compat import OrderedSet
32 from kallithea.lib.compat import OrderedSet
33 from kallithea.lib.exceptions import LdapImportError
33 from kallithea.lib.exceptions import LdapImportError
34 from kallithea.lib.utils import is_valid_repo_uri
34 from kallithea.lib.utils import is_valid_repo_uri
35 from kallithea.lib.utils2 import aslist, repo_name_slug, str2bool
35 from kallithea.lib.utils2 import aslist, repo_name_slug, str2bool
36 from kallithea.model.db import RepoGroup, Repository, User, UserGroup
36 from kallithea.model.db import RepoGroup, Repository, User, UserGroup
37
37
38
38
39 # silence warnings and pylint
39 # silence warnings and pylint
40 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
40 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
41 NotEmpty, IPAddress, CIDR, String, FancyValidator
41 NotEmpty, IPAddress, CIDR, String, FancyValidator
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 def UniqueListFromString():
46 def UniqueListFromString():
47 class _UniqueListFromString(formencode.FancyValidator):
47 class _UniqueListFromString(formencode.FancyValidator):
48 """
48 """
49 Split value on ',' and make unique while preserving order
49 Split value on ',' and make unique while preserving order
50 """
50 """
51 messages = dict(
51 messages = dict(
52 empty=_('Value cannot be an empty list'),
52 empty=_('Value cannot be an empty list'),
53 missing_value=_('Value cannot be an empty list'),
53 missing_value=_('Value cannot be an empty list'),
54 )
54 )
55
55
56 def _convert_to_python(self, value, state):
56 def _convert_to_python(self, value, state):
57 value = aslist(value, ',')
57 value = aslist(value, ',')
58 seen = set()
58 seen = set()
59 return [c for c in value if not (c in seen or seen.add(c))]
59 return [c for c in value if not (c in seen or seen.add(c))]
60
60
61 def empty_value(self, value):
61 def empty_value(self, value):
62 return []
62 return []
63
63
64 return _UniqueListFromString
64 return _UniqueListFromString
65
65
66
66
67 def ValidUsername(edit=False, old_data=None):
67 def ValidUsername(edit=False, old_data=None):
68 old_data = old_data or {}
68 old_data = old_data or {}
69
69
70 class _validator(formencode.validators.FancyValidator):
70 class _validator(formencode.validators.FancyValidator):
71 messages = {
71 messages = {
72 'username_exists': _('Username "%(username)s" already exists'),
72 'username_exists': _('Username "%(username)s" already exists'),
73 'system_invalid_username':
73 'system_invalid_username':
74 _('Username "%(username)s" cannot be used'),
74 _('Username "%(username)s" cannot be used'),
75 'invalid_username':
75 'invalid_username':
76 _('Username may only contain alphanumeric characters '
76 _('Username may only contain alphanumeric characters '
77 'underscores, periods or dashes and must begin with an '
77 'underscores, periods or dashes and must begin with an '
78 'alphanumeric character or underscore')
78 'alphanumeric character or underscore')
79 }
79 }
80
80
81 def _validate_python(self, value, state):
81 def _validate_python(self, value, state):
82 if value in ['default', 'new_user']:
82 if value in ['default', 'new_user']:
83 msg = self.message('system_invalid_username', state, username=value)
83 msg = self.message('system_invalid_username', state, username=value)
84 raise formencode.Invalid(msg, value, state)
84 raise formencode.Invalid(msg, value, state)
85 # check if user is unique
85 # check if user is unique
86 old_un = None
86 old_un = None
87 if edit:
87 if edit:
88 old_un = User.get(old_data.get('user_id')).username
88 old_un = User.get(old_data.get('user_id')).username
89
89
90 if old_un != value or not edit:
90 if old_un != value or not edit:
91 if User.get_by_username(value, case_insensitive=True):
91 if User.get_by_username(value, case_insensitive=True):
92 msg = self.message('username_exists', state, username=value)
92 msg = self.message('username_exists', state, username=value)
93 raise formencode.Invalid(msg, value, state)
93 raise formencode.Invalid(msg, value, state)
94
94
95 if re.match(r'^[a-zA-Z0-9\_]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
95 if re.match(r'^[a-zA-Z0-9\_]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
96 msg = self.message('invalid_username', state)
96 msg = self.message('invalid_username', state)
97 raise formencode.Invalid(msg, value, state)
97 raise formencode.Invalid(msg, value, state)
98 return _validator
98 return _validator
99
99
100
100
101 def ValidRegex(msg=None):
101 def ValidRegex(msg=None):
102 class _validator(formencode.validators.Regex):
102 class _validator(formencode.validators.Regex):
103 messages = dict(invalid=msg or _('The input is not valid'))
103 messages = dict(invalid=msg or _('The input is not valid'))
104 return _validator
104 return _validator
105
105
106
106
107 def ValidRepoUser():
107 def ValidRepoUser():
108 class _validator(formencode.validators.FancyValidator):
108 class _validator(formencode.validators.FancyValidator):
109 messages = {
109 messages = {
110 'invalid_username': _('Username %(username)s is not valid')
110 'invalid_username': _('Username %(username)s is not valid')
111 }
111 }
112
112
113 def _validate_python(self, value, state):
113 def _validate_python(self, value, state):
114 try:
114 try:
115 User.query().filter(User.active == True) \
115 User.query().filter(User.active == True) \
116 .filter(User.username == value).one()
116 .filter(User.username == value).one()
117 except sqlalchemy.exc.InvalidRequestError: # NoResultFound/MultipleResultsFound
117 except sqlalchemy.exc.InvalidRequestError: # NoResultFound/MultipleResultsFound
118 msg = self.message('invalid_username', state, username=value)
118 msg = self.message('invalid_username', state, username=value)
119 raise formencode.Invalid(msg, value, state,
119 raise formencode.Invalid(msg, value, state,
120 error_dict=dict(username=msg)
120 error_dict=dict(username=msg)
121 )
121 )
122
122
123 return _validator
123 return _validator
124
124
125
125
126 def ValidUserGroup(edit=False, old_data=None):
126 def ValidUserGroup(edit=False, old_data=None):
127 old_data = old_data or {}
127 old_data = old_data or {}
128
128
129 class _validator(formencode.validators.FancyValidator):
129 class _validator(formencode.validators.FancyValidator):
130 messages = {
130 messages = {
131 'invalid_group': _('Invalid user group name'),
131 'invalid_group': _('Invalid user group name'),
132 'group_exist': _('User group "%(usergroup)s" already exists'),
132 'group_exist': _('User group "%(usergroup)s" already exists'),
133 'invalid_usergroup_name':
133 'invalid_usergroup_name':
134 _('user group name may only contain alphanumeric '
134 _('user group name may only contain alphanumeric '
135 'characters underscores, periods or dashes and must begin '
135 'characters underscores, periods or dashes and must begin '
136 'with alphanumeric character')
136 'with alphanumeric character')
137 }
137 }
138
138
139 def _validate_python(self, value, state):
139 def _validate_python(self, value, state):
140 if value in ['default']:
140 if value in ['default']:
141 msg = self.message('invalid_group', state)
141 msg = self.message('invalid_group', state)
142 raise formencode.Invalid(msg, value, state,
142 raise formencode.Invalid(msg, value, state,
143 error_dict=dict(users_group_name=msg)
143 error_dict=dict(users_group_name=msg)
144 )
144 )
145 # check if group is unique
145 # check if group is unique
146 old_ugname = None
146 old_ugname = None
147 if edit:
147 if edit:
148 old_id = old_data.get('users_group_id')
148 old_id = old_data.get('users_group_id')
149 old_ugname = UserGroup.get(old_id).users_group_name
149 old_ugname = UserGroup.get(old_id).users_group_name
150
150
151 if old_ugname != value or not edit:
151 if old_ugname != value or not edit:
152 is_existing_group = UserGroup.get_by_group_name(value,
152 is_existing_group = UserGroup.get_by_group_name(value,
153 case_insensitive=True)
153 case_insensitive=True)
154 if is_existing_group:
154 if is_existing_group:
155 msg = self.message('group_exist', state, usergroup=value)
155 msg = self.message('group_exist', state, usergroup=value)
156 raise formencode.Invalid(msg, value, state,
156 raise formencode.Invalid(msg, value, state,
157 error_dict=dict(users_group_name=msg)
157 error_dict=dict(users_group_name=msg)
158 )
158 )
159
159
160 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
160 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
161 msg = self.message('invalid_usergroup_name', state)
161 msg = self.message('invalid_usergroup_name', state)
162 raise formencode.Invalid(msg, value, state,
162 raise formencode.Invalid(msg, value, state,
163 error_dict=dict(users_group_name=msg)
163 error_dict=dict(users_group_name=msg)
164 )
164 )
165
165
166 return _validator
166 return _validator
167
167
168
168
169 def ValidRepoGroup(edit=False, old_data=None):
169 def ValidRepoGroup(edit=False, old_data=None):
170 old_data = old_data or {}
170 old_data = old_data or {}
171
171
172 class _validator(formencode.validators.FancyValidator):
172 class _validator(formencode.validators.FancyValidator):
173 messages = {
173 messages = {
174 'parent_group_id': _('Cannot assign this group as parent'),
174 'parent_group_id': _('Cannot assign this group as parent'),
175 'group_exists': _('Group "%(group_name)s" already exists'),
175 'group_exists': _('Group "%(group_name)s" already exists'),
176 'repo_exists':
176 'repo_exists':
177 _('Repository with name "%(group_name)s" already exists')
177 _('Repository with name "%(group_name)s" already exists')
178 }
178 }
179
179
180 def _validate_python(self, value, state):
180 def _validate_python(self, value, state):
181 # TODO WRITE VALIDATIONS
181 # TODO WRITE VALIDATIONS
182 group_name = value.get('group_name')
182 group_name = value.get('group_name')
183 parent_group_id = value.get('parent_group_id')
183 parent_group_id = value.get('parent_group_id')
184
184
185 # slugify repo group just in case :)
185 # slugify repo group just in case :)
186 slug = repo_name_slug(group_name)
186 slug = repo_name_slug(group_name)
187
187
188 # check for parent of self
188 # check for parent of self
189 parent_of_self = lambda: (
189 parent_of_self = lambda: (
190 old_data['group_id'] == parent_group_id
190 old_data['group_id'] == parent_group_id
191 if parent_group_id else False
191 if parent_group_id else False
192 )
192 )
193 if edit and parent_of_self():
193 if edit and parent_of_self():
194 msg = self.message('parent_group_id', state)
194 msg = self.message('parent_group_id', state)
195 raise formencode.Invalid(msg, value, state,
195 raise formencode.Invalid(msg, value, state,
196 error_dict=dict(parent_group_id=msg)
196 error_dict=dict(parent_group_id=msg)
197 )
197 )
198
198
199 old_gname = None
199 old_gname = None
200 if edit:
200 if edit:
201 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
201 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
202
202
203 if old_gname != group_name or not edit:
203 if old_gname != group_name or not edit:
204
204
205 # check group
205 # check group
206 gr = RepoGroup.query() \
206 gr = RepoGroup.query() \
207 .filter(func.lower(RepoGroup.group_name) == func.lower(slug)) \
207 .filter(func.lower(RepoGroup.group_name) == func.lower(slug)) \
208 .filter(RepoGroup.parent_group_id == parent_group_id) \
208 .filter(RepoGroup.parent_group_id == parent_group_id) \
209 .scalar()
209 .scalar()
210 if gr is not None:
210 if gr is not None:
211 msg = self.message('group_exists', state, group_name=slug)
211 msg = self.message('group_exists', state, group_name=slug)
212 raise formencode.Invalid(msg, value, state,
212 raise formencode.Invalid(msg, value, state,
213 error_dict=dict(group_name=msg)
213 error_dict=dict(group_name=msg)
214 )
214 )
215
215
216 # check for same repo
216 # check for same repo
217 repo = Repository.query() \
217 repo = Repository.query() \
218 .filter(func.lower(Repository.repo_name) == func.lower(slug)) \
218 .filter(func.lower(Repository.repo_name) == func.lower(slug)) \
219 .scalar()
219 .scalar()
220 if repo is not None:
220 if repo is not None:
221 msg = self.message('repo_exists', state, group_name=slug)
221 msg = self.message('repo_exists', state, group_name=slug)
222 raise formencode.Invalid(msg, value, state,
222 raise formencode.Invalid(msg, value, state,
223 error_dict=dict(group_name=msg)
223 error_dict=dict(group_name=msg)
224 )
224 )
225
225
226 return _validator
226 return _validator
227
227
228
228
229 def ValidPassword():
229 def ValidPassword():
230 class _validator(formencode.validators.FancyValidator):
230 class _validator(formencode.validators.FancyValidator):
231 messages = {
231 messages = {
232 'invalid_password':
232 'invalid_password':
233 _('Invalid characters (non-ascii) in password')
233 _('Invalid characters (non-ascii) in password')
234 }
234 }
235
235
236 def _validate_python(self, value, state):
236 def _validate_python(self, value, state):
237 try:
237 try:
238 (value or '').decode('ascii')
238 (value or '').decode('ascii')
239 except UnicodeError:
239 except UnicodeError:
240 msg = self.message('invalid_password', state)
240 msg = self.message('invalid_password', state)
241 raise formencode.Invalid(msg, value, state,)
241 raise formencode.Invalid(msg, value, state,)
242 return _validator
242 return _validator
243
243
244
244
245 def ValidOldPassword(username):
245 def ValidOldPassword(username):
246 class _validator(formencode.validators.FancyValidator):
246 class _validator(formencode.validators.FancyValidator):
247 messages = {
247 messages = {
248 'invalid_password': _('Invalid old password')
248 'invalid_password': _('Invalid old password')
249 }
249 }
250
250
251 def _validate_python(self, value, state):
251 def _validate_python(self, value, state):
252 from kallithea.lib import auth_modules
252 from kallithea.lib import auth_modules
253 if auth_modules.authenticate(username, value, '') is None:
253 if auth_modules.authenticate(username, value, '') is None:
254 msg = self.message('invalid_password', state)
254 msg = self.message('invalid_password', state)
255 raise formencode.Invalid(msg, value, state,
255 raise formencode.Invalid(msg, value, state,
256 error_dict=dict(current_password=msg)
256 error_dict=dict(current_password=msg)
257 )
257 )
258 return _validator
258 return _validator
259
259
260
260
261 def ValidPasswordsMatch(password_field, password_confirmation_field):
261 def ValidPasswordsMatch(password_field, password_confirmation_field):
262 class _validator(formencode.validators.FancyValidator):
262 class _validator(formencode.validators.FancyValidator):
263 messages = {
263 messages = {
264 'password_mismatch': _('Passwords do not match'),
264 'password_mismatch': _('Passwords do not match'),
265 }
265 }
266
266
267 def _validate_python(self, value, state):
267 def _validate_python(self, value, state):
268 if value.get(password_field) != value[password_confirmation_field]:
268 if value.get(password_field) != value[password_confirmation_field]:
269 msg = self.message('password_mismatch', state)
269 msg = self.message('password_mismatch', state)
270 raise formencode.Invalid(msg, value, state,
270 raise formencode.Invalid(msg, value, state,
271 error_dict={password_field: msg, password_confirmation_field: msg}
271 error_dict={password_field: msg, password_confirmation_field: msg}
272 )
272 )
273 return _validator
273 return _validator
274
274
275
275
276 def ValidAuth():
276 def ValidAuth():
277 class _validator(formencode.validators.FancyValidator):
277 class _validator(formencode.validators.FancyValidator):
278 messages = {
278 messages = {
279 'invalid_auth': _(u'Invalid username or password'),
279 'invalid_auth': _(u'Invalid username or password'),
280 }
280 }
281
281
282 def _validate_python(self, value, state):
282 def _validate_python(self, value, state):
283 from kallithea.lib import auth_modules
283 from kallithea.lib import auth_modules
284
284
285 password = value['password']
285 password = value['password']
286 username = value['username']
286 username = value['username']
287
287
288 # authenticate returns unused dict but has called
288 # authenticate returns unused dict but has called
289 # plugin._authenticate which has create_or_update'ed the username user in db
289 # plugin._authenticate which has create_or_update'ed the username user in db
290 if auth_modules.authenticate(username, password) is None:
290 if auth_modules.authenticate(username, password) is None:
291 user = User.get_by_username_or_email(username)
291 user = User.get_by_username_or_email(username)
292 if user and not user.active:
292 if user and not user.active:
293 log.warning('user %s is disabled', username)
293 log.warning('user %s is disabled', username)
294 msg = self.message('invalid_auth', state)
294 msg = self.message('invalid_auth', state)
295 raise formencode.Invalid(msg, value, state,
295 raise formencode.Invalid(msg, value, state,
296 error_dict=dict(username=' ', password=msg)
296 error_dict=dict(username=' ', password=msg)
297 )
297 )
298 else:
298 else:
299 log.warning('user %s failed to authenticate', username)
299 log.warning('user %s failed to authenticate', username)
300 msg = self.message('invalid_auth', state)
300 msg = self.message('invalid_auth', state)
301 raise formencode.Invalid(msg, value, state,
301 raise formencode.Invalid(msg, value, state,
302 error_dict=dict(username=' ', password=msg)
302 error_dict=dict(username=' ', password=msg)
303 )
303 )
304 return _validator
304 return _validator
305
305
306
306
307 def ValidRepoName(edit=False, old_data=None):
307 def ValidRepoName(edit=False, old_data=None):
308 old_data = old_data or {}
308 old_data = old_data or {}
309
309
310 class _validator(formencode.validators.FancyValidator):
310 class _validator(formencode.validators.FancyValidator):
311 messages = {
311 messages = {
312 'invalid_repo_name':
312 'invalid_repo_name':
313 _('Repository name %(repo)s is not allowed'),
313 _('Repository name %(repo)s is not allowed'),
314 'repository_exists':
314 'repository_exists':
315 _('Repository named %(repo)s already exists'),
315 _('Repository named %(repo)s already exists'),
316 'repository_in_group_exists': _('Repository "%(repo)s" already '
316 'repository_in_group_exists': _('Repository "%(repo)s" already '
317 'exists in group "%(group)s"'),
317 'exists in group "%(group)s"'),
318 'same_group_exists': _('Repository group with name "%(repo)s" '
318 'same_group_exists': _('Repository group with name "%(repo)s" '
319 'already exists')
319 'already exists')
320 }
320 }
321
321
322 def _convert_to_python(self, value, state):
322 def _convert_to_python(self, value, state):
323 repo_name = repo_name_slug(value.get('repo_name', ''))
323 repo_name = repo_name_slug(value.get('repo_name', ''))
324 repo_group = value.get('repo_group')
324 repo_group = value.get('repo_group')
325 if repo_group:
325 if repo_group:
326 gr = RepoGroup.get(repo_group)
326 gr = RepoGroup.get(repo_group)
327 group_path = gr.full_path
327 group_path = gr.full_path
328 group_name = gr.group_name
328 group_name = gr.group_name
329 # value needs to be aware of group name in order to check
329 # value needs to be aware of group name in order to check
330 # db key This is an actual just the name to store in the
330 # db key This is an actual just the name to store in the
331 # database
331 # database
332 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
332 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
333 else:
333 else:
334 group_name = group_path = ''
334 group_name = group_path = ''
335 repo_name_full = repo_name
335 repo_name_full = repo_name
336
336
337 value['repo_name'] = repo_name
337 value['repo_name'] = repo_name
338 value['repo_name_full'] = repo_name_full
338 value['repo_name_full'] = repo_name_full
339 value['group_path'] = group_path
339 value['group_path'] = group_path
340 value['group_name'] = group_name
340 value['group_name'] = group_name
341 return value
341 return value
342
342
343 def _validate_python(self, value, state):
343 def _validate_python(self, value, state):
344 repo_name = value.get('repo_name')
344 repo_name = value.get('repo_name')
345 repo_name_full = value.get('repo_name_full')
345 repo_name_full = value.get('repo_name_full')
346 group_path = value.get('group_path')
346 group_path = value.get('group_path')
347 group_name = value.get('group_name')
347 group_name = value.get('group_name')
348
348
349 if repo_name in [ADMIN_PREFIX, '']:
349 if repo_name in [ADMIN_PREFIX, '']:
350 msg = self.message('invalid_repo_name', state, repo=repo_name)
350 msg = self.message('invalid_repo_name', state, repo=repo_name)
351 raise formencode.Invalid(msg, value, state,
351 raise formencode.Invalid(msg, value, state,
352 error_dict=dict(repo_name=msg)
352 error_dict=dict(repo_name=msg)
353 )
353 )
354
354
355 rename = old_data.get('repo_name') != repo_name_full
355 rename = old_data.get('repo_name') != repo_name_full
356 create = not edit
356 create = not edit
357 if rename or create:
357 if rename or create:
358 repo = Repository.get_by_repo_name(repo_name_full, case_insensitive=True)
358 repo = Repository.get_by_repo_name(repo_name_full, case_insensitive=True)
359 repo_group = RepoGroup.get_by_group_name(repo_name_full, case_insensitive=True)
359 repo_group = RepoGroup.get_by_group_name(repo_name_full, case_insensitive=True)
360 if group_path != '':
360 if group_path != '':
361 if repo is not None:
361 if repo is not None:
362 msg = self.message('repository_in_group_exists', state,
362 msg = self.message('repository_in_group_exists', state,
363 repo=repo.repo_name, group=group_name)
363 repo=repo.repo_name, group=group_name)
364 raise formencode.Invalid(msg, value, state,
364 raise formencode.Invalid(msg, value, state,
365 error_dict=dict(repo_name=msg)
365 error_dict=dict(repo_name=msg)
366 )
366 )
367 elif repo_group is not None:
367 elif repo_group is not None:
368 msg = self.message('same_group_exists', state,
368 msg = self.message('same_group_exists', state,
369 repo=repo_name)
369 repo=repo_name)
370 raise formencode.Invalid(msg, value, state,
370 raise formencode.Invalid(msg, value, state,
371 error_dict=dict(repo_name=msg)
371 error_dict=dict(repo_name=msg)
372 )
372 )
373 elif repo is not None:
373 elif repo is not None:
374 msg = self.message('repository_exists', state,
374 msg = self.message('repository_exists', state,
375 repo=repo.repo_name)
375 repo=repo.repo_name)
376 raise formencode.Invalid(msg, value, state,
376 raise formencode.Invalid(msg, value, state,
377 error_dict=dict(repo_name=msg)
377 error_dict=dict(repo_name=msg)
378 )
378 )
379 return value
379 return value
380 return _validator
380 return _validator
381
381
382
382
383 def ValidForkName(*args, **kwargs):
383 def ValidForkName(*args, **kwargs):
384 return ValidRepoName(*args, **kwargs)
384 return ValidRepoName(*args, **kwargs)
385
385
386
386
387 def SlugifyName():
387 def SlugifyName():
388 class _validator(formencode.validators.FancyValidator):
388 class _validator(formencode.validators.FancyValidator):
389
389
390 def _convert_to_python(self, value, state):
390 def _convert_to_python(self, value, state):
391 return repo_name_slug(value)
391 return repo_name_slug(value)
392
392
393 def _validate_python(self, value, state):
393 def _validate_python(self, value, state):
394 pass
394 pass
395
395
396 return _validator
396 return _validator
397
397
398
398
399 def ValidCloneUri():
399 def ValidCloneUri():
400 from kallithea.lib.utils import make_ui
400 from kallithea.lib.utils import make_ui
401
401
402 class _validator(formencode.validators.FancyValidator):
402 class _validator(formencode.validators.FancyValidator):
403 messages = {
403 messages = {
404 'clone_uri': _('Invalid repository URL'),
404 'clone_uri': _('Invalid repository URL'),
405 'invalid_clone_uri': _('Invalid repository URL. It must be a '
405 'invalid_clone_uri': _('Invalid repository URL. It must be a '
406 'valid http, https, ssh, svn+http or svn+https URL'),
406 'valid http, https, ssh, svn+http or svn+https URL'),
407 }
407 }
408
408
409 def _validate_python(self, value, state):
409 def _validate_python(self, value, state):
410 repo_type = value.get('repo_type')
410 repo_type = value.get('repo_type')
411 url = value.get('clone_uri')
411 url = value.get('clone_uri')
412
412
413 if url and url != value.get('clone_uri_hidden'):
413 if url and url != value.get('clone_uri_hidden'):
414 try:
414 try:
415 is_valid_repo_uri(repo_type, url, make_ui(clear_session=False))
415 is_valid_repo_uri(repo_type, url, make_ui())
416 except Exception:
416 except Exception:
417 log.exception('URL validation failed')
417 log.exception('URL validation failed')
418 msg = self.message('clone_uri', state)
418 msg = self.message('clone_uri', state)
419 raise formencode.Invalid(msg, value, state,
419 raise formencode.Invalid(msg, value, state,
420 error_dict=dict(clone_uri=msg)
420 error_dict=dict(clone_uri=msg)
421 )
421 )
422 return _validator
422 return _validator
423
423
424
424
425 def ValidForkType(old_data=None):
425 def ValidForkType(old_data=None):
426 old_data = old_data or {}
426 old_data = old_data or {}
427
427
428 class _validator(formencode.validators.FancyValidator):
428 class _validator(formencode.validators.FancyValidator):
429 messages = {
429 messages = {
430 'invalid_fork_type': _('Fork has to be the same type as parent')
430 'invalid_fork_type': _('Fork has to be the same type as parent')
431 }
431 }
432
432
433 def _validate_python(self, value, state):
433 def _validate_python(self, value, state):
434 if old_data['repo_type'] != value:
434 if old_data['repo_type'] != value:
435 msg = self.message('invalid_fork_type', state)
435 msg = self.message('invalid_fork_type', state)
436 raise formencode.Invalid(msg, value, state,
436 raise formencode.Invalid(msg, value, state,
437 error_dict=dict(repo_type=msg)
437 error_dict=dict(repo_type=msg)
438 )
438 )
439 return _validator
439 return _validator
440
440
441
441
442 def CanWriteGroup(old_data=None):
442 def CanWriteGroup(old_data=None):
443 class _validator(formencode.validators.FancyValidator):
443 class _validator(formencode.validators.FancyValidator):
444 messages = {
444 messages = {
445 'permission_denied': _("You don't have permissions "
445 'permission_denied': _("You don't have permissions "
446 "to create repository in this group"),
446 "to create repository in this group"),
447 'permission_denied_root': _("no permission to create repository "
447 'permission_denied_root': _("no permission to create repository "
448 "in root location")
448 "in root location")
449 }
449 }
450
450
451 def _convert_to_python(self, value, state):
451 def _convert_to_python(self, value, state):
452 # root location
452 # root location
453 if value == -1:
453 if value == -1:
454 return None
454 return None
455 return value
455 return value
456
456
457 def _validate_python(self, value, state):
457 def _validate_python(self, value, state):
458 gr = RepoGroup.get(value)
458 gr = RepoGroup.get(value)
459 gr_name = gr.group_name if gr is not None else None # None means ROOT location
459 gr_name = gr.group_name if gr is not None else None # None means ROOT location
460
460
461 # create repositories with write permission on group is set to true
461 # create repositories with write permission on group is set to true
462 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
462 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
463 group_admin = HasRepoGroupPermissionLevel('admin')(gr_name,
463 group_admin = HasRepoGroupPermissionLevel('admin')(gr_name,
464 'can write into group validator')
464 'can write into group validator')
465 group_write = HasRepoGroupPermissionLevel('write')(gr_name,
465 group_write = HasRepoGroupPermissionLevel('write')(gr_name,
466 'can write into group validator')
466 'can write into group validator')
467 forbidden = not (group_admin or (group_write and create_on_write))
467 forbidden = not (group_admin or (group_write and create_on_write))
468 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
468 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
469 gid = (old_data['repo_group'].get('group_id')
469 gid = (old_data['repo_group'].get('group_id')
470 if (old_data and 'repo_group' in old_data) else None)
470 if (old_data and 'repo_group' in old_data) else None)
471 value_changed = gid != value
471 value_changed = gid != value
472 new = not old_data
472 new = not old_data
473 # do check if we changed the value, there's a case that someone got
473 # do check if we changed the value, there's a case that someone got
474 # revoked write permissions to a repository, he still created, we
474 # revoked write permissions to a repository, he still created, we
475 # don't need to check permission if he didn't change the value of
475 # don't need to check permission if he didn't change the value of
476 # groups in form box
476 # groups in form box
477 if value_changed or new:
477 if value_changed or new:
478 # parent group need to be existing
478 # parent group need to be existing
479 if gr and forbidden:
479 if gr and forbidden:
480 msg = self.message('permission_denied', state)
480 msg = self.message('permission_denied', state)
481 raise formencode.Invalid(msg, value, state,
481 raise formencode.Invalid(msg, value, state,
482 error_dict=dict(repo_type=msg)
482 error_dict=dict(repo_type=msg)
483 )
483 )
484 ## check if we can write to root location !
484 ## check if we can write to root location !
485 elif gr is None and not can_create_repos():
485 elif gr is None and not can_create_repos():
486 msg = self.message('permission_denied_root', state)
486 msg = self.message('permission_denied_root', state)
487 raise formencode.Invalid(msg, value, state,
487 raise formencode.Invalid(msg, value, state,
488 error_dict=dict(repo_type=msg)
488 error_dict=dict(repo_type=msg)
489 )
489 )
490
490
491 return _validator
491 return _validator
492
492
493
493
494 def CanCreateGroup(can_create_in_root=False):
494 def CanCreateGroup(can_create_in_root=False):
495 class _validator(formencode.validators.FancyValidator):
495 class _validator(formencode.validators.FancyValidator):
496 messages = {
496 messages = {
497 'permission_denied': _("You don't have permissions "
497 'permission_denied': _("You don't have permissions "
498 "to create a group in this location")
498 "to create a group in this location")
499 }
499 }
500
500
501 def to_python(self, value, state):
501 def to_python(self, value, state):
502 # root location
502 # root location
503 if value == -1:
503 if value == -1:
504 return None
504 return None
505 return value
505 return value
506
506
507 def _validate_python(self, value, state):
507 def _validate_python(self, value, state):
508 gr = RepoGroup.get(value)
508 gr = RepoGroup.get(value)
509 gr_name = gr.group_name if gr is not None else None # None means ROOT location
509 gr_name = gr.group_name if gr is not None else None # None means ROOT location
510
510
511 if can_create_in_root and gr is None:
511 if can_create_in_root and gr is None:
512 # we can create in root, we're fine no validations required
512 # we can create in root, we're fine no validations required
513 return
513 return
514
514
515 forbidden_in_root = gr is None and not can_create_in_root
515 forbidden_in_root = gr is None and not can_create_in_root
516 forbidden = not HasRepoGroupPermissionLevel('admin')(gr_name, 'can create group validator')
516 forbidden = not HasRepoGroupPermissionLevel('admin')(gr_name, 'can create group validator')
517 if forbidden_in_root or forbidden:
517 if forbidden_in_root or forbidden:
518 msg = self.message('permission_denied', state)
518 msg = self.message('permission_denied', state)
519 raise formencode.Invalid(msg, value, state,
519 raise formencode.Invalid(msg, value, state,
520 error_dict=dict(parent_group_id=msg)
520 error_dict=dict(parent_group_id=msg)
521 )
521 )
522
522
523 return _validator
523 return _validator
524
524
525
525
526 def ValidPerms(type_='repo'):
526 def ValidPerms(type_='repo'):
527 if type_ == 'repo_group':
527 if type_ == 'repo_group':
528 EMPTY_PERM = 'group.none'
528 EMPTY_PERM = 'group.none'
529 elif type_ == 'repo':
529 elif type_ == 'repo':
530 EMPTY_PERM = 'repository.none'
530 EMPTY_PERM = 'repository.none'
531 elif type_ == 'user_group':
531 elif type_ == 'user_group':
532 EMPTY_PERM = 'usergroup.none'
532 EMPTY_PERM = 'usergroup.none'
533
533
534 class _validator(formencode.validators.FancyValidator):
534 class _validator(formencode.validators.FancyValidator):
535 messages = {
535 messages = {
536 'perm_new_member_name':
536 'perm_new_member_name':
537 _('This username or user group name is not valid')
537 _('This username or user group name is not valid')
538 }
538 }
539
539
540 def to_python(self, value, state):
540 def to_python(self, value, state):
541 perms_update = OrderedSet()
541 perms_update = OrderedSet()
542 perms_new = OrderedSet()
542 perms_new = OrderedSet()
543 # build a list of permission to update and new permission to create
543 # build a list of permission to update and new permission to create
544
544
545 # CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
545 # CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
546 new_perms_group = defaultdict(dict)
546 new_perms_group = defaultdict(dict)
547 for k, v in value.copy().iteritems():
547 for k, v in value.copy().iteritems():
548 if k.startswith('perm_new_member'):
548 if k.startswith('perm_new_member'):
549 del value[k]
549 del value[k]
550 _type, part = k.split('perm_new_member_')
550 _type, part = k.split('perm_new_member_')
551 args = part.split('_')
551 args = part.split('_')
552 if len(args) == 1:
552 if len(args) == 1:
553 new_perms_group[args[0]]['perm'] = v
553 new_perms_group[args[0]]['perm'] = v
554 elif len(args) == 2:
554 elif len(args) == 2:
555 _key, pos = args
555 _key, pos = args
556 new_perms_group[pos][_key] = v
556 new_perms_group[pos][_key] = v
557
557
558 # fill new permissions in order of how they were added
558 # fill new permissions in order of how they were added
559 for k in sorted(map(int, new_perms_group.keys())):
559 for k in sorted(map(int, new_perms_group.keys())):
560 perm_dict = new_perms_group[str(k)]
560 perm_dict = new_perms_group[str(k)]
561 new_member = perm_dict.get('name')
561 new_member = perm_dict.get('name')
562 new_perm = perm_dict.get('perm')
562 new_perm = perm_dict.get('perm')
563 new_type = perm_dict.get('type')
563 new_type = perm_dict.get('type')
564 if new_member and new_perm and new_type:
564 if new_member and new_perm and new_type:
565 perms_new.add((new_member, new_perm, new_type))
565 perms_new.add((new_member, new_perm, new_type))
566
566
567 for k, v in value.iteritems():
567 for k, v in value.iteritems():
568 if k.startswith('u_perm_') or k.startswith('g_perm_'):
568 if k.startswith('u_perm_') or k.startswith('g_perm_'):
569 member = k[7:]
569 member = k[7:]
570 t = {'u': 'user',
570 t = {'u': 'user',
571 'g': 'users_group'
571 'g': 'users_group'
572 }[k[0]]
572 }[k[0]]
573 if member == User.DEFAULT_USER:
573 if member == User.DEFAULT_USER:
574 if str2bool(value.get('repo_private')):
574 if str2bool(value.get('repo_private')):
575 # set none for default when updating to
575 # set none for default when updating to
576 # private repo protects against form manipulation
576 # private repo protects against form manipulation
577 v = EMPTY_PERM
577 v = EMPTY_PERM
578 perms_update.add((member, v, t))
578 perms_update.add((member, v, t))
579
579
580 value['perms_updates'] = list(perms_update)
580 value['perms_updates'] = list(perms_update)
581 value['perms_new'] = list(perms_new)
581 value['perms_new'] = list(perms_new)
582
582
583 # update permissions
583 # update permissions
584 for k, v, t in perms_new:
584 for k, v, t in perms_new:
585 try:
585 try:
586 if t == 'user':
586 if t == 'user':
587 self.user_db = User.query() \
587 self.user_db = User.query() \
588 .filter(User.active == True) \
588 .filter(User.active == True) \
589 .filter(User.username == k).one()
589 .filter(User.username == k).one()
590 if t == 'users_group':
590 if t == 'users_group':
591 self.user_db = UserGroup.query() \
591 self.user_db = UserGroup.query() \
592 .filter(UserGroup.users_group_active == True) \
592 .filter(UserGroup.users_group_active == True) \
593 .filter(UserGroup.users_group_name == k).one()
593 .filter(UserGroup.users_group_name == k).one()
594
594
595 except Exception:
595 except Exception:
596 log.exception('Updated permission failed')
596 log.exception('Updated permission failed')
597 msg = self.message('perm_new_member_type', state)
597 msg = self.message('perm_new_member_type', state)
598 raise formencode.Invalid(msg, value, state,
598 raise formencode.Invalid(msg, value, state,
599 error_dict=dict(perm_new_member_name=msg)
599 error_dict=dict(perm_new_member_name=msg)
600 )
600 )
601 return value
601 return value
602 return _validator
602 return _validator
603
603
604
604
605 def ValidSettings():
605 def ValidSettings():
606 class _validator(formencode.validators.FancyValidator):
606 class _validator(formencode.validators.FancyValidator):
607 def _convert_to_python(self, value, state):
607 def _convert_to_python(self, value, state):
608 # settings form for users that are not admin
608 # settings form for users that are not admin
609 # can't edit certain parameters, it's extra backup if they mangle
609 # can't edit certain parameters, it's extra backup if they mangle
610 # with forms
610 # with forms
611
611
612 forbidden_params = [
612 forbidden_params = [
613 'user', 'repo_type',
613 'user', 'repo_type',
614 'repo_enable_downloads', 'repo_enable_statistics'
614 'repo_enable_downloads', 'repo_enable_statistics'
615 ]
615 ]
616
616
617 for param in forbidden_params:
617 for param in forbidden_params:
618 if param in value:
618 if param in value:
619 del value[param]
619 del value[param]
620 return value
620 return value
621
621
622 def _validate_python(self, value, state):
622 def _validate_python(self, value, state):
623 pass
623 pass
624 return _validator
624 return _validator
625
625
626
626
627 def ValidPath():
627 def ValidPath():
628 class _validator(formencode.validators.FancyValidator):
628 class _validator(formencode.validators.FancyValidator):
629 messages = {
629 messages = {
630 'invalid_path': _('This is not a valid path')
630 'invalid_path': _('This is not a valid path')
631 }
631 }
632
632
633 def _validate_python(self, value, state):
633 def _validate_python(self, value, state):
634 if not os.path.isdir(value):
634 if not os.path.isdir(value):
635 msg = self.message('invalid_path', state)
635 msg = self.message('invalid_path', state)
636 raise formencode.Invalid(msg, value, state,
636 raise formencode.Invalid(msg, value, state,
637 error_dict=dict(paths_root_path=msg)
637 error_dict=dict(paths_root_path=msg)
638 )
638 )
639 return _validator
639 return _validator
640
640
641
641
642 def UniqSystemEmail(old_data=None):
642 def UniqSystemEmail(old_data=None):
643 old_data = old_data or {}
643 old_data = old_data or {}
644
644
645 class _validator(formencode.validators.FancyValidator):
645 class _validator(formencode.validators.FancyValidator):
646 messages = {
646 messages = {
647 'email_taken': _('This email address is already in use')
647 'email_taken': _('This email address is already in use')
648 }
648 }
649
649
650 def _convert_to_python(self, value, state):
650 def _convert_to_python(self, value, state):
651 return value.lower()
651 return value.lower()
652
652
653 def _validate_python(self, value, state):
653 def _validate_python(self, value, state):
654 if (old_data.get('email') or '').lower() != value:
654 if (old_data.get('email') or '').lower() != value:
655 user = User.get_by_email(value)
655 user = User.get_by_email(value)
656 if user is not None:
656 if user is not None:
657 msg = self.message('email_taken', state)
657 msg = self.message('email_taken', state)
658 raise formencode.Invalid(msg, value, state,
658 raise formencode.Invalid(msg, value, state,
659 error_dict=dict(email=msg)
659 error_dict=dict(email=msg)
660 )
660 )
661 return _validator
661 return _validator
662
662
663
663
664 def ValidSystemEmail():
664 def ValidSystemEmail():
665 class _validator(formencode.validators.FancyValidator):
665 class _validator(formencode.validators.FancyValidator):
666 messages = {
666 messages = {
667 'non_existing_email': _('Email address "%(email)s" not found')
667 'non_existing_email': _('Email address "%(email)s" not found')
668 }
668 }
669
669
670 def _convert_to_python(self, value, state):
670 def _convert_to_python(self, value, state):
671 return value.lower()
671 return value.lower()
672
672
673 def _validate_python(self, value, state):
673 def _validate_python(self, value, state):
674 user = User.get_by_email(value)
674 user = User.get_by_email(value)
675 if user is None:
675 if user is None:
676 msg = self.message('non_existing_email', state, email=value)
676 msg = self.message('non_existing_email', state, email=value)
677 raise formencode.Invalid(msg, value, state,
677 raise formencode.Invalid(msg, value, state,
678 error_dict=dict(email=msg)
678 error_dict=dict(email=msg)
679 )
679 )
680
680
681 return _validator
681 return _validator
682
682
683
683
684 def LdapLibValidator():
684 def LdapLibValidator():
685 class _validator(formencode.validators.FancyValidator):
685 class _validator(formencode.validators.FancyValidator):
686 messages = {
686 messages = {
687
687
688 }
688 }
689
689
690 def _validate_python(self, value, state):
690 def _validate_python(self, value, state):
691 try:
691 try:
692 import ldap
692 import ldap
693 ldap # pyflakes silence !
693 ldap # pyflakes silence !
694 except ImportError:
694 except ImportError:
695 raise LdapImportError()
695 raise LdapImportError()
696
696
697 return _validator
697 return _validator
698
698
699
699
700 def AttrLoginValidator():
700 def AttrLoginValidator():
701 class _validator(formencode.validators.UnicodeString):
701 class _validator(formencode.validators.UnicodeString):
702 messages = {
702 messages = {
703 'invalid_cn':
703 'invalid_cn':
704 _('The LDAP Login attribute of the CN must be specified - '
704 _('The LDAP Login attribute of the CN must be specified - '
705 'this is the name of the attribute that is equivalent '
705 'this is the name of the attribute that is equivalent '
706 'to "username"')
706 'to "username"')
707 }
707 }
708 messages['empty'] = messages['invalid_cn']
708 messages['empty'] = messages['invalid_cn']
709
709
710 return _validator
710 return _validator
711
711
712
712
713 def ValidIp():
713 def ValidIp():
714 class _validator(CIDR):
714 class _validator(CIDR):
715 messages = dict(
715 messages = dict(
716 badFormat=_('Please enter a valid IPv4 or IPv6 address'),
716 badFormat=_('Please enter a valid IPv4 or IPv6 address'),
717 illegalBits=_('The network size (bits) must be within the range'
717 illegalBits=_('The network size (bits) must be within the range'
718 ' of 0-32 (not %(bits)r)')
718 ' of 0-32 (not %(bits)r)')
719 )
719 )
720
720
721 def to_python(self, value, state):
721 def to_python(self, value, state):
722 v = super(_validator, self).to_python(value, state)
722 v = super(_validator, self).to_python(value, state)
723 v = v.strip()
723 v = v.strip()
724 net = ipaddr.IPNetwork(address=v)
724 net = ipaddr.IPNetwork(address=v)
725 if isinstance(net, ipaddr.IPv4Network):
725 if isinstance(net, ipaddr.IPv4Network):
726 # if IPv4 doesn't end with a mask, add /32
726 # if IPv4 doesn't end with a mask, add /32
727 if '/' not in value:
727 if '/' not in value:
728 v += '/32'
728 v += '/32'
729 if isinstance(net, ipaddr.IPv6Network):
729 if isinstance(net, ipaddr.IPv6Network):
730 # if IPv6 doesn't end with a mask, add /128
730 # if IPv6 doesn't end with a mask, add /128
731 if '/' not in value:
731 if '/' not in value:
732 v += '/128'
732 v += '/128'
733 return v
733 return v
734
734
735 def _validate_python(self, value, state):
735 def _validate_python(self, value, state):
736 try:
736 try:
737 addr = value.strip()
737 addr = value.strip()
738 # this raises an ValueError if address is not IPv4 or IPv6
738 # this raises an ValueError if address is not IPv4 or IPv6
739 ipaddr.IPNetwork(address=addr)
739 ipaddr.IPNetwork(address=addr)
740 except ValueError:
740 except ValueError:
741 raise formencode.Invalid(self.message('badFormat', state),
741 raise formencode.Invalid(self.message('badFormat', state),
742 value, state)
742 value, state)
743
743
744 return _validator
744 return _validator
745
745
746
746
747 def FieldKey():
747 def FieldKey():
748 class _validator(formencode.validators.FancyValidator):
748 class _validator(formencode.validators.FancyValidator):
749 messages = dict(
749 messages = dict(
750 badFormat=_('Key name can only consist of letters, '
750 badFormat=_('Key name can only consist of letters, '
751 'underscore, dash or numbers')
751 'underscore, dash or numbers')
752 )
752 )
753
753
754 def _validate_python(self, value, state):
754 def _validate_python(self, value, state):
755 if not re.match('[a-zA-Z0-9_-]+$', value):
755 if not re.match('[a-zA-Z0-9_-]+$', value):
756 raise formencode.Invalid(self.message('badFormat', state),
756 raise formencode.Invalid(self.message('badFormat', state),
757 value, state)
757 value, state)
758 return _validator
758 return _validator
759
759
760
760
761 def BasePath():
761 def BasePath():
762 class _validator(formencode.validators.FancyValidator):
762 class _validator(formencode.validators.FancyValidator):
763 messages = dict(
763 messages = dict(
764 badPath=_('Filename cannot be inside a directory')
764 badPath=_('Filename cannot be inside a directory')
765 )
765 )
766
766
767 def _convert_to_python(self, value, state):
767 def _convert_to_python(self, value, state):
768 return value
768 return value
769
769
770 def _validate_python(self, value, state):
770 def _validate_python(self, value, state):
771 if value != os.path.basename(value):
771 if value != os.path.basename(value):
772 raise formencode.Invalid(self.message('badPath', state),
772 raise formencode.Invalid(self.message('badPath', state),
773 value, state)
773 value, state)
774 return _validator
774 return _validator
775
775
776
776
777 def ValidAuthPlugins():
777 def ValidAuthPlugins():
778 class _validator(formencode.validators.FancyValidator):
778 class _validator(formencode.validators.FancyValidator):
779 messages = dict(
779 messages = dict(
780 import_duplicate=_('Plugins %(loaded)s and %(next_to_load)s both export the same name')
780 import_duplicate=_('Plugins %(loaded)s and %(next_to_load)s both export the same name')
781 )
781 )
782
782
783 def _convert_to_python(self, value, state):
783 def _convert_to_python(self, value, state):
784 # filter empty values
784 # filter empty values
785 return filter(lambda s: s not in [None, ''], value)
785 return filter(lambda s: s not in [None, ''], value)
786
786
787 def _validate_python(self, value, state):
787 def _validate_python(self, value, state):
788 from kallithea.lib import auth_modules
788 from kallithea.lib import auth_modules
789 module_list = value
789 module_list = value
790 unique_names = {}
790 unique_names = {}
791 try:
791 try:
792 for module in module_list:
792 for module in module_list:
793 plugin = auth_modules.loadplugin(module)
793 plugin = auth_modules.loadplugin(module)
794 plugin_name = plugin.name
794 plugin_name = plugin.name
795 if plugin_name in unique_names:
795 if plugin_name in unique_names:
796 msg = self.message('import_duplicate', state,
796 msg = self.message('import_duplicate', state,
797 loaded=unique_names[plugin_name],
797 loaded=unique_names[plugin_name],
798 next_to_load=plugin_name)
798 next_to_load=plugin_name)
799 raise formencode.Invalid(msg, value, state)
799 raise formencode.Invalid(msg, value, state)
800 unique_names[plugin_name] = plugin
800 unique_names[plugin_name] = plugin
801 except (ImportError, AttributeError, TypeError) as e:
801 except (ImportError, AttributeError, TypeError) as e:
802 raise formencode.Invalid(str(e), value, state)
802 raise formencode.Invalid(str(e), value, state)
803
803
804 return _validator
804 return _validator
General Comments 0
You need to be logged in to leave comments. Login now