##// END OF EJS Templates
core: no longer rely on webob exception inside get_or_404 function....
marcink -
r1956:a7a935de default
parent child Browse files
Show More
@@ -1,416 +1,417 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 from rhodecode.model import repo
29 from rhodecode.model import repo
30 from rhodecode.model import repo_group
30 from rhodecode.model import repo_group
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32 from rhodecode.model.scm import ScmModel
32 from rhodecode.model.scm import ScmModel
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 ADMIN_PREFIX = '/_admin'
37 ADMIN_PREFIX = '/_admin'
38 STATIC_FILE_PREFIX = '/_static'
38 STATIC_FILE_PREFIX = '/_static'
39
39
40 URL_NAME_REQUIREMENTS = {
40 URL_NAME_REQUIREMENTS = {
41 # group name can have a slash in them, but they must not end with a slash
41 # group name can have a slash in them, but they must not end with a slash
42 'group_name': r'.*?[^/]',
42 'group_name': r'.*?[^/]',
43 'repo_group_name': r'.*?[^/]',
43 'repo_group_name': r'.*?[^/]',
44 # repo names can have a slash in them, but they must not end with a slash
44 # repo names can have a slash in them, but they must not end with a slash
45 'repo_name': r'.*?[^/]',
45 'repo_name': r'.*?[^/]',
46 # file path eats up everything at the end
46 # file path eats up everything at the end
47 'f_path': r'.*',
47 'f_path': r'.*',
48 # reference types
48 # reference types
49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 }
51 }
52
52
53
53
54 def add_route_with_slash(config,name, pattern, **kw):
54 def add_route_with_slash(config,name, pattern, **kw):
55 config.add_route(name, pattern, **kw)
55 config.add_route(name, pattern, **kw)
56 if not pattern.endswith('/'):
56 if not pattern.endswith('/'):
57 config.add_route(name + '_slash', pattern + '/', **kw)
57 config.add_route(name + '_slash', pattern + '/', **kw)
58
58
59
59
60 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
60 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
61 """
61 """
62 Adds regex requirements to pyramid routes using a mapping dict
62 Adds regex requirements to pyramid routes using a mapping dict
63 e.g::
63 e.g::
64 add_route_requirements('{repo_name}/settings')
64 add_route_requirements('{repo_name}/settings')
65 """
65 """
66 for key, regex in requirements.items():
66 for key, regex in requirements.items():
67 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
67 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
68 return route_path
68 return route_path
69
69
70
70
71 def get_format_ref_id(repo):
71 def get_format_ref_id(repo):
72 """Returns a `repo` specific reference formatter function"""
72 """Returns a `repo` specific reference formatter function"""
73 if h.is_svn(repo):
73 if h.is_svn(repo):
74 return _format_ref_id_svn
74 return _format_ref_id_svn
75 else:
75 else:
76 return _format_ref_id
76 return _format_ref_id
77
77
78
78
79 def _format_ref_id(name, raw_id):
79 def _format_ref_id(name, raw_id):
80 """Default formatting of a given reference `name`"""
80 """Default formatting of a given reference `name`"""
81 return name
81 return name
82
82
83
83
84 def _format_ref_id_svn(name, raw_id):
84 def _format_ref_id_svn(name, raw_id):
85 """Special way of formatting a reference for Subversion including path"""
85 """Special way of formatting a reference for Subversion including path"""
86 return '%s@%s' % (name, raw_id)
86 return '%s@%s' % (name, raw_id)
87
87
88
88
89 class TemplateArgs(StrictAttributeDict):
89 class TemplateArgs(StrictAttributeDict):
90 pass
90 pass
91
91
92
92
93 class BaseAppView(object):
93 class BaseAppView(object):
94
94
95 def __init__(self, context, request):
95 def __init__(self, context, request):
96 self.request = request
96 self.request = request
97 self.context = context
97 self.context = context
98 self.session = request.session
98 self.session = request.session
99 self._rhodecode_user = request.user # auth user
99 self._rhodecode_user = request.user # auth user
100 self._rhodecode_db_user = self._rhodecode_user.get_instance()
100 self._rhodecode_db_user = self._rhodecode_user.get_instance()
101 self._maybe_needs_password_change(
101 self._maybe_needs_password_change(
102 request.matched_route.name, self._rhodecode_db_user)
102 request.matched_route.name, self._rhodecode_db_user)
103
103
104 def _maybe_needs_password_change(self, view_name, user_obj):
104 def _maybe_needs_password_change(self, view_name, user_obj):
105 log.debug('Checking if user %s needs password change on view %s',
105 log.debug('Checking if user %s needs password change on view %s',
106 user_obj, view_name)
106 user_obj, view_name)
107 skip_user_views = [
107 skip_user_views = [
108 'logout', 'login',
108 'logout', 'login',
109 'my_account_password', 'my_account_password_update'
109 'my_account_password', 'my_account_password_update'
110 ]
110 ]
111
111
112 if not user_obj:
112 if not user_obj:
113 return
113 return
114
114
115 if user_obj.username == User.DEFAULT_USER:
115 if user_obj.username == User.DEFAULT_USER:
116 return
116 return
117
117
118 now = time.time()
118 now = time.time()
119 should_change = user_obj.user_data.get('force_password_change')
119 should_change = user_obj.user_data.get('force_password_change')
120 change_after = safe_int(should_change) or 0
120 change_after = safe_int(should_change) or 0
121 if should_change and now > change_after:
121 if should_change and now > change_after:
122 log.debug('User %s requires password change', user_obj)
122 log.debug('User %s requires password change', user_obj)
123 h.flash('You are required to change your password', 'warning',
123 h.flash('You are required to change your password', 'warning',
124 ignore_duplicate=True)
124 ignore_duplicate=True)
125
125
126 if view_name not in skip_user_views:
126 if view_name not in skip_user_views:
127 raise HTTPFound(
127 raise HTTPFound(
128 self.request.route_path('my_account_password'))
128 self.request.route_path('my_account_password'))
129
129
130 def _get_local_tmpl_context(self, include_app_defaults=False):
130 def _get_local_tmpl_context(self, include_app_defaults=False):
131 c = TemplateArgs()
131 c = TemplateArgs()
132 c.auth_user = self.request.user
132 c.auth_user = self.request.user
133 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
133 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
134 c.rhodecode_user = self.request.user
134 c.rhodecode_user = self.request.user
135
135
136 if include_app_defaults:
136 if include_app_defaults:
137 # NOTE(marcink): after full pyramid migration include_app_defaults
137 # NOTE(marcink): after full pyramid migration include_app_defaults
138 # should be turned on by default
138 # should be turned on by default
139 from rhodecode.lib.base import attach_context_attributes
139 from rhodecode.lib.base import attach_context_attributes
140 attach_context_attributes(c, self.request, self.request.user.user_id)
140 attach_context_attributes(c, self.request, self.request.user.user_id)
141
141
142 return c
142 return c
143
143
144 def _register_global_c(self, tmpl_args):
144 def _register_global_c(self, tmpl_args):
145 """
145 """
146 Registers attributes to pylons global `c`
146 Registers attributes to pylons global `c`
147 """
147 """
148
148
149 # TODO(marcink): remove once pyramid migration is finished
149 # TODO(marcink): remove once pyramid migration is finished
150 from pylons import tmpl_context as c
150 from pylons import tmpl_context as c
151 try:
151 try:
152 for k, v in tmpl_args.items():
152 for k, v in tmpl_args.items():
153 setattr(c, k, v)
153 setattr(c, k, v)
154 except TypeError:
154 except TypeError:
155 log.exception('Failed to register pylons C')
155 log.exception('Failed to register pylons C')
156 pass
156 pass
157
157
158 def _get_template_context(self, tmpl_args):
158 def _get_template_context(self, tmpl_args):
159 self._register_global_c(tmpl_args)
159 self._register_global_c(tmpl_args)
160
160
161 local_tmpl_args = {
161 local_tmpl_args = {
162 'defaults': {},
162 'defaults': {},
163 'errors': {},
163 'errors': {},
164 # register a fake 'c' to be used in templates instead of global
164 # register a fake 'c' to be used in templates instead of global
165 # pylons c, after migration to pyramid we should rename it to 'c'
165 # pylons c, after migration to pyramid we should rename it to 'c'
166 # make sure we replace usage of _c in templates too
166 # make sure we replace usage of _c in templates too
167 '_c': tmpl_args
167 '_c': tmpl_args
168 }
168 }
169 local_tmpl_args.update(tmpl_args)
169 local_tmpl_args.update(tmpl_args)
170 return local_tmpl_args
170 return local_tmpl_args
171
171
172 def load_default_context(self):
172 def load_default_context(self):
173 """
173 """
174 example:
174 example:
175
175
176 def load_default_context(self):
176 def load_default_context(self):
177 c = self._get_local_tmpl_context()
177 c = self._get_local_tmpl_context()
178 c.custom_var = 'foobar'
178 c.custom_var = 'foobar'
179 self._register_global_c(c)
179 self._register_global_c(c)
180 return c
180 return c
181 """
181 """
182 raise NotImplementedError('Needs implementation in view class')
182 raise NotImplementedError('Needs implementation in view class')
183
183
184
184
185 class RepoAppView(BaseAppView):
185 class RepoAppView(BaseAppView):
186
186
187 def __init__(self, context, request):
187 def __init__(self, context, request):
188 super(RepoAppView, self).__init__(context, request)
188 super(RepoAppView, self).__init__(context, request)
189 self.db_repo = request.db_repo
189 self.db_repo = request.db_repo
190 self.db_repo_name = self.db_repo.repo_name
190 self.db_repo_name = self.db_repo.repo_name
191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
192
192
193 def _handle_missing_requirements(self, error):
193 def _handle_missing_requirements(self, error):
194 log.error(
194 log.error(
195 'Requirements are missing for repository %s: %s',
195 'Requirements are missing for repository %s: %s',
196 self.db_repo_name, error.message)
196 self.db_repo_name, error.message)
197
197
198 def _get_local_tmpl_context(self, include_app_defaults=False):
198 def _get_local_tmpl_context(self, include_app_defaults=False):
199 c = super(RepoAppView, self)._get_local_tmpl_context(
199 c = super(RepoAppView, self)._get_local_tmpl_context(
200 include_app_defaults=include_app_defaults)
200 include_app_defaults=include_app_defaults)
201
201
202 # register common vars for this type of view
202 # register common vars for this type of view
203 c.rhodecode_db_repo = self.db_repo
203 c.rhodecode_db_repo = self.db_repo
204 c.repo_name = self.db_repo_name
204 c.repo_name = self.db_repo_name
205 c.repository_pull_requests = self.db_repo_pull_requests
205 c.repository_pull_requests = self.db_repo_pull_requests
206
206
207 c.repository_requirements_missing = False
207 c.repository_requirements_missing = False
208 try:
208 try:
209 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
209 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
210 except RepositoryRequirementError as e:
210 except RepositoryRequirementError as e:
211 c.repository_requirements_missing = True
211 c.repository_requirements_missing = True
212 self._handle_missing_requirements(e)
212 self._handle_missing_requirements(e)
213
213
214 return c
214 return c
215
215
216 def _get_f_path(self, matchdict, default=None):
216 def _get_f_path(self, matchdict, default=None):
217 f_path = matchdict.get('f_path')
217 f_path = matchdict.get('f_path')
218 if f_path:
218 if f_path:
219 # fix for multiple initial slashes that causes errors for GIT
219 # fix for multiple initial slashes that causes errors for GIT
220 return f_path.lstrip('/')
220 return f_path.lstrip('/')
221
221
222 return default
222 return default
223
223
224
224 class DataGridAppView(object):
225 class DataGridAppView(object):
225 """
226 """
226 Common class to have re-usable grid rendering components
227 Common class to have re-usable grid rendering components
227 """
228 """
228
229
229 def _extract_ordering(self, request, column_map=None):
230 def _extract_ordering(self, request, column_map=None):
230 column_map = column_map or {}
231 column_map = column_map or {}
231 column_index = safe_int(request.GET.get('order[0][column]'))
232 column_index = safe_int(request.GET.get('order[0][column]'))
232 order_dir = request.GET.get(
233 order_dir = request.GET.get(
233 'order[0][dir]', 'desc')
234 'order[0][dir]', 'desc')
234 order_by = request.GET.get(
235 order_by = request.GET.get(
235 'columns[%s][data][sort]' % column_index, 'name_raw')
236 'columns[%s][data][sort]' % column_index, 'name_raw')
236
237
237 # translate datatable to DB columns
238 # translate datatable to DB columns
238 order_by = column_map.get(order_by) or order_by
239 order_by = column_map.get(order_by) or order_by
239
240
240 search_q = request.GET.get('search[value]')
241 search_q = request.GET.get('search[value]')
241 return search_q, order_by, order_dir
242 return search_q, order_by, order_dir
242
243
243 def _extract_chunk(self, request):
244 def _extract_chunk(self, request):
244 start = safe_int(request.GET.get('start'), 0)
245 start = safe_int(request.GET.get('start'), 0)
245 length = safe_int(request.GET.get('length'), 25)
246 length = safe_int(request.GET.get('length'), 25)
246 draw = safe_int(request.GET.get('draw'))
247 draw = safe_int(request.GET.get('draw'))
247 return draw, start, length
248 return draw, start, length
248
249
249
250
250 class BaseReferencesView(RepoAppView):
251 class BaseReferencesView(RepoAppView):
251 """
252 """
252 Base for reference view for branches, tags and bookmarks.
253 Base for reference view for branches, tags and bookmarks.
253 """
254 """
254 def load_default_context(self):
255 def load_default_context(self):
255 c = self._get_local_tmpl_context()
256 c = self._get_local_tmpl_context()
256
257
257 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
258 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
258 c.repo_info = self.db_repo
259 c.repo_info = self.db_repo
259
260
260 self._register_global_c(c)
261 self._register_global_c(c)
261 return c
262 return c
262
263
263 def load_refs_context(self, ref_items, partials_template):
264 def load_refs_context(self, ref_items, partials_template):
264 _render = self.request.get_partial_renderer(partials_template)
265 _render = self.request.get_partial_renderer(partials_template)
265 pre_load = ["author", "date", "message"]
266 pre_load = ["author", "date", "message"]
266
267
267 is_svn = h.is_svn(self.rhodecode_vcs_repo)
268 is_svn = h.is_svn(self.rhodecode_vcs_repo)
268 is_hg = h.is_hg(self.rhodecode_vcs_repo)
269 is_hg = h.is_hg(self.rhodecode_vcs_repo)
269
270
270 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
271 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
271
272
272 closed_refs = {}
273 closed_refs = {}
273 if is_hg:
274 if is_hg:
274 closed_refs = self.rhodecode_vcs_repo.branches_closed
275 closed_refs = self.rhodecode_vcs_repo.branches_closed
275
276
276 data = []
277 data = []
277 for ref_name, commit_id in ref_items:
278 for ref_name, commit_id in ref_items:
278 commit = self.rhodecode_vcs_repo.get_commit(
279 commit = self.rhodecode_vcs_repo.get_commit(
279 commit_id=commit_id, pre_load=pre_load)
280 commit_id=commit_id, pre_load=pre_load)
280 closed = ref_name in closed_refs
281 closed = ref_name in closed_refs
281
282
282 # TODO: johbo: Unify generation of reference links
283 # TODO: johbo: Unify generation of reference links
283 use_commit_id = '/' in ref_name or is_svn
284 use_commit_id = '/' in ref_name or is_svn
284
285
285 if use_commit_id:
286 if use_commit_id:
286 files_url = h.route_path(
287 files_url = h.route_path(
287 'repo_files',
288 'repo_files',
288 repo_name=self.db_repo_name,
289 repo_name=self.db_repo_name,
289 f_path=ref_name if is_svn else '',
290 f_path=ref_name if is_svn else '',
290 commit_id=commit_id)
291 commit_id=commit_id)
291
292
292 else:
293 else:
293 files_url = h.route_path(
294 files_url = h.route_path(
294 'repo_files',
295 'repo_files',
295 repo_name=self.db_repo_name,
296 repo_name=self.db_repo_name,
296 f_path=ref_name if is_svn else '',
297 f_path=ref_name if is_svn else '',
297 commit_id=ref_name,
298 commit_id=ref_name,
298 _query=dict(at=ref_name))
299 _query=dict(at=ref_name))
299
300
300 data.append({
301 data.append({
301 "name": _render('name', ref_name, files_url, closed),
302 "name": _render('name', ref_name, files_url, closed),
302 "name_raw": ref_name,
303 "name_raw": ref_name,
303 "date": _render('date', commit.date),
304 "date": _render('date', commit.date),
304 "date_raw": datetime_to_time(commit.date),
305 "date_raw": datetime_to_time(commit.date),
305 "author": _render('author', commit.author),
306 "author": _render('author', commit.author),
306 "commit": _render(
307 "commit": _render(
307 'commit', commit.message, commit.raw_id, commit.idx),
308 'commit', commit.message, commit.raw_id, commit.idx),
308 "commit_raw": commit.idx,
309 "commit_raw": commit.idx,
309 "compare": _render(
310 "compare": _render(
310 'compare', format_ref_id(ref_name, commit.raw_id)),
311 'compare', format_ref_id(ref_name, commit.raw_id)),
311 })
312 })
312
313
313 return data
314 return data
314
315
315
316
316 class RepoRoutePredicate(object):
317 class RepoRoutePredicate(object):
317 def __init__(self, val, config):
318 def __init__(self, val, config):
318 self.val = val
319 self.val = val
319
320
320 def text(self):
321 def text(self):
321 return 'repo_route = %s' % self.val
322 return 'repo_route = %s' % self.val
322
323
323 phash = text
324 phash = text
324
325
325 def __call__(self, info, request):
326 def __call__(self, info, request):
326
327
327 if hasattr(request, 'vcs_call'):
328 if hasattr(request, 'vcs_call'):
328 # skip vcs calls
329 # skip vcs calls
329 return
330 return
330
331
331 repo_name = info['match']['repo_name']
332 repo_name = info['match']['repo_name']
332 repo_model = repo.RepoModel()
333 repo_model = repo.RepoModel()
333 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
334 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
334
335
335 if by_name_match:
336 if by_name_match:
336 # register this as request object we can re-use later
337 # register this as request object we can re-use later
337 request.db_repo = by_name_match
338 request.db_repo = by_name_match
338 return True
339 return True
339
340
340 by_id_match = repo_model.get_repo_by_id(repo_name)
341 by_id_match = repo_model.get_repo_by_id(repo_name)
341 if by_id_match:
342 if by_id_match:
342 request.db_repo = by_id_match
343 request.db_repo = by_id_match
343 return True
344 return True
344
345
345 return False
346 return False
346
347
347
348
348 class RepoTypeRoutePredicate(object):
349 class RepoTypeRoutePredicate(object):
349 def __init__(self, val, config):
350 def __init__(self, val, config):
350 self.val = val or ['hg', 'git', 'svn']
351 self.val = val or ['hg', 'git', 'svn']
351
352
352 def text(self):
353 def text(self):
353 return 'repo_accepted_type = %s' % self.val
354 return 'repo_accepted_type = %s' % self.val
354
355
355 phash = text
356 phash = text
356
357
357 def __call__(self, info, request):
358 def __call__(self, info, request):
358 if hasattr(request, 'vcs_call'):
359 if hasattr(request, 'vcs_call'):
359 # skip vcs calls
360 # skip vcs calls
360 return
361 return
361
362
362 rhodecode_db_repo = request.db_repo
363 rhodecode_db_repo = request.db_repo
363
364
364 log.debug(
365 log.debug(
365 '%s checking repo type for %s in %s',
366 '%s checking repo type for %s in %s',
366 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
367 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
367
368
368 if rhodecode_db_repo.repo_type in self.val:
369 if rhodecode_db_repo.repo_type in self.val:
369 return True
370 return True
370 else:
371 else:
371 log.warning('Current view is not supported for repo type:%s',
372 log.warning('Current view is not supported for repo type:%s',
372 rhodecode_db_repo.repo_type)
373 rhodecode_db_repo.repo_type)
373 #
374 #
374 # h.flash(h.literal(
375 # h.flash(h.literal(
375 # _('Action not supported for %s.' % rhodecode_repo.alias)),
376 # _('Action not supported for %s.' % rhodecode_repo.alias)),
376 # category='warning')
377 # category='warning')
377 # return redirect(
378 # return redirect(
378 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
379 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
379
380
380 return False
381 return False
381
382
382
383
383 class RepoGroupRoutePredicate(object):
384 class RepoGroupRoutePredicate(object):
384 def __init__(self, val, config):
385 def __init__(self, val, config):
385 self.val = val
386 self.val = val
386
387
387 def text(self):
388 def text(self):
388 return 'repo_group_route = %s' % self.val
389 return 'repo_group_route = %s' % self.val
389
390
390 phash = text
391 phash = text
391
392
392 def __call__(self, info, request):
393 def __call__(self, info, request):
393 if hasattr(request, 'vcs_call'):
394 if hasattr(request, 'vcs_call'):
394 # skip vcs calls
395 # skip vcs calls
395 return
396 return
396
397
397 repo_group_name = info['match']['repo_group_name']
398 repo_group_name = info['match']['repo_group_name']
398 repo_group_model = repo_group.RepoGroupModel()
399 repo_group_model = repo_group.RepoGroupModel()
399 by_name_match = repo_group_model.get_by_group_name(
400 by_name_match = repo_group_model.get_by_group_name(
400 repo_group_name, cache=True)
401 repo_group_name, cache=True)
401
402
402 if by_name_match:
403 if by_name_match:
403 # register this as request object we can re-use later
404 # register this as request object we can re-use later
404 request.db_repo_group = by_name_match
405 request.db_repo_group = by_name_match
405 return True
406 return True
406
407
407 return False
408 return False
408
409
409
410
410 def includeme(config):
411 def includeme(config):
411 config.add_route_predicate(
412 config.add_route_predicate(
412 'repo_route', RepoRoutePredicate)
413 'repo_route', RepoRoutePredicate)
413 config.add_route_predicate(
414 config.add_route_predicate(
414 'repo_accepted_types', RepoTypeRoutePredicate)
415 'repo_accepted_types', RepoTypeRoutePredicate)
415 config.add_route_predicate(
416 config.add_route_predicate(
416 'repo_group_route', RepoGroupRoutePredicate)
417 'repo_group_route', RepoGroupRoutePredicate)
@@ -1,63 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23
23
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26
26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 from rhodecode.model.db import PullRequest
30 from rhodecode.model.db import PullRequest
31
31
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class AdminMainView(BaseAppView):
36 class AdminMainView(BaseAppView):
37
37
38 @LoginRequired()
38 @LoginRequired()
39 @HasPermissionAllDecorator('hg.admin')
39 @HasPermissionAllDecorator('hg.admin')
40 @view_config(
40 @view_config(
41 route_name='admin_home', request_method='GET')
41 route_name='admin_home', request_method='GET')
42 def admin_main(self):
42 def admin_main(self):
43 # redirect _admin to audit logs...
43 # redirect _admin to audit logs...
44 raise HTTPFound(h.route_path('admin_audit_logs'))
44 raise HTTPFound(h.route_path('admin_audit_logs'))
45
45
46 @LoginRequired()
46 @LoginRequired()
47 @view_config(route_name='pull_requests_global_0', request_method='GET')
47 @view_config(route_name='pull_requests_global_0', request_method='GET')
48 @view_config(route_name='pull_requests_global_1', request_method='GET')
48 @view_config(route_name='pull_requests_global_1', request_method='GET')
49 @view_config(route_name='pull_requests_global', request_method='GET')
49 @view_config(route_name='pull_requests_global', request_method='GET')
50 def pull_requests(self):
50 def pull_requests(self):
51 """
51 """
52 Global redirect for Pull Requests
52 Global redirect for Pull Requests
53
53
54 :param pull_request_id: id of pull requests in the system
54 :param pull_request_id: id of pull requests in the system
55 """
55 """
56
56
57 pull_request_id = self.request.matchdict.get('pull_request_id')
57 pull_request_id = self.request.matchdict.get('pull_request_id')
58 pull_request = PullRequest.get_or_404(pull_request_id, pyramid_exc=True)
58 pull_request = PullRequest.get_or_404(pull_request_id)
59 repo_name = pull_request.target_repo.repo_name
59 repo_name = pull_request.target_repo.repo_name
60
60
61 raise HTTPFound(
61 raise HTTPFound(
62 h.route_path('pullrequest_show', repo_name=repo_name,
62 h.route_path('pullrequest_show', repo_name=repo_name,
63 pull_request_id=pull_request_id))
63 pull_request_id=pull_request_id))
@@ -1,505 +1,505 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24
24
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from sqlalchemy.sql.functions import coalesce
27 from sqlalchemy.sql.functions import coalesce
28
28
29 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30
30
31 from rhodecode.lib import audit_logger
31 from rhodecode.lib import audit_logger
32 from rhodecode.lib.ext_json import json
32 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib.utils2 import safe_int, safe_unicode
36 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 from rhodecode.model.auth_token import AuthTokenModel
37 from rhodecode.model.auth_token import AuthTokenModel
38 from rhodecode.model.user import UserModel
38 from rhodecode.model.user import UserModel
39 from rhodecode.model.user_group import UserGroupModel
39 from rhodecode.model.user_group import UserGroupModel
40 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
40 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
41 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class AdminUsersView(BaseAppView, DataGridAppView):
46 class AdminUsersView(BaseAppView, DataGridAppView):
47 ALLOW_SCOPED_TOKENS = False
47 ALLOW_SCOPED_TOKENS = False
48 """
48 """
49 This view has alternative version inside EE, if modified please take a look
49 This view has alternative version inside EE, if modified please take a look
50 in there as well.
50 in there as well.
51 """
51 """
52
52
53 def load_default_context(self):
53 def load_default_context(self):
54 c = self._get_local_tmpl_context()
54 c = self._get_local_tmpl_context()
55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
56 self._register_global_c(c)
56 self._register_global_c(c)
57 return c
57 return c
58
58
59 def _redirect_for_default_user(self, username):
59 def _redirect_for_default_user(self, username):
60 _ = self.request.translate
60 _ = self.request.translate
61 if username == User.DEFAULT_USER:
61 if username == User.DEFAULT_USER:
62 h.flash(_("You can't edit this user"), category='warning')
62 h.flash(_("You can't edit this user"), category='warning')
63 # TODO(marcink): redirect to 'users' admin panel once this
63 # TODO(marcink): redirect to 'users' admin panel once this
64 # is a pyramid view
64 # is a pyramid view
65 raise HTTPFound('/')
65 raise HTTPFound('/')
66
66
67 @HasPermissionAllDecorator('hg.admin')
67 @HasPermissionAllDecorator('hg.admin')
68 @view_config(
68 @view_config(
69 route_name='users', request_method='GET',
69 route_name='users', request_method='GET',
70 renderer='rhodecode:templates/admin/users/users.mako')
70 renderer='rhodecode:templates/admin/users/users.mako')
71 def users_list(self):
71 def users_list(self):
72 c = self.load_default_context()
72 c = self.load_default_context()
73 return self._get_template_context(c)
73 return self._get_template_context(c)
74
74
75 @HasPermissionAllDecorator('hg.admin')
75 @HasPermissionAllDecorator('hg.admin')
76 @view_config(
76 @view_config(
77 # renderer defined below
77 # renderer defined below
78 route_name='users_data', request_method='GET',
78 route_name='users_data', request_method='GET',
79 renderer='json_ext', xhr=True)
79 renderer='json_ext', xhr=True)
80 def users_list_data(self):
80 def users_list_data(self):
81 draw, start, limit = self._extract_chunk(self.request)
81 draw, start, limit = self._extract_chunk(self.request)
82 search_q, order_by, order_dir = self._extract_ordering(self.request)
82 search_q, order_by, order_dir = self._extract_ordering(self.request)
83
83
84 _render = self.request.get_partial_renderer(
84 _render = self.request.get_partial_renderer(
85 'data_table/_dt_elements.mako')
85 'data_table/_dt_elements.mako')
86
86
87 def user_actions(user_id, username):
87 def user_actions(user_id, username):
88 return _render("user_actions", user_id, username)
88 return _render("user_actions", user_id, username)
89
89
90 users_data_total_count = User.query()\
90 users_data_total_count = User.query()\
91 .filter(User.username != User.DEFAULT_USER) \
91 .filter(User.username != User.DEFAULT_USER) \
92 .count()
92 .count()
93
93
94 # json generate
94 # json generate
95 base_q = User.query().filter(User.username != User.DEFAULT_USER)
95 base_q = User.query().filter(User.username != User.DEFAULT_USER)
96
96
97 if search_q:
97 if search_q:
98 like_expression = u'%{}%'.format(safe_unicode(search_q))
98 like_expression = u'%{}%'.format(safe_unicode(search_q))
99 base_q = base_q.filter(or_(
99 base_q = base_q.filter(or_(
100 User.username.ilike(like_expression),
100 User.username.ilike(like_expression),
101 User._email.ilike(like_expression),
101 User._email.ilike(like_expression),
102 User.name.ilike(like_expression),
102 User.name.ilike(like_expression),
103 User.lastname.ilike(like_expression),
103 User.lastname.ilike(like_expression),
104 ))
104 ))
105
105
106 users_data_total_filtered_count = base_q.count()
106 users_data_total_filtered_count = base_q.count()
107
107
108 sort_col = getattr(User, order_by, None)
108 sort_col = getattr(User, order_by, None)
109 if sort_col:
109 if sort_col:
110 if order_dir == 'asc':
110 if order_dir == 'asc':
111 # handle null values properly to order by NULL last
111 # handle null values properly to order by NULL last
112 if order_by in ['last_activity']:
112 if order_by in ['last_activity']:
113 sort_col = coalesce(sort_col, datetime.date.max)
113 sort_col = coalesce(sort_col, datetime.date.max)
114 sort_col = sort_col.asc()
114 sort_col = sort_col.asc()
115 else:
115 else:
116 # handle null values properly to order by NULL last
116 # handle null values properly to order by NULL last
117 if order_by in ['last_activity']:
117 if order_by in ['last_activity']:
118 sort_col = coalesce(sort_col, datetime.date.min)
118 sort_col = coalesce(sort_col, datetime.date.min)
119 sort_col = sort_col.desc()
119 sort_col = sort_col.desc()
120
120
121 base_q = base_q.order_by(sort_col)
121 base_q = base_q.order_by(sort_col)
122 base_q = base_q.offset(start).limit(limit)
122 base_q = base_q.offset(start).limit(limit)
123
123
124 users_list = base_q.all()
124 users_list = base_q.all()
125
125
126 users_data = []
126 users_data = []
127 for user in users_list:
127 for user in users_list:
128 users_data.append({
128 users_data.append({
129 "username": h.gravatar_with_user(self.request, user.username),
129 "username": h.gravatar_with_user(self.request, user.username),
130 "email": user.email,
130 "email": user.email,
131 "first_name": user.first_name,
131 "first_name": user.first_name,
132 "last_name": user.last_name,
132 "last_name": user.last_name,
133 "last_login": h.format_date(user.last_login),
133 "last_login": h.format_date(user.last_login),
134 "last_activity": h.format_date(user.last_activity),
134 "last_activity": h.format_date(user.last_activity),
135 "active": h.bool2icon(user.active),
135 "active": h.bool2icon(user.active),
136 "active_raw": user.active,
136 "active_raw": user.active,
137 "admin": h.bool2icon(user.admin),
137 "admin": h.bool2icon(user.admin),
138 "extern_type": user.extern_type,
138 "extern_type": user.extern_type,
139 "extern_name": user.extern_name,
139 "extern_name": user.extern_name,
140 "action": user_actions(user.user_id, user.username),
140 "action": user_actions(user.user_id, user.username),
141 })
141 })
142
142
143 data = ({
143 data = ({
144 'draw': draw,
144 'draw': draw,
145 'data': users_data,
145 'data': users_data,
146 'recordsTotal': users_data_total_count,
146 'recordsTotal': users_data_total_count,
147 'recordsFiltered': users_data_total_filtered_count,
147 'recordsFiltered': users_data_total_filtered_count,
148 })
148 })
149
149
150 return data
150 return data
151
151
152 @LoginRequired()
152 @LoginRequired()
153 @HasPermissionAllDecorator('hg.admin')
153 @HasPermissionAllDecorator('hg.admin')
154 @view_config(
154 @view_config(
155 route_name='edit_user_auth_tokens', request_method='GET',
155 route_name='edit_user_auth_tokens', request_method='GET',
156 renderer='rhodecode:templates/admin/users/user_edit.mako')
156 renderer='rhodecode:templates/admin/users/user_edit.mako')
157 def auth_tokens(self):
157 def auth_tokens(self):
158 _ = self.request.translate
158 _ = self.request.translate
159 c = self.load_default_context()
159 c = self.load_default_context()
160
160
161 user_id = self.request.matchdict.get('user_id')
161 user_id = self.request.matchdict.get('user_id')
162 c.user = User.get_or_404(user_id, pyramid_exc=True)
162 c.user = User.get_or_404(user_id)
163 self._redirect_for_default_user(c.user.username)
163 self._redirect_for_default_user(c.user.username)
164
164
165 c.active = 'auth_tokens'
165 c.active = 'auth_tokens'
166
166
167 c.lifetime_values = [
167 c.lifetime_values = [
168 (str(-1), _('forever')),
168 (str(-1), _('forever')),
169 (str(5), _('5 minutes')),
169 (str(5), _('5 minutes')),
170 (str(60), _('1 hour')),
170 (str(60), _('1 hour')),
171 (str(60 * 24), _('1 day')),
171 (str(60 * 24), _('1 day')),
172 (str(60 * 24 * 30), _('1 month')),
172 (str(60 * 24 * 30), _('1 month')),
173 ]
173 ]
174 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
174 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
175 c.role_values = [
175 c.role_values = [
176 (x, AuthTokenModel.cls._get_role_name(x))
176 (x, AuthTokenModel.cls._get_role_name(x))
177 for x in AuthTokenModel.cls.ROLES]
177 for x in AuthTokenModel.cls.ROLES]
178 c.role_options = [(c.role_values, _("Role"))]
178 c.role_options = [(c.role_values, _("Role"))]
179 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
179 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
180 c.user.user_id, show_expired=True)
180 c.user.user_id, show_expired=True)
181 return self._get_template_context(c)
181 return self._get_template_context(c)
182
182
183 def maybe_attach_token_scope(self, token):
183 def maybe_attach_token_scope(self, token):
184 # implemented in EE edition
184 # implemented in EE edition
185 pass
185 pass
186
186
187 @LoginRequired()
187 @LoginRequired()
188 @HasPermissionAllDecorator('hg.admin')
188 @HasPermissionAllDecorator('hg.admin')
189 @CSRFRequired()
189 @CSRFRequired()
190 @view_config(
190 @view_config(
191 route_name='edit_user_auth_tokens_add', request_method='POST')
191 route_name='edit_user_auth_tokens_add', request_method='POST')
192 def auth_tokens_add(self):
192 def auth_tokens_add(self):
193 _ = self.request.translate
193 _ = self.request.translate
194 c = self.load_default_context()
194 c = self.load_default_context()
195
195
196 user_id = self.request.matchdict.get('user_id')
196 user_id = self.request.matchdict.get('user_id')
197 c.user = User.get_or_404(user_id, pyramid_exc=True)
197 c.user = User.get_or_404(user_id)
198
198
199 self._redirect_for_default_user(c.user.username)
199 self._redirect_for_default_user(c.user.username)
200
200
201 user_data = c.user.get_api_data()
201 user_data = c.user.get_api_data()
202 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
203 description = self.request.POST.get('description')
203 description = self.request.POST.get('description')
204 role = self.request.POST.get('role')
204 role = self.request.POST.get('role')
205
205
206 token = AuthTokenModel().create(
206 token = AuthTokenModel().create(
207 c.user.user_id, description, lifetime, role)
207 c.user.user_id, description, lifetime, role)
208 token_data = token.get_api_data()
208 token_data = token.get_api_data()
209
209
210 self.maybe_attach_token_scope(token)
210 self.maybe_attach_token_scope(token)
211 audit_logger.store_web(
211 audit_logger.store_web(
212 'user.edit.token.add', action_data={
212 'user.edit.token.add', action_data={
213 'data': {'token': token_data, 'user': user_data}},
213 'data': {'token': token_data, 'user': user_data}},
214 user=self._rhodecode_user, )
214 user=self._rhodecode_user, )
215 Session().commit()
215 Session().commit()
216
216
217 h.flash(_("Auth token successfully created"), category='success')
217 h.flash(_("Auth token successfully created"), category='success')
218 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
218 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
219
219
220 @LoginRequired()
220 @LoginRequired()
221 @HasPermissionAllDecorator('hg.admin')
221 @HasPermissionAllDecorator('hg.admin')
222 @CSRFRequired()
222 @CSRFRequired()
223 @view_config(
223 @view_config(
224 route_name='edit_user_auth_tokens_delete', request_method='POST')
224 route_name='edit_user_auth_tokens_delete', request_method='POST')
225 def auth_tokens_delete(self):
225 def auth_tokens_delete(self):
226 _ = self.request.translate
226 _ = self.request.translate
227 c = self.load_default_context()
227 c = self.load_default_context()
228
228
229 user_id = self.request.matchdict.get('user_id')
229 user_id = self.request.matchdict.get('user_id')
230 c.user = User.get_or_404(user_id, pyramid_exc=True)
230 c.user = User.get_or_404(user_id)
231 self._redirect_for_default_user(c.user.username)
231 self._redirect_for_default_user(c.user.username)
232 user_data = c.user.get_api_data()
232 user_data = c.user.get_api_data()
233
233
234 del_auth_token = self.request.POST.get('del_auth_token')
234 del_auth_token = self.request.POST.get('del_auth_token')
235
235
236 if del_auth_token:
236 if del_auth_token:
237 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
237 token = UserApiKeys.get_or_404(del_auth_token)
238 token_data = token.get_api_data()
238 token_data = token.get_api_data()
239
239
240 AuthTokenModel().delete(del_auth_token, c.user.user_id)
240 AuthTokenModel().delete(del_auth_token, c.user.user_id)
241 audit_logger.store_web(
241 audit_logger.store_web(
242 'user.edit.token.delete', action_data={
242 'user.edit.token.delete', action_data={
243 'data': {'token': token_data, 'user': user_data}},
243 'data': {'token': token_data, 'user': user_data}},
244 user=self._rhodecode_user,)
244 user=self._rhodecode_user,)
245 Session().commit()
245 Session().commit()
246 h.flash(_("Auth token successfully deleted"), category='success')
246 h.flash(_("Auth token successfully deleted"), category='success')
247
247
248 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
248 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
249
249
250 @LoginRequired()
250 @LoginRequired()
251 @HasPermissionAllDecorator('hg.admin')
251 @HasPermissionAllDecorator('hg.admin')
252 @view_config(
252 @view_config(
253 route_name='edit_user_emails', request_method='GET',
253 route_name='edit_user_emails', request_method='GET',
254 renderer='rhodecode:templates/admin/users/user_edit.mako')
254 renderer='rhodecode:templates/admin/users/user_edit.mako')
255 def emails(self):
255 def emails(self):
256 _ = self.request.translate
256 _ = self.request.translate
257 c = self.load_default_context()
257 c = self.load_default_context()
258
258
259 user_id = self.request.matchdict.get('user_id')
259 user_id = self.request.matchdict.get('user_id')
260 c.user = User.get_or_404(user_id, pyramid_exc=True)
260 c.user = User.get_or_404(user_id)
261 self._redirect_for_default_user(c.user.username)
261 self._redirect_for_default_user(c.user.username)
262
262
263 c.active = 'emails'
263 c.active = 'emails'
264 c.user_email_map = UserEmailMap.query() \
264 c.user_email_map = UserEmailMap.query() \
265 .filter(UserEmailMap.user == c.user).all()
265 .filter(UserEmailMap.user == c.user).all()
266
266
267 return self._get_template_context(c)
267 return self._get_template_context(c)
268
268
269 @LoginRequired()
269 @LoginRequired()
270 @HasPermissionAllDecorator('hg.admin')
270 @HasPermissionAllDecorator('hg.admin')
271 @CSRFRequired()
271 @CSRFRequired()
272 @view_config(
272 @view_config(
273 route_name='edit_user_emails_add', request_method='POST')
273 route_name='edit_user_emails_add', request_method='POST')
274 def emails_add(self):
274 def emails_add(self):
275 _ = self.request.translate
275 _ = self.request.translate
276 c = self.load_default_context()
276 c = self.load_default_context()
277
277
278 user_id = self.request.matchdict.get('user_id')
278 user_id = self.request.matchdict.get('user_id')
279 c.user = User.get_or_404(user_id, pyramid_exc=True)
279 c.user = User.get_or_404(user_id)
280 self._redirect_for_default_user(c.user.username)
280 self._redirect_for_default_user(c.user.username)
281
281
282 email = self.request.POST.get('new_email')
282 email = self.request.POST.get('new_email')
283 user_data = c.user.get_api_data()
283 user_data = c.user.get_api_data()
284 try:
284 try:
285 UserModel().add_extra_email(c.user.user_id, email)
285 UserModel().add_extra_email(c.user.user_id, email)
286 audit_logger.store_web(
286 audit_logger.store_web(
287 'user.edit.email.add', action_data={'email': email, 'user': user_data},
287 'user.edit.email.add', action_data={'email': email, 'user': user_data},
288 user=self._rhodecode_user)
288 user=self._rhodecode_user)
289 Session().commit()
289 Session().commit()
290 h.flash(_("Added new email address `%s` for user account") % email,
290 h.flash(_("Added new email address `%s` for user account") % email,
291 category='success')
291 category='success')
292 except formencode.Invalid as error:
292 except formencode.Invalid as error:
293 h.flash(h.escape(error.error_dict['email']), category='error')
293 h.flash(h.escape(error.error_dict['email']), category='error')
294 except Exception:
294 except Exception:
295 log.exception("Exception during email saving")
295 log.exception("Exception during email saving")
296 h.flash(_('An error occurred during email saving'),
296 h.flash(_('An error occurred during email saving'),
297 category='error')
297 category='error')
298 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
298 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
299
299
300 @LoginRequired()
300 @LoginRequired()
301 @HasPermissionAllDecorator('hg.admin')
301 @HasPermissionAllDecorator('hg.admin')
302 @CSRFRequired()
302 @CSRFRequired()
303 @view_config(
303 @view_config(
304 route_name='edit_user_emails_delete', request_method='POST')
304 route_name='edit_user_emails_delete', request_method='POST')
305 def emails_delete(self):
305 def emails_delete(self):
306 _ = self.request.translate
306 _ = self.request.translate
307 c = self.load_default_context()
307 c = self.load_default_context()
308
308
309 user_id = self.request.matchdict.get('user_id')
309 user_id = self.request.matchdict.get('user_id')
310 c.user = User.get_or_404(user_id, pyramid_exc=True)
310 c.user = User.get_or_404(user_id)
311 self._redirect_for_default_user(c.user.username)
311 self._redirect_for_default_user(c.user.username)
312
312
313 email_id = self.request.POST.get('del_email_id')
313 email_id = self.request.POST.get('del_email_id')
314 user_model = UserModel()
314 user_model = UserModel()
315
315
316 email = UserEmailMap.query().get(email_id).email
316 email = UserEmailMap.query().get(email_id).email
317 user_data = c.user.get_api_data()
317 user_data = c.user.get_api_data()
318 user_model.delete_extra_email(c.user.user_id, email_id)
318 user_model.delete_extra_email(c.user.user_id, email_id)
319 audit_logger.store_web(
319 audit_logger.store_web(
320 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
320 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
321 user=self._rhodecode_user)
321 user=self._rhodecode_user)
322 Session().commit()
322 Session().commit()
323 h.flash(_("Removed email address from user account"),
323 h.flash(_("Removed email address from user account"),
324 category='success')
324 category='success')
325 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
325 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
326
326
327 @LoginRequired()
327 @LoginRequired()
328 @HasPermissionAllDecorator('hg.admin')
328 @HasPermissionAllDecorator('hg.admin')
329 @view_config(
329 @view_config(
330 route_name='edit_user_ips', request_method='GET',
330 route_name='edit_user_ips', request_method='GET',
331 renderer='rhodecode:templates/admin/users/user_edit.mako')
331 renderer='rhodecode:templates/admin/users/user_edit.mako')
332 def ips(self):
332 def ips(self):
333 _ = self.request.translate
333 _ = self.request.translate
334 c = self.load_default_context()
334 c = self.load_default_context()
335
335
336 user_id = self.request.matchdict.get('user_id')
336 user_id = self.request.matchdict.get('user_id')
337 c.user = User.get_or_404(user_id, pyramid_exc=True)
337 c.user = User.get_or_404(user_id)
338 self._redirect_for_default_user(c.user.username)
338 self._redirect_for_default_user(c.user.username)
339
339
340 c.active = 'ips'
340 c.active = 'ips'
341 c.user_ip_map = UserIpMap.query() \
341 c.user_ip_map = UserIpMap.query() \
342 .filter(UserIpMap.user == c.user).all()
342 .filter(UserIpMap.user == c.user).all()
343
343
344 c.inherit_default_ips = c.user.inherit_default_permissions
344 c.inherit_default_ips = c.user.inherit_default_permissions
345 c.default_user_ip_map = UserIpMap.query() \
345 c.default_user_ip_map = UserIpMap.query() \
346 .filter(UserIpMap.user == User.get_default_user()).all()
346 .filter(UserIpMap.user == User.get_default_user()).all()
347
347
348 return self._get_template_context(c)
348 return self._get_template_context(c)
349
349
350 @LoginRequired()
350 @LoginRequired()
351 @HasPermissionAllDecorator('hg.admin')
351 @HasPermissionAllDecorator('hg.admin')
352 @CSRFRequired()
352 @CSRFRequired()
353 @view_config(
353 @view_config(
354 route_name='edit_user_ips_add', request_method='POST')
354 route_name='edit_user_ips_add', request_method='POST')
355 def ips_add(self):
355 def ips_add(self):
356 _ = self.request.translate
356 _ = self.request.translate
357 c = self.load_default_context()
357 c = self.load_default_context()
358
358
359 user_id = self.request.matchdict.get('user_id')
359 user_id = self.request.matchdict.get('user_id')
360 c.user = User.get_or_404(user_id, pyramid_exc=True)
360 c.user = User.get_or_404(user_id)
361 # NOTE(marcink): this view is allowed for default users, as we can
361 # NOTE(marcink): this view is allowed for default users, as we can
362 # edit their IP white list
362 # edit their IP white list
363
363
364 user_model = UserModel()
364 user_model = UserModel()
365 desc = self.request.POST.get('description')
365 desc = self.request.POST.get('description')
366 try:
366 try:
367 ip_list = user_model.parse_ip_range(
367 ip_list = user_model.parse_ip_range(
368 self.request.POST.get('new_ip'))
368 self.request.POST.get('new_ip'))
369 except Exception as e:
369 except Exception as e:
370 ip_list = []
370 ip_list = []
371 log.exception("Exception during ip saving")
371 log.exception("Exception during ip saving")
372 h.flash(_('An error occurred during ip saving:%s' % (e,)),
372 h.flash(_('An error occurred during ip saving:%s' % (e,)),
373 category='error')
373 category='error')
374 added = []
374 added = []
375 user_data = c.user.get_api_data()
375 user_data = c.user.get_api_data()
376 for ip in ip_list:
376 for ip in ip_list:
377 try:
377 try:
378 user_model.add_extra_ip(c.user.user_id, ip, desc)
378 user_model.add_extra_ip(c.user.user_id, ip, desc)
379 audit_logger.store_web(
379 audit_logger.store_web(
380 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
380 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
381 user=self._rhodecode_user)
381 user=self._rhodecode_user)
382 Session().commit()
382 Session().commit()
383 added.append(ip)
383 added.append(ip)
384 except formencode.Invalid as error:
384 except formencode.Invalid as error:
385 msg = error.error_dict['ip']
385 msg = error.error_dict['ip']
386 h.flash(msg, category='error')
386 h.flash(msg, category='error')
387 except Exception:
387 except Exception:
388 log.exception("Exception during ip saving")
388 log.exception("Exception during ip saving")
389 h.flash(_('An error occurred during ip saving'),
389 h.flash(_('An error occurred during ip saving'),
390 category='error')
390 category='error')
391 if added:
391 if added:
392 h.flash(
392 h.flash(
393 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
393 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
394 category='success')
394 category='success')
395 if 'default_user' in self.request.POST:
395 if 'default_user' in self.request.POST:
396 # case for editing global IP list we do it for 'DEFAULT' user
396 # case for editing global IP list we do it for 'DEFAULT' user
397 raise HTTPFound(h.route_path('admin_permissions_ips'))
397 raise HTTPFound(h.route_path('admin_permissions_ips'))
398 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
398 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
399
399
400 @LoginRequired()
400 @LoginRequired()
401 @HasPermissionAllDecorator('hg.admin')
401 @HasPermissionAllDecorator('hg.admin')
402 @CSRFRequired()
402 @CSRFRequired()
403 @view_config(
403 @view_config(
404 route_name='edit_user_ips_delete', request_method='POST')
404 route_name='edit_user_ips_delete', request_method='POST')
405 def ips_delete(self):
405 def ips_delete(self):
406 _ = self.request.translate
406 _ = self.request.translate
407 c = self.load_default_context()
407 c = self.load_default_context()
408
408
409 user_id = self.request.matchdict.get('user_id')
409 user_id = self.request.matchdict.get('user_id')
410 c.user = User.get_or_404(user_id, pyramid_exc=True)
410 c.user = User.get_or_404(user_id)
411 # NOTE(marcink): this view is allowed for default users, as we can
411 # NOTE(marcink): this view is allowed for default users, as we can
412 # edit their IP white list
412 # edit their IP white list
413
413
414 ip_id = self.request.POST.get('del_ip_id')
414 ip_id = self.request.POST.get('del_ip_id')
415 user_model = UserModel()
415 user_model = UserModel()
416 user_data = c.user.get_api_data()
416 user_data = c.user.get_api_data()
417 ip = UserIpMap.query().get(ip_id).ip_addr
417 ip = UserIpMap.query().get(ip_id).ip_addr
418 user_model.delete_extra_ip(c.user.user_id, ip_id)
418 user_model.delete_extra_ip(c.user.user_id, ip_id)
419 audit_logger.store_web(
419 audit_logger.store_web(
420 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
420 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
421 user=self._rhodecode_user)
421 user=self._rhodecode_user)
422 Session().commit()
422 Session().commit()
423 h.flash(_("Removed ip address from user whitelist"), category='success')
423 h.flash(_("Removed ip address from user whitelist"), category='success')
424
424
425 if 'default_user' in self.request.POST:
425 if 'default_user' in self.request.POST:
426 # case for editing global IP list we do it for 'DEFAULT' user
426 # case for editing global IP list we do it for 'DEFAULT' user
427 raise HTTPFound(h.route_path('admin_permissions_ips'))
427 raise HTTPFound(h.route_path('admin_permissions_ips'))
428 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
428 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
429
429
430 @LoginRequired()
430 @LoginRequired()
431 @HasPermissionAllDecorator('hg.admin')
431 @HasPermissionAllDecorator('hg.admin')
432 @view_config(
432 @view_config(
433 route_name='edit_user_groups_management', request_method='GET',
433 route_name='edit_user_groups_management', request_method='GET',
434 renderer='rhodecode:templates/admin/users/user_edit.mako')
434 renderer='rhodecode:templates/admin/users/user_edit.mako')
435 def groups_management(self):
435 def groups_management(self):
436 c = self.load_default_context()
436 c = self.load_default_context()
437
437
438 user_id = self.request.matchdict.get('user_id')
438 user_id = self.request.matchdict.get('user_id')
439 c.user = User.get_or_404(user_id, pyramid_exc=True)
439 c.user = User.get_or_404(user_id)
440 c.data = c.user.group_member
440 c.data = c.user.group_member
441 self._redirect_for_default_user(c.user.username)
441 self._redirect_for_default_user(c.user.username)
442 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
442 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
443 for group in c.user.group_member]
443 for group in c.user.group_member]
444 c.groups = json.dumps(groups)
444 c.groups = json.dumps(groups)
445 c.active = 'groups'
445 c.active = 'groups'
446
446
447 return self._get_template_context(c)
447 return self._get_template_context(c)
448
448
449 @LoginRequired()
449 @LoginRequired()
450 @HasPermissionAllDecorator('hg.admin')
450 @HasPermissionAllDecorator('hg.admin')
451 @CSRFRequired()
451 @CSRFRequired()
452 @view_config(
452 @view_config(
453 route_name='edit_user_groups_management_updates', request_method='POST')
453 route_name='edit_user_groups_management_updates', request_method='POST')
454 def groups_management_updates(self):
454 def groups_management_updates(self):
455 _ = self.request.translate
455 _ = self.request.translate
456 c = self.load_default_context()
456 c = self.load_default_context()
457
457
458 user_id = self.request.matchdict.get('user_id')
458 user_id = self.request.matchdict.get('user_id')
459 c.user = User.get_or_404(user_id, pyramid_exc=True)
459 c.user = User.get_or_404(user_id)
460 self._redirect_for_default_user(c.user.username)
460 self._redirect_for_default_user(c.user.username)
461
461
462 users_groups = set(self.request.POST.getall('users_group_id'))
462 users_groups = set(self.request.POST.getall('users_group_id'))
463 users_groups_model = []
463 users_groups_model = []
464
464
465 for ugid in users_groups:
465 for ugid in users_groups:
466 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
466 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
467 user_group_model = UserGroupModel()
467 user_group_model = UserGroupModel()
468 user_group_model.change_groups(c.user, users_groups_model)
468 user_group_model.change_groups(c.user, users_groups_model)
469
469
470 Session().commit()
470 Session().commit()
471 c.active = 'user_groups_management'
471 c.active = 'user_groups_management'
472 h.flash(_("Groups successfully changed"), category='success')
472 h.flash(_("Groups successfully changed"), category='success')
473
473
474 return HTTPFound(h.route_path(
474 return HTTPFound(h.route_path(
475 'edit_user_groups_management', user_id=user_id))
475 'edit_user_groups_management', user_id=user_id))
476
476
477 @LoginRequired()
477 @LoginRequired()
478 @HasPermissionAllDecorator('hg.admin')
478 @HasPermissionAllDecorator('hg.admin')
479 @view_config(
479 @view_config(
480 route_name='edit_user_audit_logs', request_method='GET',
480 route_name='edit_user_audit_logs', request_method='GET',
481 renderer='rhodecode:templates/admin/users/user_edit.mako')
481 renderer='rhodecode:templates/admin/users/user_edit.mako')
482 def user_audit_logs(self):
482 def user_audit_logs(self):
483 _ = self.request.translate
483 _ = self.request.translate
484 c = self.load_default_context()
484 c = self.load_default_context()
485
485
486 user_id = self.request.matchdict.get('user_id')
486 user_id = self.request.matchdict.get('user_id')
487 c.user = User.get_or_404(user_id, pyramid_exc=True)
487 c.user = User.get_or_404(user_id)
488 self._redirect_for_default_user(c.user.username)
488 self._redirect_for_default_user(c.user.username)
489 c.active = 'audit'
489 c.active = 'audit'
490
490
491 p = safe_int(self.request.GET.get('page', 1), 1)
491 p = safe_int(self.request.GET.get('page', 1), 1)
492
492
493 filter_term = self.request.GET.get('filter')
493 filter_term = self.request.GET.get('filter')
494 user_log = UserModel().get_user_log(c.user, filter_term)
494 user_log = UserModel().get_user_log(c.user, filter_term)
495
495
496 def url_generator(**kw):
496 def url_generator(**kw):
497 if filter_term:
497 if filter_term:
498 kw['filter'] = filter_term
498 kw['filter'] = filter_term
499 return self.request.current_route_path(_query=kw)
499 return self.request.current_route_path(_query=kw)
500
500
501 c.audit_logs = h.Page(
501 c.audit_logs = h.Page(
502 user_log, page=p, items_per_page=10, url=url_generator)
502 user_log, page=p, items_per_page=10, url=url_generator)
503 c.filter_term = filter_term
503 c.filter_term = filter_term
504 return self._get_template_context(c)
504 return self._get_template_context(c)
505
505
@@ -1,586 +1,586 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23
23
24 import formencode
24 import formencode
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode import forms
31 from rhodecode import forms
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import audit_logger
33 from rhodecode.lib import audit_logger
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.channelstream import channelstream_request, \
36 from rhodecode.lib.channelstream import channelstream_request, \
37 ChannelstreamException
37 ChannelstreamException
38 from rhodecode.lib.utils2 import safe_int, md5, str2bool
38 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 from rhodecode.model.auth_token import AuthTokenModel
39 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.comment import CommentsModel
40 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
42 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
43 PullRequest)
43 PullRequest)
44 from rhodecode.model.forms import UserForm
44 from rhodecode.model.forms import UserForm
45 from rhodecode.model.meta import Session
45 from rhodecode.model.meta import Session
46 from rhodecode.model.pull_request import PullRequestModel
46 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.scm import RepoList
47 from rhodecode.model.scm import RepoList
48 from rhodecode.model.user import UserModel
48 from rhodecode.model.user import UserModel
49 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.validation_schema.schemas import user_schema
50 from rhodecode.model.validation_schema.schemas import user_schema
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MyAccountView(BaseAppView, DataGridAppView):
55 class MyAccountView(BaseAppView, DataGridAppView):
56 ALLOW_SCOPED_TOKENS = False
56 ALLOW_SCOPED_TOKENS = False
57 """
57 """
58 This view has alternative version inside EE, if modified please take a look
58 This view has alternative version inside EE, if modified please take a look
59 in there as well.
59 in there as well.
60 """
60 """
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context()
63 c = self._get_local_tmpl_context()
64 c.user = c.auth_user.get_instance()
64 c.user = c.auth_user.get_instance()
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 self._register_global_c(c)
66 self._register_global_c(c)
67 return c
67 return c
68
68
69 @LoginRequired()
69 @LoginRequired()
70 @NotAnonymous()
70 @NotAnonymous()
71 @view_config(
71 @view_config(
72 route_name='my_account_profile', request_method='GET',
72 route_name='my_account_profile', request_method='GET',
73 renderer='rhodecode:templates/admin/my_account/my_account.mako')
73 renderer='rhodecode:templates/admin/my_account/my_account.mako')
74 def my_account_profile(self):
74 def my_account_profile(self):
75 c = self.load_default_context()
75 c = self.load_default_context()
76 c.active = 'profile'
76 c.active = 'profile'
77 return self._get_template_context(c)
77 return self._get_template_context(c)
78
78
79 @LoginRequired()
79 @LoginRequired()
80 @NotAnonymous()
80 @NotAnonymous()
81 @view_config(
81 @view_config(
82 route_name='my_account_password', request_method='GET',
82 route_name='my_account_password', request_method='GET',
83 renderer='rhodecode:templates/admin/my_account/my_account.mako')
83 renderer='rhodecode:templates/admin/my_account/my_account.mako')
84 def my_account_password(self):
84 def my_account_password(self):
85 c = self.load_default_context()
85 c = self.load_default_context()
86 c.active = 'password'
86 c.active = 'password'
87 c.extern_type = c.user.extern_type
87 c.extern_type = c.user.extern_type
88
88
89 schema = user_schema.ChangePasswordSchema().bind(
89 schema = user_schema.ChangePasswordSchema().bind(
90 username=c.user.username)
90 username=c.user.username)
91
91
92 form = forms.Form(
92 form = forms.Form(
93 schema,
93 schema,
94 action=h.route_path('my_account_password_update'),
94 action=h.route_path('my_account_password_update'),
95 buttons=(forms.buttons.save, forms.buttons.reset))
95 buttons=(forms.buttons.save, forms.buttons.reset))
96
96
97 c.form = form
97 c.form = form
98 return self._get_template_context(c)
98 return self._get_template_context(c)
99
99
100 @LoginRequired()
100 @LoginRequired()
101 @NotAnonymous()
101 @NotAnonymous()
102 @CSRFRequired()
102 @CSRFRequired()
103 @view_config(
103 @view_config(
104 route_name='my_account_password_update', request_method='POST',
104 route_name='my_account_password_update', request_method='POST',
105 renderer='rhodecode:templates/admin/my_account/my_account.mako')
105 renderer='rhodecode:templates/admin/my_account/my_account.mako')
106 def my_account_password_update(self):
106 def my_account_password_update(self):
107 _ = self.request.translate
107 _ = self.request.translate
108 c = self.load_default_context()
108 c = self.load_default_context()
109 c.active = 'password'
109 c.active = 'password'
110 c.extern_type = c.user.extern_type
110 c.extern_type = c.user.extern_type
111
111
112 schema = user_schema.ChangePasswordSchema().bind(
112 schema = user_schema.ChangePasswordSchema().bind(
113 username=c.user.username)
113 username=c.user.username)
114
114
115 form = forms.Form(
115 form = forms.Form(
116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117
117
118 if c.extern_type != 'rhodecode':
118 if c.extern_type != 'rhodecode':
119 raise HTTPFound(self.request.route_path('my_account_password'))
119 raise HTTPFound(self.request.route_path('my_account_password'))
120
120
121 controls = self.request.POST.items()
121 controls = self.request.POST.items()
122 try:
122 try:
123 valid_data = form.validate(controls)
123 valid_data = form.validate(controls)
124 UserModel().update_user(c.user.user_id, **valid_data)
124 UserModel().update_user(c.user.user_id, **valid_data)
125 c.user.update_userdata(force_password_change=False)
125 c.user.update_userdata(force_password_change=False)
126 Session().commit()
126 Session().commit()
127 except forms.ValidationFailure as e:
127 except forms.ValidationFailure as e:
128 c.form = e
128 c.form = e
129 return self._get_template_context(c)
129 return self._get_template_context(c)
130
130
131 except Exception:
131 except Exception:
132 log.exception("Exception updating password")
132 log.exception("Exception updating password")
133 h.flash(_('Error occurred during update of user password'),
133 h.flash(_('Error occurred during update of user password'),
134 category='error')
134 category='error')
135 else:
135 else:
136 instance = c.auth_user.get_instance()
136 instance = c.auth_user.get_instance()
137 self.session.setdefault('rhodecode_user', {}).update(
137 self.session.setdefault('rhodecode_user', {}).update(
138 {'password': md5(instance.password)})
138 {'password': md5(instance.password)})
139 self.session.save()
139 self.session.save()
140 h.flash(_("Successfully updated password"), category='success')
140 h.flash(_("Successfully updated password"), category='success')
141
141
142 raise HTTPFound(self.request.route_path('my_account_password'))
142 raise HTTPFound(self.request.route_path('my_account_password'))
143
143
144 @LoginRequired()
144 @LoginRequired()
145 @NotAnonymous()
145 @NotAnonymous()
146 @view_config(
146 @view_config(
147 route_name='my_account_auth_tokens', request_method='GET',
147 route_name='my_account_auth_tokens', request_method='GET',
148 renderer='rhodecode:templates/admin/my_account/my_account.mako')
148 renderer='rhodecode:templates/admin/my_account/my_account.mako')
149 def my_account_auth_tokens(self):
149 def my_account_auth_tokens(self):
150 _ = self.request.translate
150 _ = self.request.translate
151
151
152 c = self.load_default_context()
152 c = self.load_default_context()
153 c.active = 'auth_tokens'
153 c.active = 'auth_tokens'
154
154
155 c.lifetime_values = [
155 c.lifetime_values = [
156 (str(-1), _('forever')),
156 (str(-1), _('forever')),
157 (str(5), _('5 minutes')),
157 (str(5), _('5 minutes')),
158 (str(60), _('1 hour')),
158 (str(60), _('1 hour')),
159 (str(60 * 24), _('1 day')),
159 (str(60 * 24), _('1 day')),
160 (str(60 * 24 * 30), _('1 month')),
160 (str(60 * 24 * 30), _('1 month')),
161 ]
161 ]
162 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
162 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
163 c.role_values = [
163 c.role_values = [
164 (x, AuthTokenModel.cls._get_role_name(x))
164 (x, AuthTokenModel.cls._get_role_name(x))
165 for x in AuthTokenModel.cls.ROLES]
165 for x in AuthTokenModel.cls.ROLES]
166 c.role_options = [(c.role_values, _("Role"))]
166 c.role_options = [(c.role_values, _("Role"))]
167 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
167 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
168 c.user.user_id, show_expired=True)
168 c.user.user_id, show_expired=True)
169 return self._get_template_context(c)
169 return self._get_template_context(c)
170
170
171 def maybe_attach_token_scope(self, token):
171 def maybe_attach_token_scope(self, token):
172 # implemented in EE edition
172 # implemented in EE edition
173 pass
173 pass
174
174
175 @LoginRequired()
175 @LoginRequired()
176 @NotAnonymous()
176 @NotAnonymous()
177 @CSRFRequired()
177 @CSRFRequired()
178 @view_config(
178 @view_config(
179 route_name='my_account_auth_tokens_add', request_method='POST',)
179 route_name='my_account_auth_tokens_add', request_method='POST',)
180 def my_account_auth_tokens_add(self):
180 def my_account_auth_tokens_add(self):
181 _ = self.request.translate
181 _ = self.request.translate
182 c = self.load_default_context()
182 c = self.load_default_context()
183
183
184 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
184 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
185 description = self.request.POST.get('description')
185 description = self.request.POST.get('description')
186 role = self.request.POST.get('role')
186 role = self.request.POST.get('role')
187
187
188 token = AuthTokenModel().create(
188 token = AuthTokenModel().create(
189 c.user.user_id, description, lifetime, role)
189 c.user.user_id, description, lifetime, role)
190 token_data = token.get_api_data()
190 token_data = token.get_api_data()
191
191
192 self.maybe_attach_token_scope(token)
192 self.maybe_attach_token_scope(token)
193 audit_logger.store_web(
193 audit_logger.store_web(
194 'user.edit.token.add', action_data={
194 'user.edit.token.add', action_data={
195 'data': {'token': token_data, 'user': 'self'}},
195 'data': {'token': token_data, 'user': 'self'}},
196 user=self._rhodecode_user, )
196 user=self._rhodecode_user, )
197 Session().commit()
197 Session().commit()
198
198
199 h.flash(_("Auth token successfully created"), category='success')
199 h.flash(_("Auth token successfully created"), category='success')
200 return HTTPFound(h.route_path('my_account_auth_tokens'))
200 return HTTPFound(h.route_path('my_account_auth_tokens'))
201
201
202 @LoginRequired()
202 @LoginRequired()
203 @NotAnonymous()
203 @NotAnonymous()
204 @CSRFRequired()
204 @CSRFRequired()
205 @view_config(
205 @view_config(
206 route_name='my_account_auth_tokens_delete', request_method='POST')
206 route_name='my_account_auth_tokens_delete', request_method='POST')
207 def my_account_auth_tokens_delete(self):
207 def my_account_auth_tokens_delete(self):
208 _ = self.request.translate
208 _ = self.request.translate
209 c = self.load_default_context()
209 c = self.load_default_context()
210
210
211 del_auth_token = self.request.POST.get('del_auth_token')
211 del_auth_token = self.request.POST.get('del_auth_token')
212
212
213 if del_auth_token:
213 if del_auth_token:
214 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
214 token = UserApiKeys.get_or_404(del_auth_token)
215 token_data = token.get_api_data()
215 token_data = token.get_api_data()
216
216
217 AuthTokenModel().delete(del_auth_token, c.user.user_id)
217 AuthTokenModel().delete(del_auth_token, c.user.user_id)
218 audit_logger.store_web(
218 audit_logger.store_web(
219 'user.edit.token.delete', action_data={
219 'user.edit.token.delete', action_data={
220 'data': {'token': token_data, 'user': 'self'}},
220 'data': {'token': token_data, 'user': 'self'}},
221 user=self._rhodecode_user,)
221 user=self._rhodecode_user,)
222 Session().commit()
222 Session().commit()
223 h.flash(_("Auth token successfully deleted"), category='success')
223 h.flash(_("Auth token successfully deleted"), category='success')
224
224
225 return HTTPFound(h.route_path('my_account_auth_tokens'))
225 return HTTPFound(h.route_path('my_account_auth_tokens'))
226
226
227 @LoginRequired()
227 @LoginRequired()
228 @NotAnonymous()
228 @NotAnonymous()
229 @view_config(
229 @view_config(
230 route_name='my_account_emails', request_method='GET',
230 route_name='my_account_emails', request_method='GET',
231 renderer='rhodecode:templates/admin/my_account/my_account.mako')
231 renderer='rhodecode:templates/admin/my_account/my_account.mako')
232 def my_account_emails(self):
232 def my_account_emails(self):
233 _ = self.request.translate
233 _ = self.request.translate
234
234
235 c = self.load_default_context()
235 c = self.load_default_context()
236 c.active = 'emails'
236 c.active = 'emails'
237
237
238 c.user_email_map = UserEmailMap.query()\
238 c.user_email_map = UserEmailMap.query()\
239 .filter(UserEmailMap.user == c.user).all()
239 .filter(UserEmailMap.user == c.user).all()
240 return self._get_template_context(c)
240 return self._get_template_context(c)
241
241
242 @LoginRequired()
242 @LoginRequired()
243 @NotAnonymous()
243 @NotAnonymous()
244 @CSRFRequired()
244 @CSRFRequired()
245 @view_config(
245 @view_config(
246 route_name='my_account_emails_add', request_method='POST')
246 route_name='my_account_emails_add', request_method='POST')
247 def my_account_emails_add(self):
247 def my_account_emails_add(self):
248 _ = self.request.translate
248 _ = self.request.translate
249 c = self.load_default_context()
249 c = self.load_default_context()
250
250
251 email = self.request.POST.get('new_email')
251 email = self.request.POST.get('new_email')
252
252
253 try:
253 try:
254 UserModel().add_extra_email(c.user.user_id, email)
254 UserModel().add_extra_email(c.user.user_id, email)
255 audit_logger.store_web(
255 audit_logger.store_web(
256 'user.edit.email.add', action_data={
256 'user.edit.email.add', action_data={
257 'data': {'email': email, 'user': 'self'}},
257 'data': {'email': email, 'user': 'self'}},
258 user=self._rhodecode_user,)
258 user=self._rhodecode_user,)
259
259
260 Session().commit()
260 Session().commit()
261 h.flash(_("Added new email address `%s` for user account") % email,
261 h.flash(_("Added new email address `%s` for user account") % email,
262 category='success')
262 category='success')
263 except formencode.Invalid as error:
263 except formencode.Invalid as error:
264 h.flash(h.escape(error.error_dict['email']), category='error')
264 h.flash(h.escape(error.error_dict['email']), category='error')
265 except Exception:
265 except Exception:
266 log.exception("Exception in my_account_emails")
266 log.exception("Exception in my_account_emails")
267 h.flash(_('An error occurred during email saving'),
267 h.flash(_('An error occurred during email saving'),
268 category='error')
268 category='error')
269 return HTTPFound(h.route_path('my_account_emails'))
269 return HTTPFound(h.route_path('my_account_emails'))
270
270
271 @LoginRequired()
271 @LoginRequired()
272 @NotAnonymous()
272 @NotAnonymous()
273 @CSRFRequired()
273 @CSRFRequired()
274 @view_config(
274 @view_config(
275 route_name='my_account_emails_delete', request_method='POST')
275 route_name='my_account_emails_delete', request_method='POST')
276 def my_account_emails_delete(self):
276 def my_account_emails_delete(self):
277 _ = self.request.translate
277 _ = self.request.translate
278 c = self.load_default_context()
278 c = self.load_default_context()
279
279
280 del_email_id = self.request.POST.get('del_email_id')
280 del_email_id = self.request.POST.get('del_email_id')
281 if del_email_id:
281 if del_email_id:
282 email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email
282 email = UserEmailMap.get_or_404(del_email_id).email
283 UserModel().delete_extra_email(c.user.user_id, del_email_id)
283 UserModel().delete_extra_email(c.user.user_id, del_email_id)
284 audit_logger.store_web(
284 audit_logger.store_web(
285 'user.edit.email.delete', action_data={
285 'user.edit.email.delete', action_data={
286 'data': {'email': email, 'user': 'self'}},
286 'data': {'email': email, 'user': 'self'}},
287 user=self._rhodecode_user,)
287 user=self._rhodecode_user,)
288 Session().commit()
288 Session().commit()
289 h.flash(_("Email successfully deleted"),
289 h.flash(_("Email successfully deleted"),
290 category='success')
290 category='success')
291 return HTTPFound(h.route_path('my_account_emails'))
291 return HTTPFound(h.route_path('my_account_emails'))
292
292
293 @LoginRequired()
293 @LoginRequired()
294 @NotAnonymous()
294 @NotAnonymous()
295 @CSRFRequired()
295 @CSRFRequired()
296 @view_config(
296 @view_config(
297 route_name='my_account_notifications_test_channelstream',
297 route_name='my_account_notifications_test_channelstream',
298 request_method='POST', renderer='json_ext')
298 request_method='POST', renderer='json_ext')
299 def my_account_notifications_test_channelstream(self):
299 def my_account_notifications_test_channelstream(self):
300 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
300 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
301 self._rhodecode_user.username, datetime.datetime.now())
301 self._rhodecode_user.username, datetime.datetime.now())
302 payload = {
302 payload = {
303 # 'channel': 'broadcast',
303 # 'channel': 'broadcast',
304 'type': 'message',
304 'type': 'message',
305 'timestamp': datetime.datetime.utcnow(),
305 'timestamp': datetime.datetime.utcnow(),
306 'user': 'system',
306 'user': 'system',
307 'pm_users': [self._rhodecode_user.username],
307 'pm_users': [self._rhodecode_user.username],
308 'message': {
308 'message': {
309 'message': message,
309 'message': message,
310 'level': 'info',
310 'level': 'info',
311 'topic': '/notifications'
311 'topic': '/notifications'
312 }
312 }
313 }
313 }
314
314
315 registry = self.request.registry
315 registry = self.request.registry
316 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
316 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
317 channelstream_config = rhodecode_plugins.get('channelstream', {})
317 channelstream_config = rhodecode_plugins.get('channelstream', {})
318
318
319 try:
319 try:
320 channelstream_request(channelstream_config, [payload], '/message')
320 channelstream_request(channelstream_config, [payload], '/message')
321 except ChannelstreamException as e:
321 except ChannelstreamException as e:
322 log.exception('Failed to send channelstream data')
322 log.exception('Failed to send channelstream data')
323 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
323 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
324 return {"response": 'Channelstream data sent. '
324 return {"response": 'Channelstream data sent. '
325 'You should see a new live message now.'}
325 'You should see a new live message now.'}
326
326
327 def _load_my_repos_data(self, watched=False):
327 def _load_my_repos_data(self, watched=False):
328 if watched:
328 if watched:
329 admin = False
329 admin = False
330 follows_repos = Session().query(UserFollowing)\
330 follows_repos = Session().query(UserFollowing)\
331 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
331 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
332 .options(joinedload(UserFollowing.follows_repository))\
332 .options(joinedload(UserFollowing.follows_repository))\
333 .all()
333 .all()
334 repo_list = [x.follows_repository for x in follows_repos]
334 repo_list = [x.follows_repository for x in follows_repos]
335 else:
335 else:
336 admin = True
336 admin = True
337 repo_list = Repository.get_all_repos(
337 repo_list = Repository.get_all_repos(
338 user_id=self._rhodecode_user.user_id)
338 user_id=self._rhodecode_user.user_id)
339 repo_list = RepoList(repo_list, perm_set=[
339 repo_list = RepoList(repo_list, perm_set=[
340 'repository.read', 'repository.write', 'repository.admin'])
340 'repository.read', 'repository.write', 'repository.admin'])
341
341
342 repos_data = RepoModel().get_repos_as_dict(
342 repos_data = RepoModel().get_repos_as_dict(
343 repo_list=repo_list, admin=admin)
343 repo_list=repo_list, admin=admin)
344 # json used to render the grid
344 # json used to render the grid
345 return json.dumps(repos_data)
345 return json.dumps(repos_data)
346
346
347 @LoginRequired()
347 @LoginRequired()
348 @NotAnonymous()
348 @NotAnonymous()
349 @view_config(
349 @view_config(
350 route_name='my_account_repos', request_method='GET',
350 route_name='my_account_repos', request_method='GET',
351 renderer='rhodecode:templates/admin/my_account/my_account.mako')
351 renderer='rhodecode:templates/admin/my_account/my_account.mako')
352 def my_account_repos(self):
352 def my_account_repos(self):
353 c = self.load_default_context()
353 c = self.load_default_context()
354 c.active = 'repos'
354 c.active = 'repos'
355
355
356 # json used to render the grid
356 # json used to render the grid
357 c.data = self._load_my_repos_data()
357 c.data = self._load_my_repos_data()
358 return self._get_template_context(c)
358 return self._get_template_context(c)
359
359
360 @LoginRequired()
360 @LoginRequired()
361 @NotAnonymous()
361 @NotAnonymous()
362 @view_config(
362 @view_config(
363 route_name='my_account_watched', request_method='GET',
363 route_name='my_account_watched', request_method='GET',
364 renderer='rhodecode:templates/admin/my_account/my_account.mako')
364 renderer='rhodecode:templates/admin/my_account/my_account.mako')
365 def my_account_watched(self):
365 def my_account_watched(self):
366 c = self.load_default_context()
366 c = self.load_default_context()
367 c.active = 'watched'
367 c.active = 'watched'
368
368
369 # json used to render the grid
369 # json used to render the grid
370 c.data = self._load_my_repos_data(watched=True)
370 c.data = self._load_my_repos_data(watched=True)
371 return self._get_template_context(c)
371 return self._get_template_context(c)
372
372
373 @LoginRequired()
373 @LoginRequired()
374 @NotAnonymous()
374 @NotAnonymous()
375 @view_config(
375 @view_config(
376 route_name='my_account_perms', request_method='GET',
376 route_name='my_account_perms', request_method='GET',
377 renderer='rhodecode:templates/admin/my_account/my_account.mako')
377 renderer='rhodecode:templates/admin/my_account/my_account.mako')
378 def my_account_perms(self):
378 def my_account_perms(self):
379 c = self.load_default_context()
379 c = self.load_default_context()
380 c.active = 'perms'
380 c.active = 'perms'
381
381
382 c.perm_user = c.auth_user
382 c.perm_user = c.auth_user
383 return self._get_template_context(c)
383 return self._get_template_context(c)
384
384
385 @LoginRequired()
385 @LoginRequired()
386 @NotAnonymous()
386 @NotAnonymous()
387 @view_config(
387 @view_config(
388 route_name='my_account_notifications', request_method='GET',
388 route_name='my_account_notifications', request_method='GET',
389 renderer='rhodecode:templates/admin/my_account/my_account.mako')
389 renderer='rhodecode:templates/admin/my_account/my_account.mako')
390 def my_notifications(self):
390 def my_notifications(self):
391 c = self.load_default_context()
391 c = self.load_default_context()
392 c.active = 'notifications'
392 c.active = 'notifications'
393
393
394 return self._get_template_context(c)
394 return self._get_template_context(c)
395
395
396 @LoginRequired()
396 @LoginRequired()
397 @NotAnonymous()
397 @NotAnonymous()
398 @CSRFRequired()
398 @CSRFRequired()
399 @view_config(
399 @view_config(
400 route_name='my_account_notifications_toggle_visibility',
400 route_name='my_account_notifications_toggle_visibility',
401 request_method='POST', renderer='json_ext')
401 request_method='POST', renderer='json_ext')
402 def my_notifications_toggle_visibility(self):
402 def my_notifications_toggle_visibility(self):
403 user = self._rhodecode_db_user
403 user = self._rhodecode_db_user
404 new_status = not user.user_data.get('notification_status', True)
404 new_status = not user.user_data.get('notification_status', True)
405 user.update_userdata(notification_status=new_status)
405 user.update_userdata(notification_status=new_status)
406 Session().commit()
406 Session().commit()
407 return user.user_data['notification_status']
407 return user.user_data['notification_status']
408
408
409 @LoginRequired()
409 @LoginRequired()
410 @NotAnonymous()
410 @NotAnonymous()
411 @view_config(
411 @view_config(
412 route_name='my_account_edit',
412 route_name='my_account_edit',
413 request_method='GET',
413 request_method='GET',
414 renderer='rhodecode:templates/admin/my_account/my_account.mako')
414 renderer='rhodecode:templates/admin/my_account/my_account.mako')
415 def my_account_edit(self):
415 def my_account_edit(self):
416 c = self.load_default_context()
416 c = self.load_default_context()
417 c.active = 'profile_edit'
417 c.active = 'profile_edit'
418
418
419 c.perm_user = c.auth_user
419 c.perm_user = c.auth_user
420 c.extern_type = c.user.extern_type
420 c.extern_type = c.user.extern_type
421 c.extern_name = c.user.extern_name
421 c.extern_name = c.user.extern_name
422
422
423 defaults = c.user.get_dict()
423 defaults = c.user.get_dict()
424
424
425 data = render('rhodecode:templates/admin/my_account/my_account.mako',
425 data = render('rhodecode:templates/admin/my_account/my_account.mako',
426 self._get_template_context(c), self.request)
426 self._get_template_context(c), self.request)
427 html = formencode.htmlfill.render(
427 html = formencode.htmlfill.render(
428 data,
428 data,
429 defaults=defaults,
429 defaults=defaults,
430 encoding="UTF-8",
430 encoding="UTF-8",
431 force_defaults=False
431 force_defaults=False
432 )
432 )
433 return Response(html)
433 return Response(html)
434
434
435 @LoginRequired()
435 @LoginRequired()
436 @NotAnonymous()
436 @NotAnonymous()
437 @CSRFRequired()
437 @CSRFRequired()
438 @view_config(
438 @view_config(
439 route_name='my_account_update',
439 route_name='my_account_update',
440 request_method='POST',
440 request_method='POST',
441 renderer='rhodecode:templates/admin/my_account/my_account.mako')
441 renderer='rhodecode:templates/admin/my_account/my_account.mako')
442 def my_account_update(self):
442 def my_account_update(self):
443 _ = self.request.translate
443 _ = self.request.translate
444 c = self.load_default_context()
444 c = self.load_default_context()
445 c.active = 'profile_edit'
445 c.active = 'profile_edit'
446
446
447 c.perm_user = c.auth_user
447 c.perm_user = c.auth_user
448 c.extern_type = c.user.extern_type
448 c.extern_type = c.user.extern_type
449 c.extern_name = c.user.extern_name
449 c.extern_name = c.user.extern_name
450
450
451 _form = UserForm(edit=True,
451 _form = UserForm(edit=True,
452 old_data={'user_id': self._rhodecode_user.user_id,
452 old_data={'user_id': self._rhodecode_user.user_id,
453 'email': self._rhodecode_user.email})()
453 'email': self._rhodecode_user.email})()
454 form_result = {}
454 form_result = {}
455 try:
455 try:
456 post_data = dict(self.request.POST)
456 post_data = dict(self.request.POST)
457 post_data['new_password'] = ''
457 post_data['new_password'] = ''
458 post_data['password_confirmation'] = ''
458 post_data['password_confirmation'] = ''
459 form_result = _form.to_python(post_data)
459 form_result = _form.to_python(post_data)
460 # skip updating those attrs for my account
460 # skip updating those attrs for my account
461 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
461 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
462 'new_password', 'password_confirmation']
462 'new_password', 'password_confirmation']
463 # TODO: plugin should define if username can be updated
463 # TODO: plugin should define if username can be updated
464 if c.extern_type != "rhodecode":
464 if c.extern_type != "rhodecode":
465 # forbid updating username for external accounts
465 # forbid updating username for external accounts
466 skip_attrs.append('username')
466 skip_attrs.append('username')
467
467
468 UserModel().update_user(
468 UserModel().update_user(
469 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
469 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
470 **form_result)
470 **form_result)
471 h.flash(_('Your account was updated successfully'),
471 h.flash(_('Your account was updated successfully'),
472 category='success')
472 category='success')
473 Session().commit()
473 Session().commit()
474
474
475 except formencode.Invalid as errors:
475 except formencode.Invalid as errors:
476 data = render(
476 data = render(
477 'rhodecode:templates/admin/my_account/my_account.mako',
477 'rhodecode:templates/admin/my_account/my_account.mako',
478 self._get_template_context(c), self.request)
478 self._get_template_context(c), self.request)
479
479
480 html = formencode.htmlfill.render(
480 html = formencode.htmlfill.render(
481 data,
481 data,
482 defaults=errors.value,
482 defaults=errors.value,
483 errors=errors.error_dict or {},
483 errors=errors.error_dict or {},
484 prefix_error=False,
484 prefix_error=False,
485 encoding="UTF-8",
485 encoding="UTF-8",
486 force_defaults=False)
486 force_defaults=False)
487 return Response(html)
487 return Response(html)
488
488
489 except Exception:
489 except Exception:
490 log.exception("Exception updating user")
490 log.exception("Exception updating user")
491 h.flash(_('Error occurred during update of user %s')
491 h.flash(_('Error occurred during update of user %s')
492 % form_result.get('username'), category='error')
492 % form_result.get('username'), category='error')
493 raise HTTPFound(h.route_path('my_account_profile'))
493 raise HTTPFound(h.route_path('my_account_profile'))
494
494
495 raise HTTPFound(h.route_path('my_account_profile'))
495 raise HTTPFound(h.route_path('my_account_profile'))
496
496
497 def _get_pull_requests_list(self, statuses):
497 def _get_pull_requests_list(self, statuses):
498 draw, start, limit = self._extract_chunk(self.request)
498 draw, start, limit = self._extract_chunk(self.request)
499 search_q, order_by, order_dir = self._extract_ordering(self.request)
499 search_q, order_by, order_dir = self._extract_ordering(self.request)
500 _render = self.request.get_partial_renderer(
500 _render = self.request.get_partial_renderer(
501 'data_table/_dt_elements.mako')
501 'data_table/_dt_elements.mako')
502
502
503 pull_requests = PullRequestModel().get_im_participating_in(
503 pull_requests = PullRequestModel().get_im_participating_in(
504 user_id=self._rhodecode_user.user_id,
504 user_id=self._rhodecode_user.user_id,
505 statuses=statuses,
505 statuses=statuses,
506 offset=start, length=limit, order_by=order_by,
506 offset=start, length=limit, order_by=order_by,
507 order_dir=order_dir)
507 order_dir=order_dir)
508
508
509 pull_requests_total_count = PullRequestModel().count_im_participating_in(
509 pull_requests_total_count = PullRequestModel().count_im_participating_in(
510 user_id=self._rhodecode_user.user_id, statuses=statuses)
510 user_id=self._rhodecode_user.user_id, statuses=statuses)
511
511
512 data = []
512 data = []
513 comments_model = CommentsModel()
513 comments_model = CommentsModel()
514 for pr in pull_requests:
514 for pr in pull_requests:
515 repo_id = pr.target_repo_id
515 repo_id = pr.target_repo_id
516 comments = comments_model.get_all_comments(
516 comments = comments_model.get_all_comments(
517 repo_id, pull_request=pr)
517 repo_id, pull_request=pr)
518 owned = pr.user_id == self._rhodecode_user.user_id
518 owned = pr.user_id == self._rhodecode_user.user_id
519
519
520 data.append({
520 data.append({
521 'target_repo': _render('pullrequest_target_repo',
521 'target_repo': _render('pullrequest_target_repo',
522 pr.target_repo.repo_name),
522 pr.target_repo.repo_name),
523 'name': _render('pullrequest_name',
523 'name': _render('pullrequest_name',
524 pr.pull_request_id, pr.target_repo.repo_name,
524 pr.pull_request_id, pr.target_repo.repo_name,
525 short=True),
525 short=True),
526 'name_raw': pr.pull_request_id,
526 'name_raw': pr.pull_request_id,
527 'status': _render('pullrequest_status',
527 'status': _render('pullrequest_status',
528 pr.calculated_review_status()),
528 pr.calculated_review_status()),
529 'title': _render(
529 'title': _render(
530 'pullrequest_title', pr.title, pr.description),
530 'pullrequest_title', pr.title, pr.description),
531 'description': h.escape(pr.description),
531 'description': h.escape(pr.description),
532 'updated_on': _render('pullrequest_updated_on',
532 'updated_on': _render('pullrequest_updated_on',
533 h.datetime_to_time(pr.updated_on)),
533 h.datetime_to_time(pr.updated_on)),
534 'updated_on_raw': h.datetime_to_time(pr.updated_on),
534 'updated_on_raw': h.datetime_to_time(pr.updated_on),
535 'created_on': _render('pullrequest_updated_on',
535 'created_on': _render('pullrequest_updated_on',
536 h.datetime_to_time(pr.created_on)),
536 h.datetime_to_time(pr.created_on)),
537 'created_on_raw': h.datetime_to_time(pr.created_on),
537 'created_on_raw': h.datetime_to_time(pr.created_on),
538 'author': _render('pullrequest_author',
538 'author': _render('pullrequest_author',
539 pr.author.full_contact, ),
539 pr.author.full_contact, ),
540 'author_raw': pr.author.full_name,
540 'author_raw': pr.author.full_name,
541 'comments': _render('pullrequest_comments', len(comments)),
541 'comments': _render('pullrequest_comments', len(comments)),
542 'comments_raw': len(comments),
542 'comments_raw': len(comments),
543 'closed': pr.is_closed(),
543 'closed': pr.is_closed(),
544 'owned': owned
544 'owned': owned
545 })
545 })
546
546
547 # json used to render the grid
547 # json used to render the grid
548 data = ({
548 data = ({
549 'draw': draw,
549 'draw': draw,
550 'data': data,
550 'data': data,
551 'recordsTotal': pull_requests_total_count,
551 'recordsTotal': pull_requests_total_count,
552 'recordsFiltered': pull_requests_total_count,
552 'recordsFiltered': pull_requests_total_count,
553 })
553 })
554 return data
554 return data
555
555
556 @LoginRequired()
556 @LoginRequired()
557 @NotAnonymous()
557 @NotAnonymous()
558 @view_config(
558 @view_config(
559 route_name='my_account_pullrequests',
559 route_name='my_account_pullrequests',
560 request_method='GET',
560 request_method='GET',
561 renderer='rhodecode:templates/admin/my_account/my_account.mako')
561 renderer='rhodecode:templates/admin/my_account/my_account.mako')
562 def my_account_pullrequests(self):
562 def my_account_pullrequests(self):
563 c = self.load_default_context()
563 c = self.load_default_context()
564 c.active = 'pullrequests'
564 c.active = 'pullrequests'
565 req_get = self.request.GET
565 req_get = self.request.GET
566
566
567 c.closed = str2bool(req_get.get('pr_show_closed'))
567 c.closed = str2bool(req_get.get('pr_show_closed'))
568
568
569 return self._get_template_context(c)
569 return self._get_template_context(c)
570
570
571 @LoginRequired()
571 @LoginRequired()
572 @NotAnonymous()
572 @NotAnonymous()
573 @view_config(
573 @view_config(
574 route_name='my_account_pullrequests_data',
574 route_name='my_account_pullrequests_data',
575 request_method='GET', renderer='json_ext')
575 request_method='GET', renderer='json_ext')
576 def my_account_pullrequests_data(self):
576 def my_account_pullrequests_data(self):
577 req_get = self.request.GET
577 req_get = self.request.GET
578 closed = str2bool(req_get.get('closed'))
578 closed = str2bool(req_get.get('closed'))
579
579
580 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
580 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
581 if closed:
581 if closed:
582 statuses += [PullRequest.STATUS_CLOSED]
582 statuses += [PullRequest.STATUS_CLOSED]
583
583
584 data = self._get_pull_requests_list(statuses=statuses)
584 data = self._get_pull_requests_list(statuses=statuses)
585 return data
585 return data
586
586
@@ -1,557 +1,557 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31
31
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35
35
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 import rhodecode.lib.helpers as h
38 import rhodecode.lib.helpers as h
39 from rhodecode.lib.utils2 import safe_unicode, safe_int
39 from rhodecode.lib.utils2 import safe_unicode, safe_int
40 from rhodecode.lib.vcs.backends.base import EmptyCommit
40 from rhodecode.lib.vcs.backends.base import EmptyCommit
41 from rhodecode.lib.vcs.exceptions import (
41 from rhodecode.lib.vcs.exceptions import (
42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
44 from rhodecode.model.changeset_status import ChangesetStatusModel
44 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.comment import CommentsModel
45 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 def _update_with_GET(params, request):
52 def _update_with_GET(params, request):
53 for k in ['diff1', 'diff2', 'diff']:
53 for k in ['diff1', 'diff2', 'diff']:
54 params[k] += request.GET.getall(k)
54 params[k] += request.GET.getall(k)
55
55
56
56
57 def get_ignore_ws(fid, request):
57 def get_ignore_ws(fid, request):
58 ig_ws_global = request.GET.get('ignorews')
58 ig_ws_global = request.GET.get('ignorews')
59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
60 if ig_ws:
60 if ig_ws:
61 try:
61 try:
62 return int(ig_ws[0].split(':')[-1])
62 return int(ig_ws[0].split(':')[-1])
63 except Exception:
63 except Exception:
64 pass
64 pass
65 return ig_ws_global
65 return ig_ws_global
66
66
67
67
68 def _ignorews_url(request, fileid=None):
68 def _ignorews_url(request, fileid=None):
69 _ = request.translate
69 _ = request.translate
70 fileid = str(fileid) if fileid else None
70 fileid = str(fileid) if fileid else None
71 params = collections.defaultdict(list)
71 params = collections.defaultdict(list)
72 _update_with_GET(params, request)
72 _update_with_GET(params, request)
73 label = _('Show whitespace')
73 label = _('Show whitespace')
74 tooltiplbl = _('Show whitespace for all diffs')
74 tooltiplbl = _('Show whitespace for all diffs')
75 ig_ws = get_ignore_ws(fileid, request)
75 ig_ws = get_ignore_ws(fileid, request)
76 ln_ctx = get_line_ctx(fileid, request)
76 ln_ctx = get_line_ctx(fileid, request)
77
77
78 if ig_ws is None:
78 if ig_ws is None:
79 params['ignorews'] += [1]
79 params['ignorews'] += [1]
80 label = _('Ignore whitespace')
80 label = _('Ignore whitespace')
81 tooltiplbl = _('Ignore whitespace for all diffs')
81 tooltiplbl = _('Ignore whitespace for all diffs')
82 ctx_key = 'context'
82 ctx_key = 'context'
83 ctx_val = ln_ctx
83 ctx_val = ln_ctx
84
84
85 # if we have passed in ln_ctx pass it along to our params
85 # if we have passed in ln_ctx pass it along to our params
86 if ln_ctx:
86 if ln_ctx:
87 params[ctx_key] += [ctx_val]
87 params[ctx_key] += [ctx_val]
88
88
89 if fileid:
89 if fileid:
90 params['anchor'] = 'a_' + fileid
90 params['anchor'] = 'a_' + fileid
91 return h.link_to(label, request.current_route_path(_query=params),
91 return h.link_to(label, request.current_route_path(_query=params),
92 title=tooltiplbl, class_='tooltip')
92 title=tooltiplbl, class_='tooltip')
93
93
94
94
95 def get_line_ctx(fid, request):
95 def get_line_ctx(fid, request):
96 ln_ctx_global = request.GET.get('context')
96 ln_ctx_global = request.GET.get('context')
97 if fid:
97 if fid:
98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
99 else:
99 else:
100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
102 if ln_ctx:
102 if ln_ctx:
103 ln_ctx = [ln_ctx]
103 ln_ctx = [ln_ctx]
104
104
105 if ln_ctx:
105 if ln_ctx:
106 retval = ln_ctx[0].split(':')[-1]
106 retval = ln_ctx[0].split(':')[-1]
107 else:
107 else:
108 retval = ln_ctx_global
108 retval = ln_ctx_global
109
109
110 try:
110 try:
111 return int(retval)
111 return int(retval)
112 except Exception:
112 except Exception:
113 return 3
113 return 3
114
114
115
115
116 def _context_url(request, fileid=None):
116 def _context_url(request, fileid=None):
117 """
117 """
118 Generates a url for context lines.
118 Generates a url for context lines.
119
119
120 :param fileid:
120 :param fileid:
121 """
121 """
122
122
123 _ = request.translate
123 _ = request.translate
124 fileid = str(fileid) if fileid else None
124 fileid = str(fileid) if fileid else None
125 ig_ws = get_ignore_ws(fileid, request)
125 ig_ws = get_ignore_ws(fileid, request)
126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
127
127
128 params = collections.defaultdict(list)
128 params = collections.defaultdict(list)
129 _update_with_GET(params, request)
129 _update_with_GET(params, request)
130
130
131 if ln_ctx > 0:
131 if ln_ctx > 0:
132 params['context'] += [ln_ctx]
132 params['context'] += [ln_ctx]
133
133
134 if ig_ws:
134 if ig_ws:
135 ig_ws_key = 'ignorews'
135 ig_ws_key = 'ignorews'
136 ig_ws_val = 1
136 ig_ws_val = 1
137 params[ig_ws_key] += [ig_ws_val]
137 params[ig_ws_key] += [ig_ws_val]
138
138
139 lbl = _('Increase context')
139 lbl = _('Increase context')
140 tooltiplbl = _('Increase context for all diffs')
140 tooltiplbl = _('Increase context for all diffs')
141
141
142 if fileid:
142 if fileid:
143 params['anchor'] = 'a_' + fileid
143 params['anchor'] = 'a_' + fileid
144 return h.link_to(lbl, request.current_route_path(_query=params),
144 return h.link_to(lbl, request.current_route_path(_query=params),
145 title=tooltiplbl, class_='tooltip')
145 title=tooltiplbl, class_='tooltip')
146
146
147
147
148 class RepoCommitsView(RepoAppView):
148 class RepoCommitsView(RepoAppView):
149 def load_default_context(self):
149 def load_default_context(self):
150 c = self._get_local_tmpl_context(include_app_defaults=True)
150 c = self._get_local_tmpl_context(include_app_defaults=True)
151
151
152 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
152 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
153 c.repo_info = self.db_repo
153 c.repo_info = self.db_repo
154 c.rhodecode_repo = self.rhodecode_vcs_repo
154 c.rhodecode_repo = self.rhodecode_vcs_repo
155
155
156 self._register_global_c(c)
156 self._register_global_c(c)
157 return c
157 return c
158
158
159 def _commit(self, commit_id_range, method):
159 def _commit(self, commit_id_range, method):
160 _ = self.request.translate
160 _ = self.request.translate
161 c = self.load_default_context()
161 c = self.load_default_context()
162 c.ignorews_url = _ignorews_url
162 c.ignorews_url = _ignorews_url
163 c.context_url = _context_url
163 c.context_url = _context_url
164 c.fulldiff = self.request.GET.get('fulldiff')
164 c.fulldiff = self.request.GET.get('fulldiff')
165
165
166 # fetch global flags of ignore ws or context lines
166 # fetch global flags of ignore ws or context lines
167 context_lcl = get_line_ctx('', self.request)
167 context_lcl = get_line_ctx('', self.request)
168 ign_whitespace_lcl = get_ignore_ws('', self.request)
168 ign_whitespace_lcl = get_ignore_ws('', self.request)
169
169
170 # diff_limit will cut off the whole diff if the limit is applied
170 # diff_limit will cut off the whole diff if the limit is applied
171 # otherwise it will just hide the big files from the front-end
171 # otherwise it will just hide the big files from the front-end
172 diff_limit = c.visual.cut_off_limit_diff
172 diff_limit = c.visual.cut_off_limit_diff
173 file_limit = c.visual.cut_off_limit_file
173 file_limit = c.visual.cut_off_limit_file
174
174
175 # get ranges of commit ids if preset
175 # get ranges of commit ids if preset
176 commit_range = commit_id_range.split('...')[:2]
176 commit_range = commit_id_range.split('...')[:2]
177
177
178 try:
178 try:
179 pre_load = ['affected_files', 'author', 'branch', 'date',
179 pre_load = ['affected_files', 'author', 'branch', 'date',
180 'message', 'parents']
180 'message', 'parents']
181
181
182 if len(commit_range) == 2:
182 if len(commit_range) == 2:
183 commits = self.rhodecode_vcs_repo.get_commits(
183 commits = self.rhodecode_vcs_repo.get_commits(
184 start_id=commit_range[0], end_id=commit_range[1],
184 start_id=commit_range[0], end_id=commit_range[1],
185 pre_load=pre_load)
185 pre_load=pre_load)
186 commits = list(commits)
186 commits = list(commits)
187 else:
187 else:
188 commits = [self.rhodecode_vcs_repo.get_commit(
188 commits = [self.rhodecode_vcs_repo.get_commit(
189 commit_id=commit_id_range, pre_load=pre_load)]
189 commit_id=commit_id_range, pre_load=pre_load)]
190
190
191 c.commit_ranges = commits
191 c.commit_ranges = commits
192 if not c.commit_ranges:
192 if not c.commit_ranges:
193 raise RepositoryError(
193 raise RepositoryError(
194 'The commit range returned an empty result')
194 'The commit range returned an empty result')
195 except CommitDoesNotExistError:
195 except CommitDoesNotExistError:
196 msg = _('No such commit exists for this repository')
196 msg = _('No such commit exists for this repository')
197 h.flash(msg, category='error')
197 h.flash(msg, category='error')
198 raise HTTPNotFound()
198 raise HTTPNotFound()
199 except Exception:
199 except Exception:
200 log.exception("General failure")
200 log.exception("General failure")
201 raise HTTPNotFound()
201 raise HTTPNotFound()
202
202
203 c.changes = OrderedDict()
203 c.changes = OrderedDict()
204 c.lines_added = 0
204 c.lines_added = 0
205 c.lines_deleted = 0
205 c.lines_deleted = 0
206
206
207 # auto collapse if we have more than limit
207 # auto collapse if we have more than limit
208 collapse_limit = diffs.DiffProcessor._collapse_commits_over
208 collapse_limit = diffs.DiffProcessor._collapse_commits_over
209 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
209 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
210
210
211 c.commit_statuses = ChangesetStatus.STATUSES
211 c.commit_statuses = ChangesetStatus.STATUSES
212 c.inline_comments = []
212 c.inline_comments = []
213 c.files = []
213 c.files = []
214
214
215 c.statuses = []
215 c.statuses = []
216 c.comments = []
216 c.comments = []
217 c.unresolved_comments = []
217 c.unresolved_comments = []
218 if len(c.commit_ranges) == 1:
218 if len(c.commit_ranges) == 1:
219 commit = c.commit_ranges[0]
219 commit = c.commit_ranges[0]
220 c.comments = CommentsModel().get_comments(
220 c.comments = CommentsModel().get_comments(
221 self.db_repo.repo_id,
221 self.db_repo.repo_id,
222 revision=commit.raw_id)
222 revision=commit.raw_id)
223 c.statuses.append(ChangesetStatusModel().get_status(
223 c.statuses.append(ChangesetStatusModel().get_status(
224 self.db_repo.repo_id, commit.raw_id))
224 self.db_repo.repo_id, commit.raw_id))
225 # comments from PR
225 # comments from PR
226 statuses = ChangesetStatusModel().get_statuses(
226 statuses = ChangesetStatusModel().get_statuses(
227 self.db_repo.repo_id, commit.raw_id,
227 self.db_repo.repo_id, commit.raw_id,
228 with_revisions=True)
228 with_revisions=True)
229 prs = set(st.pull_request for st in statuses
229 prs = set(st.pull_request for st in statuses
230 if st.pull_request is not None)
230 if st.pull_request is not None)
231 # from associated statuses, check the pull requests, and
231 # from associated statuses, check the pull requests, and
232 # show comments from them
232 # show comments from them
233 for pr in prs:
233 for pr in prs:
234 c.comments.extend(pr.comments)
234 c.comments.extend(pr.comments)
235
235
236 c.unresolved_comments = CommentsModel()\
236 c.unresolved_comments = CommentsModel()\
237 .get_commit_unresolved_todos(commit.raw_id)
237 .get_commit_unresolved_todos(commit.raw_id)
238
238
239 diff = None
239 diff = None
240 # Iterate over ranges (default commit view is always one commit)
240 # Iterate over ranges (default commit view is always one commit)
241 for commit in c.commit_ranges:
241 for commit in c.commit_ranges:
242 c.changes[commit.raw_id] = []
242 c.changes[commit.raw_id] = []
243
243
244 commit2 = commit
244 commit2 = commit
245 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
245 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
246
246
247 _diff = self.rhodecode_vcs_repo.get_diff(
247 _diff = self.rhodecode_vcs_repo.get_diff(
248 commit1, commit2,
248 commit1, commit2,
249 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
249 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
250 diff_processor = diffs.DiffProcessor(
250 diff_processor = diffs.DiffProcessor(
251 _diff, format='newdiff', diff_limit=diff_limit,
251 _diff, format='newdiff', diff_limit=diff_limit,
252 file_limit=file_limit, show_full_diff=c.fulldiff)
252 file_limit=file_limit, show_full_diff=c.fulldiff)
253
253
254 commit_changes = OrderedDict()
254 commit_changes = OrderedDict()
255 if method == 'show':
255 if method == 'show':
256 _parsed = diff_processor.prepare()
256 _parsed = diff_processor.prepare()
257 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
257 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
258
258
259 _parsed = diff_processor.prepare()
259 _parsed = diff_processor.prepare()
260
260
261 def _node_getter(commit):
261 def _node_getter(commit):
262 def get_node(fname):
262 def get_node(fname):
263 try:
263 try:
264 return commit.get_node(fname)
264 return commit.get_node(fname)
265 except NodeDoesNotExistError:
265 except NodeDoesNotExistError:
266 return None
266 return None
267 return get_node
267 return get_node
268
268
269 inline_comments = CommentsModel().get_inline_comments(
269 inline_comments = CommentsModel().get_inline_comments(
270 self.db_repo.repo_id, revision=commit.raw_id)
270 self.db_repo.repo_id, revision=commit.raw_id)
271 c.inline_cnt = CommentsModel().get_inline_comments_count(
271 c.inline_cnt = CommentsModel().get_inline_comments_count(
272 inline_comments)
272 inline_comments)
273
273
274 diffset = codeblocks.DiffSet(
274 diffset = codeblocks.DiffSet(
275 repo_name=self.db_repo_name,
275 repo_name=self.db_repo_name,
276 source_node_getter=_node_getter(commit1),
276 source_node_getter=_node_getter(commit1),
277 target_node_getter=_node_getter(commit2),
277 target_node_getter=_node_getter(commit2),
278 comments=inline_comments)
278 comments=inline_comments)
279 diffset = diffset.render_patchset(
279 diffset = diffset.render_patchset(
280 _parsed, commit1.raw_id, commit2.raw_id)
280 _parsed, commit1.raw_id, commit2.raw_id)
281
281
282 c.changes[commit.raw_id] = diffset
282 c.changes[commit.raw_id] = diffset
283 else:
283 else:
284 # downloads/raw we only need RAW diff nothing else
284 # downloads/raw we only need RAW diff nothing else
285 diff = diff_processor.as_raw()
285 diff = diff_processor.as_raw()
286 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
286 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
287
287
288 # sort comments by how they were generated
288 # sort comments by how they were generated
289 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
289 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
290
290
291 if len(c.commit_ranges) == 1:
291 if len(c.commit_ranges) == 1:
292 c.commit = c.commit_ranges[0]
292 c.commit = c.commit_ranges[0]
293 c.parent_tmpl = ''.join(
293 c.parent_tmpl = ''.join(
294 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
294 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
295
295
296 if method == 'download':
296 if method == 'download':
297 response = Response(diff)
297 response = Response(diff)
298 response.content_type = 'text/plain'
298 response.content_type = 'text/plain'
299 response.content_disposition = (
299 response.content_disposition = (
300 'attachment; filename=%s.diff' % commit_id_range[:12])
300 'attachment; filename=%s.diff' % commit_id_range[:12])
301 return response
301 return response
302 elif method == 'patch':
302 elif method == 'patch':
303 c.diff = safe_unicode(diff)
303 c.diff = safe_unicode(diff)
304 patch = render(
304 patch = render(
305 'rhodecode:templates/changeset/patch_changeset.mako',
305 'rhodecode:templates/changeset/patch_changeset.mako',
306 self._get_template_context(c), self.request)
306 self._get_template_context(c), self.request)
307 response = Response(patch)
307 response = Response(patch)
308 response.content_type = 'text/plain'
308 response.content_type = 'text/plain'
309 return response
309 return response
310 elif method == 'raw':
310 elif method == 'raw':
311 response = Response(diff)
311 response = Response(diff)
312 response.content_type = 'text/plain'
312 response.content_type = 'text/plain'
313 return response
313 return response
314 elif method == 'show':
314 elif method == 'show':
315 if len(c.commit_ranges) == 1:
315 if len(c.commit_ranges) == 1:
316 html = render(
316 html = render(
317 'rhodecode:templates/changeset/changeset.mako',
317 'rhodecode:templates/changeset/changeset.mako',
318 self._get_template_context(c), self.request)
318 self._get_template_context(c), self.request)
319 return Response(html)
319 return Response(html)
320 else:
320 else:
321 c.ancestor = None
321 c.ancestor = None
322 c.target_repo = self.db_repo
322 c.target_repo = self.db_repo
323 html = render(
323 html = render(
324 'rhodecode:templates/changeset/changeset_range.mako',
324 'rhodecode:templates/changeset/changeset_range.mako',
325 self._get_template_context(c), self.request)
325 self._get_template_context(c), self.request)
326 return Response(html)
326 return Response(html)
327
327
328 raise HTTPBadRequest()
328 raise HTTPBadRequest()
329
329
330 @LoginRequired()
330 @LoginRequired()
331 @HasRepoPermissionAnyDecorator(
331 @HasRepoPermissionAnyDecorator(
332 'repository.read', 'repository.write', 'repository.admin')
332 'repository.read', 'repository.write', 'repository.admin')
333 @view_config(
333 @view_config(
334 route_name='repo_commit', request_method='GET',
334 route_name='repo_commit', request_method='GET',
335 renderer=None)
335 renderer=None)
336 def repo_commit_show(self):
336 def repo_commit_show(self):
337 commit_id = self.request.matchdict['commit_id']
337 commit_id = self.request.matchdict['commit_id']
338 return self._commit(commit_id, method='show')
338 return self._commit(commit_id, method='show')
339
339
340 @LoginRequired()
340 @LoginRequired()
341 @HasRepoPermissionAnyDecorator(
341 @HasRepoPermissionAnyDecorator(
342 'repository.read', 'repository.write', 'repository.admin')
342 'repository.read', 'repository.write', 'repository.admin')
343 @view_config(
343 @view_config(
344 route_name='repo_commit_raw', request_method='GET',
344 route_name='repo_commit_raw', request_method='GET',
345 renderer=None)
345 renderer=None)
346 @view_config(
346 @view_config(
347 route_name='repo_commit_raw_deprecated', request_method='GET',
347 route_name='repo_commit_raw_deprecated', request_method='GET',
348 renderer=None)
348 renderer=None)
349 def repo_commit_raw(self):
349 def repo_commit_raw(self):
350 commit_id = self.request.matchdict['commit_id']
350 commit_id = self.request.matchdict['commit_id']
351 return self._commit(commit_id, method='raw')
351 return self._commit(commit_id, method='raw')
352
352
353 @LoginRequired()
353 @LoginRequired()
354 @HasRepoPermissionAnyDecorator(
354 @HasRepoPermissionAnyDecorator(
355 'repository.read', 'repository.write', 'repository.admin')
355 'repository.read', 'repository.write', 'repository.admin')
356 @view_config(
356 @view_config(
357 route_name='repo_commit_patch', request_method='GET',
357 route_name='repo_commit_patch', request_method='GET',
358 renderer=None)
358 renderer=None)
359 def repo_commit_patch(self):
359 def repo_commit_patch(self):
360 commit_id = self.request.matchdict['commit_id']
360 commit_id = self.request.matchdict['commit_id']
361 return self._commit(commit_id, method='patch')
361 return self._commit(commit_id, method='patch')
362
362
363 @LoginRequired()
363 @LoginRequired()
364 @HasRepoPermissionAnyDecorator(
364 @HasRepoPermissionAnyDecorator(
365 'repository.read', 'repository.write', 'repository.admin')
365 'repository.read', 'repository.write', 'repository.admin')
366 @view_config(
366 @view_config(
367 route_name='repo_commit_download', request_method='GET',
367 route_name='repo_commit_download', request_method='GET',
368 renderer=None)
368 renderer=None)
369 def repo_commit_download(self):
369 def repo_commit_download(self):
370 commit_id = self.request.matchdict['commit_id']
370 commit_id = self.request.matchdict['commit_id']
371 return self._commit(commit_id, method='download')
371 return self._commit(commit_id, method='download')
372
372
373 @LoginRequired()
373 @LoginRequired()
374 @NotAnonymous()
374 @NotAnonymous()
375 @HasRepoPermissionAnyDecorator(
375 @HasRepoPermissionAnyDecorator(
376 'repository.read', 'repository.write', 'repository.admin')
376 'repository.read', 'repository.write', 'repository.admin')
377 @CSRFRequired()
377 @CSRFRequired()
378 @view_config(
378 @view_config(
379 route_name='repo_commit_comment_create', request_method='POST',
379 route_name='repo_commit_comment_create', request_method='POST',
380 renderer='json_ext')
380 renderer='json_ext')
381 def repo_commit_comment_create(self):
381 def repo_commit_comment_create(self):
382 _ = self.request.translate
382 _ = self.request.translate
383 commit_id = self.request.matchdict['commit_id']
383 commit_id = self.request.matchdict['commit_id']
384
384
385 c = self.load_default_context()
385 c = self.load_default_context()
386 status = self.request.POST.get('changeset_status', None)
386 status = self.request.POST.get('changeset_status', None)
387 text = self.request.POST.get('text')
387 text = self.request.POST.get('text')
388 comment_type = self.request.POST.get('comment_type')
388 comment_type = self.request.POST.get('comment_type')
389 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
389 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
390
390
391 if status:
391 if status:
392 text = text or (_('Status change %(transition_icon)s %(status)s')
392 text = text or (_('Status change %(transition_icon)s %(status)s')
393 % {'transition_icon': '>',
393 % {'transition_icon': '>',
394 'status': ChangesetStatus.get_status_lbl(status)})
394 'status': ChangesetStatus.get_status_lbl(status)})
395
395
396 multi_commit_ids = []
396 multi_commit_ids = []
397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
399 if _commit_id not in multi_commit_ids:
399 if _commit_id not in multi_commit_ids:
400 multi_commit_ids.append(_commit_id)
400 multi_commit_ids.append(_commit_id)
401
401
402 commit_ids = multi_commit_ids or [commit_id]
402 commit_ids = multi_commit_ids or [commit_id]
403
403
404 comment = None
404 comment = None
405 for current_id in filter(None, commit_ids):
405 for current_id in filter(None, commit_ids):
406 comment = CommentsModel().create(
406 comment = CommentsModel().create(
407 text=text,
407 text=text,
408 repo=self.db_repo.repo_id,
408 repo=self.db_repo.repo_id,
409 user=self._rhodecode_db_user.user_id,
409 user=self._rhodecode_db_user.user_id,
410 commit_id=current_id,
410 commit_id=current_id,
411 f_path=self.request.POST.get('f_path'),
411 f_path=self.request.POST.get('f_path'),
412 line_no=self.request.POST.get('line'),
412 line_no=self.request.POST.get('line'),
413 status_change=(ChangesetStatus.get_status_lbl(status)
413 status_change=(ChangesetStatus.get_status_lbl(status)
414 if status else None),
414 if status else None),
415 status_change_type=status,
415 status_change_type=status,
416 comment_type=comment_type,
416 comment_type=comment_type,
417 resolves_comment_id=resolves_comment_id
417 resolves_comment_id=resolves_comment_id
418 )
418 )
419
419
420 # get status if set !
420 # get status if set !
421 if status:
421 if status:
422 # if latest status was from pull request and it's closed
422 # if latest status was from pull request and it's closed
423 # disallow changing status !
423 # disallow changing status !
424 # dont_allow_on_closed_pull_request = True !
424 # dont_allow_on_closed_pull_request = True !
425
425
426 try:
426 try:
427 ChangesetStatusModel().set_status(
427 ChangesetStatusModel().set_status(
428 self.db_repo.repo_id,
428 self.db_repo.repo_id,
429 status,
429 status,
430 self._rhodecode_db_user.user_id,
430 self._rhodecode_db_user.user_id,
431 comment,
431 comment,
432 revision=current_id,
432 revision=current_id,
433 dont_allow_on_closed_pull_request=True
433 dont_allow_on_closed_pull_request=True
434 )
434 )
435 except StatusChangeOnClosedPullRequestError:
435 except StatusChangeOnClosedPullRequestError:
436 msg = _('Changing the status of a commit associated with '
436 msg = _('Changing the status of a commit associated with '
437 'a closed pull request is not allowed')
437 'a closed pull request is not allowed')
438 log.exception(msg)
438 log.exception(msg)
439 h.flash(msg, category='warning')
439 h.flash(msg, category='warning')
440 raise HTTPFound(h.route_path(
440 raise HTTPFound(h.route_path(
441 'repo_commit', repo_name=self.db_repo_name,
441 'repo_commit', repo_name=self.db_repo_name,
442 commit_id=current_id))
442 commit_id=current_id))
443
443
444 # finalize, commit and redirect
444 # finalize, commit and redirect
445 Session().commit()
445 Session().commit()
446
446
447 data = {
447 data = {
448 'target_id': h.safeid(h.safe_unicode(
448 'target_id': h.safeid(h.safe_unicode(
449 self.request.POST.get('f_path'))),
449 self.request.POST.get('f_path'))),
450 }
450 }
451 if comment:
451 if comment:
452 c.co = comment
452 c.co = comment
453 rendered_comment = render(
453 rendered_comment = render(
454 'rhodecode:templates/changeset/changeset_comment_block.mako',
454 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 self._get_template_context(c), self.request)
455 self._get_template_context(c), self.request)
456
456
457 data.update(comment.get_dict())
457 data.update(comment.get_dict())
458 data.update({'rendered_text': rendered_comment})
458 data.update({'rendered_text': rendered_comment})
459
459
460 return data
460 return data
461
461
462 @LoginRequired()
462 @LoginRequired()
463 @NotAnonymous()
463 @NotAnonymous()
464 @HasRepoPermissionAnyDecorator(
464 @HasRepoPermissionAnyDecorator(
465 'repository.read', 'repository.write', 'repository.admin')
465 'repository.read', 'repository.write', 'repository.admin')
466 @CSRFRequired()
466 @CSRFRequired()
467 @view_config(
467 @view_config(
468 route_name='repo_commit_comment_preview', request_method='POST',
468 route_name='repo_commit_comment_preview', request_method='POST',
469 renderer='string', xhr=True)
469 renderer='string', xhr=True)
470 def repo_commit_comment_preview(self):
470 def repo_commit_comment_preview(self):
471 # Technically a CSRF token is not needed as no state changes with this
471 # Technically a CSRF token is not needed as no state changes with this
472 # call. However, as this is a POST is better to have it, so automated
472 # call. However, as this is a POST is better to have it, so automated
473 # tools don't flag it as potential CSRF.
473 # tools don't flag it as potential CSRF.
474 # Post is required because the payload could be bigger than the maximum
474 # Post is required because the payload could be bigger than the maximum
475 # allowed by GET.
475 # allowed by GET.
476
476
477 text = self.request.POST.get('text')
477 text = self.request.POST.get('text')
478 renderer = self.request.POST.get('renderer') or 'rst'
478 renderer = self.request.POST.get('renderer') or 'rst'
479 if text:
479 if text:
480 return h.render(text, renderer=renderer, mentions=True)
480 return h.render(text, renderer=renderer, mentions=True)
481 return ''
481 return ''
482
482
483 @LoginRequired()
483 @LoginRequired()
484 @NotAnonymous()
484 @NotAnonymous()
485 @HasRepoPermissionAnyDecorator(
485 @HasRepoPermissionAnyDecorator(
486 'repository.read', 'repository.write', 'repository.admin')
486 'repository.read', 'repository.write', 'repository.admin')
487 @CSRFRequired()
487 @CSRFRequired()
488 @view_config(
488 @view_config(
489 route_name='repo_commit_comment_delete', request_method='POST',
489 route_name='repo_commit_comment_delete', request_method='POST',
490 renderer='json_ext')
490 renderer='json_ext')
491 def repo_commit_comment_delete(self):
491 def repo_commit_comment_delete(self):
492 commit_id = self.request.matchdict['commit_id']
492 commit_id = self.request.matchdict['commit_id']
493 comment_id = self.request.matchdict['comment_id']
493 comment_id = self.request.matchdict['comment_id']
494
494
495 comment = ChangesetComment.get_or_404(safe_int(comment_id))
495 comment = ChangesetComment.get_or_404(comment_id)
496 if not comment:
496 if not comment:
497 log.debug('Comment with id:%s not found, skipping', comment_id)
497 log.debug('Comment with id:%s not found, skipping', comment_id)
498 # comment already deleted in another call probably
498 # comment already deleted in another call probably
499 return True
499 return True
500
500
501 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
501 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
502 super_admin = h.HasPermissionAny('hg.admin')()
502 super_admin = h.HasPermissionAny('hg.admin')()
503 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
503 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
504 is_repo_comment = comment.repo.repo_name == self.db_repo_name
504 is_repo_comment = comment.repo.repo_name == self.db_repo_name
505 comment_repo_admin = is_repo_admin and is_repo_comment
505 comment_repo_admin = is_repo_admin and is_repo_comment
506
506
507 if super_admin or comment_owner or comment_repo_admin:
507 if super_admin or comment_owner or comment_repo_admin:
508 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
508 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
509 Session().commit()
509 Session().commit()
510 return True
510 return True
511 else:
511 else:
512 log.warning('No permissions for user %s to delete comment_id: %s',
512 log.warning('No permissions for user %s to delete comment_id: %s',
513 self._rhodecode_db_user, comment_id)
513 self._rhodecode_db_user, comment_id)
514 raise HTTPNotFound()
514 raise HTTPNotFound()
515
515
516 @LoginRequired()
516 @LoginRequired()
517 @HasRepoPermissionAnyDecorator(
517 @HasRepoPermissionAnyDecorator(
518 'repository.read', 'repository.write', 'repository.admin')
518 'repository.read', 'repository.write', 'repository.admin')
519 @view_config(
519 @view_config(
520 route_name='repo_commit_data', request_method='GET',
520 route_name='repo_commit_data', request_method='GET',
521 renderer='json_ext', xhr=True)
521 renderer='json_ext', xhr=True)
522 def repo_commit_data(self):
522 def repo_commit_data(self):
523 commit_id = self.request.matchdict['commit_id']
523 commit_id = self.request.matchdict['commit_id']
524 self.load_default_context()
524 self.load_default_context()
525
525
526 try:
526 try:
527 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
527 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
528 except CommitDoesNotExistError as e:
528 except CommitDoesNotExistError as e:
529 return EmptyCommit(message=str(e))
529 return EmptyCommit(message=str(e))
530
530
531 @LoginRequired()
531 @LoginRequired()
532 @HasRepoPermissionAnyDecorator(
532 @HasRepoPermissionAnyDecorator(
533 'repository.read', 'repository.write', 'repository.admin')
533 'repository.read', 'repository.write', 'repository.admin')
534 @view_config(
534 @view_config(
535 route_name='repo_commit_children', request_method='GET',
535 route_name='repo_commit_children', request_method='GET',
536 renderer='json_ext', xhr=True)
536 renderer='json_ext', xhr=True)
537 def repo_commit_children(self):
537 def repo_commit_children(self):
538 commit_id = self.request.matchdict['commit_id']
538 commit_id = self.request.matchdict['commit_id']
539 self.load_default_context()
539 self.load_default_context()
540
540
541 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
541 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
542 result = {"results": commit.children}
542 result = {"results": commit.children}
543 return result
543 return result
544
544
545 @LoginRequired()
545 @LoginRequired()
546 @HasRepoPermissionAnyDecorator(
546 @HasRepoPermissionAnyDecorator(
547 'repository.read', 'repository.write', 'repository.admin')
547 'repository.read', 'repository.write', 'repository.admin')
548 @view_config(
548 @view_config(
549 route_name='repo_commit_parents', request_method='GET',
549 route_name='repo_commit_parents', request_method='GET',
550 renderer='json_ext')
550 renderer='json_ext')
551 def repo_commit_parents(self):
551 def repo_commit_parents(self):
552 commit_id = self.request.matchdict['commit_id']
552 commit_id = self.request.matchdict['commit_id']
553 self.load_default_context()
553 self.load_default_context()
554
554
555 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
555 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
556 result = {"results": commit.parents}
556 result = {"results": commit.parents}
557 return result
557 return result
@@ -1,1018 +1,1018 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import peppercorn
24 import peppercorn
25 import formencode
25 import formencode
26 import logging
26 import logging
27 import collections
27 import collections
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from pyramid.httpexceptions import HTTPFound
34 from pyramid.httpexceptions import HTTPFound
35 from sqlalchemy.sql import func
35 from sqlalchemy.sql import func
36 from sqlalchemy.sql.expression import or_
36 from sqlalchemy.sql.expression import or_
37
37
38 from rhodecode import events
38 from rhodecode import events
39 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
39 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.base import (
41 from rhodecode.lib.base import (
42 BaseRepoController, render, vcs_operation_context)
42 BaseRepoController, render, vcs_operation_context)
43 from rhodecode.lib.auth import (
43 from rhodecode.lib.auth import (
44 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 HasAcceptedRepoType, XHRRequired)
45 HasAcceptedRepoType, XHRRequired)
46 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils2 import (
48 from rhodecode.lib.utils2 import (
49 safe_int, safe_str, str2bool, safe_unicode)
49 safe_int, safe_str, str2bool, safe_unicode)
50 from rhodecode.lib.vcs.backends.base import (
50 from rhodecode.lib.vcs.backends.base import (
51 EmptyCommit, UpdateFailureReason, EmptyRepository)
51 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 from rhodecode.lib.vcs.exceptions import (
52 from rhodecode.lib.vcs.exceptions import (
53 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
53 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 NodeDoesNotExistError)
54 NodeDoesNotExistError)
55
55
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
58 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 Repository, PullRequestVersion)
59 Repository, PullRequestVersion)
60 from rhodecode.model.forms import PullRequestForm
60 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
62 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 class PullrequestsController(BaseRepoController):
67 class PullrequestsController(BaseRepoController):
68
68
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
71 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
72 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73
73
74 @LoginRequired()
74 @LoginRequired()
75 @NotAnonymous()
75 @NotAnonymous()
76 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
76 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 'repository.admin')
77 'repository.admin')
78 @HasAcceptedRepoType('git', 'hg')
78 @HasAcceptedRepoType('git', 'hg')
79 def index(self):
79 def index(self):
80 source_repo = c.rhodecode_db_repo
80 source_repo = c.rhodecode_db_repo
81
81
82 try:
82 try:
83 source_repo.scm_instance().get_commit()
83 source_repo.scm_instance().get_commit()
84 except EmptyRepositoryError:
84 except EmptyRepositoryError:
85 h.flash(h.literal(_('There are no commits yet')),
85 h.flash(h.literal(_('There are no commits yet')),
86 category='warning')
86 category='warning')
87 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
87 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88
88
89 commit_id = request.GET.get('commit')
89 commit_id = request.GET.get('commit')
90 branch_ref = request.GET.get('branch')
90 branch_ref = request.GET.get('branch')
91 bookmark_ref = request.GET.get('bookmark')
91 bookmark_ref = request.GET.get('bookmark')
92
92
93 try:
93 try:
94 source_repo_data = PullRequestModel().generate_repo_data(
94 source_repo_data = PullRequestModel().generate_repo_data(
95 source_repo, commit_id=commit_id,
95 source_repo, commit_id=commit_id,
96 branch=branch_ref, bookmark=bookmark_ref)
96 branch=branch_ref, bookmark=bookmark_ref)
97 except CommitDoesNotExistError as e:
97 except CommitDoesNotExistError as e:
98 log.exception(e)
98 log.exception(e)
99 h.flash(_('Commit does not exist'), 'error')
99 h.flash(_('Commit does not exist'), 'error')
100 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
100 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101
101
102 default_target_repo = source_repo
102 default_target_repo = source_repo
103
103
104 if source_repo.parent:
104 if source_repo.parent:
105 parent_vcs_obj = source_repo.parent.scm_instance()
105 parent_vcs_obj = source_repo.parent.scm_instance()
106 if parent_vcs_obj and not parent_vcs_obj.is_empty():
106 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 # change default if we have a parent repo
107 # change default if we have a parent repo
108 default_target_repo = source_repo.parent
108 default_target_repo = source_repo.parent
109
109
110 target_repo_data = PullRequestModel().generate_repo_data(
110 target_repo_data = PullRequestModel().generate_repo_data(
111 default_target_repo)
111 default_target_repo)
112
112
113 selected_source_ref = source_repo_data['refs']['selected_ref']
113 selected_source_ref = source_repo_data['refs']['selected_ref']
114
114
115 title_source_ref = selected_source_ref.split(':', 2)[1]
115 title_source_ref = selected_source_ref.split(':', 2)[1]
116 c.default_title = PullRequestModel().generate_pullrequest_title(
116 c.default_title = PullRequestModel().generate_pullrequest_title(
117 source=source_repo.repo_name,
117 source=source_repo.repo_name,
118 source_ref=title_source_ref,
118 source_ref=title_source_ref,
119 target=default_target_repo.repo_name
119 target=default_target_repo.repo_name
120 )
120 )
121
121
122 c.default_repo_data = {
122 c.default_repo_data = {
123 'source_repo_name': source_repo.repo_name,
123 'source_repo_name': source_repo.repo_name,
124 'source_refs_json': json.dumps(source_repo_data),
124 'source_refs_json': json.dumps(source_repo_data),
125 'target_repo_name': default_target_repo.repo_name,
125 'target_repo_name': default_target_repo.repo_name,
126 'target_refs_json': json.dumps(target_repo_data),
126 'target_refs_json': json.dumps(target_repo_data),
127 }
127 }
128 c.default_source_ref = selected_source_ref
128 c.default_source_ref = selected_source_ref
129
129
130 return render('/pullrequests/pullrequest.mako')
130 return render('/pullrequests/pullrequest.mako')
131
131
132 @LoginRequired()
132 @LoginRequired()
133 @NotAnonymous()
133 @NotAnonymous()
134 @XHRRequired()
134 @XHRRequired()
135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 'repository.admin')
136 'repository.admin')
137 @jsonify
137 @jsonify
138 def get_repo_refs(self, repo_name, target_repo_name):
138 def get_repo_refs(self, repo_name, target_repo_name):
139 repo = Repository.get_by_repo_name(target_repo_name)
139 repo = Repository.get_by_repo_name(target_repo_name)
140 if not repo:
140 if not repo:
141 raise HTTPNotFound
141 raise HTTPNotFound
142 return PullRequestModel().generate_repo_data(repo)
142 return PullRequestModel().generate_repo_data(repo)
143
143
144 @LoginRequired()
144 @LoginRequired()
145 @NotAnonymous()
145 @NotAnonymous()
146 @XHRRequired()
146 @XHRRequired()
147 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
147 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 'repository.admin')
148 'repository.admin')
149 @jsonify
149 @jsonify
150 def get_repo_destinations(self, repo_name):
150 def get_repo_destinations(self, repo_name):
151 repo = Repository.get_by_repo_name(repo_name)
151 repo = Repository.get_by_repo_name(repo_name)
152 if not repo:
152 if not repo:
153 raise HTTPNotFound
153 raise HTTPNotFound
154 filter_query = request.GET.get('query')
154 filter_query = request.GET.get('query')
155
155
156 query = Repository.query() \
156 query = Repository.query() \
157 .order_by(func.length(Repository.repo_name)) \
157 .order_by(func.length(Repository.repo_name)) \
158 .filter(or_(
158 .filter(or_(
159 Repository.repo_name == repo.repo_name,
159 Repository.repo_name == repo.repo_name,
160 Repository.fork_id == repo.repo_id))
160 Repository.fork_id == repo.repo_id))
161
161
162 if filter_query:
162 if filter_query:
163 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
163 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 query = query.filter(
164 query = query.filter(
165 Repository.repo_name.ilike(ilike_expression))
165 Repository.repo_name.ilike(ilike_expression))
166
166
167 add_parent = False
167 add_parent = False
168 if repo.parent:
168 if repo.parent:
169 if filter_query in repo.parent.repo_name:
169 if filter_query in repo.parent.repo_name:
170 parent_vcs_obj = repo.parent.scm_instance()
170 parent_vcs_obj = repo.parent.scm_instance()
171 if parent_vcs_obj and not parent_vcs_obj.is_empty():
171 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 add_parent = True
172 add_parent = True
173
173
174 limit = 20 - 1 if add_parent else 20
174 limit = 20 - 1 if add_parent else 20
175 all_repos = query.limit(limit).all()
175 all_repos = query.limit(limit).all()
176 if add_parent:
176 if add_parent:
177 all_repos += [repo.parent]
177 all_repos += [repo.parent]
178
178
179 repos = []
179 repos = []
180 for obj in self.scm_model.get_repos(all_repos):
180 for obj in self.scm_model.get_repos(all_repos):
181 repos.append({
181 repos.append({
182 'id': obj['name'],
182 'id': obj['name'],
183 'text': obj['name'],
183 'text': obj['name'],
184 'type': 'repo',
184 'type': 'repo',
185 'obj': obj['dbrepo']
185 'obj': obj['dbrepo']
186 })
186 })
187
187
188 data = {
188 data = {
189 'more': False,
189 'more': False,
190 'results': [{
190 'results': [{
191 'text': _('Repositories'),
191 'text': _('Repositories'),
192 'children': repos
192 'children': repos
193 }] if repos else []
193 }] if repos else []
194 }
194 }
195 return data
195 return data
196
196
197 @LoginRequired()
197 @LoginRequired()
198 @NotAnonymous()
198 @NotAnonymous()
199 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
199 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 'repository.admin')
200 'repository.admin')
201 @HasAcceptedRepoType('git', 'hg')
201 @HasAcceptedRepoType('git', 'hg')
202 @auth.CSRFRequired()
202 @auth.CSRFRequired()
203 def create(self, repo_name):
203 def create(self, repo_name):
204 repo = Repository.get_by_repo_name(repo_name)
204 repo = Repository.get_by_repo_name(repo_name)
205 if not repo:
205 if not repo:
206 raise HTTPNotFound
206 raise HTTPNotFound
207
207
208 controls = peppercorn.parse(request.POST.items())
208 controls = peppercorn.parse(request.POST.items())
209
209
210 try:
210 try:
211 _form = PullRequestForm(repo.repo_id)().to_python(controls)
211 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 except formencode.Invalid as errors:
212 except formencode.Invalid as errors:
213 if errors.error_dict.get('revisions'):
213 if errors.error_dict.get('revisions'):
214 msg = 'Revisions: %s' % errors.error_dict['revisions']
214 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 elif errors.error_dict.get('pullrequest_title'):
215 elif errors.error_dict.get('pullrequest_title'):
216 msg = _('Pull request requires a title with min. 3 chars')
216 msg = _('Pull request requires a title with min. 3 chars')
217 else:
217 else:
218 msg = _('Error creating pull request: {}').format(errors)
218 msg = _('Error creating pull request: {}').format(errors)
219 log.exception(msg)
219 log.exception(msg)
220 h.flash(msg, 'error')
220 h.flash(msg, 'error')
221
221
222 # would rather just go back to form ...
222 # would rather just go back to form ...
223 return redirect(url('pullrequest_home', repo_name=repo_name))
223 return redirect(url('pullrequest_home', repo_name=repo_name))
224
224
225 source_repo = _form['source_repo']
225 source_repo = _form['source_repo']
226 source_ref = _form['source_ref']
226 source_ref = _form['source_ref']
227 target_repo = _form['target_repo']
227 target_repo = _form['target_repo']
228 target_ref = _form['target_ref']
228 target_ref = _form['target_ref']
229 commit_ids = _form['revisions'][::-1]
229 commit_ids = _form['revisions'][::-1]
230
230
231 # find the ancestor for this pr
231 # find the ancestor for this pr
232 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
232 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
233 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234
234
235 source_scm = source_db_repo.scm_instance()
235 source_scm = source_db_repo.scm_instance()
236 target_scm = target_db_repo.scm_instance()
236 target_scm = target_db_repo.scm_instance()
237
237
238 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
238 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
239 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240
240
241 ancestor = source_scm.get_common_ancestor(
241 ancestor = source_scm.get_common_ancestor(
242 source_commit.raw_id, target_commit.raw_id, target_scm)
242 source_commit.raw_id, target_commit.raw_id, target_scm)
243
243
244 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
244 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
245 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246
246
247 pullrequest_title = _form['pullrequest_title']
247 pullrequest_title = _form['pullrequest_title']
248 title_source_ref = source_ref.split(':', 2)[1]
248 title_source_ref = source_ref.split(':', 2)[1]
249 if not pullrequest_title:
249 if not pullrequest_title:
250 pullrequest_title = PullRequestModel().generate_pullrequest_title(
250 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 source=source_repo,
251 source=source_repo,
252 source_ref=title_source_ref,
252 source_ref=title_source_ref,
253 target=target_repo
253 target=target_repo
254 )
254 )
255
255
256 description = _form['pullrequest_desc']
256 description = _form['pullrequest_desc']
257
257
258 get_default_reviewers_data, validate_default_reviewers = \
258 get_default_reviewers_data, validate_default_reviewers = \
259 PullRequestModel().get_reviewer_functions()
259 PullRequestModel().get_reviewer_functions()
260
260
261 # recalculate reviewers logic, to make sure we can validate this
261 # recalculate reviewers logic, to make sure we can validate this
262 reviewer_rules = get_default_reviewers_data(
262 reviewer_rules = get_default_reviewers_data(
263 c.rhodecode_user.get_instance(), source_db_repo,
263 c.rhodecode_user.get_instance(), source_db_repo,
264 source_commit, target_db_repo, target_commit)
264 source_commit, target_db_repo, target_commit)
265
265
266 given_reviewers = _form['review_members']
266 given_reviewers = _form['review_members']
267 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
267 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
268
268
269 try:
269 try:
270 pull_request = PullRequestModel().create(
270 pull_request = PullRequestModel().create(
271 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
271 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 target_ref, commit_ids, reviewers, pullrequest_title,
272 target_ref, commit_ids, reviewers, pullrequest_title,
273 description, reviewer_rules
273 description, reviewer_rules
274 )
274 )
275 Session().commit()
275 Session().commit()
276 h.flash(_('Successfully opened new pull request'),
276 h.flash(_('Successfully opened new pull request'),
277 category='success')
277 category='success')
278 except Exception as e:
278 except Exception as e:
279 msg = _('Error occurred during creation of this pull request.')
279 msg = _('Error occurred during creation of this pull request.')
280 log.exception(msg)
280 log.exception(msg)
281 h.flash(msg, category='error')
281 h.flash(msg, category='error')
282 return redirect(url('pullrequest_home', repo_name=repo_name))
282 return redirect(url('pullrequest_home', repo_name=repo_name))
283
283
284 raise HTTPFound(
284 raise HTTPFound(
285 h.route_path('pullrequest_show', repo_name=target_repo,
285 h.route_path('pullrequest_show', repo_name=target_repo,
286 pull_request_id=pull_request.pull_request_id))
286 pull_request_id=pull_request.pull_request_id))
287
287
288 @LoginRequired()
288 @LoginRequired()
289 @NotAnonymous()
289 @NotAnonymous()
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 'repository.admin')
291 'repository.admin')
292 @auth.CSRFRequired()
292 @auth.CSRFRequired()
293 @jsonify
293 @jsonify
294 def update(self, repo_name, pull_request_id):
294 def update(self, repo_name, pull_request_id):
295 pull_request_id = safe_int(pull_request_id)
295 pull_request_id = safe_int(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
297 # only owner or admin can update it
297 # only owner or admin can update it
298 allowed_to_update = PullRequestModel().check_user_update(
298 allowed_to_update = PullRequestModel().check_user_update(
299 pull_request, c.rhodecode_user)
299 pull_request, c.rhodecode_user)
300 if allowed_to_update:
300 if allowed_to_update:
301 controls = peppercorn.parse(request.POST.items())
301 controls = peppercorn.parse(request.POST.items())
302
302
303 if 'review_members' in controls:
303 if 'review_members' in controls:
304 self._update_reviewers(
304 self._update_reviewers(
305 pull_request_id, controls['review_members'],
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
306 pull_request.reviewer_data)
307 elif str2bool(request.POST.get('update_commits', 'false')):
307 elif str2bool(request.POST.get('update_commits', 'false')):
308 self._update_commits(pull_request)
308 self._update_commits(pull_request)
309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
310 self._edit_pull_request(pull_request)
310 self._edit_pull_request(pull_request)
311 else:
311 else:
312 raise HTTPBadRequest()
312 raise HTTPBadRequest()
313 return True
313 return True
314 raise HTTPForbidden()
314 raise HTTPForbidden()
315
315
316 def _edit_pull_request(self, pull_request):
316 def _edit_pull_request(self, pull_request):
317 try:
317 try:
318 PullRequestModel().edit(
318 PullRequestModel().edit(
319 pull_request, request.POST.get('title'),
319 pull_request, request.POST.get('title'),
320 request.POST.get('description'), c.rhodecode_user)
320 request.POST.get('description'), c.rhodecode_user)
321 except ValueError:
321 except ValueError:
322 msg = _(u'Cannot update closed pull requests.')
322 msg = _(u'Cannot update closed pull requests.')
323 h.flash(msg, category='error')
323 h.flash(msg, category='error')
324 return
324 return
325 else:
325 else:
326 Session().commit()
326 Session().commit()
327
327
328 msg = _(u'Pull request title & description updated.')
328 msg = _(u'Pull request title & description updated.')
329 h.flash(msg, category='success')
329 h.flash(msg, category='success')
330 return
330 return
331
331
332 def _update_commits(self, pull_request):
332 def _update_commits(self, pull_request):
333 resp = PullRequestModel().update_commits(pull_request)
333 resp = PullRequestModel().update_commits(pull_request)
334
334
335 if resp.executed:
335 if resp.executed:
336
336
337 if resp.target_changed and resp.source_changed:
337 if resp.target_changed and resp.source_changed:
338 changed = 'target and source repositories'
338 changed = 'target and source repositories'
339 elif resp.target_changed and not resp.source_changed:
339 elif resp.target_changed and not resp.source_changed:
340 changed = 'target repository'
340 changed = 'target repository'
341 elif not resp.target_changed and resp.source_changed:
341 elif not resp.target_changed and resp.source_changed:
342 changed = 'source repository'
342 changed = 'source repository'
343 else:
343 else:
344 changed = 'nothing'
344 changed = 'nothing'
345
345
346 msg = _(
346 msg = _(
347 u'Pull request updated to "{source_commit_id}" with '
347 u'Pull request updated to "{source_commit_id}" with '
348 u'{count_added} added, {count_removed} removed commits. '
348 u'{count_added} added, {count_removed} removed commits. '
349 u'Source of changes: {change_source}')
349 u'Source of changes: {change_source}')
350 msg = msg.format(
350 msg = msg.format(
351 source_commit_id=pull_request.source_ref_parts.commit_id,
351 source_commit_id=pull_request.source_ref_parts.commit_id,
352 count_added=len(resp.changes.added),
352 count_added=len(resp.changes.added),
353 count_removed=len(resp.changes.removed),
353 count_removed=len(resp.changes.removed),
354 change_source=changed)
354 change_source=changed)
355 h.flash(msg, category='success')
355 h.flash(msg, category='success')
356
356
357 registry = get_current_registry()
357 registry = get_current_registry()
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 if channelstream_config.get('enabled'):
360 if channelstream_config.get('enabled'):
361 message = msg + (
361 message = msg + (
362 ' - <a onclick="window.location.reload()">'
362 ' - <a onclick="window.location.reload()">'
363 '<strong>{}</strong></a>'.format(_('Reload page')))
363 '<strong>{}</strong></a>'.format(_('Reload page')))
364 channel = '/repo${}$/pr/{}'.format(
364 channel = '/repo${}$/pr/{}'.format(
365 pull_request.target_repo.repo_name,
365 pull_request.target_repo.repo_name,
366 pull_request.pull_request_id
366 pull_request.pull_request_id
367 )
367 )
368 payload = {
368 payload = {
369 'type': 'message',
369 'type': 'message',
370 'user': 'system',
370 'user': 'system',
371 'exclude_users': [request.user.username],
371 'exclude_users': [request.user.username],
372 'channel': channel,
372 'channel': channel,
373 'message': {
373 'message': {
374 'message': message,
374 'message': message,
375 'level': 'success',
375 'level': 'success',
376 'topic': '/notifications'
376 'topic': '/notifications'
377 }
377 }
378 }
378 }
379 channelstream_request(
379 channelstream_request(
380 channelstream_config, [payload], '/message',
380 channelstream_config, [payload], '/message',
381 raise_exc=False)
381 raise_exc=False)
382 else:
382 else:
383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
384 warning_reasons = [
384 warning_reasons = [
385 UpdateFailureReason.NO_CHANGE,
385 UpdateFailureReason.NO_CHANGE,
386 UpdateFailureReason.WRONG_REF_TYPE,
386 UpdateFailureReason.WRONG_REF_TYPE,
387 ]
387 ]
388 category = 'warning' if resp.reason in warning_reasons else 'error'
388 category = 'warning' if resp.reason in warning_reasons else 'error'
389 h.flash(msg, category=category)
389 h.flash(msg, category=category)
390
390
391 @auth.CSRFRequired()
391 @auth.CSRFRequired()
392 @LoginRequired()
392 @LoginRequired()
393 @NotAnonymous()
393 @NotAnonymous()
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 'repository.admin')
395 'repository.admin')
396 def merge(self, repo_name, pull_request_id):
396 def merge(self, repo_name, pull_request_id):
397 """
397 """
398 POST /{repo_name}/pull-request/{pull_request_id}
398 POST /{repo_name}/pull-request/{pull_request_id}
399
399
400 Merge will perform a server-side merge of the specified
400 Merge will perform a server-side merge of the specified
401 pull request, if the pull request is approved and mergeable.
401 pull request, if the pull request is approved and mergeable.
402 After successful merging, the pull request is automatically
402 After successful merging, the pull request is automatically
403 closed, with a relevant comment.
403 closed, with a relevant comment.
404 """
404 """
405 pull_request_id = safe_int(pull_request_id)
405 pull_request_id = safe_int(pull_request_id)
406 pull_request = PullRequest.get_or_404(pull_request_id)
406 pull_request = PullRequest.get_or_404(pull_request_id)
407 user = c.rhodecode_user
407 user = c.rhodecode_user
408
408
409 check = MergeCheck.validate(pull_request, user)
409 check = MergeCheck.validate(pull_request, user)
410 merge_possible = not check.failed
410 merge_possible = not check.failed
411
411
412 for err_type, error_msg in check.errors:
412 for err_type, error_msg in check.errors:
413 h.flash(error_msg, category=err_type)
413 h.flash(error_msg, category=err_type)
414
414
415 if merge_possible:
415 if merge_possible:
416 log.debug("Pre-conditions checked, trying to merge.")
416 log.debug("Pre-conditions checked, trying to merge.")
417 extras = vcs_operation_context(
417 extras = vcs_operation_context(
418 request.environ, repo_name=pull_request.target_repo.repo_name,
418 request.environ, repo_name=pull_request.target_repo.repo_name,
419 username=user.username, action='push',
419 username=user.username, action='push',
420 scm=pull_request.target_repo.repo_type)
420 scm=pull_request.target_repo.repo_type)
421 self._merge_pull_request(pull_request, user, extras)
421 self._merge_pull_request(pull_request, user, extras)
422
422
423 raise HTTPFound(
423 raise HTTPFound(
424 h.route_path('pullrequest_show',
424 h.route_path('pullrequest_show',
425 repo_name=pull_request.target_repo.repo_name,
425 repo_name=pull_request.target_repo.repo_name,
426 pull_request_id=pull_request.pull_request_id))
426 pull_request_id=pull_request.pull_request_id))
427
427
428 def _merge_pull_request(self, pull_request, user, extras):
428 def _merge_pull_request(self, pull_request, user, extras):
429 merge_resp = PullRequestModel().merge(
429 merge_resp = PullRequestModel().merge(
430 pull_request, user, extras=extras)
430 pull_request, user, extras=extras)
431
431
432 if merge_resp.executed:
432 if merge_resp.executed:
433 log.debug("The merge was successful, closing the pull request.")
433 log.debug("The merge was successful, closing the pull request.")
434 PullRequestModel().close_pull_request(
434 PullRequestModel().close_pull_request(
435 pull_request.pull_request_id, user)
435 pull_request.pull_request_id, user)
436 Session().commit()
436 Session().commit()
437 msg = _('Pull request was successfully merged and closed.')
437 msg = _('Pull request was successfully merged and closed.')
438 h.flash(msg, category='success')
438 h.flash(msg, category='success')
439 else:
439 else:
440 log.debug(
440 log.debug(
441 "The merge was not successful. Merge response: %s",
441 "The merge was not successful. Merge response: %s",
442 merge_resp)
442 merge_resp)
443 msg = PullRequestModel().merge_status_message(
443 msg = PullRequestModel().merge_status_message(
444 merge_resp.failure_reason)
444 merge_resp.failure_reason)
445 h.flash(msg, category='error')
445 h.flash(msg, category='error')
446
446
447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
448
448
449 get_default_reviewers_data, validate_default_reviewers = \
449 get_default_reviewers_data, validate_default_reviewers = \
450 PullRequestModel().get_reviewer_functions()
450 PullRequestModel().get_reviewer_functions()
451
451
452 try:
452 try:
453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
454 except ValueError as e:
454 except ValueError as e:
455 log.error('Reviewers Validation: {}'.format(e))
455 log.error('Reviewers Validation: {}'.format(e))
456 h.flash(e, category='error')
456 h.flash(e, category='error')
457 return
457 return
458
458
459 PullRequestModel().update_reviewers(
459 PullRequestModel().update_reviewers(
460 pull_request_id, reviewers, c.rhodecode_user)
460 pull_request_id, reviewers, c.rhodecode_user)
461 h.flash(_('Pull request reviewers updated.'), category='success')
461 h.flash(_('Pull request reviewers updated.'), category='success')
462 Session().commit()
462 Session().commit()
463
463
464 @LoginRequired()
464 @LoginRequired()
465 @NotAnonymous()
465 @NotAnonymous()
466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 'repository.admin')
467 'repository.admin')
468 @auth.CSRFRequired()
468 @auth.CSRFRequired()
469 @jsonify
469 @jsonify
470 def delete(self, repo_name, pull_request_id):
470 def delete(self, repo_name, pull_request_id):
471 pull_request_id = safe_int(pull_request_id)
471 pull_request_id = safe_int(pull_request_id)
472 pull_request = PullRequest.get_or_404(pull_request_id)
472 pull_request = PullRequest.get_or_404(pull_request_id)
473
473
474 pr_closed = pull_request.is_closed()
474 pr_closed = pull_request.is_closed()
475 allowed_to_delete = PullRequestModel().check_user_delete(
475 allowed_to_delete = PullRequestModel().check_user_delete(
476 pull_request, c.rhodecode_user) and not pr_closed
476 pull_request, c.rhodecode_user) and not pr_closed
477
477
478 # only owner can delete it !
478 # only owner can delete it !
479 if allowed_to_delete:
479 if allowed_to_delete:
480 PullRequestModel().delete(pull_request, c.rhodecode_user)
480 PullRequestModel().delete(pull_request, c.rhodecode_user)
481 Session().commit()
481 Session().commit()
482 h.flash(_('Successfully deleted pull request'),
482 h.flash(_('Successfully deleted pull request'),
483 category='success')
483 category='success')
484 return redirect(url('my_account_pullrequests'))
484 return redirect(url('my_account_pullrequests'))
485
485
486 h.flash(_('Your are not allowed to delete this pull request'),
486 h.flash(_('Your are not allowed to delete this pull request'),
487 category='error')
487 category='error')
488 raise HTTPForbidden()
488 raise HTTPForbidden()
489
489
490 def _get_pr_version(self, pull_request_id, version=None):
490 def _get_pr_version(self, pull_request_id, version=None):
491 pull_request_id = safe_int(pull_request_id)
491 pull_request_id = safe_int(pull_request_id)
492 at_version = None
492 at_version = None
493
493
494 if version and version == 'latest':
494 if version and version == 'latest':
495 pull_request_ver = PullRequest.get(pull_request_id)
495 pull_request_ver = PullRequest.get(pull_request_id)
496 pull_request_obj = pull_request_ver
496 pull_request_obj = pull_request_ver
497 _org_pull_request_obj = pull_request_obj
497 _org_pull_request_obj = pull_request_obj
498 at_version = 'latest'
498 at_version = 'latest'
499 elif version:
499 elif version:
500 pull_request_ver = PullRequestVersion.get_or_404(version)
500 pull_request_ver = PullRequestVersion.get_or_404(version)
501 pull_request_obj = pull_request_ver
501 pull_request_obj = pull_request_ver
502 _org_pull_request_obj = pull_request_ver.pull_request
502 _org_pull_request_obj = pull_request_ver.pull_request
503 at_version = pull_request_ver.pull_request_version_id
503 at_version = pull_request_ver.pull_request_version_id
504 else:
504 else:
505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
506 pull_request_id)
506 pull_request_id)
507
507
508 pull_request_display_obj = PullRequest.get_pr_display_object(
508 pull_request_display_obj = PullRequest.get_pr_display_object(
509 pull_request_obj, _org_pull_request_obj)
509 pull_request_obj, _org_pull_request_obj)
510
510
511 return _org_pull_request_obj, pull_request_obj, \
511 return _org_pull_request_obj, pull_request_obj, \
512 pull_request_display_obj, at_version
512 pull_request_display_obj, at_version
513
513
514 def _get_diffset(
514 def _get_diffset(
515 self, source_repo, source_ref_id, target_ref_id, target_commit,
515 self, source_repo, source_ref_id, target_ref_id, target_commit,
516 source_commit, diff_limit, file_limit, display_inline_comments):
516 source_commit, diff_limit, file_limit, display_inline_comments):
517 vcs_diff = PullRequestModel().get_diff(
517 vcs_diff = PullRequestModel().get_diff(
518 source_repo, source_ref_id, target_ref_id)
518 source_repo, source_ref_id, target_ref_id)
519
519
520 diff_processor = diffs.DiffProcessor(
520 diff_processor = diffs.DiffProcessor(
521 vcs_diff, format='newdiff', diff_limit=diff_limit,
521 vcs_diff, format='newdiff', diff_limit=diff_limit,
522 file_limit=file_limit, show_full_diff=c.fulldiff)
522 file_limit=file_limit, show_full_diff=c.fulldiff)
523
523
524 _parsed = diff_processor.prepare()
524 _parsed = diff_processor.prepare()
525
525
526 def _node_getter(commit):
526 def _node_getter(commit):
527 def get_node(fname):
527 def get_node(fname):
528 try:
528 try:
529 return commit.get_node(fname)
529 return commit.get_node(fname)
530 except NodeDoesNotExistError:
530 except NodeDoesNotExistError:
531 return None
531 return None
532
532
533 return get_node
533 return get_node
534
534
535 diffset = codeblocks.DiffSet(
535 diffset = codeblocks.DiffSet(
536 repo_name=c.repo_name,
536 repo_name=c.repo_name,
537 source_repo_name=c.source_repo.repo_name,
537 source_repo_name=c.source_repo.repo_name,
538 source_node_getter=_node_getter(target_commit),
538 source_node_getter=_node_getter(target_commit),
539 target_node_getter=_node_getter(source_commit),
539 target_node_getter=_node_getter(source_commit),
540 comments=display_inline_comments
540 comments=display_inline_comments
541 )
541 )
542 diffset = diffset.render_patchset(
542 diffset = diffset.render_patchset(
543 _parsed, target_commit.raw_id, source_commit.raw_id)
543 _parsed, target_commit.raw_id, source_commit.raw_id)
544
544
545 return diffset
545 return diffset
546
546
547 @LoginRequired()
547 @LoginRequired()
548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
549 'repository.admin')
549 'repository.admin')
550 def show(self, repo_name, pull_request_id):
550 def show(self, repo_name, pull_request_id):
551 pull_request_id = safe_int(pull_request_id)
551 pull_request_id = safe_int(pull_request_id)
552 version = request.GET.get('version')
552 version = request.GET.get('version')
553 from_version = request.GET.get('from_version') or version
553 from_version = request.GET.get('from_version') or version
554 merge_checks = request.GET.get('merge_checks')
554 merge_checks = request.GET.get('merge_checks')
555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
556
556
557 (pull_request_latest,
557 (pull_request_latest,
558 pull_request_at_ver,
558 pull_request_at_ver,
559 pull_request_display_obj,
559 pull_request_display_obj,
560 at_version) = self._get_pr_version(
560 at_version) = self._get_pr_version(
561 pull_request_id, version=version)
561 pull_request_id, version=version)
562 pr_closed = pull_request_latest.is_closed()
562 pr_closed = pull_request_latest.is_closed()
563
563
564 if pr_closed and (version or from_version):
564 if pr_closed and (version or from_version):
565 # not allow to browse versions
565 # not allow to browse versions
566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
567 pull_request_id=pull_request_id))
567 pull_request_id=pull_request_id))
568
568
569 versions = pull_request_display_obj.versions()
569 versions = pull_request_display_obj.versions()
570
570
571 c.at_version = at_version
571 c.at_version = at_version
572 c.at_version_num = (at_version
572 c.at_version_num = (at_version
573 if at_version and at_version != 'latest'
573 if at_version and at_version != 'latest'
574 else None)
574 else None)
575 c.at_version_pos = ChangesetComment.get_index_from_version(
575 c.at_version_pos = ChangesetComment.get_index_from_version(
576 c.at_version_num, versions)
576 c.at_version_num, versions)
577
577
578 (prev_pull_request_latest,
578 (prev_pull_request_latest,
579 prev_pull_request_at_ver,
579 prev_pull_request_at_ver,
580 prev_pull_request_display_obj,
580 prev_pull_request_display_obj,
581 prev_at_version) = self._get_pr_version(
581 prev_at_version) = self._get_pr_version(
582 pull_request_id, version=from_version)
582 pull_request_id, version=from_version)
583
583
584 c.from_version = prev_at_version
584 c.from_version = prev_at_version
585 c.from_version_num = (prev_at_version
585 c.from_version_num = (prev_at_version
586 if prev_at_version and prev_at_version != 'latest'
586 if prev_at_version and prev_at_version != 'latest'
587 else None)
587 else None)
588 c.from_version_pos = ChangesetComment.get_index_from_version(
588 c.from_version_pos = ChangesetComment.get_index_from_version(
589 c.from_version_num, versions)
589 c.from_version_num, versions)
590
590
591 # define if we're in COMPARE mode or VIEW at version mode
591 # define if we're in COMPARE mode or VIEW at version mode
592 compare = at_version != prev_at_version
592 compare = at_version != prev_at_version
593
593
594 # pull_requests repo_name we opened it against
594 # pull_requests repo_name we opened it against
595 # ie. target_repo must match
595 # ie. target_repo must match
596 if repo_name != pull_request_at_ver.target_repo.repo_name:
596 if repo_name != pull_request_at_ver.target_repo.repo_name:
597 raise HTTPNotFound
597 raise HTTPNotFound
598
598
599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
600 pull_request_at_ver)
600 pull_request_at_ver)
601
601
602 c.pull_request = pull_request_display_obj
602 c.pull_request = pull_request_display_obj
603 c.pull_request_latest = pull_request_latest
603 c.pull_request_latest = pull_request_latest
604
604
605 if compare or (at_version and not at_version == 'latest'):
605 if compare or (at_version and not at_version == 'latest'):
606 c.allowed_to_change_status = False
606 c.allowed_to_change_status = False
607 c.allowed_to_update = False
607 c.allowed_to_update = False
608 c.allowed_to_merge = False
608 c.allowed_to_merge = False
609 c.allowed_to_delete = False
609 c.allowed_to_delete = False
610 c.allowed_to_comment = False
610 c.allowed_to_comment = False
611 c.allowed_to_close = False
611 c.allowed_to_close = False
612 else:
612 else:
613 can_change_status = PullRequestModel().check_user_change_status(
613 can_change_status = PullRequestModel().check_user_change_status(
614 pull_request_at_ver, c.rhodecode_user)
614 pull_request_at_ver, c.rhodecode_user)
615 c.allowed_to_change_status = can_change_status and not pr_closed
615 c.allowed_to_change_status = can_change_status and not pr_closed
616
616
617 c.allowed_to_update = PullRequestModel().check_user_update(
617 c.allowed_to_update = PullRequestModel().check_user_update(
618 pull_request_latest, c.rhodecode_user) and not pr_closed
618 pull_request_latest, c.rhodecode_user) and not pr_closed
619 c.allowed_to_merge = PullRequestModel().check_user_merge(
619 c.allowed_to_merge = PullRequestModel().check_user_merge(
620 pull_request_latest, c.rhodecode_user) and not pr_closed
620 pull_request_latest, c.rhodecode_user) and not pr_closed
621 c.allowed_to_delete = PullRequestModel().check_user_delete(
621 c.allowed_to_delete = PullRequestModel().check_user_delete(
622 pull_request_latest, c.rhodecode_user) and not pr_closed
622 pull_request_latest, c.rhodecode_user) and not pr_closed
623 c.allowed_to_comment = not pr_closed
623 c.allowed_to_comment = not pr_closed
624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
625
625
626 c.forbid_adding_reviewers = False
626 c.forbid_adding_reviewers = False
627 c.forbid_author_to_review = False
627 c.forbid_author_to_review = False
628 c.forbid_commit_author_to_review = False
628 c.forbid_commit_author_to_review = False
629
629
630 if pull_request_latest.reviewer_data and \
630 if pull_request_latest.reviewer_data and \
631 'rules' in pull_request_latest.reviewer_data:
631 'rules' in pull_request_latest.reviewer_data:
632 rules = pull_request_latest.reviewer_data['rules'] or {}
632 rules = pull_request_latest.reviewer_data['rules'] or {}
633 try:
633 try:
634 c.forbid_adding_reviewers = rules.get(
634 c.forbid_adding_reviewers = rules.get(
635 'forbid_adding_reviewers')
635 'forbid_adding_reviewers')
636 c.forbid_author_to_review = rules.get(
636 c.forbid_author_to_review = rules.get(
637 'forbid_author_to_review')
637 'forbid_author_to_review')
638 c.forbid_commit_author_to_review = rules.get(
638 c.forbid_commit_author_to_review = rules.get(
639 'forbid_commit_author_to_review')
639 'forbid_commit_author_to_review')
640 except Exception:
640 except Exception:
641 pass
641 pass
642
642
643 # check merge capabilities
643 # check merge capabilities
644 _merge_check = MergeCheck.validate(
644 _merge_check = MergeCheck.validate(
645 pull_request_latest, user=c.rhodecode_user)
645 pull_request_latest, user=c.rhodecode_user)
646 c.pr_merge_errors = _merge_check.error_details
646 c.pr_merge_errors = _merge_check.error_details
647 c.pr_merge_possible = not _merge_check.failed
647 c.pr_merge_possible = not _merge_check.failed
648 c.pr_merge_message = _merge_check.merge_msg
648 c.pr_merge_message = _merge_check.merge_msg
649
649
650 c.pull_request_review_status = _merge_check.review_status
650 c.pull_request_review_status = _merge_check.review_status
651 if merge_checks:
651 if merge_checks:
652 return render('/pullrequests/pullrequest_merge_checks.mako')
652 return render('/pullrequests/pullrequest_merge_checks.mako')
653
653
654 comments_model = CommentsModel()
654 comments_model = CommentsModel()
655
655
656 # reviewers and statuses
656 # reviewers and statuses
657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
659
659
660 # GENERAL COMMENTS with versions #
660 # GENERAL COMMENTS with versions #
661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
662 q = q.order_by(ChangesetComment.comment_id.asc())
662 q = q.order_by(ChangesetComment.comment_id.asc())
663 general_comments = q
663 general_comments = q
664
664
665 # pick comments we want to render at current version
665 # pick comments we want to render at current version
666 c.comment_versions = comments_model.aggregate_comments(
666 c.comment_versions = comments_model.aggregate_comments(
667 general_comments, versions, c.at_version_num)
667 general_comments, versions, c.at_version_num)
668 c.comments = c.comment_versions[c.at_version_num]['until']
668 c.comments = c.comment_versions[c.at_version_num]['until']
669
669
670 # INLINE COMMENTS with versions #
670 # INLINE COMMENTS with versions #
671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
672 q = q.order_by(ChangesetComment.comment_id.asc())
672 q = q.order_by(ChangesetComment.comment_id.asc())
673 inline_comments = q
673 inline_comments = q
674
674
675 c.inline_versions = comments_model.aggregate_comments(
675 c.inline_versions = comments_model.aggregate_comments(
676 inline_comments, versions, c.at_version_num, inline=True)
676 inline_comments, versions, c.at_version_num, inline=True)
677
677
678 # inject latest version
678 # inject latest version
679 latest_ver = PullRequest.get_pr_display_object(
679 latest_ver = PullRequest.get_pr_display_object(
680 pull_request_latest, pull_request_latest)
680 pull_request_latest, pull_request_latest)
681
681
682 c.versions = versions + [latest_ver]
682 c.versions = versions + [latest_ver]
683
683
684 # if we use version, then do not show later comments
684 # if we use version, then do not show later comments
685 # than current version
685 # than current version
686 display_inline_comments = collections.defaultdict(
686 display_inline_comments = collections.defaultdict(
687 lambda: collections.defaultdict(list))
687 lambda: collections.defaultdict(list))
688 for co in inline_comments:
688 for co in inline_comments:
689 if c.at_version_num:
689 if c.at_version_num:
690 # pick comments that are at least UPTO given version, so we
690 # pick comments that are at least UPTO given version, so we
691 # don't render comments for higher version
691 # don't render comments for higher version
692 should_render = co.pull_request_version_id and \
692 should_render = co.pull_request_version_id and \
693 co.pull_request_version_id <= c.at_version_num
693 co.pull_request_version_id <= c.at_version_num
694 else:
694 else:
695 # showing all, for 'latest'
695 # showing all, for 'latest'
696 should_render = True
696 should_render = True
697
697
698 if should_render:
698 if should_render:
699 display_inline_comments[co.f_path][co.line_no].append(co)
699 display_inline_comments[co.f_path][co.line_no].append(co)
700
700
701 # load diff data into template context, if we use compare mode then
701 # load diff data into template context, if we use compare mode then
702 # diff is calculated based on changes between versions of PR
702 # diff is calculated based on changes between versions of PR
703
703
704 source_repo = pull_request_at_ver.source_repo
704 source_repo = pull_request_at_ver.source_repo
705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
706
706
707 target_repo = pull_request_at_ver.target_repo
707 target_repo = pull_request_at_ver.target_repo
708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
709
709
710 if compare:
710 if compare:
711 # in compare switch the diff base to latest commit from prev version
711 # in compare switch the diff base to latest commit from prev version
712 target_ref_id = prev_pull_request_display_obj.revisions[0]
712 target_ref_id = prev_pull_request_display_obj.revisions[0]
713
713
714 # despite opening commits for bookmarks/branches/tags, we always
714 # despite opening commits for bookmarks/branches/tags, we always
715 # convert this to rev to prevent changes after bookmark or branch change
715 # convert this to rev to prevent changes after bookmark or branch change
716 c.source_ref_type = 'rev'
716 c.source_ref_type = 'rev'
717 c.source_ref = source_ref_id
717 c.source_ref = source_ref_id
718
718
719 c.target_ref_type = 'rev'
719 c.target_ref_type = 'rev'
720 c.target_ref = target_ref_id
720 c.target_ref = target_ref_id
721
721
722 c.source_repo = source_repo
722 c.source_repo = source_repo
723 c.target_repo = target_repo
723 c.target_repo = target_repo
724
724
725 # diff_limit is the old behavior, will cut off the whole diff
725 # diff_limit is the old behavior, will cut off the whole diff
726 # if the limit is applied otherwise will just hide the
726 # if the limit is applied otherwise will just hide the
727 # big files from the front-end
727 # big files from the front-end
728 diff_limit = self.cut_off_limit_diff
728 diff_limit = self.cut_off_limit_diff
729 file_limit = self.cut_off_limit_file
729 file_limit = self.cut_off_limit_file
730
730
731 c.commit_ranges = []
731 c.commit_ranges = []
732 source_commit = EmptyCommit()
732 source_commit = EmptyCommit()
733 target_commit = EmptyCommit()
733 target_commit = EmptyCommit()
734 c.missing_requirements = False
734 c.missing_requirements = False
735
735
736 source_scm = source_repo.scm_instance()
736 source_scm = source_repo.scm_instance()
737 target_scm = target_repo.scm_instance()
737 target_scm = target_repo.scm_instance()
738
738
739 # try first shadow repo, fallback to regular repo
739 # try first shadow repo, fallback to regular repo
740 try:
740 try:
741 commits_source_repo = pull_request_latest.get_shadow_repo()
741 commits_source_repo = pull_request_latest.get_shadow_repo()
742 except Exception:
742 except Exception:
743 log.debug('Failed to get shadow repo', exc_info=True)
743 log.debug('Failed to get shadow repo', exc_info=True)
744 commits_source_repo = source_scm
744 commits_source_repo = source_scm
745
745
746 c.commits_source_repo = commits_source_repo
746 c.commits_source_repo = commits_source_repo
747 commit_cache = {}
747 commit_cache = {}
748 try:
748 try:
749 pre_load = ["author", "branch", "date", "message"]
749 pre_load = ["author", "branch", "date", "message"]
750 show_revs = pull_request_at_ver.revisions
750 show_revs = pull_request_at_ver.revisions
751 for rev in show_revs:
751 for rev in show_revs:
752 comm = commits_source_repo.get_commit(
752 comm = commits_source_repo.get_commit(
753 commit_id=rev, pre_load=pre_load)
753 commit_id=rev, pre_load=pre_load)
754 c.commit_ranges.append(comm)
754 c.commit_ranges.append(comm)
755 commit_cache[comm.raw_id] = comm
755 commit_cache[comm.raw_id] = comm
756
756
757 # Order here matters, we first need to get target, and then
757 # Order here matters, we first need to get target, and then
758 # the source
758 # the source
759 target_commit = commits_source_repo.get_commit(
759 target_commit = commits_source_repo.get_commit(
760 commit_id=safe_str(target_ref_id))
760 commit_id=safe_str(target_ref_id))
761
761
762 source_commit = commits_source_repo.get_commit(
762 source_commit = commits_source_repo.get_commit(
763 commit_id=safe_str(source_ref_id))
763 commit_id=safe_str(source_ref_id))
764
764
765 except CommitDoesNotExistError:
765 except CommitDoesNotExistError:
766 log.warning(
766 log.warning(
767 'Failed to get commit from `{}` repo'.format(
767 'Failed to get commit from `{}` repo'.format(
768 commits_source_repo), exc_info=True)
768 commits_source_repo), exc_info=True)
769 except RepositoryRequirementError:
769 except RepositoryRequirementError:
770 log.warning(
770 log.warning(
771 'Failed to get all required data from repo', exc_info=True)
771 'Failed to get all required data from repo', exc_info=True)
772 c.missing_requirements = True
772 c.missing_requirements = True
773
773
774 c.ancestor = None # set it to None, to hide it from PR view
774 c.ancestor = None # set it to None, to hide it from PR view
775
775
776 try:
776 try:
777 ancestor_id = source_scm.get_common_ancestor(
777 ancestor_id = source_scm.get_common_ancestor(
778 source_commit.raw_id, target_commit.raw_id, target_scm)
778 source_commit.raw_id, target_commit.raw_id, target_scm)
779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
780 except Exception:
780 except Exception:
781 c.ancestor_commit = None
781 c.ancestor_commit = None
782
782
783 c.statuses = source_repo.statuses(
783 c.statuses = source_repo.statuses(
784 [x.raw_id for x in c.commit_ranges])
784 [x.raw_id for x in c.commit_ranges])
785
785
786 # auto collapse if we have more than limit
786 # auto collapse if we have more than limit
787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
789 c.compare_mode = compare
789 c.compare_mode = compare
790
790
791 c.missing_commits = False
791 c.missing_commits = False
792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
793 or source_commit == target_commit):
793 or source_commit == target_commit):
794
794
795 c.missing_commits = True
795 c.missing_commits = True
796 else:
796 else:
797
797
798 c.diffset = self._get_diffset(
798 c.diffset = self._get_diffset(
799 commits_source_repo, source_ref_id, target_ref_id,
799 commits_source_repo, source_ref_id, target_ref_id,
800 target_commit, source_commit,
800 target_commit, source_commit,
801 diff_limit, file_limit, display_inline_comments)
801 diff_limit, file_limit, display_inline_comments)
802
802
803 c.limited_diff = c.diffset.limited_diff
803 c.limited_diff = c.diffset.limited_diff
804
804
805 # calculate removed files that are bound to comments
805 # calculate removed files that are bound to comments
806 comment_deleted_files = [
806 comment_deleted_files = [
807 fname for fname in display_inline_comments
807 fname for fname in display_inline_comments
808 if fname not in c.diffset.file_stats]
808 if fname not in c.diffset.file_stats]
809
809
810 c.deleted_files_comments = collections.defaultdict(dict)
810 c.deleted_files_comments = collections.defaultdict(dict)
811 for fname, per_line_comments in display_inline_comments.items():
811 for fname, per_line_comments in display_inline_comments.items():
812 if fname in comment_deleted_files:
812 if fname in comment_deleted_files:
813 c.deleted_files_comments[fname]['stats'] = 0
813 c.deleted_files_comments[fname]['stats'] = 0
814 c.deleted_files_comments[fname]['comments'] = list()
814 c.deleted_files_comments[fname]['comments'] = list()
815 for lno, comments in per_line_comments.items():
815 for lno, comments in per_line_comments.items():
816 c.deleted_files_comments[fname]['comments'].extend(
816 c.deleted_files_comments[fname]['comments'].extend(
817 comments)
817 comments)
818
818
819 # this is a hack to properly display links, when creating PR, the
819 # this is a hack to properly display links, when creating PR, the
820 # compare view and others uses different notation, and
820 # compare view and others uses different notation, and
821 # compare_commits.mako renders links based on the target_repo.
821 # compare_commits.mako renders links based on the target_repo.
822 # We need to swap that here to generate it properly on the html side
822 # We need to swap that here to generate it properly on the html side
823 c.target_repo = c.source_repo
823 c.target_repo = c.source_repo
824
824
825 c.commit_statuses = ChangesetStatus.STATUSES
825 c.commit_statuses = ChangesetStatus.STATUSES
826
826
827 c.show_version_changes = not pr_closed
827 c.show_version_changes = not pr_closed
828 if c.show_version_changes:
828 if c.show_version_changes:
829 cur_obj = pull_request_at_ver
829 cur_obj = pull_request_at_ver
830 prev_obj = prev_pull_request_at_ver
830 prev_obj = prev_pull_request_at_ver
831
831
832 old_commit_ids = prev_obj.revisions
832 old_commit_ids = prev_obj.revisions
833 new_commit_ids = cur_obj.revisions
833 new_commit_ids = cur_obj.revisions
834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
835 old_commit_ids, new_commit_ids)
835 old_commit_ids, new_commit_ids)
836 c.commit_changes_summary = commit_changes
836 c.commit_changes_summary = commit_changes
837
837
838 # calculate the diff for commits between versions
838 # calculate the diff for commits between versions
839 c.commit_changes = []
839 c.commit_changes = []
840 mark = lambda cs, fw: list(
840 mark = lambda cs, fw: list(
841 h.itertools.izip_longest([], cs, fillvalue=fw))
841 h.itertools.izip_longest([], cs, fillvalue=fw))
842 for c_type, raw_id in mark(commit_changes.added, 'a') \
842 for c_type, raw_id in mark(commit_changes.added, 'a') \
843 + mark(commit_changes.removed, 'r') \
843 + mark(commit_changes.removed, 'r') \
844 + mark(commit_changes.common, 'c'):
844 + mark(commit_changes.common, 'c'):
845
845
846 if raw_id in commit_cache:
846 if raw_id in commit_cache:
847 commit = commit_cache[raw_id]
847 commit = commit_cache[raw_id]
848 else:
848 else:
849 try:
849 try:
850 commit = commits_source_repo.get_commit(raw_id)
850 commit = commits_source_repo.get_commit(raw_id)
851 except CommitDoesNotExistError:
851 except CommitDoesNotExistError:
852 # in case we fail extracting still use "dummy" commit
852 # in case we fail extracting still use "dummy" commit
853 # for display in commit diff
853 # for display in commit diff
854 commit = h.AttributeDict(
854 commit = h.AttributeDict(
855 {'raw_id': raw_id,
855 {'raw_id': raw_id,
856 'message': 'EMPTY or MISSING COMMIT'})
856 'message': 'EMPTY or MISSING COMMIT'})
857 c.commit_changes.append([c_type, commit])
857 c.commit_changes.append([c_type, commit])
858
858
859 # current user review statuses for each version
859 # current user review statuses for each version
860 c.review_versions = {}
860 c.review_versions = {}
861 if c.rhodecode_user.user_id in allowed_reviewers:
861 if c.rhodecode_user.user_id in allowed_reviewers:
862 for co in general_comments:
862 for co in general_comments:
863 if co.author.user_id == c.rhodecode_user.user_id:
863 if co.author.user_id == c.rhodecode_user.user_id:
864 # each comment has a status change
864 # each comment has a status change
865 status = co.status_change
865 status = co.status_change
866 if status:
866 if status:
867 _ver_pr = status[0].comment.pull_request_version_id
867 _ver_pr = status[0].comment.pull_request_version_id
868 c.review_versions[_ver_pr] = status[0]
868 c.review_versions[_ver_pr] = status[0]
869
869
870 return render('/pullrequests/pullrequest_show.mako')
870 return render('/pullrequests/pullrequest_show.mako')
871
871
872 @LoginRequired()
872 @LoginRequired()
873 @NotAnonymous()
873 @NotAnonymous()
874 @HasRepoPermissionAnyDecorator(
874 @HasRepoPermissionAnyDecorator(
875 'repository.read', 'repository.write', 'repository.admin')
875 'repository.read', 'repository.write', 'repository.admin')
876 @auth.CSRFRequired()
876 @auth.CSRFRequired()
877 @jsonify
877 @jsonify
878 def comment(self, repo_name, pull_request_id):
878 def comment(self, repo_name, pull_request_id):
879 pull_request_id = safe_int(pull_request_id)
879 pull_request_id = safe_int(pull_request_id)
880 pull_request = PullRequest.get_or_404(pull_request_id)
880 pull_request = PullRequest.get_or_404(pull_request_id)
881 if pull_request.is_closed():
881 if pull_request.is_closed():
882 log.debug('comment: forbidden because pull request is closed')
882 log.debug('comment: forbidden because pull request is closed')
883 raise HTTPForbidden()
883 raise HTTPForbidden()
884
884
885 status = request.POST.get('changeset_status', None)
885 status = request.POST.get('changeset_status', None)
886 text = request.POST.get('text')
886 text = request.POST.get('text')
887 comment_type = request.POST.get('comment_type')
887 comment_type = request.POST.get('comment_type')
888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
889 close_pull_request = request.POST.get('close_pull_request')
889 close_pull_request = request.POST.get('close_pull_request')
890
890
891 # the logic here should work like following, if we submit close
891 # the logic here should work like following, if we submit close
892 # pr comment, use `close_pull_request_with_comment` function
892 # pr comment, use `close_pull_request_with_comment` function
893 # else handle regular comment logic
893 # else handle regular comment logic
894 user = c.rhodecode_user
894 user = c.rhodecode_user
895 repo = c.rhodecode_db_repo
895 repo = c.rhodecode_db_repo
896
896
897 if close_pull_request:
897 if close_pull_request:
898 # only owner or admin or person with write permissions
898 # only owner or admin or person with write permissions
899 allowed_to_close = PullRequestModel().check_user_update(
899 allowed_to_close = PullRequestModel().check_user_update(
900 pull_request, c.rhodecode_user)
900 pull_request, c.rhodecode_user)
901 if not allowed_to_close:
901 if not allowed_to_close:
902 log.debug('comment: forbidden because not allowed to close '
902 log.debug('comment: forbidden because not allowed to close '
903 'pull request %s', pull_request_id)
903 'pull request %s', pull_request_id)
904 raise HTTPForbidden()
904 raise HTTPForbidden()
905 comment, status = PullRequestModel().close_pull_request_with_comment(
905 comment, status = PullRequestModel().close_pull_request_with_comment(
906 pull_request, user, repo, message=text)
906 pull_request, user, repo, message=text)
907 Session().flush()
907 Session().flush()
908 events.trigger(
908 events.trigger(
909 events.PullRequestCommentEvent(pull_request, comment))
909 events.PullRequestCommentEvent(pull_request, comment))
910
910
911 else:
911 else:
912 # regular comment case, could be inline, or one with status.
912 # regular comment case, could be inline, or one with status.
913 # for that one we check also permissions
913 # for that one we check also permissions
914
914
915 allowed_to_change_status = PullRequestModel().check_user_change_status(
915 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 pull_request, c.rhodecode_user)
916 pull_request, c.rhodecode_user)
917
917
918 if status and allowed_to_change_status:
918 if status and allowed_to_change_status:
919 message = (_('Status change %(transition_icon)s %(status)s')
919 message = (_('Status change %(transition_icon)s %(status)s')
920 % {'transition_icon': '>',
920 % {'transition_icon': '>',
921 'status': ChangesetStatus.get_status_lbl(status)})
921 'status': ChangesetStatus.get_status_lbl(status)})
922 text = text or message
922 text = text or message
923
923
924 comment = CommentsModel().create(
924 comment = CommentsModel().create(
925 text=text,
925 text=text,
926 repo=c.rhodecode_db_repo.repo_id,
926 repo=c.rhodecode_db_repo.repo_id,
927 user=c.rhodecode_user.user_id,
927 user=c.rhodecode_user.user_id,
928 pull_request=pull_request_id,
928 pull_request=pull_request_id,
929 f_path=request.POST.get('f_path'),
929 f_path=request.POST.get('f_path'),
930 line_no=request.POST.get('line'),
930 line_no=request.POST.get('line'),
931 status_change=(ChangesetStatus.get_status_lbl(status)
931 status_change=(ChangesetStatus.get_status_lbl(status)
932 if status and allowed_to_change_status else None),
932 if status and allowed_to_change_status else None),
933 status_change_type=(status
933 status_change_type=(status
934 if status and allowed_to_change_status else None),
934 if status and allowed_to_change_status else None),
935 comment_type=comment_type,
935 comment_type=comment_type,
936 resolves_comment_id=resolves_comment_id
936 resolves_comment_id=resolves_comment_id
937 )
937 )
938
938
939 if allowed_to_change_status:
939 if allowed_to_change_status:
940 # calculate old status before we change it
940 # calculate old status before we change it
941 old_calculated_status = pull_request.calculated_review_status()
941 old_calculated_status = pull_request.calculated_review_status()
942
942
943 # get status if set !
943 # get status if set !
944 if status:
944 if status:
945 ChangesetStatusModel().set_status(
945 ChangesetStatusModel().set_status(
946 c.rhodecode_db_repo.repo_id,
946 c.rhodecode_db_repo.repo_id,
947 status,
947 status,
948 c.rhodecode_user.user_id,
948 c.rhodecode_user.user_id,
949 comment,
949 comment,
950 pull_request=pull_request_id
950 pull_request=pull_request_id
951 )
951 )
952
952
953 Session().flush()
953 Session().flush()
954 events.trigger(
954 events.trigger(
955 events.PullRequestCommentEvent(pull_request, comment))
955 events.PullRequestCommentEvent(pull_request, comment))
956
956
957 # we now calculate the status of pull request, and based on that
957 # we now calculate the status of pull request, and based on that
958 # calculation we set the commits status
958 # calculation we set the commits status
959 calculated_status = pull_request.calculated_review_status()
959 calculated_status = pull_request.calculated_review_status()
960 if old_calculated_status != calculated_status:
960 if old_calculated_status != calculated_status:
961 PullRequestModel()._trigger_pull_request_hook(
961 PullRequestModel()._trigger_pull_request_hook(
962 pull_request, c.rhodecode_user, 'review_status_change')
962 pull_request, c.rhodecode_user, 'review_status_change')
963
963
964 Session().commit()
964 Session().commit()
965
965
966 if not request.is_xhr:
966 if not request.is_xhr:
967 raise HTTPFound(
967 raise HTTPFound(
968 h.route_path('pullrequest_show',
968 h.route_path('pullrequest_show',
969 repo_name=repo_name,
969 repo_name=repo_name,
970 pull_request_id=pull_request_id))
970 pull_request_id=pull_request_id))
971
971
972 data = {
972 data = {
973 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
973 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
974 }
974 }
975 if comment:
975 if comment:
976 c.co = comment
976 c.co = comment
977 rendered_comment = render('changeset/changeset_comment_block.mako')
977 rendered_comment = render('changeset/changeset_comment_block.mako')
978 data.update(comment.get_dict())
978 data.update(comment.get_dict())
979 data.update({'rendered_text': rendered_comment})
979 data.update({'rendered_text': rendered_comment})
980
980
981 return data
981 return data
982
982
983 @LoginRequired()
983 @LoginRequired()
984 @NotAnonymous()
984 @NotAnonymous()
985 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
985 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
986 'repository.admin')
986 'repository.admin')
987 @auth.CSRFRequired()
987 @auth.CSRFRequired()
988 @jsonify
988 @jsonify
989 def delete_comment(self, repo_name, comment_id):
989 def delete_comment(self, repo_name, comment_id):
990 comment = ChangesetComment.get_or_404(safe_int(comment_id))
990 comment = ChangesetComment.get_or_404(comment_id)
991 if not comment:
991 if not comment:
992 log.debug('Comment with id:%s not found, skipping', comment_id)
992 log.debug('Comment with id:%s not found, skipping', comment_id)
993 # comment already deleted in another call probably
993 # comment already deleted in another call probably
994 return True
994 return True
995
995
996 if comment.pull_request.is_closed():
996 if comment.pull_request.is_closed():
997 # don't allow deleting comments on closed pull request
997 # don't allow deleting comments on closed pull request
998 raise HTTPForbidden()
998 raise HTTPForbidden()
999
999
1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1001 super_admin = h.HasPermissionAny('hg.admin')()
1001 super_admin = h.HasPermissionAny('hg.admin')()
1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1004 comment_repo_admin = is_repo_admin and is_repo_comment
1004 comment_repo_admin = is_repo_admin and is_repo_comment
1005
1005
1006 if super_admin or comment_owner or comment_repo_admin:
1006 if super_admin or comment_owner or comment_repo_admin:
1007 old_calculated_status = comment.pull_request.calculated_review_status()
1007 old_calculated_status = comment.pull_request.calculated_review_status()
1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1009 Session().commit()
1009 Session().commit()
1010 calculated_status = comment.pull_request.calculated_review_status()
1010 calculated_status = comment.pull_request.calculated_review_status()
1011 if old_calculated_status != calculated_status:
1011 if old_calculated_status != calculated_status:
1012 PullRequestModel()._trigger_pull_request_hook(
1012 PullRequestModel()._trigger_pull_request_hook(
1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1014 return True
1014 return True
1015 else:
1015 else:
1016 log.warning('No permissions for user %s to delete comment_id: %s',
1016 log.warning('No permissions for user %s to delete comment_id: %s',
1017 c.rhodecode_user, comment_id)
1017 c.rhodecode_user, comment_id)
1018 raise HTTPNotFound()
1018 raise HTTPNotFound()
@@ -1,4122 +1,4113 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from zope.cachedescriptors.property import Lazy as LazyProperty
45 from zope.cachedescriptors.property import Lazy as LazyProperty
46
46
47 from pyramid.threadlocal import get_current_request
47 from pyramid.threadlocal import get_current_request
48
48
49 from rhodecode.translation import _
49 from rhodecode.translation import _
50 from rhodecode.lib.vcs import get_vcs_instance
50 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.utils2 import (
52 from rhodecode.lib.utils2 import (
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 glob2re, StrictAttributeDict, cleaned_uri)
55 glob2re, StrictAttributeDict, cleaned_uri)
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.ext_json import json
57 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.caching_query import FromCache
58 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.encrypt import AESCipher
59 from rhodecode.lib.encrypt import AESCipher
60
60
61 from rhodecode.model.meta import Base, Session
61 from rhodecode.model.meta import Base, Session
62
62
63 URL_SEP = '/'
63 URL_SEP = '/'
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66 # =============================================================================
66 # =============================================================================
67 # BASE CLASSES
67 # BASE CLASSES
68 # =============================================================================
68 # =============================================================================
69
69
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # beaker.session.secret if first is not set.
71 # beaker.session.secret if first is not set.
72 # and initialized at environment.py
72 # and initialized at environment.py
73 ENCRYPTION_KEY = None
73 ENCRYPTION_KEY = None
74
74
75 # used to sort permissions by types, '#' used here is not allowed to be in
75 # used to sort permissions by types, '#' used here is not allowed to be in
76 # usernames, and it's very early in sorted string.printable table.
76 # usernames, and it's very early in sorted string.printable table.
77 PERMISSION_TYPE_SORT = {
77 PERMISSION_TYPE_SORT = {
78 'admin': '####',
78 'admin': '####',
79 'write': '###',
79 'write': '###',
80 'read': '##',
80 'read': '##',
81 'none': '#',
81 'none': '#',
82 }
82 }
83
83
84
84
85 def display_sort(obj):
85 def display_sort(obj):
86 """
86 """
87 Sort function used to sort permissions in .permissions() function of
87 Sort function used to sort permissions in .permissions() function of
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 of all other resources
89 of all other resources
90 """
90 """
91
91
92 if obj.username == User.DEFAULT_USER:
92 if obj.username == User.DEFAULT_USER:
93 return '#####'
93 return '#####'
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 return prefix + obj.username
95 return prefix + obj.username
96
96
97
97
98 def _hash_key(k):
98 def _hash_key(k):
99 return md5_safe(k)
99 return md5_safe(k)
100
100
101
101
102 class EncryptedTextValue(TypeDecorator):
102 class EncryptedTextValue(TypeDecorator):
103 """
103 """
104 Special column for encrypted long text data, use like::
104 Special column for encrypted long text data, use like::
105
105
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107
107
108 This column is intelligent so if value is in unencrypted form it return
108 This column is intelligent so if value is in unencrypted form it return
109 unencrypted form, but on save it always encrypts
109 unencrypted form, but on save it always encrypts
110 """
110 """
111 impl = Text
111 impl = Text
112
112
113 def process_bind_param(self, value, dialect):
113 def process_bind_param(self, value, dialect):
114 if not value:
114 if not value:
115 return value
115 return value
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 # protect against double encrypting if someone manually starts
117 # protect against double encrypting if someone manually starts
118 # doing
118 # doing
119 raise ValueError('value needs to be in unencrypted format, ie. '
119 raise ValueError('value needs to be in unencrypted format, ie. '
120 'not starting with enc$aes')
120 'not starting with enc$aes')
121 return 'enc$aes_hmac$%s' % AESCipher(
121 return 'enc$aes_hmac$%s' % AESCipher(
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
123
123
124 def process_result_value(self, value, dialect):
124 def process_result_value(self, value, dialect):
125 import rhodecode
125 import rhodecode
126
126
127 if not value:
127 if not value:
128 return value
128 return value
129
129
130 parts = value.split('$', 3)
130 parts = value.split('$', 3)
131 if not len(parts) == 3:
131 if not len(parts) == 3:
132 # probably not encrypted values
132 # probably not encrypted values
133 return value
133 return value
134 else:
134 else:
135 if parts[0] != 'enc':
135 if parts[0] != 'enc':
136 # parts ok but without our header ?
136 # parts ok but without our header ?
137 return value
137 return value
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 'rhodecode.encrypted_values.strict') or True)
139 'rhodecode.encrypted_values.strict') or True)
140 # at that stage we know it's our encryption
140 # at that stage we know it's our encryption
141 if parts[1] == 'aes':
141 if parts[1] == 'aes':
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 elif parts[1] == 'aes_hmac':
143 elif parts[1] == 'aes_hmac':
144 decrypted_data = AESCipher(
144 decrypted_data = AESCipher(
145 ENCRYPTION_KEY, hmac=True,
145 ENCRYPTION_KEY, hmac=True,
146 strict_verification=enc_strict_mode).decrypt(parts[2])
146 strict_verification=enc_strict_mode).decrypt(parts[2])
147 else:
147 else:
148 raise ValueError(
148 raise ValueError(
149 'Encryption type part is wrong, must be `aes` '
149 'Encryption type part is wrong, must be `aes` '
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 return decrypted_data
151 return decrypted_data
152
152
153
153
154 class BaseModel(object):
154 class BaseModel(object):
155 """
155 """
156 Base Model for all classes
156 Base Model for all classes
157 """
157 """
158
158
159 @classmethod
159 @classmethod
160 def _get_keys(cls):
160 def _get_keys(cls):
161 """return column names for this model """
161 """return column names for this model """
162 return class_mapper(cls).c.keys()
162 return class_mapper(cls).c.keys()
163
163
164 def get_dict(self):
164 def get_dict(self):
165 """
165 """
166 return dict with keys and values corresponding
166 return dict with keys and values corresponding
167 to this model data """
167 to this model data """
168
168
169 d = {}
169 d = {}
170 for k in self._get_keys():
170 for k in self._get_keys():
171 d[k] = getattr(self, k)
171 d[k] = getattr(self, k)
172
172
173 # also use __json__() if present to get additional fields
173 # also use __json__() if present to get additional fields
174 _json_attr = getattr(self, '__json__', None)
174 _json_attr = getattr(self, '__json__', None)
175 if _json_attr:
175 if _json_attr:
176 # update with attributes from __json__
176 # update with attributes from __json__
177 if callable(_json_attr):
177 if callable(_json_attr):
178 _json_attr = _json_attr()
178 _json_attr = _json_attr()
179 for k, val in _json_attr.iteritems():
179 for k, val in _json_attr.iteritems():
180 d[k] = val
180 d[k] = val
181 return d
181 return d
182
182
183 def get_appstruct(self):
183 def get_appstruct(self):
184 """return list with keys and values tuples corresponding
184 """return list with keys and values tuples corresponding
185 to this model data """
185 to this model data """
186
186
187 l = []
187 l = []
188 for k in self._get_keys():
188 for k in self._get_keys():
189 l.append((k, getattr(self, k),))
189 l.append((k, getattr(self, k),))
190 return l
190 return l
191
191
192 def populate_obj(self, populate_dict):
192 def populate_obj(self, populate_dict):
193 """populate model with data from given populate_dict"""
193 """populate model with data from given populate_dict"""
194
194
195 for k in self._get_keys():
195 for k in self._get_keys():
196 if k in populate_dict:
196 if k in populate_dict:
197 setattr(self, k, populate_dict[k])
197 setattr(self, k, populate_dict[k])
198
198
199 @classmethod
199 @classmethod
200 def query(cls):
200 def query(cls):
201 return Session().query(cls)
201 return Session().query(cls)
202
202
203 @classmethod
203 @classmethod
204 def get(cls, id_):
204 def get(cls, id_):
205 if id_:
205 if id_:
206 return cls.query().get(id_)
206 return cls.query().get(id_)
207
207
208 @classmethod
208 @classmethod
209 def get_or_404(cls, id_, pyramid_exc=False):
209 def get_or_404(cls, id_):
210 if pyramid_exc:
210 from pyramid.httpexceptions import HTTPNotFound
211 # NOTE(marcink): backward compat, once migration to pyramid
212 # this should only use pyramid exceptions
213 from pyramid.httpexceptions import HTTPNotFound
214 else:
215 from webob.exc import HTTPNotFound
216
211
217 try:
212 try:
218 id_ = int(id_)
213 id_ = int(id_)
219 except (TypeError, ValueError):
214 except (TypeError, ValueError):
220 raise HTTPNotFound
215 raise HTTPNotFound()
221
216
222 res = cls.query().get(id_)
217 res = cls.query().get(id_)
223 if not res:
218 if not res:
224 raise HTTPNotFound
219 raise HTTPNotFound()
225 return res
220 return res
226
221
227 @classmethod
222 @classmethod
228 def getAll(cls):
223 def getAll(cls):
229 # deprecated and left for backward compatibility
224 # deprecated and left for backward compatibility
230 return cls.get_all()
225 return cls.get_all()
231
226
232 @classmethod
227 @classmethod
233 def get_all(cls):
228 def get_all(cls):
234 return cls.query().all()
229 return cls.query().all()
235
230
236 @classmethod
231 @classmethod
237 def delete(cls, id_):
232 def delete(cls, id_):
238 obj = cls.query().get(id_)
233 obj = cls.query().get(id_)
239 Session().delete(obj)
234 Session().delete(obj)
240
235
241 @classmethod
236 @classmethod
242 def identity_cache(cls, session, attr_name, value):
237 def identity_cache(cls, session, attr_name, value):
243 exist_in_session = []
238 exist_in_session = []
244 for (item_cls, pkey), instance in session.identity_map.items():
239 for (item_cls, pkey), instance in session.identity_map.items():
245 if cls == item_cls and getattr(instance, attr_name) == value:
240 if cls == item_cls and getattr(instance, attr_name) == value:
246 exist_in_session.append(instance)
241 exist_in_session.append(instance)
247 if exist_in_session:
242 if exist_in_session:
248 if len(exist_in_session) == 1:
243 if len(exist_in_session) == 1:
249 return exist_in_session[0]
244 return exist_in_session[0]
250 log.exception(
245 log.exception(
251 'multiple objects with attr %s and '
246 'multiple objects with attr %s and '
252 'value %s found with same name: %r',
247 'value %s found with same name: %r',
253 attr_name, value, exist_in_session)
248 attr_name, value, exist_in_session)
254
249
255 def __repr__(self):
250 def __repr__(self):
256 if hasattr(self, '__unicode__'):
251 if hasattr(self, '__unicode__'):
257 # python repr needs to return str
252 # python repr needs to return str
258 try:
253 try:
259 return safe_str(self.__unicode__())
254 return safe_str(self.__unicode__())
260 except UnicodeDecodeError:
255 except UnicodeDecodeError:
261 pass
256 pass
262 return '<DB:%s>' % (self.__class__.__name__)
257 return '<DB:%s>' % (self.__class__.__name__)
263
258
264
259
265 class RhodeCodeSetting(Base, BaseModel):
260 class RhodeCodeSetting(Base, BaseModel):
266 __tablename__ = 'rhodecode_settings'
261 __tablename__ = 'rhodecode_settings'
267 __table_args__ = (
262 __table_args__ = (
268 UniqueConstraint('app_settings_name'),
263 UniqueConstraint('app_settings_name'),
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 {'extend_existing': True, 'mysql_engine': 'InnoDB',
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
271 )
266 )
272
267
273 SETTINGS_TYPES = {
268 SETTINGS_TYPES = {
274 'str': safe_str,
269 'str': safe_str,
275 'int': safe_int,
270 'int': safe_int,
276 'unicode': safe_unicode,
271 'unicode': safe_unicode,
277 'bool': str2bool,
272 'bool': str2bool,
278 'list': functools.partial(aslist, sep=',')
273 'list': functools.partial(aslist, sep=',')
279 }
274 }
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
281 GLOBAL_CONF_KEY = 'app_settings'
276 GLOBAL_CONF_KEY = 'app_settings'
282
277
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
287
282
288 def __init__(self, key='', val='', type='unicode'):
283 def __init__(self, key='', val='', type='unicode'):
289 self.app_settings_name = key
284 self.app_settings_name = key
290 self.app_settings_type = type
285 self.app_settings_type = type
291 self.app_settings_value = val
286 self.app_settings_value = val
292
287
293 @validates('_app_settings_value')
288 @validates('_app_settings_value')
294 def validate_settings_value(self, key, val):
289 def validate_settings_value(self, key, val):
295 assert type(val) == unicode
290 assert type(val) == unicode
296 return val
291 return val
297
292
298 @hybrid_property
293 @hybrid_property
299 def app_settings_value(self):
294 def app_settings_value(self):
300 v = self._app_settings_value
295 v = self._app_settings_value
301 _type = self.app_settings_type
296 _type = self.app_settings_type
302 if _type:
297 if _type:
303 _type = self.app_settings_type.split('.')[0]
298 _type = self.app_settings_type.split('.')[0]
304 # decode the encrypted value
299 # decode the encrypted value
305 if 'encrypted' in self.app_settings_type:
300 if 'encrypted' in self.app_settings_type:
306 cipher = EncryptedTextValue()
301 cipher = EncryptedTextValue()
307 v = safe_unicode(cipher.process_result_value(v, None))
302 v = safe_unicode(cipher.process_result_value(v, None))
308
303
309 converter = self.SETTINGS_TYPES.get(_type) or \
304 converter = self.SETTINGS_TYPES.get(_type) or \
310 self.SETTINGS_TYPES['unicode']
305 self.SETTINGS_TYPES['unicode']
311 return converter(v)
306 return converter(v)
312
307
313 @app_settings_value.setter
308 @app_settings_value.setter
314 def app_settings_value(self, val):
309 def app_settings_value(self, val):
315 """
310 """
316 Setter that will always make sure we use unicode in app_settings_value
311 Setter that will always make sure we use unicode in app_settings_value
317
312
318 :param val:
313 :param val:
319 """
314 """
320 val = safe_unicode(val)
315 val = safe_unicode(val)
321 # encode the encrypted value
316 # encode the encrypted value
322 if 'encrypted' in self.app_settings_type:
317 if 'encrypted' in self.app_settings_type:
323 cipher = EncryptedTextValue()
318 cipher = EncryptedTextValue()
324 val = safe_unicode(cipher.process_bind_param(val, None))
319 val = safe_unicode(cipher.process_bind_param(val, None))
325 self._app_settings_value = val
320 self._app_settings_value = val
326
321
327 @hybrid_property
322 @hybrid_property
328 def app_settings_type(self):
323 def app_settings_type(self):
329 return self._app_settings_type
324 return self._app_settings_type
330
325
331 @app_settings_type.setter
326 @app_settings_type.setter
332 def app_settings_type(self, val):
327 def app_settings_type(self, val):
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 if val.split('.')[0] not in self.SETTINGS_TYPES:
334 raise Exception('type must be one of %s got %s'
329 raise Exception('type must be one of %s got %s'
335 % (self.SETTINGS_TYPES.keys(), val))
330 % (self.SETTINGS_TYPES.keys(), val))
336 self._app_settings_type = val
331 self._app_settings_type = val
337
332
338 def __unicode__(self):
333 def __unicode__(self):
339 return u"<%s('%s:%s[%s]')>" % (
334 return u"<%s('%s:%s[%s]')>" % (
340 self.__class__.__name__,
335 self.__class__.__name__,
341 self.app_settings_name, self.app_settings_value,
336 self.app_settings_name, self.app_settings_value,
342 self.app_settings_type
337 self.app_settings_type
343 )
338 )
344
339
345
340
346 class RhodeCodeUi(Base, BaseModel):
341 class RhodeCodeUi(Base, BaseModel):
347 __tablename__ = 'rhodecode_ui'
342 __tablename__ = 'rhodecode_ui'
348 __table_args__ = (
343 __table_args__ = (
349 UniqueConstraint('ui_key'),
344 UniqueConstraint('ui_key'),
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 {'extend_existing': True, 'mysql_engine': 'InnoDB',
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
352 )
347 )
353
348
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 HOOK_REPO_SIZE = 'changegroup.repo_size'
355 # HG
350 # HG
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
357 HOOK_PULL = 'outgoing.pull_logger'
352 HOOK_PULL = 'outgoing.pull_logger'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
354 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
360 HOOK_PUSH = 'changegroup.push_logger'
355 HOOK_PUSH = 'changegroup.push_logger'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
356 HOOK_PUSH_KEY = 'pushkey.key_push'
362
357
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
358 # TODO: johbo: Unify way how hooks are configured for git and hg,
364 # git part is currently hardcoded.
359 # git part is currently hardcoded.
365
360
366 # SVN PATTERNS
361 # SVN PATTERNS
367 SVN_BRANCH_ID = 'vcs_svn_branch'
362 SVN_BRANCH_ID = 'vcs_svn_branch'
368 SVN_TAG_ID = 'vcs_svn_tag'
363 SVN_TAG_ID = 'vcs_svn_tag'
369
364
370 ui_id = Column(
365 ui_id = Column(
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
366 "ui_id", Integer(), nullable=False, unique=True, default=None,
372 primary_key=True)
367 primary_key=True)
373 ui_section = Column(
368 ui_section = Column(
374 "ui_section", String(255), nullable=True, unique=None, default=None)
369 "ui_section", String(255), nullable=True, unique=None, default=None)
375 ui_key = Column(
370 ui_key = Column(
376 "ui_key", String(255), nullable=True, unique=None, default=None)
371 "ui_key", String(255), nullable=True, unique=None, default=None)
377 ui_value = Column(
372 ui_value = Column(
378 "ui_value", String(255), nullable=True, unique=None, default=None)
373 "ui_value", String(255), nullable=True, unique=None, default=None)
379 ui_active = Column(
374 ui_active = Column(
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
381
376
382 def __repr__(self):
377 def __repr__(self):
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
384 self.ui_key, self.ui_value)
379 self.ui_key, self.ui_value)
385
380
386
381
387 class RepoRhodeCodeSetting(Base, BaseModel):
382 class RepoRhodeCodeSetting(Base, BaseModel):
388 __tablename__ = 'repo_rhodecode_settings'
383 __tablename__ = 'repo_rhodecode_settings'
389 __table_args__ = (
384 __table_args__ = (
390 UniqueConstraint(
385 UniqueConstraint(
391 'app_settings_name', 'repository_id',
386 'app_settings_name', 'repository_id',
392 name='uq_repo_rhodecode_setting_name_repo_id'),
387 name='uq_repo_rhodecode_setting_name_repo_id'),
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
395 )
390 )
396
391
397 repository_id = Column(
392 repository_id = Column(
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
399 nullable=False)
394 nullable=False)
400 app_settings_id = Column(
395 app_settings_id = Column(
401 "app_settings_id", Integer(), nullable=False, unique=True,
396 "app_settings_id", Integer(), nullable=False, unique=True,
402 default=None, primary_key=True)
397 default=None, primary_key=True)
403 app_settings_name = Column(
398 app_settings_name = Column(
404 "app_settings_name", String(255), nullable=True, unique=None,
399 "app_settings_name", String(255), nullable=True, unique=None,
405 default=None)
400 default=None)
406 _app_settings_value = Column(
401 _app_settings_value = Column(
407 "app_settings_value", String(4096), nullable=True, unique=None,
402 "app_settings_value", String(4096), nullable=True, unique=None,
408 default=None)
403 default=None)
409 _app_settings_type = Column(
404 _app_settings_type = Column(
410 "app_settings_type", String(255), nullable=True, unique=None,
405 "app_settings_type", String(255), nullable=True, unique=None,
411 default=None)
406 default=None)
412
407
413 repository = relationship('Repository')
408 repository = relationship('Repository')
414
409
415 def __init__(self, repository_id, key='', val='', type='unicode'):
410 def __init__(self, repository_id, key='', val='', type='unicode'):
416 self.repository_id = repository_id
411 self.repository_id = repository_id
417 self.app_settings_name = key
412 self.app_settings_name = key
418 self.app_settings_type = type
413 self.app_settings_type = type
419 self.app_settings_value = val
414 self.app_settings_value = val
420
415
421 @validates('_app_settings_value')
416 @validates('_app_settings_value')
422 def validate_settings_value(self, key, val):
417 def validate_settings_value(self, key, val):
423 assert type(val) == unicode
418 assert type(val) == unicode
424 return val
419 return val
425
420
426 @hybrid_property
421 @hybrid_property
427 def app_settings_value(self):
422 def app_settings_value(self):
428 v = self._app_settings_value
423 v = self._app_settings_value
429 type_ = self.app_settings_type
424 type_ = self.app_settings_type
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
432 return converter(v)
427 return converter(v)
433
428
434 @app_settings_value.setter
429 @app_settings_value.setter
435 def app_settings_value(self, val):
430 def app_settings_value(self, val):
436 """
431 """
437 Setter that will always make sure we use unicode in app_settings_value
432 Setter that will always make sure we use unicode in app_settings_value
438
433
439 :param val:
434 :param val:
440 """
435 """
441 self._app_settings_value = safe_unicode(val)
436 self._app_settings_value = safe_unicode(val)
442
437
443 @hybrid_property
438 @hybrid_property
444 def app_settings_type(self):
439 def app_settings_type(self):
445 return self._app_settings_type
440 return self._app_settings_type
446
441
447 @app_settings_type.setter
442 @app_settings_type.setter
448 def app_settings_type(self, val):
443 def app_settings_type(self, val):
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
450 if val not in SETTINGS_TYPES:
445 if val not in SETTINGS_TYPES:
451 raise Exception('type must be one of %s got %s'
446 raise Exception('type must be one of %s got %s'
452 % (SETTINGS_TYPES.keys(), val))
447 % (SETTINGS_TYPES.keys(), val))
453 self._app_settings_type = val
448 self._app_settings_type = val
454
449
455 def __unicode__(self):
450 def __unicode__(self):
456 return u"<%s('%s:%s:%s[%s]')>" % (
451 return u"<%s('%s:%s:%s[%s]')>" % (
457 self.__class__.__name__, self.repository.repo_name,
452 self.__class__.__name__, self.repository.repo_name,
458 self.app_settings_name, self.app_settings_value,
453 self.app_settings_name, self.app_settings_value,
459 self.app_settings_type
454 self.app_settings_type
460 )
455 )
461
456
462
457
463 class RepoRhodeCodeUi(Base, BaseModel):
458 class RepoRhodeCodeUi(Base, BaseModel):
464 __tablename__ = 'repo_rhodecode_ui'
459 __tablename__ = 'repo_rhodecode_ui'
465 __table_args__ = (
460 __table_args__ = (
466 UniqueConstraint(
461 UniqueConstraint(
467 'repository_id', 'ui_section', 'ui_key',
462 'repository_id', 'ui_section', 'ui_key',
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
471 )
466 )
472
467
473 repository_id = Column(
468 repository_id = Column(
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
475 nullable=False)
470 nullable=False)
476 ui_id = Column(
471 ui_id = Column(
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
472 "ui_id", Integer(), nullable=False, unique=True, default=None,
478 primary_key=True)
473 primary_key=True)
479 ui_section = Column(
474 ui_section = Column(
480 "ui_section", String(255), nullable=True, unique=None, default=None)
475 "ui_section", String(255), nullable=True, unique=None, default=None)
481 ui_key = Column(
476 ui_key = Column(
482 "ui_key", String(255), nullable=True, unique=None, default=None)
477 "ui_key", String(255), nullable=True, unique=None, default=None)
483 ui_value = Column(
478 ui_value = Column(
484 "ui_value", String(255), nullable=True, unique=None, default=None)
479 "ui_value", String(255), nullable=True, unique=None, default=None)
485 ui_active = Column(
480 ui_active = Column(
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
487
482
488 repository = relationship('Repository')
483 repository = relationship('Repository')
489
484
490 def __repr__(self):
485 def __repr__(self):
491 return '<%s[%s:%s]%s=>%s]>' % (
486 return '<%s[%s:%s]%s=>%s]>' % (
492 self.__class__.__name__, self.repository.repo_name,
487 self.__class__.__name__, self.repository.repo_name,
493 self.ui_section, self.ui_key, self.ui_value)
488 self.ui_section, self.ui_key, self.ui_value)
494
489
495
490
496 class User(Base, BaseModel):
491 class User(Base, BaseModel):
497 __tablename__ = 'users'
492 __tablename__ = 'users'
498 __table_args__ = (
493 __table_args__ = (
499 UniqueConstraint('username'), UniqueConstraint('email'),
494 UniqueConstraint('username'), UniqueConstraint('email'),
500 Index('u_username_idx', 'username'),
495 Index('u_username_idx', 'username'),
501 Index('u_email_idx', 'email'),
496 Index('u_email_idx', 'email'),
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
504 )
499 )
505 DEFAULT_USER = 'default'
500 DEFAULT_USER = 'default'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
508
503
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
505 username = Column("username", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
506 password = Column("password", String(255), nullable=True, unique=None, default=None)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
513 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
519
514
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
517 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
526
521
527 user_log = relationship('UserLog')
522 user_log = relationship('UserLog')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
529
524
530 repositories = relationship('Repository')
525 repositories = relationship('Repository')
531 repository_groups = relationship('RepoGroup')
526 repository_groups = relationship('RepoGroup')
532 user_groups = relationship('UserGroup')
527 user_groups = relationship('UserGroup')
533
528
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
536
531
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
540
535
541 group_member = relationship('UserGroupMember', cascade='all')
536 group_member = relationship('UserGroupMember', cascade='all')
542
537
543 notifications = relationship('UserNotification', cascade='all')
538 notifications = relationship('UserNotification', cascade='all')
544 # notifications assigned to this user
539 # notifications assigned to this user
545 user_created_notifications = relationship('Notification', cascade='all')
540 user_created_notifications = relationship('Notification', cascade='all')
546 # comments created by this user
541 # comments created by this user
547 user_comments = relationship('ChangesetComment', cascade='all')
542 user_comments = relationship('ChangesetComment', cascade='all')
548 # user profile extra info
543 # user profile extra info
549 user_emails = relationship('UserEmailMap', cascade='all')
544 user_emails = relationship('UserEmailMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
545 user_ip_map = relationship('UserIpMap', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
552 # gists
547 # gists
553 user_gists = relationship('Gist', cascade='all')
548 user_gists = relationship('Gist', cascade='all')
554 # user pull requests
549 # user pull requests
555 user_pull_requests = relationship('PullRequest', cascade='all')
550 user_pull_requests = relationship('PullRequest', cascade='all')
556 # external identities
551 # external identities
557 extenal_identities = relationship(
552 extenal_identities = relationship(
558 'ExternalIdentity',
553 'ExternalIdentity',
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
560 cascade='all')
555 cascade='all')
561
556
562 def __unicode__(self):
557 def __unicode__(self):
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
564 self.user_id, self.username)
559 self.user_id, self.username)
565
560
566 @hybrid_property
561 @hybrid_property
567 def email(self):
562 def email(self):
568 return self._email
563 return self._email
569
564
570 @email.setter
565 @email.setter
571 def email(self, val):
566 def email(self, val):
572 self._email = val.lower() if val else None
567 self._email = val.lower() if val else None
573
568
574 @hybrid_property
569 @hybrid_property
575 def first_name(self):
570 def first_name(self):
576 from rhodecode.lib import helpers as h
571 from rhodecode.lib import helpers as h
577 if self.name:
572 if self.name:
578 return h.escape(self.name)
573 return h.escape(self.name)
579 return self.name
574 return self.name
580
575
581 @hybrid_property
576 @hybrid_property
582 def last_name(self):
577 def last_name(self):
583 from rhodecode.lib import helpers as h
578 from rhodecode.lib import helpers as h
584 if self.lastname:
579 if self.lastname:
585 return h.escape(self.lastname)
580 return h.escape(self.lastname)
586 return self.lastname
581 return self.lastname
587
582
588 @hybrid_property
583 @hybrid_property
589 def api_key(self):
584 def api_key(self):
590 """
585 """
591 Fetch if exist an auth-token with role ALL connected to this user
586 Fetch if exist an auth-token with role ALL connected to this user
592 """
587 """
593 user_auth_token = UserApiKeys.query()\
588 user_auth_token = UserApiKeys.query()\
594 .filter(UserApiKeys.user_id == self.user_id)\
589 .filter(UserApiKeys.user_id == self.user_id)\
595 .filter(or_(UserApiKeys.expires == -1,
590 .filter(or_(UserApiKeys.expires == -1,
596 UserApiKeys.expires >= time.time()))\
591 UserApiKeys.expires >= time.time()))\
597 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
592 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
598 if user_auth_token:
593 if user_auth_token:
599 user_auth_token = user_auth_token.api_key
594 user_auth_token = user_auth_token.api_key
600
595
601 return user_auth_token
596 return user_auth_token
602
597
603 @api_key.setter
598 @api_key.setter
604 def api_key(self, val):
599 def api_key(self, val):
605 # don't allow to set API key this is deprecated for now
600 # don't allow to set API key this is deprecated for now
606 self._api_key = None
601 self._api_key = None
607
602
608 @property
603 @property
609 def reviewer_pull_requests(self):
604 def reviewer_pull_requests(self):
610 return PullRequestReviewers.query() \
605 return PullRequestReviewers.query() \
611 .options(joinedload(PullRequestReviewers.pull_request)) \
606 .options(joinedload(PullRequestReviewers.pull_request)) \
612 .filter(PullRequestReviewers.user_id == self.user_id) \
607 .filter(PullRequestReviewers.user_id == self.user_id) \
613 .all()
608 .all()
614
609
615 @property
610 @property
616 def firstname(self):
611 def firstname(self):
617 # alias for future
612 # alias for future
618 return self.name
613 return self.name
619
614
620 @property
615 @property
621 def emails(self):
616 def emails(self):
622 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
617 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
623 return [self.email] + [x.email for x in other]
618 return [self.email] + [x.email for x in other]
624
619
625 @property
620 @property
626 def auth_tokens(self):
621 def auth_tokens(self):
627 auth_tokens = self.get_auth_tokens()
622 auth_tokens = self.get_auth_tokens()
628 return [x.api_key for x in auth_tokens]
623 return [x.api_key for x in auth_tokens]
629
624
630 def get_auth_tokens(self):
625 def get_auth_tokens(self):
631 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
626 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
632
627
633 @property
628 @property
634 def feed_token(self):
629 def feed_token(self):
635 return self.get_feed_token()
630 return self.get_feed_token()
636
631
637 def get_feed_token(self):
632 def get_feed_token(self):
638 feed_tokens = UserApiKeys.query()\
633 feed_tokens = UserApiKeys.query()\
639 .filter(UserApiKeys.user == self)\
634 .filter(UserApiKeys.user == self)\
640 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
635 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
641 .all()
636 .all()
642 if feed_tokens:
637 if feed_tokens:
643 return feed_tokens[0].api_key
638 return feed_tokens[0].api_key
644 return 'NO_FEED_TOKEN_AVAILABLE'
639 return 'NO_FEED_TOKEN_AVAILABLE'
645
640
646 @classmethod
641 @classmethod
647 def extra_valid_auth_tokens(cls, user, role=None):
642 def extra_valid_auth_tokens(cls, user, role=None):
648 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
643 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
649 .filter(or_(UserApiKeys.expires == -1,
644 .filter(or_(UserApiKeys.expires == -1,
650 UserApiKeys.expires >= time.time()))
645 UserApiKeys.expires >= time.time()))
651 if role:
646 if role:
652 tokens = tokens.filter(or_(UserApiKeys.role == role,
647 tokens = tokens.filter(or_(UserApiKeys.role == role,
653 UserApiKeys.role == UserApiKeys.ROLE_ALL))
648 UserApiKeys.role == UserApiKeys.ROLE_ALL))
654 return tokens.all()
649 return tokens.all()
655
650
656 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
651 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
657 from rhodecode.lib import auth
652 from rhodecode.lib import auth
658
653
659 log.debug('Trying to authenticate user: %s via auth-token, '
654 log.debug('Trying to authenticate user: %s via auth-token, '
660 'and roles: %s', self, roles)
655 'and roles: %s', self, roles)
661
656
662 if not auth_token:
657 if not auth_token:
663 return False
658 return False
664
659
665 crypto_backend = auth.crypto_backend()
660 crypto_backend = auth.crypto_backend()
666
661
667 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
662 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
668 tokens_q = UserApiKeys.query()\
663 tokens_q = UserApiKeys.query()\
669 .filter(UserApiKeys.user_id == self.user_id)\
664 .filter(UserApiKeys.user_id == self.user_id)\
670 .filter(or_(UserApiKeys.expires == -1,
665 .filter(or_(UserApiKeys.expires == -1,
671 UserApiKeys.expires >= time.time()))
666 UserApiKeys.expires >= time.time()))
672
667
673 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
668 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
674
669
675 plain_tokens = []
670 plain_tokens = []
676 hash_tokens = []
671 hash_tokens = []
677
672
678 for token in tokens_q.all():
673 for token in tokens_q.all():
679 # verify scope first
674 # verify scope first
680 if token.repo_id:
675 if token.repo_id:
681 # token has a scope, we need to verify it
676 # token has a scope, we need to verify it
682 if scope_repo_id != token.repo_id:
677 if scope_repo_id != token.repo_id:
683 log.debug(
678 log.debug(
684 'Scope mismatch: token has a set repo scope: %s, '
679 'Scope mismatch: token has a set repo scope: %s, '
685 'and calling scope is:%s, skipping further checks',
680 'and calling scope is:%s, skipping further checks',
686 token.repo, scope_repo_id)
681 token.repo, scope_repo_id)
687 # token has a scope, and it doesn't match, skip token
682 # token has a scope, and it doesn't match, skip token
688 continue
683 continue
689
684
690 if token.api_key.startswith(crypto_backend.ENC_PREF):
685 if token.api_key.startswith(crypto_backend.ENC_PREF):
691 hash_tokens.append(token.api_key)
686 hash_tokens.append(token.api_key)
692 else:
687 else:
693 plain_tokens.append(token.api_key)
688 plain_tokens.append(token.api_key)
694
689
695 is_plain_match = auth_token in plain_tokens
690 is_plain_match = auth_token in plain_tokens
696 if is_plain_match:
691 if is_plain_match:
697 return True
692 return True
698
693
699 for hashed in hash_tokens:
694 for hashed in hash_tokens:
700 # TODO(marcink): this is expensive to calculate, but most secure
695 # TODO(marcink): this is expensive to calculate, but most secure
701 match = crypto_backend.hash_check(auth_token, hashed)
696 match = crypto_backend.hash_check(auth_token, hashed)
702 if match:
697 if match:
703 return True
698 return True
704
699
705 return False
700 return False
706
701
707 @property
702 @property
708 def ip_addresses(self):
703 def ip_addresses(self):
709 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
704 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
710 return [x.ip_addr for x in ret]
705 return [x.ip_addr for x in ret]
711
706
712 @property
707 @property
713 def username_and_name(self):
708 def username_and_name(self):
714 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
709 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
715
710
716 @property
711 @property
717 def username_or_name_or_email(self):
712 def username_or_name_or_email(self):
718 full_name = self.full_name if self.full_name is not ' ' else None
713 full_name = self.full_name if self.full_name is not ' ' else None
719 return self.username or full_name or self.email
714 return self.username or full_name or self.email
720
715
721 @property
716 @property
722 def full_name(self):
717 def full_name(self):
723 return '%s %s' % (self.first_name, self.last_name)
718 return '%s %s' % (self.first_name, self.last_name)
724
719
725 @property
720 @property
726 def full_name_or_username(self):
721 def full_name_or_username(self):
727 return ('%s %s' % (self.first_name, self.last_name)
722 return ('%s %s' % (self.first_name, self.last_name)
728 if (self.first_name and self.last_name) else self.username)
723 if (self.first_name and self.last_name) else self.username)
729
724
730 @property
725 @property
731 def full_contact(self):
726 def full_contact(self):
732 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
727 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
733
728
734 @property
729 @property
735 def short_contact(self):
730 def short_contact(self):
736 return '%s %s' % (self.first_name, self.last_name)
731 return '%s %s' % (self.first_name, self.last_name)
737
732
738 @property
733 @property
739 def is_admin(self):
734 def is_admin(self):
740 return self.admin
735 return self.admin
741
736
742 @property
737 @property
743 def AuthUser(self):
738 def AuthUser(self):
744 """
739 """
745 Returns instance of AuthUser for this user
740 Returns instance of AuthUser for this user
746 """
741 """
747 from rhodecode.lib.auth import AuthUser
742 from rhodecode.lib.auth import AuthUser
748 return AuthUser(user_id=self.user_id, username=self.username)
743 return AuthUser(user_id=self.user_id, username=self.username)
749
744
750 @hybrid_property
745 @hybrid_property
751 def user_data(self):
746 def user_data(self):
752 if not self._user_data:
747 if not self._user_data:
753 return {}
748 return {}
754
749
755 try:
750 try:
756 return json.loads(self._user_data)
751 return json.loads(self._user_data)
757 except TypeError:
752 except TypeError:
758 return {}
753 return {}
759
754
760 @user_data.setter
755 @user_data.setter
761 def user_data(self, val):
756 def user_data(self, val):
762 if not isinstance(val, dict):
757 if not isinstance(val, dict):
763 raise Exception('user_data must be dict, got %s' % type(val))
758 raise Exception('user_data must be dict, got %s' % type(val))
764 try:
759 try:
765 self._user_data = json.dumps(val)
760 self._user_data = json.dumps(val)
766 except Exception:
761 except Exception:
767 log.error(traceback.format_exc())
762 log.error(traceback.format_exc())
768
763
769 @classmethod
764 @classmethod
770 def get_by_username(cls, username, case_insensitive=False,
765 def get_by_username(cls, username, case_insensitive=False,
771 cache=False, identity_cache=False):
766 cache=False, identity_cache=False):
772 session = Session()
767 session = Session()
773
768
774 if case_insensitive:
769 if case_insensitive:
775 q = cls.query().filter(
770 q = cls.query().filter(
776 func.lower(cls.username) == func.lower(username))
771 func.lower(cls.username) == func.lower(username))
777 else:
772 else:
778 q = cls.query().filter(cls.username == username)
773 q = cls.query().filter(cls.username == username)
779
774
780 if cache:
775 if cache:
781 if identity_cache:
776 if identity_cache:
782 val = cls.identity_cache(session, 'username', username)
777 val = cls.identity_cache(session, 'username', username)
783 if val:
778 if val:
784 return val
779 return val
785 else:
780 else:
786 cache_key = "get_user_by_name_%s" % _hash_key(username)
781 cache_key = "get_user_by_name_%s" % _hash_key(username)
787 q = q.options(
782 q = q.options(
788 FromCache("sql_cache_short", cache_key))
783 FromCache("sql_cache_short", cache_key))
789
784
790 return q.scalar()
785 return q.scalar()
791
786
792 @classmethod
787 @classmethod
793 def get_by_auth_token(cls, auth_token, cache=False):
788 def get_by_auth_token(cls, auth_token, cache=False):
794 q = UserApiKeys.query()\
789 q = UserApiKeys.query()\
795 .filter(UserApiKeys.api_key == auth_token)\
790 .filter(UserApiKeys.api_key == auth_token)\
796 .filter(or_(UserApiKeys.expires == -1,
791 .filter(or_(UserApiKeys.expires == -1,
797 UserApiKeys.expires >= time.time()))
792 UserApiKeys.expires >= time.time()))
798 if cache:
793 if cache:
799 q = q.options(
794 q = q.options(
800 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
795 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
801
796
802 match = q.first()
797 match = q.first()
803 if match:
798 if match:
804 return match.user
799 return match.user
805
800
806 @classmethod
801 @classmethod
807 def get_by_email(cls, email, case_insensitive=False, cache=False):
802 def get_by_email(cls, email, case_insensitive=False, cache=False):
808
803
809 if case_insensitive:
804 if case_insensitive:
810 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
805 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
811
806
812 else:
807 else:
813 q = cls.query().filter(cls.email == email)
808 q = cls.query().filter(cls.email == email)
814
809
815 email_key = _hash_key(email)
810 email_key = _hash_key(email)
816 if cache:
811 if cache:
817 q = q.options(
812 q = q.options(
818 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
813 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
819
814
820 ret = q.scalar()
815 ret = q.scalar()
821 if ret is None:
816 if ret is None:
822 q = UserEmailMap.query()
817 q = UserEmailMap.query()
823 # try fetching in alternate email map
818 # try fetching in alternate email map
824 if case_insensitive:
819 if case_insensitive:
825 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
820 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
826 else:
821 else:
827 q = q.filter(UserEmailMap.email == email)
822 q = q.filter(UserEmailMap.email == email)
828 q = q.options(joinedload(UserEmailMap.user))
823 q = q.options(joinedload(UserEmailMap.user))
829 if cache:
824 if cache:
830 q = q.options(
825 q = q.options(
831 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
826 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
832 ret = getattr(q.scalar(), 'user', None)
827 ret = getattr(q.scalar(), 'user', None)
833
828
834 return ret
829 return ret
835
830
836 @classmethod
831 @classmethod
837 def get_from_cs_author(cls, author):
832 def get_from_cs_author(cls, author):
838 """
833 """
839 Tries to get User objects out of commit author string
834 Tries to get User objects out of commit author string
840
835
841 :param author:
836 :param author:
842 """
837 """
843 from rhodecode.lib.helpers import email, author_name
838 from rhodecode.lib.helpers import email, author_name
844 # Valid email in the attribute passed, see if they're in the system
839 # Valid email in the attribute passed, see if they're in the system
845 _email = email(author)
840 _email = email(author)
846 if _email:
841 if _email:
847 user = cls.get_by_email(_email, case_insensitive=True)
842 user = cls.get_by_email(_email, case_insensitive=True)
848 if user:
843 if user:
849 return user
844 return user
850 # Maybe we can match by username?
845 # Maybe we can match by username?
851 _author = author_name(author)
846 _author = author_name(author)
852 user = cls.get_by_username(_author, case_insensitive=True)
847 user = cls.get_by_username(_author, case_insensitive=True)
853 if user:
848 if user:
854 return user
849 return user
855
850
856 def update_userdata(self, **kwargs):
851 def update_userdata(self, **kwargs):
857 usr = self
852 usr = self
858 old = usr.user_data
853 old = usr.user_data
859 old.update(**kwargs)
854 old.update(**kwargs)
860 usr.user_data = old
855 usr.user_data = old
861 Session().add(usr)
856 Session().add(usr)
862 log.debug('updated userdata with ', kwargs)
857 log.debug('updated userdata with ', kwargs)
863
858
864 def update_lastlogin(self):
859 def update_lastlogin(self):
865 """Update user lastlogin"""
860 """Update user lastlogin"""
866 self.last_login = datetime.datetime.now()
861 self.last_login = datetime.datetime.now()
867 Session().add(self)
862 Session().add(self)
868 log.debug('updated user %s lastlogin', self.username)
863 log.debug('updated user %s lastlogin', self.username)
869
864
870 def update_lastactivity(self):
865 def update_lastactivity(self):
871 """Update user lastactivity"""
866 """Update user lastactivity"""
872 self.last_activity = datetime.datetime.now()
867 self.last_activity = datetime.datetime.now()
873 Session().add(self)
868 Session().add(self)
874 log.debug('updated user %s lastactivity', self.username)
869 log.debug('updated user %s lastactivity', self.username)
875
870
876 def update_password(self, new_password):
871 def update_password(self, new_password):
877 from rhodecode.lib.auth import get_crypt_password
872 from rhodecode.lib.auth import get_crypt_password
878
873
879 self.password = get_crypt_password(new_password)
874 self.password = get_crypt_password(new_password)
880 Session().add(self)
875 Session().add(self)
881
876
882 @classmethod
877 @classmethod
883 def get_first_super_admin(cls):
878 def get_first_super_admin(cls):
884 user = User.query().filter(User.admin == true()).first()
879 user = User.query().filter(User.admin == true()).first()
885 if user is None:
880 if user is None:
886 raise Exception('FATAL: Missing administrative account!')
881 raise Exception('FATAL: Missing administrative account!')
887 return user
882 return user
888
883
889 @classmethod
884 @classmethod
890 def get_all_super_admins(cls):
885 def get_all_super_admins(cls):
891 """
886 """
892 Returns all admin accounts sorted by username
887 Returns all admin accounts sorted by username
893 """
888 """
894 return User.query().filter(User.admin == true())\
889 return User.query().filter(User.admin == true())\
895 .order_by(User.username.asc()).all()
890 .order_by(User.username.asc()).all()
896
891
897 @classmethod
892 @classmethod
898 def get_default_user(cls, cache=False, refresh=False):
893 def get_default_user(cls, cache=False, refresh=False):
899 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
894 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
900 if user is None:
895 if user is None:
901 raise Exception('FATAL: Missing default account!')
896 raise Exception('FATAL: Missing default account!')
902 if refresh:
897 if refresh:
903 # The default user might be based on outdated state which
898 # The default user might be based on outdated state which
904 # has been loaded from the cache.
899 # has been loaded from the cache.
905 # A call to refresh() ensures that the
900 # A call to refresh() ensures that the
906 # latest state from the database is used.
901 # latest state from the database is used.
907 Session().refresh(user)
902 Session().refresh(user)
908 return user
903 return user
909
904
910 def _get_default_perms(self, user, suffix=''):
905 def _get_default_perms(self, user, suffix=''):
911 from rhodecode.model.permission import PermissionModel
906 from rhodecode.model.permission import PermissionModel
912 return PermissionModel().get_default_perms(user.user_perms, suffix)
907 return PermissionModel().get_default_perms(user.user_perms, suffix)
913
908
914 def get_default_perms(self, suffix=''):
909 def get_default_perms(self, suffix=''):
915 return self._get_default_perms(self, suffix)
910 return self._get_default_perms(self, suffix)
916
911
917 def get_api_data(self, include_secrets=False, details='full'):
912 def get_api_data(self, include_secrets=False, details='full'):
918 """
913 """
919 Common function for generating user related data for API
914 Common function for generating user related data for API
920
915
921 :param include_secrets: By default secrets in the API data will be replaced
916 :param include_secrets: By default secrets in the API data will be replaced
922 by a placeholder value to prevent exposing this data by accident. In case
917 by a placeholder value to prevent exposing this data by accident. In case
923 this data shall be exposed, set this flag to ``True``.
918 this data shall be exposed, set this flag to ``True``.
924
919
925 :param details: details can be 'basic|full' basic gives only a subset of
920 :param details: details can be 'basic|full' basic gives only a subset of
926 the available user information that includes user_id, name and emails.
921 the available user information that includes user_id, name and emails.
927 """
922 """
928 user = self
923 user = self
929 user_data = self.user_data
924 user_data = self.user_data
930 data = {
925 data = {
931 'user_id': user.user_id,
926 'user_id': user.user_id,
932 'username': user.username,
927 'username': user.username,
933 'firstname': user.name,
928 'firstname': user.name,
934 'lastname': user.lastname,
929 'lastname': user.lastname,
935 'email': user.email,
930 'email': user.email,
936 'emails': user.emails,
931 'emails': user.emails,
937 }
932 }
938 if details == 'basic':
933 if details == 'basic':
939 return data
934 return data
940
935
941 auth_token_length = 40
936 auth_token_length = 40
942 auth_token_replacement = '*' * auth_token_length
937 auth_token_replacement = '*' * auth_token_length
943
938
944 extras = {
939 extras = {
945 'auth_tokens': [auth_token_replacement],
940 'auth_tokens': [auth_token_replacement],
946 'active': user.active,
941 'active': user.active,
947 'admin': user.admin,
942 'admin': user.admin,
948 'extern_type': user.extern_type,
943 'extern_type': user.extern_type,
949 'extern_name': user.extern_name,
944 'extern_name': user.extern_name,
950 'last_login': user.last_login,
945 'last_login': user.last_login,
951 'last_activity': user.last_activity,
946 'last_activity': user.last_activity,
952 'ip_addresses': user.ip_addresses,
947 'ip_addresses': user.ip_addresses,
953 'language': user_data.get('language')
948 'language': user_data.get('language')
954 }
949 }
955 data.update(extras)
950 data.update(extras)
956
951
957 if include_secrets:
952 if include_secrets:
958 data['auth_tokens'] = user.auth_tokens
953 data['auth_tokens'] = user.auth_tokens
959 return data
954 return data
960
955
961 def __json__(self):
956 def __json__(self):
962 data = {
957 data = {
963 'full_name': self.full_name,
958 'full_name': self.full_name,
964 'full_name_or_username': self.full_name_or_username,
959 'full_name_or_username': self.full_name_or_username,
965 'short_contact': self.short_contact,
960 'short_contact': self.short_contact,
966 'full_contact': self.full_contact,
961 'full_contact': self.full_contact,
967 }
962 }
968 data.update(self.get_api_data())
963 data.update(self.get_api_data())
969 return data
964 return data
970
965
971
966
972 class UserApiKeys(Base, BaseModel):
967 class UserApiKeys(Base, BaseModel):
973 __tablename__ = 'user_api_keys'
968 __tablename__ = 'user_api_keys'
974 __table_args__ = (
969 __table_args__ = (
975 Index('uak_api_key_idx', 'api_key'),
970 Index('uak_api_key_idx', 'api_key'),
976 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
971 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
977 UniqueConstraint('api_key'),
972 UniqueConstraint('api_key'),
978 {'extend_existing': True, 'mysql_engine': 'InnoDB',
973 {'extend_existing': True, 'mysql_engine': 'InnoDB',
979 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
974 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
980 )
975 )
981 __mapper_args__ = {}
976 __mapper_args__ = {}
982
977
983 # ApiKey role
978 # ApiKey role
984 ROLE_ALL = 'token_role_all'
979 ROLE_ALL = 'token_role_all'
985 ROLE_HTTP = 'token_role_http'
980 ROLE_HTTP = 'token_role_http'
986 ROLE_VCS = 'token_role_vcs'
981 ROLE_VCS = 'token_role_vcs'
987 ROLE_API = 'token_role_api'
982 ROLE_API = 'token_role_api'
988 ROLE_FEED = 'token_role_feed'
983 ROLE_FEED = 'token_role_feed'
989 ROLE_PASSWORD_RESET = 'token_password_reset'
984 ROLE_PASSWORD_RESET = 'token_password_reset'
990
985
991 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
986 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
992
987
993 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
988 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
994 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
989 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
995 api_key = Column("api_key", String(255), nullable=False, unique=True)
990 api_key = Column("api_key", String(255), nullable=False, unique=True)
996 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
991 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
997 expires = Column('expires', Float(53), nullable=False)
992 expires = Column('expires', Float(53), nullable=False)
998 role = Column('role', String(255), nullable=True)
993 role = Column('role', String(255), nullable=True)
999 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
994 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1000
995
1001 # scope columns
996 # scope columns
1002 repo_id = Column(
997 repo_id = Column(
1003 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
998 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1004 nullable=True, unique=None, default=None)
999 nullable=True, unique=None, default=None)
1005 repo = relationship('Repository', lazy='joined')
1000 repo = relationship('Repository', lazy='joined')
1006
1001
1007 repo_group_id = Column(
1002 repo_group_id = Column(
1008 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1003 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1009 nullable=True, unique=None, default=None)
1004 nullable=True, unique=None, default=None)
1010 repo_group = relationship('RepoGroup', lazy='joined')
1005 repo_group = relationship('RepoGroup', lazy='joined')
1011
1006
1012 user = relationship('User', lazy='joined')
1007 user = relationship('User', lazy='joined')
1013
1008
1014 def __unicode__(self):
1009 def __unicode__(self):
1015 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1010 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1016
1011
1017 def __json__(self):
1012 def __json__(self):
1018 data = {
1013 data = {
1019 'auth_token': self.api_key,
1014 'auth_token': self.api_key,
1020 'role': self.role,
1015 'role': self.role,
1021 'scope': self.scope_humanized,
1016 'scope': self.scope_humanized,
1022 'expired': self.expired
1017 'expired': self.expired
1023 }
1018 }
1024 return data
1019 return data
1025
1020
1026 def get_api_data(self, include_secrets=False):
1021 def get_api_data(self, include_secrets=False):
1027 data = self.__json__()
1022 data = self.__json__()
1028 if include_secrets:
1023 if include_secrets:
1029 return data
1024 return data
1030 else:
1025 else:
1031 data['auth_token'] = self.token_obfuscated
1026 data['auth_token'] = self.token_obfuscated
1032 return data
1027 return data
1033
1028
1034 @hybrid_property
1029 @hybrid_property
1035 def description_safe(self):
1030 def description_safe(self):
1036 from rhodecode.lib import helpers as h
1031 from rhodecode.lib import helpers as h
1037 return h.escape(self.description)
1032 return h.escape(self.description)
1038
1033
1039 @property
1034 @property
1040 def expired(self):
1035 def expired(self):
1041 if self.expires == -1:
1036 if self.expires == -1:
1042 return False
1037 return False
1043 return time.time() > self.expires
1038 return time.time() > self.expires
1044
1039
1045 @classmethod
1040 @classmethod
1046 def _get_role_name(cls, role):
1041 def _get_role_name(cls, role):
1047 return {
1042 return {
1048 cls.ROLE_ALL: _('all'),
1043 cls.ROLE_ALL: _('all'),
1049 cls.ROLE_HTTP: _('http/web interface'),
1044 cls.ROLE_HTTP: _('http/web interface'),
1050 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1045 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1051 cls.ROLE_API: _('api calls'),
1046 cls.ROLE_API: _('api calls'),
1052 cls.ROLE_FEED: _('feed access'),
1047 cls.ROLE_FEED: _('feed access'),
1053 }.get(role, role)
1048 }.get(role, role)
1054
1049
1055 @property
1050 @property
1056 def role_humanized(self):
1051 def role_humanized(self):
1057 return self._get_role_name(self.role)
1052 return self._get_role_name(self.role)
1058
1053
1059 def _get_scope(self):
1054 def _get_scope(self):
1060 if self.repo:
1055 if self.repo:
1061 return repr(self.repo)
1056 return repr(self.repo)
1062 if self.repo_group:
1057 if self.repo_group:
1063 return repr(self.repo_group) + ' (recursive)'
1058 return repr(self.repo_group) + ' (recursive)'
1064 return 'global'
1059 return 'global'
1065
1060
1066 @property
1061 @property
1067 def scope_humanized(self):
1062 def scope_humanized(self):
1068 return self._get_scope()
1063 return self._get_scope()
1069
1064
1070 @property
1065 @property
1071 def token_obfuscated(self):
1066 def token_obfuscated(self):
1072 if self.api_key:
1067 if self.api_key:
1073 return self.api_key[:4] + "****"
1068 return self.api_key[:4] + "****"
1074
1069
1075
1070
1076 class UserEmailMap(Base, BaseModel):
1071 class UserEmailMap(Base, BaseModel):
1077 __tablename__ = 'user_email_map'
1072 __tablename__ = 'user_email_map'
1078 __table_args__ = (
1073 __table_args__ = (
1079 Index('uem_email_idx', 'email'),
1074 Index('uem_email_idx', 'email'),
1080 UniqueConstraint('email'),
1075 UniqueConstraint('email'),
1081 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1076 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1082 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1077 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1083 )
1078 )
1084 __mapper_args__ = {}
1079 __mapper_args__ = {}
1085
1080
1086 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1081 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1087 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1082 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1088 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1083 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1089 user = relationship('User', lazy='joined')
1084 user = relationship('User', lazy='joined')
1090
1085
1091 @validates('_email')
1086 @validates('_email')
1092 def validate_email(self, key, email):
1087 def validate_email(self, key, email):
1093 # check if this email is not main one
1088 # check if this email is not main one
1094 main_email = Session().query(User).filter(User.email == email).scalar()
1089 main_email = Session().query(User).filter(User.email == email).scalar()
1095 if main_email is not None:
1090 if main_email is not None:
1096 raise AttributeError('email %s is present is user table' % email)
1091 raise AttributeError('email %s is present is user table' % email)
1097 return email
1092 return email
1098
1093
1099 @hybrid_property
1094 @hybrid_property
1100 def email(self):
1095 def email(self):
1101 return self._email
1096 return self._email
1102
1097
1103 @email.setter
1098 @email.setter
1104 def email(self, val):
1099 def email(self, val):
1105 self._email = val.lower() if val else None
1100 self._email = val.lower() if val else None
1106
1101
1107
1102
1108 class UserIpMap(Base, BaseModel):
1103 class UserIpMap(Base, BaseModel):
1109 __tablename__ = 'user_ip_map'
1104 __tablename__ = 'user_ip_map'
1110 __table_args__ = (
1105 __table_args__ = (
1111 UniqueConstraint('user_id', 'ip_addr'),
1106 UniqueConstraint('user_id', 'ip_addr'),
1112 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1107 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1113 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1108 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1114 )
1109 )
1115 __mapper_args__ = {}
1110 __mapper_args__ = {}
1116
1111
1117 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1112 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1118 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1113 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1119 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1114 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1120 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1115 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1121 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1116 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1122 user = relationship('User', lazy='joined')
1117 user = relationship('User', lazy='joined')
1123
1118
1124 @hybrid_property
1119 @hybrid_property
1125 def description_safe(self):
1120 def description_safe(self):
1126 from rhodecode.lib import helpers as h
1121 from rhodecode.lib import helpers as h
1127 return h.escape(self.description)
1122 return h.escape(self.description)
1128
1123
1129 @classmethod
1124 @classmethod
1130 def _get_ip_range(cls, ip_addr):
1125 def _get_ip_range(cls, ip_addr):
1131 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1126 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1132 return [str(net.network_address), str(net.broadcast_address)]
1127 return [str(net.network_address), str(net.broadcast_address)]
1133
1128
1134 def __json__(self):
1129 def __json__(self):
1135 return {
1130 return {
1136 'ip_addr': self.ip_addr,
1131 'ip_addr': self.ip_addr,
1137 'ip_range': self._get_ip_range(self.ip_addr),
1132 'ip_range': self._get_ip_range(self.ip_addr),
1138 }
1133 }
1139
1134
1140 def __unicode__(self):
1135 def __unicode__(self):
1141 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1136 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1142 self.user_id, self.ip_addr)
1137 self.user_id, self.ip_addr)
1143
1138
1144
1139
1145 class UserLog(Base, BaseModel):
1140 class UserLog(Base, BaseModel):
1146 __tablename__ = 'user_logs'
1141 __tablename__ = 'user_logs'
1147 __table_args__ = (
1142 __table_args__ = (
1148 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1143 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1149 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1144 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1150 )
1145 )
1151 VERSION_1 = 'v1'
1146 VERSION_1 = 'v1'
1152 VERSION_2 = 'v2'
1147 VERSION_2 = 'v2'
1153 VERSIONS = [VERSION_1, VERSION_2]
1148 VERSIONS = [VERSION_1, VERSION_2]
1154
1149
1155 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1150 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1156 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1151 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1157 username = Column("username", String(255), nullable=True, unique=None, default=None)
1152 username = Column("username", String(255), nullable=True, unique=None, default=None)
1158 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1153 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1159 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1154 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1160 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1155 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1161 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1156 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1162 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1157 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1163
1158
1164 version = Column("version", String(255), nullable=True, default=VERSION_1)
1159 version = Column("version", String(255), nullable=True, default=VERSION_1)
1165 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1160 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1166 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1161 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1167
1162
1168 def __unicode__(self):
1163 def __unicode__(self):
1169 return u"<%s('id:%s:%s')>" % (
1164 return u"<%s('id:%s:%s')>" % (
1170 self.__class__.__name__, self.repository_name, self.action)
1165 self.__class__.__name__, self.repository_name, self.action)
1171
1166
1172 def __json__(self):
1167 def __json__(self):
1173 return {
1168 return {
1174 'user_id': self.user_id,
1169 'user_id': self.user_id,
1175 'username': self.username,
1170 'username': self.username,
1176 'repository_id': self.repository_id,
1171 'repository_id': self.repository_id,
1177 'repository_name': self.repository_name,
1172 'repository_name': self.repository_name,
1178 'user_ip': self.user_ip,
1173 'user_ip': self.user_ip,
1179 'action_date': self.action_date,
1174 'action_date': self.action_date,
1180 'action': self.action,
1175 'action': self.action,
1181 }
1176 }
1182
1177
1183 @property
1178 @property
1184 def action_as_day(self):
1179 def action_as_day(self):
1185 return datetime.date(*self.action_date.timetuple()[:3])
1180 return datetime.date(*self.action_date.timetuple()[:3])
1186
1181
1187 user = relationship('User')
1182 user = relationship('User')
1188 repository = relationship('Repository', cascade='')
1183 repository = relationship('Repository', cascade='')
1189
1184
1190
1185
1191 class UserGroup(Base, BaseModel):
1186 class UserGroup(Base, BaseModel):
1192 __tablename__ = 'users_groups'
1187 __tablename__ = 'users_groups'
1193 __table_args__ = (
1188 __table_args__ = (
1194 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1189 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1195 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1190 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1196 )
1191 )
1197
1192
1198 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1193 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1199 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1194 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1200 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1195 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1201 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1196 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1202 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1197 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1203 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1198 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1204 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1199 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1205 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1200 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1206
1201
1207 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1202 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1208 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1203 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1209 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1204 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1210 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1205 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1211 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1206 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1212 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1207 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1213
1208
1214 user = relationship('User')
1209 user = relationship('User')
1215
1210
1216 @hybrid_property
1211 @hybrid_property
1217 def description_safe(self):
1212 def description_safe(self):
1218 from rhodecode.lib import helpers as h
1213 from rhodecode.lib import helpers as h
1219 return h.escape(self.description)
1214 return h.escape(self.description)
1220
1215
1221 @hybrid_property
1216 @hybrid_property
1222 def group_data(self):
1217 def group_data(self):
1223 if not self._group_data:
1218 if not self._group_data:
1224 return {}
1219 return {}
1225
1220
1226 try:
1221 try:
1227 return json.loads(self._group_data)
1222 return json.loads(self._group_data)
1228 except TypeError:
1223 except TypeError:
1229 return {}
1224 return {}
1230
1225
1231 @group_data.setter
1226 @group_data.setter
1232 def group_data(self, val):
1227 def group_data(self, val):
1233 try:
1228 try:
1234 self._group_data = json.dumps(val)
1229 self._group_data = json.dumps(val)
1235 except Exception:
1230 except Exception:
1236 log.error(traceback.format_exc())
1231 log.error(traceback.format_exc())
1237
1232
1238 def __unicode__(self):
1233 def __unicode__(self):
1239 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1234 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1240 self.users_group_id,
1235 self.users_group_id,
1241 self.users_group_name)
1236 self.users_group_name)
1242
1237
1243 @classmethod
1238 @classmethod
1244 def get_by_group_name(cls, group_name, cache=False,
1239 def get_by_group_name(cls, group_name, cache=False,
1245 case_insensitive=False):
1240 case_insensitive=False):
1246 if case_insensitive:
1241 if case_insensitive:
1247 q = cls.query().filter(func.lower(cls.users_group_name) ==
1242 q = cls.query().filter(func.lower(cls.users_group_name) ==
1248 func.lower(group_name))
1243 func.lower(group_name))
1249
1244
1250 else:
1245 else:
1251 q = cls.query().filter(cls.users_group_name == group_name)
1246 q = cls.query().filter(cls.users_group_name == group_name)
1252 if cache:
1247 if cache:
1253 q = q.options(
1248 q = q.options(
1254 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1249 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1255 return q.scalar()
1250 return q.scalar()
1256
1251
1257 @classmethod
1252 @classmethod
1258 def get(cls, user_group_id, cache=False):
1253 def get(cls, user_group_id, cache=False):
1259 user_group = cls.query()
1254 user_group = cls.query()
1260 if cache:
1255 if cache:
1261 user_group = user_group.options(
1256 user_group = user_group.options(
1262 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1257 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1263 return user_group.get(user_group_id)
1258 return user_group.get(user_group_id)
1264
1259
1265 def permissions(self, with_admins=True, with_owner=True):
1260 def permissions(self, with_admins=True, with_owner=True):
1266 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1261 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1267 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1262 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1268 joinedload(UserUserGroupToPerm.user),
1263 joinedload(UserUserGroupToPerm.user),
1269 joinedload(UserUserGroupToPerm.permission),)
1264 joinedload(UserUserGroupToPerm.permission),)
1270
1265
1271 # get owners and admins and permissions. We do a trick of re-writing
1266 # get owners and admins and permissions. We do a trick of re-writing
1272 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1267 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1273 # has a global reference and changing one object propagates to all
1268 # has a global reference and changing one object propagates to all
1274 # others. This means if admin is also an owner admin_row that change
1269 # others. This means if admin is also an owner admin_row that change
1275 # would propagate to both objects
1270 # would propagate to both objects
1276 perm_rows = []
1271 perm_rows = []
1277 for _usr in q.all():
1272 for _usr in q.all():
1278 usr = AttributeDict(_usr.user.get_dict())
1273 usr = AttributeDict(_usr.user.get_dict())
1279 usr.permission = _usr.permission.permission_name
1274 usr.permission = _usr.permission.permission_name
1280 perm_rows.append(usr)
1275 perm_rows.append(usr)
1281
1276
1282 # filter the perm rows by 'default' first and then sort them by
1277 # filter the perm rows by 'default' first and then sort them by
1283 # admin,write,read,none permissions sorted again alphabetically in
1278 # admin,write,read,none permissions sorted again alphabetically in
1284 # each group
1279 # each group
1285 perm_rows = sorted(perm_rows, key=display_sort)
1280 perm_rows = sorted(perm_rows, key=display_sort)
1286
1281
1287 _admin_perm = 'usergroup.admin'
1282 _admin_perm = 'usergroup.admin'
1288 owner_row = []
1283 owner_row = []
1289 if with_owner:
1284 if with_owner:
1290 usr = AttributeDict(self.user.get_dict())
1285 usr = AttributeDict(self.user.get_dict())
1291 usr.owner_row = True
1286 usr.owner_row = True
1292 usr.permission = _admin_perm
1287 usr.permission = _admin_perm
1293 owner_row.append(usr)
1288 owner_row.append(usr)
1294
1289
1295 super_admin_rows = []
1290 super_admin_rows = []
1296 if with_admins:
1291 if with_admins:
1297 for usr in User.get_all_super_admins():
1292 for usr in User.get_all_super_admins():
1298 # if this admin is also owner, don't double the record
1293 # if this admin is also owner, don't double the record
1299 if usr.user_id == owner_row[0].user_id:
1294 if usr.user_id == owner_row[0].user_id:
1300 owner_row[0].admin_row = True
1295 owner_row[0].admin_row = True
1301 else:
1296 else:
1302 usr = AttributeDict(usr.get_dict())
1297 usr = AttributeDict(usr.get_dict())
1303 usr.admin_row = True
1298 usr.admin_row = True
1304 usr.permission = _admin_perm
1299 usr.permission = _admin_perm
1305 super_admin_rows.append(usr)
1300 super_admin_rows.append(usr)
1306
1301
1307 return super_admin_rows + owner_row + perm_rows
1302 return super_admin_rows + owner_row + perm_rows
1308
1303
1309 def permission_user_groups(self):
1304 def permission_user_groups(self):
1310 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1305 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1311 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1306 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1312 joinedload(UserGroupUserGroupToPerm.target_user_group),
1307 joinedload(UserGroupUserGroupToPerm.target_user_group),
1313 joinedload(UserGroupUserGroupToPerm.permission),)
1308 joinedload(UserGroupUserGroupToPerm.permission),)
1314
1309
1315 perm_rows = []
1310 perm_rows = []
1316 for _user_group in q.all():
1311 for _user_group in q.all():
1317 usr = AttributeDict(_user_group.user_group.get_dict())
1312 usr = AttributeDict(_user_group.user_group.get_dict())
1318 usr.permission = _user_group.permission.permission_name
1313 usr.permission = _user_group.permission.permission_name
1319 perm_rows.append(usr)
1314 perm_rows.append(usr)
1320
1315
1321 return perm_rows
1316 return perm_rows
1322
1317
1323 def _get_default_perms(self, user_group, suffix=''):
1318 def _get_default_perms(self, user_group, suffix=''):
1324 from rhodecode.model.permission import PermissionModel
1319 from rhodecode.model.permission import PermissionModel
1325 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1320 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1326
1321
1327 def get_default_perms(self, suffix=''):
1322 def get_default_perms(self, suffix=''):
1328 return self._get_default_perms(self, suffix)
1323 return self._get_default_perms(self, suffix)
1329
1324
1330 def get_api_data(self, with_group_members=True, include_secrets=False):
1325 def get_api_data(self, with_group_members=True, include_secrets=False):
1331 """
1326 """
1332 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1327 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1333 basically forwarded.
1328 basically forwarded.
1334
1329
1335 """
1330 """
1336 user_group = self
1331 user_group = self
1337 data = {
1332 data = {
1338 'users_group_id': user_group.users_group_id,
1333 'users_group_id': user_group.users_group_id,
1339 'group_name': user_group.users_group_name,
1334 'group_name': user_group.users_group_name,
1340 'group_description': user_group.user_group_description,
1335 'group_description': user_group.user_group_description,
1341 'active': user_group.users_group_active,
1336 'active': user_group.users_group_active,
1342 'owner': user_group.user.username,
1337 'owner': user_group.user.username,
1343 'owner_email': user_group.user.email,
1338 'owner_email': user_group.user.email,
1344 }
1339 }
1345
1340
1346 if with_group_members:
1341 if with_group_members:
1347 users = []
1342 users = []
1348 for user in user_group.members:
1343 for user in user_group.members:
1349 user = user.user
1344 user = user.user
1350 users.append(user.get_api_data(include_secrets=include_secrets))
1345 users.append(user.get_api_data(include_secrets=include_secrets))
1351 data['users'] = users
1346 data['users'] = users
1352
1347
1353 return data
1348 return data
1354
1349
1355
1350
1356 class UserGroupMember(Base, BaseModel):
1351 class UserGroupMember(Base, BaseModel):
1357 __tablename__ = 'users_groups_members'
1352 __tablename__ = 'users_groups_members'
1358 __table_args__ = (
1353 __table_args__ = (
1359 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1354 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1360 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1355 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1361 )
1356 )
1362
1357
1363 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1358 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1364 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1359 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1365 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1360 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1366
1361
1367 user = relationship('User', lazy='joined')
1362 user = relationship('User', lazy='joined')
1368 users_group = relationship('UserGroup')
1363 users_group = relationship('UserGroup')
1369
1364
1370 def __init__(self, gr_id='', u_id=''):
1365 def __init__(self, gr_id='', u_id=''):
1371 self.users_group_id = gr_id
1366 self.users_group_id = gr_id
1372 self.user_id = u_id
1367 self.user_id = u_id
1373
1368
1374
1369
1375 class RepositoryField(Base, BaseModel):
1370 class RepositoryField(Base, BaseModel):
1376 __tablename__ = 'repositories_fields'
1371 __tablename__ = 'repositories_fields'
1377 __table_args__ = (
1372 __table_args__ = (
1378 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1373 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1379 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1374 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1380 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1375 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1381 )
1376 )
1382 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1377 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1383
1378
1384 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1379 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1385 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1380 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1386 field_key = Column("field_key", String(250))
1381 field_key = Column("field_key", String(250))
1387 field_label = Column("field_label", String(1024), nullable=False)
1382 field_label = Column("field_label", String(1024), nullable=False)
1388 field_value = Column("field_value", String(10000), nullable=False)
1383 field_value = Column("field_value", String(10000), nullable=False)
1389 field_desc = Column("field_desc", String(1024), nullable=False)
1384 field_desc = Column("field_desc", String(1024), nullable=False)
1390 field_type = Column("field_type", String(255), nullable=False, unique=None)
1385 field_type = Column("field_type", String(255), nullable=False, unique=None)
1391 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1386 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1392
1387
1393 repository = relationship('Repository')
1388 repository = relationship('Repository')
1394
1389
1395 @property
1390 @property
1396 def field_key_prefixed(self):
1391 def field_key_prefixed(self):
1397 return 'ex_%s' % self.field_key
1392 return 'ex_%s' % self.field_key
1398
1393
1399 @classmethod
1394 @classmethod
1400 def un_prefix_key(cls, key):
1395 def un_prefix_key(cls, key):
1401 if key.startswith(cls.PREFIX):
1396 if key.startswith(cls.PREFIX):
1402 return key[len(cls.PREFIX):]
1397 return key[len(cls.PREFIX):]
1403 return key
1398 return key
1404
1399
1405 @classmethod
1400 @classmethod
1406 def get_by_key_name(cls, key, repo):
1401 def get_by_key_name(cls, key, repo):
1407 row = cls.query()\
1402 row = cls.query()\
1408 .filter(cls.repository == repo)\
1403 .filter(cls.repository == repo)\
1409 .filter(cls.field_key == key).scalar()
1404 .filter(cls.field_key == key).scalar()
1410 return row
1405 return row
1411
1406
1412
1407
1413 class Repository(Base, BaseModel):
1408 class Repository(Base, BaseModel):
1414 __tablename__ = 'repositories'
1409 __tablename__ = 'repositories'
1415 __table_args__ = (
1410 __table_args__ = (
1416 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1411 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1417 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1412 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1418 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1413 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1419 )
1414 )
1420 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1415 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1421 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1416 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1422
1417
1423 STATE_CREATED = 'repo_state_created'
1418 STATE_CREATED = 'repo_state_created'
1424 STATE_PENDING = 'repo_state_pending'
1419 STATE_PENDING = 'repo_state_pending'
1425 STATE_ERROR = 'repo_state_error'
1420 STATE_ERROR = 'repo_state_error'
1426
1421
1427 LOCK_AUTOMATIC = 'lock_auto'
1422 LOCK_AUTOMATIC = 'lock_auto'
1428 LOCK_API = 'lock_api'
1423 LOCK_API = 'lock_api'
1429 LOCK_WEB = 'lock_web'
1424 LOCK_WEB = 'lock_web'
1430 LOCK_PULL = 'lock_pull'
1425 LOCK_PULL = 'lock_pull'
1431
1426
1432 NAME_SEP = URL_SEP
1427 NAME_SEP = URL_SEP
1433
1428
1434 repo_id = Column(
1429 repo_id = Column(
1435 "repo_id", Integer(), nullable=False, unique=True, default=None,
1430 "repo_id", Integer(), nullable=False, unique=True, default=None,
1436 primary_key=True)
1431 primary_key=True)
1437 _repo_name = Column(
1432 _repo_name = Column(
1438 "repo_name", Text(), nullable=False, default=None)
1433 "repo_name", Text(), nullable=False, default=None)
1439 _repo_name_hash = Column(
1434 _repo_name_hash = Column(
1440 "repo_name_hash", String(255), nullable=False, unique=True)
1435 "repo_name_hash", String(255), nullable=False, unique=True)
1441 repo_state = Column("repo_state", String(255), nullable=True)
1436 repo_state = Column("repo_state", String(255), nullable=True)
1442
1437
1443 clone_uri = Column(
1438 clone_uri = Column(
1444 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1439 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1445 default=None)
1440 default=None)
1446 repo_type = Column(
1441 repo_type = Column(
1447 "repo_type", String(255), nullable=False, unique=False, default=None)
1442 "repo_type", String(255), nullable=False, unique=False, default=None)
1448 user_id = Column(
1443 user_id = Column(
1449 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1444 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1450 unique=False, default=None)
1445 unique=False, default=None)
1451 private = Column(
1446 private = Column(
1452 "private", Boolean(), nullable=True, unique=None, default=None)
1447 "private", Boolean(), nullable=True, unique=None, default=None)
1453 enable_statistics = Column(
1448 enable_statistics = Column(
1454 "statistics", Boolean(), nullable=True, unique=None, default=True)
1449 "statistics", Boolean(), nullable=True, unique=None, default=True)
1455 enable_downloads = Column(
1450 enable_downloads = Column(
1456 "downloads", Boolean(), nullable=True, unique=None, default=True)
1451 "downloads", Boolean(), nullable=True, unique=None, default=True)
1457 description = Column(
1452 description = Column(
1458 "description", String(10000), nullable=True, unique=None, default=None)
1453 "description", String(10000), nullable=True, unique=None, default=None)
1459 created_on = Column(
1454 created_on = Column(
1460 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1455 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1461 default=datetime.datetime.now)
1456 default=datetime.datetime.now)
1462 updated_on = Column(
1457 updated_on = Column(
1463 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1458 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1464 default=datetime.datetime.now)
1459 default=datetime.datetime.now)
1465 _landing_revision = Column(
1460 _landing_revision = Column(
1466 "landing_revision", String(255), nullable=False, unique=False,
1461 "landing_revision", String(255), nullable=False, unique=False,
1467 default=None)
1462 default=None)
1468 enable_locking = Column(
1463 enable_locking = Column(
1469 "enable_locking", Boolean(), nullable=False, unique=None,
1464 "enable_locking", Boolean(), nullable=False, unique=None,
1470 default=False)
1465 default=False)
1471 _locked = Column(
1466 _locked = Column(
1472 "locked", String(255), nullable=True, unique=False, default=None)
1467 "locked", String(255), nullable=True, unique=False, default=None)
1473 _changeset_cache = Column(
1468 _changeset_cache = Column(
1474 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1469 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1475
1470
1476 fork_id = Column(
1471 fork_id = Column(
1477 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1472 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1478 nullable=True, unique=False, default=None)
1473 nullable=True, unique=False, default=None)
1479 group_id = Column(
1474 group_id = Column(
1480 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1475 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1481 unique=False, default=None)
1476 unique=False, default=None)
1482
1477
1483 user = relationship('User', lazy='joined')
1478 user = relationship('User', lazy='joined')
1484 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1479 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1485 group = relationship('RepoGroup', lazy='joined')
1480 group = relationship('RepoGroup', lazy='joined')
1486 repo_to_perm = relationship(
1481 repo_to_perm = relationship(
1487 'UserRepoToPerm', cascade='all',
1482 'UserRepoToPerm', cascade='all',
1488 order_by='UserRepoToPerm.repo_to_perm_id')
1483 order_by='UserRepoToPerm.repo_to_perm_id')
1489 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1484 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1490 stats = relationship('Statistics', cascade='all', uselist=False)
1485 stats = relationship('Statistics', cascade='all', uselist=False)
1491
1486
1492 followers = relationship(
1487 followers = relationship(
1493 'UserFollowing',
1488 'UserFollowing',
1494 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1489 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1495 cascade='all')
1490 cascade='all')
1496 extra_fields = relationship(
1491 extra_fields = relationship(
1497 'RepositoryField', cascade="all, delete, delete-orphan")
1492 'RepositoryField', cascade="all, delete, delete-orphan")
1498 logs = relationship('UserLog')
1493 logs = relationship('UserLog')
1499 comments = relationship(
1494 comments = relationship(
1500 'ChangesetComment', cascade="all, delete, delete-orphan")
1495 'ChangesetComment', cascade="all, delete, delete-orphan")
1501 pull_requests_source = relationship(
1496 pull_requests_source = relationship(
1502 'PullRequest',
1497 'PullRequest',
1503 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1498 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1504 cascade="all, delete, delete-orphan")
1499 cascade="all, delete, delete-orphan")
1505 pull_requests_target = relationship(
1500 pull_requests_target = relationship(
1506 'PullRequest',
1501 'PullRequest',
1507 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1502 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1508 cascade="all, delete, delete-orphan")
1503 cascade="all, delete, delete-orphan")
1509 ui = relationship('RepoRhodeCodeUi', cascade="all")
1504 ui = relationship('RepoRhodeCodeUi', cascade="all")
1510 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1505 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1511 integrations = relationship('Integration',
1506 integrations = relationship('Integration',
1512 cascade="all, delete, delete-orphan")
1507 cascade="all, delete, delete-orphan")
1513
1508
1514 def __unicode__(self):
1509 def __unicode__(self):
1515 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1510 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1516 safe_unicode(self.repo_name))
1511 safe_unicode(self.repo_name))
1517
1512
1518 @hybrid_property
1513 @hybrid_property
1519 def description_safe(self):
1514 def description_safe(self):
1520 from rhodecode.lib import helpers as h
1515 from rhodecode.lib import helpers as h
1521 return h.escape(self.description)
1516 return h.escape(self.description)
1522
1517
1523 @hybrid_property
1518 @hybrid_property
1524 def landing_rev(self):
1519 def landing_rev(self):
1525 # always should return [rev_type, rev]
1520 # always should return [rev_type, rev]
1526 if self._landing_revision:
1521 if self._landing_revision:
1527 _rev_info = self._landing_revision.split(':')
1522 _rev_info = self._landing_revision.split(':')
1528 if len(_rev_info) < 2:
1523 if len(_rev_info) < 2:
1529 _rev_info.insert(0, 'rev')
1524 _rev_info.insert(0, 'rev')
1530 return [_rev_info[0], _rev_info[1]]
1525 return [_rev_info[0], _rev_info[1]]
1531 return [None, None]
1526 return [None, None]
1532
1527
1533 @landing_rev.setter
1528 @landing_rev.setter
1534 def landing_rev(self, val):
1529 def landing_rev(self, val):
1535 if ':' not in val:
1530 if ':' not in val:
1536 raise ValueError('value must be delimited with `:` and consist '
1531 raise ValueError('value must be delimited with `:` and consist '
1537 'of <rev_type>:<rev>, got %s instead' % val)
1532 'of <rev_type>:<rev>, got %s instead' % val)
1538 self._landing_revision = val
1533 self._landing_revision = val
1539
1534
1540 @hybrid_property
1535 @hybrid_property
1541 def locked(self):
1536 def locked(self):
1542 if self._locked:
1537 if self._locked:
1543 user_id, timelocked, reason = self._locked.split(':')
1538 user_id, timelocked, reason = self._locked.split(':')
1544 lock_values = int(user_id), timelocked, reason
1539 lock_values = int(user_id), timelocked, reason
1545 else:
1540 else:
1546 lock_values = [None, None, None]
1541 lock_values = [None, None, None]
1547 return lock_values
1542 return lock_values
1548
1543
1549 @locked.setter
1544 @locked.setter
1550 def locked(self, val):
1545 def locked(self, val):
1551 if val and isinstance(val, (list, tuple)):
1546 if val and isinstance(val, (list, tuple)):
1552 self._locked = ':'.join(map(str, val))
1547 self._locked = ':'.join(map(str, val))
1553 else:
1548 else:
1554 self._locked = None
1549 self._locked = None
1555
1550
1556 @hybrid_property
1551 @hybrid_property
1557 def changeset_cache(self):
1552 def changeset_cache(self):
1558 from rhodecode.lib.vcs.backends.base import EmptyCommit
1553 from rhodecode.lib.vcs.backends.base import EmptyCommit
1559 dummy = EmptyCommit().__json__()
1554 dummy = EmptyCommit().__json__()
1560 if not self._changeset_cache:
1555 if not self._changeset_cache:
1561 return dummy
1556 return dummy
1562 try:
1557 try:
1563 return json.loads(self._changeset_cache)
1558 return json.loads(self._changeset_cache)
1564 except TypeError:
1559 except TypeError:
1565 return dummy
1560 return dummy
1566 except Exception:
1561 except Exception:
1567 log.error(traceback.format_exc())
1562 log.error(traceback.format_exc())
1568 return dummy
1563 return dummy
1569
1564
1570 @changeset_cache.setter
1565 @changeset_cache.setter
1571 def changeset_cache(self, val):
1566 def changeset_cache(self, val):
1572 try:
1567 try:
1573 self._changeset_cache = json.dumps(val)
1568 self._changeset_cache = json.dumps(val)
1574 except Exception:
1569 except Exception:
1575 log.error(traceback.format_exc())
1570 log.error(traceback.format_exc())
1576
1571
1577 @hybrid_property
1572 @hybrid_property
1578 def repo_name(self):
1573 def repo_name(self):
1579 return self._repo_name
1574 return self._repo_name
1580
1575
1581 @repo_name.setter
1576 @repo_name.setter
1582 def repo_name(self, value):
1577 def repo_name(self, value):
1583 self._repo_name = value
1578 self._repo_name = value
1584 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1579 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1585
1580
1586 @classmethod
1581 @classmethod
1587 def normalize_repo_name(cls, repo_name):
1582 def normalize_repo_name(cls, repo_name):
1588 """
1583 """
1589 Normalizes os specific repo_name to the format internally stored inside
1584 Normalizes os specific repo_name to the format internally stored inside
1590 database using URL_SEP
1585 database using URL_SEP
1591
1586
1592 :param cls:
1587 :param cls:
1593 :param repo_name:
1588 :param repo_name:
1594 """
1589 """
1595 return cls.NAME_SEP.join(repo_name.split(os.sep))
1590 return cls.NAME_SEP.join(repo_name.split(os.sep))
1596
1591
1597 @classmethod
1592 @classmethod
1598 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1593 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1599 session = Session()
1594 session = Session()
1600 q = session.query(cls).filter(cls.repo_name == repo_name)
1595 q = session.query(cls).filter(cls.repo_name == repo_name)
1601
1596
1602 if cache:
1597 if cache:
1603 if identity_cache:
1598 if identity_cache:
1604 val = cls.identity_cache(session, 'repo_name', repo_name)
1599 val = cls.identity_cache(session, 'repo_name', repo_name)
1605 if val:
1600 if val:
1606 return val
1601 return val
1607 else:
1602 else:
1608 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1603 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1609 q = q.options(
1604 q = q.options(
1610 FromCache("sql_cache_short", cache_key))
1605 FromCache("sql_cache_short", cache_key))
1611
1606
1612 return q.scalar()
1607 return q.scalar()
1613
1608
1614 @classmethod
1609 @classmethod
1615 def get_by_full_path(cls, repo_full_path):
1610 def get_by_full_path(cls, repo_full_path):
1616 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1611 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1617 repo_name = cls.normalize_repo_name(repo_name)
1612 repo_name = cls.normalize_repo_name(repo_name)
1618 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1613 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1619
1614
1620 @classmethod
1615 @classmethod
1621 def get_repo_forks(cls, repo_id):
1616 def get_repo_forks(cls, repo_id):
1622 return cls.query().filter(Repository.fork_id == repo_id)
1617 return cls.query().filter(Repository.fork_id == repo_id)
1623
1618
1624 @classmethod
1619 @classmethod
1625 def base_path(cls):
1620 def base_path(cls):
1626 """
1621 """
1627 Returns base path when all repos are stored
1622 Returns base path when all repos are stored
1628
1623
1629 :param cls:
1624 :param cls:
1630 """
1625 """
1631 q = Session().query(RhodeCodeUi)\
1626 q = Session().query(RhodeCodeUi)\
1632 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1627 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1633 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1628 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1634 return q.one().ui_value
1629 return q.one().ui_value
1635
1630
1636 @classmethod
1631 @classmethod
1637 def is_valid(cls, repo_name):
1632 def is_valid(cls, repo_name):
1638 """
1633 """
1639 returns True if given repo name is a valid filesystem repository
1634 returns True if given repo name is a valid filesystem repository
1640
1635
1641 :param cls:
1636 :param cls:
1642 :param repo_name:
1637 :param repo_name:
1643 """
1638 """
1644 from rhodecode.lib.utils import is_valid_repo
1639 from rhodecode.lib.utils import is_valid_repo
1645
1640
1646 return is_valid_repo(repo_name, cls.base_path())
1641 return is_valid_repo(repo_name, cls.base_path())
1647
1642
1648 @classmethod
1643 @classmethod
1649 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1644 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1650 case_insensitive=True):
1645 case_insensitive=True):
1651 q = Repository.query()
1646 q = Repository.query()
1652
1647
1653 if not isinstance(user_id, Optional):
1648 if not isinstance(user_id, Optional):
1654 q = q.filter(Repository.user_id == user_id)
1649 q = q.filter(Repository.user_id == user_id)
1655
1650
1656 if not isinstance(group_id, Optional):
1651 if not isinstance(group_id, Optional):
1657 q = q.filter(Repository.group_id == group_id)
1652 q = q.filter(Repository.group_id == group_id)
1658
1653
1659 if case_insensitive:
1654 if case_insensitive:
1660 q = q.order_by(func.lower(Repository.repo_name))
1655 q = q.order_by(func.lower(Repository.repo_name))
1661 else:
1656 else:
1662 q = q.order_by(Repository.repo_name)
1657 q = q.order_by(Repository.repo_name)
1663 return q.all()
1658 return q.all()
1664
1659
1665 @property
1660 @property
1666 def forks(self):
1661 def forks(self):
1667 """
1662 """
1668 Return forks of this repo
1663 Return forks of this repo
1669 """
1664 """
1670 return Repository.get_repo_forks(self.repo_id)
1665 return Repository.get_repo_forks(self.repo_id)
1671
1666
1672 @property
1667 @property
1673 def parent(self):
1668 def parent(self):
1674 """
1669 """
1675 Returns fork parent
1670 Returns fork parent
1676 """
1671 """
1677 return self.fork
1672 return self.fork
1678
1673
1679 @property
1674 @property
1680 def just_name(self):
1675 def just_name(self):
1681 return self.repo_name.split(self.NAME_SEP)[-1]
1676 return self.repo_name.split(self.NAME_SEP)[-1]
1682
1677
1683 @property
1678 @property
1684 def groups_with_parents(self):
1679 def groups_with_parents(self):
1685 groups = []
1680 groups = []
1686 if self.group is None:
1681 if self.group is None:
1687 return groups
1682 return groups
1688
1683
1689 cur_gr = self.group
1684 cur_gr = self.group
1690 groups.insert(0, cur_gr)
1685 groups.insert(0, cur_gr)
1691 while 1:
1686 while 1:
1692 gr = getattr(cur_gr, 'parent_group', None)
1687 gr = getattr(cur_gr, 'parent_group', None)
1693 cur_gr = cur_gr.parent_group
1688 cur_gr = cur_gr.parent_group
1694 if gr is None:
1689 if gr is None:
1695 break
1690 break
1696 groups.insert(0, gr)
1691 groups.insert(0, gr)
1697
1692
1698 return groups
1693 return groups
1699
1694
1700 @property
1695 @property
1701 def groups_and_repo(self):
1696 def groups_and_repo(self):
1702 return self.groups_with_parents, self
1697 return self.groups_with_parents, self
1703
1698
1704 @LazyProperty
1699 @LazyProperty
1705 def repo_path(self):
1700 def repo_path(self):
1706 """
1701 """
1707 Returns base full path for that repository means where it actually
1702 Returns base full path for that repository means where it actually
1708 exists on a filesystem
1703 exists on a filesystem
1709 """
1704 """
1710 q = Session().query(RhodeCodeUi).filter(
1705 q = Session().query(RhodeCodeUi).filter(
1711 RhodeCodeUi.ui_key == self.NAME_SEP)
1706 RhodeCodeUi.ui_key == self.NAME_SEP)
1712 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1707 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1713 return q.one().ui_value
1708 return q.one().ui_value
1714
1709
1715 @property
1710 @property
1716 def repo_full_path(self):
1711 def repo_full_path(self):
1717 p = [self.repo_path]
1712 p = [self.repo_path]
1718 # we need to split the name by / since this is how we store the
1713 # we need to split the name by / since this is how we store the
1719 # names in the database, but that eventually needs to be converted
1714 # names in the database, but that eventually needs to be converted
1720 # into a valid system path
1715 # into a valid system path
1721 p += self.repo_name.split(self.NAME_SEP)
1716 p += self.repo_name.split(self.NAME_SEP)
1722 return os.path.join(*map(safe_unicode, p))
1717 return os.path.join(*map(safe_unicode, p))
1723
1718
1724 @property
1719 @property
1725 def cache_keys(self):
1720 def cache_keys(self):
1726 """
1721 """
1727 Returns associated cache keys for that repo
1722 Returns associated cache keys for that repo
1728 """
1723 """
1729 return CacheKey.query()\
1724 return CacheKey.query()\
1730 .filter(CacheKey.cache_args == self.repo_name)\
1725 .filter(CacheKey.cache_args == self.repo_name)\
1731 .order_by(CacheKey.cache_key)\
1726 .order_by(CacheKey.cache_key)\
1732 .all()
1727 .all()
1733
1728
1734 def get_new_name(self, repo_name):
1729 def get_new_name(self, repo_name):
1735 """
1730 """
1736 returns new full repository name based on assigned group and new new
1731 returns new full repository name based on assigned group and new new
1737
1732
1738 :param group_name:
1733 :param group_name:
1739 """
1734 """
1740 path_prefix = self.group.full_path_splitted if self.group else []
1735 path_prefix = self.group.full_path_splitted if self.group else []
1741 return self.NAME_SEP.join(path_prefix + [repo_name])
1736 return self.NAME_SEP.join(path_prefix + [repo_name])
1742
1737
1743 @property
1738 @property
1744 def _config(self):
1739 def _config(self):
1745 """
1740 """
1746 Returns db based config object.
1741 Returns db based config object.
1747 """
1742 """
1748 from rhodecode.lib.utils import make_db_config
1743 from rhodecode.lib.utils import make_db_config
1749 return make_db_config(clear_session=False, repo=self)
1744 return make_db_config(clear_session=False, repo=self)
1750
1745
1751 def permissions(self, with_admins=True, with_owner=True):
1746 def permissions(self, with_admins=True, with_owner=True):
1752 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1747 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1753 q = q.options(joinedload(UserRepoToPerm.repository),
1748 q = q.options(joinedload(UserRepoToPerm.repository),
1754 joinedload(UserRepoToPerm.user),
1749 joinedload(UserRepoToPerm.user),
1755 joinedload(UserRepoToPerm.permission),)
1750 joinedload(UserRepoToPerm.permission),)
1756
1751
1757 # get owners and admins and permissions. We do a trick of re-writing
1752 # get owners and admins and permissions. We do a trick of re-writing
1758 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1753 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1759 # has a global reference and changing one object propagates to all
1754 # has a global reference and changing one object propagates to all
1760 # others. This means if admin is also an owner admin_row that change
1755 # others. This means if admin is also an owner admin_row that change
1761 # would propagate to both objects
1756 # would propagate to both objects
1762 perm_rows = []
1757 perm_rows = []
1763 for _usr in q.all():
1758 for _usr in q.all():
1764 usr = AttributeDict(_usr.user.get_dict())
1759 usr = AttributeDict(_usr.user.get_dict())
1765 usr.permission = _usr.permission.permission_name
1760 usr.permission = _usr.permission.permission_name
1766 perm_rows.append(usr)
1761 perm_rows.append(usr)
1767
1762
1768 # filter the perm rows by 'default' first and then sort them by
1763 # filter the perm rows by 'default' first and then sort them by
1769 # admin,write,read,none permissions sorted again alphabetically in
1764 # admin,write,read,none permissions sorted again alphabetically in
1770 # each group
1765 # each group
1771 perm_rows = sorted(perm_rows, key=display_sort)
1766 perm_rows = sorted(perm_rows, key=display_sort)
1772
1767
1773 _admin_perm = 'repository.admin'
1768 _admin_perm = 'repository.admin'
1774 owner_row = []
1769 owner_row = []
1775 if with_owner:
1770 if with_owner:
1776 usr = AttributeDict(self.user.get_dict())
1771 usr = AttributeDict(self.user.get_dict())
1777 usr.owner_row = True
1772 usr.owner_row = True
1778 usr.permission = _admin_perm
1773 usr.permission = _admin_perm
1779 owner_row.append(usr)
1774 owner_row.append(usr)
1780
1775
1781 super_admin_rows = []
1776 super_admin_rows = []
1782 if with_admins:
1777 if with_admins:
1783 for usr in User.get_all_super_admins():
1778 for usr in User.get_all_super_admins():
1784 # if this admin is also owner, don't double the record
1779 # if this admin is also owner, don't double the record
1785 if usr.user_id == owner_row[0].user_id:
1780 if usr.user_id == owner_row[0].user_id:
1786 owner_row[0].admin_row = True
1781 owner_row[0].admin_row = True
1787 else:
1782 else:
1788 usr = AttributeDict(usr.get_dict())
1783 usr = AttributeDict(usr.get_dict())
1789 usr.admin_row = True
1784 usr.admin_row = True
1790 usr.permission = _admin_perm
1785 usr.permission = _admin_perm
1791 super_admin_rows.append(usr)
1786 super_admin_rows.append(usr)
1792
1787
1793 return super_admin_rows + owner_row + perm_rows
1788 return super_admin_rows + owner_row + perm_rows
1794
1789
1795 def permission_user_groups(self):
1790 def permission_user_groups(self):
1796 q = UserGroupRepoToPerm.query().filter(
1791 q = UserGroupRepoToPerm.query().filter(
1797 UserGroupRepoToPerm.repository == self)
1792 UserGroupRepoToPerm.repository == self)
1798 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1793 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1799 joinedload(UserGroupRepoToPerm.users_group),
1794 joinedload(UserGroupRepoToPerm.users_group),
1800 joinedload(UserGroupRepoToPerm.permission),)
1795 joinedload(UserGroupRepoToPerm.permission),)
1801
1796
1802 perm_rows = []
1797 perm_rows = []
1803 for _user_group in q.all():
1798 for _user_group in q.all():
1804 usr = AttributeDict(_user_group.users_group.get_dict())
1799 usr = AttributeDict(_user_group.users_group.get_dict())
1805 usr.permission = _user_group.permission.permission_name
1800 usr.permission = _user_group.permission.permission_name
1806 perm_rows.append(usr)
1801 perm_rows.append(usr)
1807
1802
1808 return perm_rows
1803 return perm_rows
1809
1804
1810 def get_api_data(self, include_secrets=False):
1805 def get_api_data(self, include_secrets=False):
1811 """
1806 """
1812 Common function for generating repo api data
1807 Common function for generating repo api data
1813
1808
1814 :param include_secrets: See :meth:`User.get_api_data`.
1809 :param include_secrets: See :meth:`User.get_api_data`.
1815
1810
1816 """
1811 """
1817 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1812 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1818 # move this methods on models level.
1813 # move this methods on models level.
1819 from rhodecode.model.settings import SettingsModel
1814 from rhodecode.model.settings import SettingsModel
1820 from rhodecode.model.repo import RepoModel
1815 from rhodecode.model.repo import RepoModel
1821
1816
1822 repo = self
1817 repo = self
1823 _user_id, _time, _reason = self.locked
1818 _user_id, _time, _reason = self.locked
1824
1819
1825 data = {
1820 data = {
1826 'repo_id': repo.repo_id,
1821 'repo_id': repo.repo_id,
1827 'repo_name': repo.repo_name,
1822 'repo_name': repo.repo_name,
1828 'repo_type': repo.repo_type,
1823 'repo_type': repo.repo_type,
1829 'clone_uri': repo.clone_uri or '',
1824 'clone_uri': repo.clone_uri or '',
1830 'url': RepoModel().get_url(self),
1825 'url': RepoModel().get_url(self),
1831 'private': repo.private,
1826 'private': repo.private,
1832 'created_on': repo.created_on,
1827 'created_on': repo.created_on,
1833 'description': repo.description_safe,
1828 'description': repo.description_safe,
1834 'landing_rev': repo.landing_rev,
1829 'landing_rev': repo.landing_rev,
1835 'owner': repo.user.username,
1830 'owner': repo.user.username,
1836 'fork_of': repo.fork.repo_name if repo.fork else None,
1831 'fork_of': repo.fork.repo_name if repo.fork else None,
1837 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1832 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1838 'enable_statistics': repo.enable_statistics,
1833 'enable_statistics': repo.enable_statistics,
1839 'enable_locking': repo.enable_locking,
1834 'enable_locking': repo.enable_locking,
1840 'enable_downloads': repo.enable_downloads,
1835 'enable_downloads': repo.enable_downloads,
1841 'last_changeset': repo.changeset_cache,
1836 'last_changeset': repo.changeset_cache,
1842 'locked_by': User.get(_user_id).get_api_data(
1837 'locked_by': User.get(_user_id).get_api_data(
1843 include_secrets=include_secrets) if _user_id else None,
1838 include_secrets=include_secrets) if _user_id else None,
1844 'locked_date': time_to_datetime(_time) if _time else None,
1839 'locked_date': time_to_datetime(_time) if _time else None,
1845 'lock_reason': _reason if _reason else None,
1840 'lock_reason': _reason if _reason else None,
1846 }
1841 }
1847
1842
1848 # TODO: mikhail: should be per-repo settings here
1843 # TODO: mikhail: should be per-repo settings here
1849 rc_config = SettingsModel().get_all_settings()
1844 rc_config = SettingsModel().get_all_settings()
1850 repository_fields = str2bool(
1845 repository_fields = str2bool(
1851 rc_config.get('rhodecode_repository_fields'))
1846 rc_config.get('rhodecode_repository_fields'))
1852 if repository_fields:
1847 if repository_fields:
1853 for f in self.extra_fields:
1848 for f in self.extra_fields:
1854 data[f.field_key_prefixed] = f.field_value
1849 data[f.field_key_prefixed] = f.field_value
1855
1850
1856 return data
1851 return data
1857
1852
1858 @classmethod
1853 @classmethod
1859 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1854 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1860 if not lock_time:
1855 if not lock_time:
1861 lock_time = time.time()
1856 lock_time = time.time()
1862 if not lock_reason:
1857 if not lock_reason:
1863 lock_reason = cls.LOCK_AUTOMATIC
1858 lock_reason = cls.LOCK_AUTOMATIC
1864 repo.locked = [user_id, lock_time, lock_reason]
1859 repo.locked = [user_id, lock_time, lock_reason]
1865 Session().add(repo)
1860 Session().add(repo)
1866 Session().commit()
1861 Session().commit()
1867
1862
1868 @classmethod
1863 @classmethod
1869 def unlock(cls, repo):
1864 def unlock(cls, repo):
1870 repo.locked = None
1865 repo.locked = None
1871 Session().add(repo)
1866 Session().add(repo)
1872 Session().commit()
1867 Session().commit()
1873
1868
1874 @classmethod
1869 @classmethod
1875 def getlock(cls, repo):
1870 def getlock(cls, repo):
1876 return repo.locked
1871 return repo.locked
1877
1872
1878 def is_user_lock(self, user_id):
1873 def is_user_lock(self, user_id):
1879 if self.lock[0]:
1874 if self.lock[0]:
1880 lock_user_id = safe_int(self.lock[0])
1875 lock_user_id = safe_int(self.lock[0])
1881 user_id = safe_int(user_id)
1876 user_id = safe_int(user_id)
1882 # both are ints, and they are equal
1877 # both are ints, and they are equal
1883 return all([lock_user_id, user_id]) and lock_user_id == user_id
1878 return all([lock_user_id, user_id]) and lock_user_id == user_id
1884
1879
1885 return False
1880 return False
1886
1881
1887 def get_locking_state(self, action, user_id, only_when_enabled=True):
1882 def get_locking_state(self, action, user_id, only_when_enabled=True):
1888 """
1883 """
1889 Checks locking on this repository, if locking is enabled and lock is
1884 Checks locking on this repository, if locking is enabled and lock is
1890 present returns a tuple of make_lock, locked, locked_by.
1885 present returns a tuple of make_lock, locked, locked_by.
1891 make_lock can have 3 states None (do nothing) True, make lock
1886 make_lock can have 3 states None (do nothing) True, make lock
1892 False release lock, This value is later propagated to hooks, which
1887 False release lock, This value is later propagated to hooks, which
1893 do the locking. Think about this as signals passed to hooks what to do.
1888 do the locking. Think about this as signals passed to hooks what to do.
1894
1889
1895 """
1890 """
1896 # TODO: johbo: This is part of the business logic and should be moved
1891 # TODO: johbo: This is part of the business logic and should be moved
1897 # into the RepositoryModel.
1892 # into the RepositoryModel.
1898
1893
1899 if action not in ('push', 'pull'):
1894 if action not in ('push', 'pull'):
1900 raise ValueError("Invalid action value: %s" % repr(action))
1895 raise ValueError("Invalid action value: %s" % repr(action))
1901
1896
1902 # defines if locked error should be thrown to user
1897 # defines if locked error should be thrown to user
1903 currently_locked = False
1898 currently_locked = False
1904 # defines if new lock should be made, tri-state
1899 # defines if new lock should be made, tri-state
1905 make_lock = None
1900 make_lock = None
1906 repo = self
1901 repo = self
1907 user = User.get(user_id)
1902 user = User.get(user_id)
1908
1903
1909 lock_info = repo.locked
1904 lock_info = repo.locked
1910
1905
1911 if repo and (repo.enable_locking or not only_when_enabled):
1906 if repo and (repo.enable_locking or not only_when_enabled):
1912 if action == 'push':
1907 if action == 'push':
1913 # check if it's already locked !, if it is compare users
1908 # check if it's already locked !, if it is compare users
1914 locked_by_user_id = lock_info[0]
1909 locked_by_user_id = lock_info[0]
1915 if user.user_id == locked_by_user_id:
1910 if user.user_id == locked_by_user_id:
1916 log.debug(
1911 log.debug(
1917 'Got `push` action from user %s, now unlocking', user)
1912 'Got `push` action from user %s, now unlocking', user)
1918 # unlock if we have push from user who locked
1913 # unlock if we have push from user who locked
1919 make_lock = False
1914 make_lock = False
1920 else:
1915 else:
1921 # we're not the same user who locked, ban with
1916 # we're not the same user who locked, ban with
1922 # code defined in settings (default is 423 HTTP Locked) !
1917 # code defined in settings (default is 423 HTTP Locked) !
1923 log.debug('Repo %s is currently locked by %s', repo, user)
1918 log.debug('Repo %s is currently locked by %s', repo, user)
1924 currently_locked = True
1919 currently_locked = True
1925 elif action == 'pull':
1920 elif action == 'pull':
1926 # [0] user [1] date
1921 # [0] user [1] date
1927 if lock_info[0] and lock_info[1]:
1922 if lock_info[0] and lock_info[1]:
1928 log.debug('Repo %s is currently locked by %s', repo, user)
1923 log.debug('Repo %s is currently locked by %s', repo, user)
1929 currently_locked = True
1924 currently_locked = True
1930 else:
1925 else:
1931 log.debug('Setting lock on repo %s by %s', repo, user)
1926 log.debug('Setting lock on repo %s by %s', repo, user)
1932 make_lock = True
1927 make_lock = True
1933
1928
1934 else:
1929 else:
1935 log.debug('Repository %s do not have locking enabled', repo)
1930 log.debug('Repository %s do not have locking enabled', repo)
1936
1931
1937 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1932 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1938 make_lock, currently_locked, lock_info)
1933 make_lock, currently_locked, lock_info)
1939
1934
1940 from rhodecode.lib.auth import HasRepoPermissionAny
1935 from rhodecode.lib.auth import HasRepoPermissionAny
1941 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1936 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1942 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1937 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1943 # if we don't have at least write permission we cannot make a lock
1938 # if we don't have at least write permission we cannot make a lock
1944 log.debug('lock state reset back to FALSE due to lack '
1939 log.debug('lock state reset back to FALSE due to lack '
1945 'of at least read permission')
1940 'of at least read permission')
1946 make_lock = False
1941 make_lock = False
1947
1942
1948 return make_lock, currently_locked, lock_info
1943 return make_lock, currently_locked, lock_info
1949
1944
1950 @property
1945 @property
1951 def last_db_change(self):
1946 def last_db_change(self):
1952 return self.updated_on
1947 return self.updated_on
1953
1948
1954 @property
1949 @property
1955 def clone_uri_hidden(self):
1950 def clone_uri_hidden(self):
1956 clone_uri = self.clone_uri
1951 clone_uri = self.clone_uri
1957 if clone_uri:
1952 if clone_uri:
1958 import urlobject
1953 import urlobject
1959 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1954 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1960 if url_obj.password:
1955 if url_obj.password:
1961 clone_uri = url_obj.with_password('*****')
1956 clone_uri = url_obj.with_password('*****')
1962 return clone_uri
1957 return clone_uri
1963
1958
1964 def clone_url(self, **override):
1959 def clone_url(self, **override):
1965 from rhodecode.model.settings import SettingsModel
1960 from rhodecode.model.settings import SettingsModel
1966
1961
1967 uri_tmpl = None
1962 uri_tmpl = None
1968 if 'with_id' in override:
1963 if 'with_id' in override:
1969 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1964 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1970 del override['with_id']
1965 del override['with_id']
1971
1966
1972 if 'uri_tmpl' in override:
1967 if 'uri_tmpl' in override:
1973 uri_tmpl = override['uri_tmpl']
1968 uri_tmpl = override['uri_tmpl']
1974 del override['uri_tmpl']
1969 del override['uri_tmpl']
1975
1970
1976 # we didn't override our tmpl from **overrides
1971 # we didn't override our tmpl from **overrides
1977 if not uri_tmpl:
1972 if not uri_tmpl:
1978 rc_config = SettingsModel().get_all_settings(cache=True)
1973 rc_config = SettingsModel().get_all_settings(cache=True)
1979 uri_tmpl = rc_config.get(
1974 uri_tmpl = rc_config.get(
1980 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
1975 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
1981
1976
1982 request = get_current_request()
1977 request = get_current_request()
1983 return get_clone_url(request=request,
1978 return get_clone_url(request=request,
1984 uri_tmpl=uri_tmpl,
1979 uri_tmpl=uri_tmpl,
1985 repo_name=self.repo_name,
1980 repo_name=self.repo_name,
1986 repo_id=self.repo_id, **override)
1981 repo_id=self.repo_id, **override)
1987
1982
1988 def set_state(self, state):
1983 def set_state(self, state):
1989 self.repo_state = state
1984 self.repo_state = state
1990 Session().add(self)
1985 Session().add(self)
1991 #==========================================================================
1986 #==========================================================================
1992 # SCM PROPERTIES
1987 # SCM PROPERTIES
1993 #==========================================================================
1988 #==========================================================================
1994
1989
1995 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1990 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1996 return get_commit_safe(
1991 return get_commit_safe(
1997 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1992 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1998
1993
1999 def get_changeset(self, rev=None, pre_load=None):
1994 def get_changeset(self, rev=None, pre_load=None):
2000 warnings.warn("Use get_commit", DeprecationWarning)
1995 warnings.warn("Use get_commit", DeprecationWarning)
2001 commit_id = None
1996 commit_id = None
2002 commit_idx = None
1997 commit_idx = None
2003 if isinstance(rev, basestring):
1998 if isinstance(rev, basestring):
2004 commit_id = rev
1999 commit_id = rev
2005 else:
2000 else:
2006 commit_idx = rev
2001 commit_idx = rev
2007 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2002 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2008 pre_load=pre_load)
2003 pre_load=pre_load)
2009
2004
2010 def get_landing_commit(self):
2005 def get_landing_commit(self):
2011 """
2006 """
2012 Returns landing commit, or if that doesn't exist returns the tip
2007 Returns landing commit, or if that doesn't exist returns the tip
2013 """
2008 """
2014 _rev_type, _rev = self.landing_rev
2009 _rev_type, _rev = self.landing_rev
2015 commit = self.get_commit(_rev)
2010 commit = self.get_commit(_rev)
2016 if isinstance(commit, EmptyCommit):
2011 if isinstance(commit, EmptyCommit):
2017 return self.get_commit()
2012 return self.get_commit()
2018 return commit
2013 return commit
2019
2014
2020 def update_commit_cache(self, cs_cache=None, config=None):
2015 def update_commit_cache(self, cs_cache=None, config=None):
2021 """
2016 """
2022 Update cache of last changeset for repository, keys should be::
2017 Update cache of last changeset for repository, keys should be::
2023
2018
2024 short_id
2019 short_id
2025 raw_id
2020 raw_id
2026 revision
2021 revision
2027 parents
2022 parents
2028 message
2023 message
2029 date
2024 date
2030 author
2025 author
2031
2026
2032 :param cs_cache:
2027 :param cs_cache:
2033 """
2028 """
2034 from rhodecode.lib.vcs.backends.base import BaseChangeset
2029 from rhodecode.lib.vcs.backends.base import BaseChangeset
2035 if cs_cache is None:
2030 if cs_cache is None:
2036 # use no-cache version here
2031 # use no-cache version here
2037 scm_repo = self.scm_instance(cache=False, config=config)
2032 scm_repo = self.scm_instance(cache=False, config=config)
2038 if scm_repo:
2033 if scm_repo:
2039 cs_cache = scm_repo.get_commit(
2034 cs_cache = scm_repo.get_commit(
2040 pre_load=["author", "date", "message", "parents"])
2035 pre_load=["author", "date", "message", "parents"])
2041 else:
2036 else:
2042 cs_cache = EmptyCommit()
2037 cs_cache = EmptyCommit()
2043
2038
2044 if isinstance(cs_cache, BaseChangeset):
2039 if isinstance(cs_cache, BaseChangeset):
2045 cs_cache = cs_cache.__json__()
2040 cs_cache = cs_cache.__json__()
2046
2041
2047 def is_outdated(new_cs_cache):
2042 def is_outdated(new_cs_cache):
2048 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2043 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2049 new_cs_cache['revision'] != self.changeset_cache['revision']):
2044 new_cs_cache['revision'] != self.changeset_cache['revision']):
2050 return True
2045 return True
2051 return False
2046 return False
2052
2047
2053 # check if we have maybe already latest cached revision
2048 # check if we have maybe already latest cached revision
2054 if is_outdated(cs_cache) or not self.changeset_cache:
2049 if is_outdated(cs_cache) or not self.changeset_cache:
2055 _default = datetime.datetime.fromtimestamp(0)
2050 _default = datetime.datetime.fromtimestamp(0)
2056 last_change = cs_cache.get('date') or _default
2051 last_change = cs_cache.get('date') or _default
2057 log.debug('updated repo %s with new cs cache %s',
2052 log.debug('updated repo %s with new cs cache %s',
2058 self.repo_name, cs_cache)
2053 self.repo_name, cs_cache)
2059 self.updated_on = last_change
2054 self.updated_on = last_change
2060 self.changeset_cache = cs_cache
2055 self.changeset_cache = cs_cache
2061 Session().add(self)
2056 Session().add(self)
2062 Session().commit()
2057 Session().commit()
2063 else:
2058 else:
2064 log.debug('Skipping update_commit_cache for repo:`%s` '
2059 log.debug('Skipping update_commit_cache for repo:`%s` '
2065 'commit already with latest changes', self.repo_name)
2060 'commit already with latest changes', self.repo_name)
2066
2061
2067 @property
2062 @property
2068 def tip(self):
2063 def tip(self):
2069 return self.get_commit('tip')
2064 return self.get_commit('tip')
2070
2065
2071 @property
2066 @property
2072 def author(self):
2067 def author(self):
2073 return self.tip.author
2068 return self.tip.author
2074
2069
2075 @property
2070 @property
2076 def last_change(self):
2071 def last_change(self):
2077 return self.scm_instance().last_change
2072 return self.scm_instance().last_change
2078
2073
2079 def get_comments(self, revisions=None):
2074 def get_comments(self, revisions=None):
2080 """
2075 """
2081 Returns comments for this repository grouped by revisions
2076 Returns comments for this repository grouped by revisions
2082
2077
2083 :param revisions: filter query by revisions only
2078 :param revisions: filter query by revisions only
2084 """
2079 """
2085 cmts = ChangesetComment.query()\
2080 cmts = ChangesetComment.query()\
2086 .filter(ChangesetComment.repo == self)
2081 .filter(ChangesetComment.repo == self)
2087 if revisions:
2082 if revisions:
2088 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2083 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2089 grouped = collections.defaultdict(list)
2084 grouped = collections.defaultdict(list)
2090 for cmt in cmts.all():
2085 for cmt in cmts.all():
2091 grouped[cmt.revision].append(cmt)
2086 grouped[cmt.revision].append(cmt)
2092 return grouped
2087 return grouped
2093
2088
2094 def statuses(self, revisions=None):
2089 def statuses(self, revisions=None):
2095 """
2090 """
2096 Returns statuses for this repository
2091 Returns statuses for this repository
2097
2092
2098 :param revisions: list of revisions to get statuses for
2093 :param revisions: list of revisions to get statuses for
2099 """
2094 """
2100 statuses = ChangesetStatus.query()\
2095 statuses = ChangesetStatus.query()\
2101 .filter(ChangesetStatus.repo == self)\
2096 .filter(ChangesetStatus.repo == self)\
2102 .filter(ChangesetStatus.version == 0)
2097 .filter(ChangesetStatus.version == 0)
2103
2098
2104 if revisions:
2099 if revisions:
2105 # Try doing the filtering in chunks to avoid hitting limits
2100 # Try doing the filtering in chunks to avoid hitting limits
2106 size = 500
2101 size = 500
2107 status_results = []
2102 status_results = []
2108 for chunk in xrange(0, len(revisions), size):
2103 for chunk in xrange(0, len(revisions), size):
2109 status_results += statuses.filter(
2104 status_results += statuses.filter(
2110 ChangesetStatus.revision.in_(
2105 ChangesetStatus.revision.in_(
2111 revisions[chunk: chunk+size])
2106 revisions[chunk: chunk+size])
2112 ).all()
2107 ).all()
2113 else:
2108 else:
2114 status_results = statuses.all()
2109 status_results = statuses.all()
2115
2110
2116 grouped = {}
2111 grouped = {}
2117
2112
2118 # maybe we have open new pullrequest without a status?
2113 # maybe we have open new pullrequest without a status?
2119 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2114 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2120 status_lbl = ChangesetStatus.get_status_lbl(stat)
2115 status_lbl = ChangesetStatus.get_status_lbl(stat)
2121 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2116 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2122 for rev in pr.revisions:
2117 for rev in pr.revisions:
2123 pr_id = pr.pull_request_id
2118 pr_id = pr.pull_request_id
2124 pr_repo = pr.target_repo.repo_name
2119 pr_repo = pr.target_repo.repo_name
2125 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2120 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2126
2121
2127 for stat in status_results:
2122 for stat in status_results:
2128 pr_id = pr_repo = None
2123 pr_id = pr_repo = None
2129 if stat.pull_request:
2124 if stat.pull_request:
2130 pr_id = stat.pull_request.pull_request_id
2125 pr_id = stat.pull_request.pull_request_id
2131 pr_repo = stat.pull_request.target_repo.repo_name
2126 pr_repo = stat.pull_request.target_repo.repo_name
2132 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2127 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2133 pr_id, pr_repo]
2128 pr_id, pr_repo]
2134 return grouped
2129 return grouped
2135
2130
2136 # ==========================================================================
2131 # ==========================================================================
2137 # SCM CACHE INSTANCE
2132 # SCM CACHE INSTANCE
2138 # ==========================================================================
2133 # ==========================================================================
2139
2134
2140 def scm_instance(self, **kwargs):
2135 def scm_instance(self, **kwargs):
2141 import rhodecode
2136 import rhodecode
2142
2137
2143 # Passing a config will not hit the cache currently only used
2138 # Passing a config will not hit the cache currently only used
2144 # for repo2dbmapper
2139 # for repo2dbmapper
2145 config = kwargs.pop('config', None)
2140 config = kwargs.pop('config', None)
2146 cache = kwargs.pop('cache', None)
2141 cache = kwargs.pop('cache', None)
2147 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2142 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2148 # if cache is NOT defined use default global, else we have a full
2143 # if cache is NOT defined use default global, else we have a full
2149 # control over cache behaviour
2144 # control over cache behaviour
2150 if cache is None and full_cache and not config:
2145 if cache is None and full_cache and not config:
2151 return self._get_instance_cached()
2146 return self._get_instance_cached()
2152 return self._get_instance(cache=bool(cache), config=config)
2147 return self._get_instance(cache=bool(cache), config=config)
2153
2148
2154 def _get_instance_cached(self):
2149 def _get_instance_cached(self):
2155 @cache_region('long_term')
2150 @cache_region('long_term')
2156 def _get_repo(cache_key):
2151 def _get_repo(cache_key):
2157 return self._get_instance()
2152 return self._get_instance()
2158
2153
2159 invalidator_context = CacheKey.repo_context_cache(
2154 invalidator_context = CacheKey.repo_context_cache(
2160 _get_repo, self.repo_name, None, thread_scoped=True)
2155 _get_repo, self.repo_name, None, thread_scoped=True)
2161
2156
2162 with invalidator_context as context:
2157 with invalidator_context as context:
2163 context.invalidate()
2158 context.invalidate()
2164 repo = context.compute()
2159 repo = context.compute()
2165
2160
2166 return repo
2161 return repo
2167
2162
2168 def _get_instance(self, cache=True, config=None):
2163 def _get_instance(self, cache=True, config=None):
2169 config = config or self._config
2164 config = config or self._config
2170 custom_wire = {
2165 custom_wire = {
2171 'cache': cache # controls the vcs.remote cache
2166 'cache': cache # controls the vcs.remote cache
2172 }
2167 }
2173 repo = get_vcs_instance(
2168 repo = get_vcs_instance(
2174 repo_path=safe_str(self.repo_full_path),
2169 repo_path=safe_str(self.repo_full_path),
2175 config=config,
2170 config=config,
2176 with_wire=custom_wire,
2171 with_wire=custom_wire,
2177 create=False,
2172 create=False,
2178 _vcs_alias=self.repo_type)
2173 _vcs_alias=self.repo_type)
2179
2174
2180 return repo
2175 return repo
2181
2176
2182 def __json__(self):
2177 def __json__(self):
2183 return {'landing_rev': self.landing_rev}
2178 return {'landing_rev': self.landing_rev}
2184
2179
2185 def get_dict(self):
2180 def get_dict(self):
2186
2181
2187 # Since we transformed `repo_name` to a hybrid property, we need to
2182 # Since we transformed `repo_name` to a hybrid property, we need to
2188 # keep compatibility with the code which uses `repo_name` field.
2183 # keep compatibility with the code which uses `repo_name` field.
2189
2184
2190 result = super(Repository, self).get_dict()
2185 result = super(Repository, self).get_dict()
2191 result['repo_name'] = result.pop('_repo_name', None)
2186 result['repo_name'] = result.pop('_repo_name', None)
2192 return result
2187 return result
2193
2188
2194
2189
2195 class RepoGroup(Base, BaseModel):
2190 class RepoGroup(Base, BaseModel):
2196 __tablename__ = 'groups'
2191 __tablename__ = 'groups'
2197 __table_args__ = (
2192 __table_args__ = (
2198 UniqueConstraint('group_name', 'group_parent_id'),
2193 UniqueConstraint('group_name', 'group_parent_id'),
2199 CheckConstraint('group_id != group_parent_id'),
2194 CheckConstraint('group_id != group_parent_id'),
2200 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2195 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2201 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2196 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2202 )
2197 )
2203 __mapper_args__ = {'order_by': 'group_name'}
2198 __mapper_args__ = {'order_by': 'group_name'}
2204
2199
2205 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2200 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2206
2201
2207 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2202 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2208 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2203 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2209 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2204 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2210 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2205 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2211 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2206 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2212 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2207 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2213 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2208 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2214 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2209 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2215 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2210 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2216
2211
2217 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2212 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2218 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2213 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2219 parent_group = relationship('RepoGroup', remote_side=group_id)
2214 parent_group = relationship('RepoGroup', remote_side=group_id)
2220 user = relationship('User')
2215 user = relationship('User')
2221 integrations = relationship('Integration',
2216 integrations = relationship('Integration',
2222 cascade="all, delete, delete-orphan")
2217 cascade="all, delete, delete-orphan")
2223
2218
2224 def __init__(self, group_name='', parent_group=None):
2219 def __init__(self, group_name='', parent_group=None):
2225 self.group_name = group_name
2220 self.group_name = group_name
2226 self.parent_group = parent_group
2221 self.parent_group = parent_group
2227
2222
2228 def __unicode__(self):
2223 def __unicode__(self):
2229 return u"<%s('id:%s:%s')>" % (
2224 return u"<%s('id:%s:%s')>" % (
2230 self.__class__.__name__, self.group_id, self.group_name)
2225 self.__class__.__name__, self.group_id, self.group_name)
2231
2226
2232 @hybrid_property
2227 @hybrid_property
2233 def description_safe(self):
2228 def description_safe(self):
2234 from rhodecode.lib import helpers as h
2229 from rhodecode.lib import helpers as h
2235 return h.escape(self.group_description)
2230 return h.escape(self.group_description)
2236
2231
2237 @classmethod
2232 @classmethod
2238 def _generate_choice(cls, repo_group):
2233 def _generate_choice(cls, repo_group):
2239 from webhelpers.html import literal as _literal
2234 from webhelpers.html import literal as _literal
2240 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2235 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2241 return repo_group.group_id, _name(repo_group.full_path_splitted)
2236 return repo_group.group_id, _name(repo_group.full_path_splitted)
2242
2237
2243 @classmethod
2238 @classmethod
2244 def groups_choices(cls, groups=None, show_empty_group=True):
2239 def groups_choices(cls, groups=None, show_empty_group=True):
2245 if not groups:
2240 if not groups:
2246 groups = cls.query().all()
2241 groups = cls.query().all()
2247
2242
2248 repo_groups = []
2243 repo_groups = []
2249 if show_empty_group:
2244 if show_empty_group:
2250 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2245 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2251
2246
2252 repo_groups.extend([cls._generate_choice(x) for x in groups])
2247 repo_groups.extend([cls._generate_choice(x) for x in groups])
2253
2248
2254 repo_groups = sorted(
2249 repo_groups = sorted(
2255 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2250 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2256 return repo_groups
2251 return repo_groups
2257
2252
2258 @classmethod
2253 @classmethod
2259 def url_sep(cls):
2254 def url_sep(cls):
2260 return URL_SEP
2255 return URL_SEP
2261
2256
2262 @classmethod
2257 @classmethod
2263 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2258 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2264 if case_insensitive:
2259 if case_insensitive:
2265 gr = cls.query().filter(func.lower(cls.group_name)
2260 gr = cls.query().filter(func.lower(cls.group_name)
2266 == func.lower(group_name))
2261 == func.lower(group_name))
2267 else:
2262 else:
2268 gr = cls.query().filter(cls.group_name == group_name)
2263 gr = cls.query().filter(cls.group_name == group_name)
2269 if cache:
2264 if cache:
2270 name_key = _hash_key(group_name)
2265 name_key = _hash_key(group_name)
2271 gr = gr.options(
2266 gr = gr.options(
2272 FromCache("sql_cache_short", "get_group_%s" % name_key))
2267 FromCache("sql_cache_short", "get_group_%s" % name_key))
2273 return gr.scalar()
2268 return gr.scalar()
2274
2269
2275 @classmethod
2270 @classmethod
2276 def get_user_personal_repo_group(cls, user_id):
2271 def get_user_personal_repo_group(cls, user_id):
2277 user = User.get(user_id)
2272 user = User.get(user_id)
2278 if user.username == User.DEFAULT_USER:
2273 if user.username == User.DEFAULT_USER:
2279 return None
2274 return None
2280
2275
2281 return cls.query()\
2276 return cls.query()\
2282 .filter(cls.personal == true()) \
2277 .filter(cls.personal == true()) \
2283 .filter(cls.user == user).scalar()
2278 .filter(cls.user == user).scalar()
2284
2279
2285 @classmethod
2280 @classmethod
2286 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2281 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2287 case_insensitive=True):
2282 case_insensitive=True):
2288 q = RepoGroup.query()
2283 q = RepoGroup.query()
2289
2284
2290 if not isinstance(user_id, Optional):
2285 if not isinstance(user_id, Optional):
2291 q = q.filter(RepoGroup.user_id == user_id)
2286 q = q.filter(RepoGroup.user_id == user_id)
2292
2287
2293 if not isinstance(group_id, Optional):
2288 if not isinstance(group_id, Optional):
2294 q = q.filter(RepoGroup.group_parent_id == group_id)
2289 q = q.filter(RepoGroup.group_parent_id == group_id)
2295
2290
2296 if case_insensitive:
2291 if case_insensitive:
2297 q = q.order_by(func.lower(RepoGroup.group_name))
2292 q = q.order_by(func.lower(RepoGroup.group_name))
2298 else:
2293 else:
2299 q = q.order_by(RepoGroup.group_name)
2294 q = q.order_by(RepoGroup.group_name)
2300 return q.all()
2295 return q.all()
2301
2296
2302 @property
2297 @property
2303 def parents(self):
2298 def parents(self):
2304 parents_recursion_limit = 10
2299 parents_recursion_limit = 10
2305 groups = []
2300 groups = []
2306 if self.parent_group is None:
2301 if self.parent_group is None:
2307 return groups
2302 return groups
2308 cur_gr = self.parent_group
2303 cur_gr = self.parent_group
2309 groups.insert(0, cur_gr)
2304 groups.insert(0, cur_gr)
2310 cnt = 0
2305 cnt = 0
2311 while 1:
2306 while 1:
2312 cnt += 1
2307 cnt += 1
2313 gr = getattr(cur_gr, 'parent_group', None)
2308 gr = getattr(cur_gr, 'parent_group', None)
2314 cur_gr = cur_gr.parent_group
2309 cur_gr = cur_gr.parent_group
2315 if gr is None:
2310 if gr is None:
2316 break
2311 break
2317 if cnt == parents_recursion_limit:
2312 if cnt == parents_recursion_limit:
2318 # this will prevent accidental infinit loops
2313 # this will prevent accidental infinit loops
2319 log.error(('more than %s parents found for group %s, stopping '
2314 log.error(('more than %s parents found for group %s, stopping '
2320 'recursive parent fetching' % (parents_recursion_limit, self)))
2315 'recursive parent fetching' % (parents_recursion_limit, self)))
2321 break
2316 break
2322
2317
2323 groups.insert(0, gr)
2318 groups.insert(0, gr)
2324 return groups
2319 return groups
2325
2320
2326 @property
2321 @property
2327 def last_db_change(self):
2322 def last_db_change(self):
2328 return self.updated_on
2323 return self.updated_on
2329
2324
2330 @property
2325 @property
2331 def children(self):
2326 def children(self):
2332 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2327 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2333
2328
2334 @property
2329 @property
2335 def name(self):
2330 def name(self):
2336 return self.group_name.split(RepoGroup.url_sep())[-1]
2331 return self.group_name.split(RepoGroup.url_sep())[-1]
2337
2332
2338 @property
2333 @property
2339 def full_path(self):
2334 def full_path(self):
2340 return self.group_name
2335 return self.group_name
2341
2336
2342 @property
2337 @property
2343 def full_path_splitted(self):
2338 def full_path_splitted(self):
2344 return self.group_name.split(RepoGroup.url_sep())
2339 return self.group_name.split(RepoGroup.url_sep())
2345
2340
2346 @property
2341 @property
2347 def repositories(self):
2342 def repositories(self):
2348 return Repository.query()\
2343 return Repository.query()\
2349 .filter(Repository.group == self)\
2344 .filter(Repository.group == self)\
2350 .order_by(Repository.repo_name)
2345 .order_by(Repository.repo_name)
2351
2346
2352 @property
2347 @property
2353 def repositories_recursive_count(self):
2348 def repositories_recursive_count(self):
2354 cnt = self.repositories.count()
2349 cnt = self.repositories.count()
2355
2350
2356 def children_count(group):
2351 def children_count(group):
2357 cnt = 0
2352 cnt = 0
2358 for child in group.children:
2353 for child in group.children:
2359 cnt += child.repositories.count()
2354 cnt += child.repositories.count()
2360 cnt += children_count(child)
2355 cnt += children_count(child)
2361 return cnt
2356 return cnt
2362
2357
2363 return cnt + children_count(self)
2358 return cnt + children_count(self)
2364
2359
2365 def _recursive_objects(self, include_repos=True):
2360 def _recursive_objects(self, include_repos=True):
2366 all_ = []
2361 all_ = []
2367
2362
2368 def _get_members(root_gr):
2363 def _get_members(root_gr):
2369 if include_repos:
2364 if include_repos:
2370 for r in root_gr.repositories:
2365 for r in root_gr.repositories:
2371 all_.append(r)
2366 all_.append(r)
2372 childs = root_gr.children.all()
2367 childs = root_gr.children.all()
2373 if childs:
2368 if childs:
2374 for gr in childs:
2369 for gr in childs:
2375 all_.append(gr)
2370 all_.append(gr)
2376 _get_members(gr)
2371 _get_members(gr)
2377
2372
2378 _get_members(self)
2373 _get_members(self)
2379 return [self] + all_
2374 return [self] + all_
2380
2375
2381 def recursive_groups_and_repos(self):
2376 def recursive_groups_and_repos(self):
2382 """
2377 """
2383 Recursive return all groups, with repositories in those groups
2378 Recursive return all groups, with repositories in those groups
2384 """
2379 """
2385 return self._recursive_objects()
2380 return self._recursive_objects()
2386
2381
2387 def recursive_groups(self):
2382 def recursive_groups(self):
2388 """
2383 """
2389 Returns all children groups for this group including children of children
2384 Returns all children groups for this group including children of children
2390 """
2385 """
2391 return self._recursive_objects(include_repos=False)
2386 return self._recursive_objects(include_repos=False)
2392
2387
2393 def get_new_name(self, group_name):
2388 def get_new_name(self, group_name):
2394 """
2389 """
2395 returns new full group name based on parent and new name
2390 returns new full group name based on parent and new name
2396
2391
2397 :param group_name:
2392 :param group_name:
2398 """
2393 """
2399 path_prefix = (self.parent_group.full_path_splitted if
2394 path_prefix = (self.parent_group.full_path_splitted if
2400 self.parent_group else [])
2395 self.parent_group else [])
2401 return RepoGroup.url_sep().join(path_prefix + [group_name])
2396 return RepoGroup.url_sep().join(path_prefix + [group_name])
2402
2397
2403 def permissions(self, with_admins=True, with_owner=True):
2398 def permissions(self, with_admins=True, with_owner=True):
2404 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2399 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2405 q = q.options(joinedload(UserRepoGroupToPerm.group),
2400 q = q.options(joinedload(UserRepoGroupToPerm.group),
2406 joinedload(UserRepoGroupToPerm.user),
2401 joinedload(UserRepoGroupToPerm.user),
2407 joinedload(UserRepoGroupToPerm.permission),)
2402 joinedload(UserRepoGroupToPerm.permission),)
2408
2403
2409 # get owners and admins and permissions. We do a trick of re-writing
2404 # get owners and admins and permissions. We do a trick of re-writing
2410 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2405 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2411 # has a global reference and changing one object propagates to all
2406 # has a global reference and changing one object propagates to all
2412 # others. This means if admin is also an owner admin_row that change
2407 # others. This means if admin is also an owner admin_row that change
2413 # would propagate to both objects
2408 # would propagate to both objects
2414 perm_rows = []
2409 perm_rows = []
2415 for _usr in q.all():
2410 for _usr in q.all():
2416 usr = AttributeDict(_usr.user.get_dict())
2411 usr = AttributeDict(_usr.user.get_dict())
2417 usr.permission = _usr.permission.permission_name
2412 usr.permission = _usr.permission.permission_name
2418 perm_rows.append(usr)
2413 perm_rows.append(usr)
2419
2414
2420 # filter the perm rows by 'default' first and then sort them by
2415 # filter the perm rows by 'default' first and then sort them by
2421 # admin,write,read,none permissions sorted again alphabetically in
2416 # admin,write,read,none permissions sorted again alphabetically in
2422 # each group
2417 # each group
2423 perm_rows = sorted(perm_rows, key=display_sort)
2418 perm_rows = sorted(perm_rows, key=display_sort)
2424
2419
2425 _admin_perm = 'group.admin'
2420 _admin_perm = 'group.admin'
2426 owner_row = []
2421 owner_row = []
2427 if with_owner:
2422 if with_owner:
2428 usr = AttributeDict(self.user.get_dict())
2423 usr = AttributeDict(self.user.get_dict())
2429 usr.owner_row = True
2424 usr.owner_row = True
2430 usr.permission = _admin_perm
2425 usr.permission = _admin_perm
2431 owner_row.append(usr)
2426 owner_row.append(usr)
2432
2427
2433 super_admin_rows = []
2428 super_admin_rows = []
2434 if with_admins:
2429 if with_admins:
2435 for usr in User.get_all_super_admins():
2430 for usr in User.get_all_super_admins():
2436 # if this admin is also owner, don't double the record
2431 # if this admin is also owner, don't double the record
2437 if usr.user_id == owner_row[0].user_id:
2432 if usr.user_id == owner_row[0].user_id:
2438 owner_row[0].admin_row = True
2433 owner_row[0].admin_row = True
2439 else:
2434 else:
2440 usr = AttributeDict(usr.get_dict())
2435 usr = AttributeDict(usr.get_dict())
2441 usr.admin_row = True
2436 usr.admin_row = True
2442 usr.permission = _admin_perm
2437 usr.permission = _admin_perm
2443 super_admin_rows.append(usr)
2438 super_admin_rows.append(usr)
2444
2439
2445 return super_admin_rows + owner_row + perm_rows
2440 return super_admin_rows + owner_row + perm_rows
2446
2441
2447 def permission_user_groups(self):
2442 def permission_user_groups(self):
2448 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2443 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2449 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2444 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2450 joinedload(UserGroupRepoGroupToPerm.users_group),
2445 joinedload(UserGroupRepoGroupToPerm.users_group),
2451 joinedload(UserGroupRepoGroupToPerm.permission),)
2446 joinedload(UserGroupRepoGroupToPerm.permission),)
2452
2447
2453 perm_rows = []
2448 perm_rows = []
2454 for _user_group in q.all():
2449 for _user_group in q.all():
2455 usr = AttributeDict(_user_group.users_group.get_dict())
2450 usr = AttributeDict(_user_group.users_group.get_dict())
2456 usr.permission = _user_group.permission.permission_name
2451 usr.permission = _user_group.permission.permission_name
2457 perm_rows.append(usr)
2452 perm_rows.append(usr)
2458
2453
2459 return perm_rows
2454 return perm_rows
2460
2455
2461 def get_api_data(self):
2456 def get_api_data(self):
2462 """
2457 """
2463 Common function for generating api data
2458 Common function for generating api data
2464
2459
2465 """
2460 """
2466 group = self
2461 group = self
2467 data = {
2462 data = {
2468 'group_id': group.group_id,
2463 'group_id': group.group_id,
2469 'group_name': group.group_name,
2464 'group_name': group.group_name,
2470 'group_description': group.description_safe,
2465 'group_description': group.description_safe,
2471 'parent_group': group.parent_group.group_name if group.parent_group else None,
2466 'parent_group': group.parent_group.group_name if group.parent_group else None,
2472 'repositories': [x.repo_name for x in group.repositories],
2467 'repositories': [x.repo_name for x in group.repositories],
2473 'owner': group.user.username,
2468 'owner': group.user.username,
2474 }
2469 }
2475 return data
2470 return data
2476
2471
2477
2472
2478 class Permission(Base, BaseModel):
2473 class Permission(Base, BaseModel):
2479 __tablename__ = 'permissions'
2474 __tablename__ = 'permissions'
2480 __table_args__ = (
2475 __table_args__ = (
2481 Index('p_perm_name_idx', 'permission_name'),
2476 Index('p_perm_name_idx', 'permission_name'),
2482 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2477 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2483 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2478 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2484 )
2479 )
2485 PERMS = [
2480 PERMS = [
2486 ('hg.admin', _('RhodeCode Super Administrator')),
2481 ('hg.admin', _('RhodeCode Super Administrator')),
2487
2482
2488 ('repository.none', _('Repository no access')),
2483 ('repository.none', _('Repository no access')),
2489 ('repository.read', _('Repository read access')),
2484 ('repository.read', _('Repository read access')),
2490 ('repository.write', _('Repository write access')),
2485 ('repository.write', _('Repository write access')),
2491 ('repository.admin', _('Repository admin access')),
2486 ('repository.admin', _('Repository admin access')),
2492
2487
2493 ('group.none', _('Repository group no access')),
2488 ('group.none', _('Repository group no access')),
2494 ('group.read', _('Repository group read access')),
2489 ('group.read', _('Repository group read access')),
2495 ('group.write', _('Repository group write access')),
2490 ('group.write', _('Repository group write access')),
2496 ('group.admin', _('Repository group admin access')),
2491 ('group.admin', _('Repository group admin access')),
2497
2492
2498 ('usergroup.none', _('User group no access')),
2493 ('usergroup.none', _('User group no access')),
2499 ('usergroup.read', _('User group read access')),
2494 ('usergroup.read', _('User group read access')),
2500 ('usergroup.write', _('User group write access')),
2495 ('usergroup.write', _('User group write access')),
2501 ('usergroup.admin', _('User group admin access')),
2496 ('usergroup.admin', _('User group admin access')),
2502
2497
2503 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2498 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2504 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2499 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2505
2500
2506 ('hg.usergroup.create.false', _('User Group creation disabled')),
2501 ('hg.usergroup.create.false', _('User Group creation disabled')),
2507 ('hg.usergroup.create.true', _('User Group creation enabled')),
2502 ('hg.usergroup.create.true', _('User Group creation enabled')),
2508
2503
2509 ('hg.create.none', _('Repository creation disabled')),
2504 ('hg.create.none', _('Repository creation disabled')),
2510 ('hg.create.repository', _('Repository creation enabled')),
2505 ('hg.create.repository', _('Repository creation enabled')),
2511 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2506 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2512 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2507 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2513
2508
2514 ('hg.fork.none', _('Repository forking disabled')),
2509 ('hg.fork.none', _('Repository forking disabled')),
2515 ('hg.fork.repository', _('Repository forking enabled')),
2510 ('hg.fork.repository', _('Repository forking enabled')),
2516
2511
2517 ('hg.register.none', _('Registration disabled')),
2512 ('hg.register.none', _('Registration disabled')),
2518 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2513 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2519 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2514 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2520
2515
2521 ('hg.password_reset.enabled', _('Password reset enabled')),
2516 ('hg.password_reset.enabled', _('Password reset enabled')),
2522 ('hg.password_reset.hidden', _('Password reset hidden')),
2517 ('hg.password_reset.hidden', _('Password reset hidden')),
2523 ('hg.password_reset.disabled', _('Password reset disabled')),
2518 ('hg.password_reset.disabled', _('Password reset disabled')),
2524
2519
2525 ('hg.extern_activate.manual', _('Manual activation of external account')),
2520 ('hg.extern_activate.manual', _('Manual activation of external account')),
2526 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2521 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2527
2522
2528 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2523 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2529 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2524 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2530 ]
2525 ]
2531
2526
2532 # definition of system default permissions for DEFAULT user
2527 # definition of system default permissions for DEFAULT user
2533 DEFAULT_USER_PERMISSIONS = [
2528 DEFAULT_USER_PERMISSIONS = [
2534 'repository.read',
2529 'repository.read',
2535 'group.read',
2530 'group.read',
2536 'usergroup.read',
2531 'usergroup.read',
2537 'hg.create.repository',
2532 'hg.create.repository',
2538 'hg.repogroup.create.false',
2533 'hg.repogroup.create.false',
2539 'hg.usergroup.create.false',
2534 'hg.usergroup.create.false',
2540 'hg.create.write_on_repogroup.true',
2535 'hg.create.write_on_repogroup.true',
2541 'hg.fork.repository',
2536 'hg.fork.repository',
2542 'hg.register.manual_activate',
2537 'hg.register.manual_activate',
2543 'hg.password_reset.enabled',
2538 'hg.password_reset.enabled',
2544 'hg.extern_activate.auto',
2539 'hg.extern_activate.auto',
2545 'hg.inherit_default_perms.true',
2540 'hg.inherit_default_perms.true',
2546 ]
2541 ]
2547
2542
2548 # defines which permissions are more important higher the more important
2543 # defines which permissions are more important higher the more important
2549 # Weight defines which permissions are more important.
2544 # Weight defines which permissions are more important.
2550 # The higher number the more important.
2545 # The higher number the more important.
2551 PERM_WEIGHTS = {
2546 PERM_WEIGHTS = {
2552 'repository.none': 0,
2547 'repository.none': 0,
2553 'repository.read': 1,
2548 'repository.read': 1,
2554 'repository.write': 3,
2549 'repository.write': 3,
2555 'repository.admin': 4,
2550 'repository.admin': 4,
2556
2551
2557 'group.none': 0,
2552 'group.none': 0,
2558 'group.read': 1,
2553 'group.read': 1,
2559 'group.write': 3,
2554 'group.write': 3,
2560 'group.admin': 4,
2555 'group.admin': 4,
2561
2556
2562 'usergroup.none': 0,
2557 'usergroup.none': 0,
2563 'usergroup.read': 1,
2558 'usergroup.read': 1,
2564 'usergroup.write': 3,
2559 'usergroup.write': 3,
2565 'usergroup.admin': 4,
2560 'usergroup.admin': 4,
2566
2561
2567 'hg.repogroup.create.false': 0,
2562 'hg.repogroup.create.false': 0,
2568 'hg.repogroup.create.true': 1,
2563 'hg.repogroup.create.true': 1,
2569
2564
2570 'hg.usergroup.create.false': 0,
2565 'hg.usergroup.create.false': 0,
2571 'hg.usergroup.create.true': 1,
2566 'hg.usergroup.create.true': 1,
2572
2567
2573 'hg.fork.none': 0,
2568 'hg.fork.none': 0,
2574 'hg.fork.repository': 1,
2569 'hg.fork.repository': 1,
2575 'hg.create.none': 0,
2570 'hg.create.none': 0,
2576 'hg.create.repository': 1
2571 'hg.create.repository': 1
2577 }
2572 }
2578
2573
2579 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2574 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2580 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2575 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2581 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2576 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2582
2577
2583 def __unicode__(self):
2578 def __unicode__(self):
2584 return u"<%s('%s:%s')>" % (
2579 return u"<%s('%s:%s')>" % (
2585 self.__class__.__name__, self.permission_id, self.permission_name
2580 self.__class__.__name__, self.permission_id, self.permission_name
2586 )
2581 )
2587
2582
2588 @classmethod
2583 @classmethod
2589 def get_by_key(cls, key):
2584 def get_by_key(cls, key):
2590 return cls.query().filter(cls.permission_name == key).scalar()
2585 return cls.query().filter(cls.permission_name == key).scalar()
2591
2586
2592 @classmethod
2587 @classmethod
2593 def get_default_repo_perms(cls, user_id, repo_id=None):
2588 def get_default_repo_perms(cls, user_id, repo_id=None):
2594 q = Session().query(UserRepoToPerm, Repository, Permission)\
2589 q = Session().query(UserRepoToPerm, Repository, Permission)\
2595 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2590 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2596 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2591 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2597 .filter(UserRepoToPerm.user_id == user_id)
2592 .filter(UserRepoToPerm.user_id == user_id)
2598 if repo_id:
2593 if repo_id:
2599 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2594 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2600 return q.all()
2595 return q.all()
2601
2596
2602 @classmethod
2597 @classmethod
2603 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2598 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2604 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2599 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2605 .join(
2600 .join(
2606 Permission,
2601 Permission,
2607 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2602 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2608 .join(
2603 .join(
2609 Repository,
2604 Repository,
2610 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2605 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2611 .join(
2606 .join(
2612 UserGroup,
2607 UserGroup,
2613 UserGroupRepoToPerm.users_group_id ==
2608 UserGroupRepoToPerm.users_group_id ==
2614 UserGroup.users_group_id)\
2609 UserGroup.users_group_id)\
2615 .join(
2610 .join(
2616 UserGroupMember,
2611 UserGroupMember,
2617 UserGroupRepoToPerm.users_group_id ==
2612 UserGroupRepoToPerm.users_group_id ==
2618 UserGroupMember.users_group_id)\
2613 UserGroupMember.users_group_id)\
2619 .filter(
2614 .filter(
2620 UserGroupMember.user_id == user_id,
2615 UserGroupMember.user_id == user_id,
2621 UserGroup.users_group_active == true())
2616 UserGroup.users_group_active == true())
2622 if repo_id:
2617 if repo_id:
2623 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2618 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2624 return q.all()
2619 return q.all()
2625
2620
2626 @classmethod
2621 @classmethod
2627 def get_default_group_perms(cls, user_id, repo_group_id=None):
2622 def get_default_group_perms(cls, user_id, repo_group_id=None):
2628 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2623 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2629 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2624 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2630 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2625 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2631 .filter(UserRepoGroupToPerm.user_id == user_id)
2626 .filter(UserRepoGroupToPerm.user_id == user_id)
2632 if repo_group_id:
2627 if repo_group_id:
2633 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2628 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2634 return q.all()
2629 return q.all()
2635
2630
2636 @classmethod
2631 @classmethod
2637 def get_default_group_perms_from_user_group(
2632 def get_default_group_perms_from_user_group(
2638 cls, user_id, repo_group_id=None):
2633 cls, user_id, repo_group_id=None):
2639 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2634 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2640 .join(
2635 .join(
2641 Permission,
2636 Permission,
2642 UserGroupRepoGroupToPerm.permission_id ==
2637 UserGroupRepoGroupToPerm.permission_id ==
2643 Permission.permission_id)\
2638 Permission.permission_id)\
2644 .join(
2639 .join(
2645 RepoGroup,
2640 RepoGroup,
2646 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2641 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2647 .join(
2642 .join(
2648 UserGroup,
2643 UserGroup,
2649 UserGroupRepoGroupToPerm.users_group_id ==
2644 UserGroupRepoGroupToPerm.users_group_id ==
2650 UserGroup.users_group_id)\
2645 UserGroup.users_group_id)\
2651 .join(
2646 .join(
2652 UserGroupMember,
2647 UserGroupMember,
2653 UserGroupRepoGroupToPerm.users_group_id ==
2648 UserGroupRepoGroupToPerm.users_group_id ==
2654 UserGroupMember.users_group_id)\
2649 UserGroupMember.users_group_id)\
2655 .filter(
2650 .filter(
2656 UserGroupMember.user_id == user_id,
2651 UserGroupMember.user_id == user_id,
2657 UserGroup.users_group_active == true())
2652 UserGroup.users_group_active == true())
2658 if repo_group_id:
2653 if repo_group_id:
2659 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2654 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2660 return q.all()
2655 return q.all()
2661
2656
2662 @classmethod
2657 @classmethod
2663 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2658 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2664 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2659 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2665 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2660 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2666 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2661 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2667 .filter(UserUserGroupToPerm.user_id == user_id)
2662 .filter(UserUserGroupToPerm.user_id == user_id)
2668 if user_group_id:
2663 if user_group_id:
2669 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2664 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2670 return q.all()
2665 return q.all()
2671
2666
2672 @classmethod
2667 @classmethod
2673 def get_default_user_group_perms_from_user_group(
2668 def get_default_user_group_perms_from_user_group(
2674 cls, user_id, user_group_id=None):
2669 cls, user_id, user_group_id=None):
2675 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2670 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2676 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2671 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2677 .join(
2672 .join(
2678 Permission,
2673 Permission,
2679 UserGroupUserGroupToPerm.permission_id ==
2674 UserGroupUserGroupToPerm.permission_id ==
2680 Permission.permission_id)\
2675 Permission.permission_id)\
2681 .join(
2676 .join(
2682 TargetUserGroup,
2677 TargetUserGroup,
2683 UserGroupUserGroupToPerm.target_user_group_id ==
2678 UserGroupUserGroupToPerm.target_user_group_id ==
2684 TargetUserGroup.users_group_id)\
2679 TargetUserGroup.users_group_id)\
2685 .join(
2680 .join(
2686 UserGroup,
2681 UserGroup,
2687 UserGroupUserGroupToPerm.user_group_id ==
2682 UserGroupUserGroupToPerm.user_group_id ==
2688 UserGroup.users_group_id)\
2683 UserGroup.users_group_id)\
2689 .join(
2684 .join(
2690 UserGroupMember,
2685 UserGroupMember,
2691 UserGroupUserGroupToPerm.user_group_id ==
2686 UserGroupUserGroupToPerm.user_group_id ==
2692 UserGroupMember.users_group_id)\
2687 UserGroupMember.users_group_id)\
2693 .filter(
2688 .filter(
2694 UserGroupMember.user_id == user_id,
2689 UserGroupMember.user_id == user_id,
2695 UserGroup.users_group_active == true())
2690 UserGroup.users_group_active == true())
2696 if user_group_id:
2691 if user_group_id:
2697 q = q.filter(
2692 q = q.filter(
2698 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2693 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2699
2694
2700 return q.all()
2695 return q.all()
2701
2696
2702
2697
2703 class UserRepoToPerm(Base, BaseModel):
2698 class UserRepoToPerm(Base, BaseModel):
2704 __tablename__ = 'repo_to_perm'
2699 __tablename__ = 'repo_to_perm'
2705 __table_args__ = (
2700 __table_args__ = (
2706 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2701 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2707 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2702 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2708 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2703 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2709 )
2704 )
2710 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2705 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2711 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2706 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2712 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2707 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2713 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2708 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2714
2709
2715 user = relationship('User')
2710 user = relationship('User')
2716 repository = relationship('Repository')
2711 repository = relationship('Repository')
2717 permission = relationship('Permission')
2712 permission = relationship('Permission')
2718
2713
2719 @classmethod
2714 @classmethod
2720 def create(cls, user, repository, permission):
2715 def create(cls, user, repository, permission):
2721 n = cls()
2716 n = cls()
2722 n.user = user
2717 n.user = user
2723 n.repository = repository
2718 n.repository = repository
2724 n.permission = permission
2719 n.permission = permission
2725 Session().add(n)
2720 Session().add(n)
2726 return n
2721 return n
2727
2722
2728 def __unicode__(self):
2723 def __unicode__(self):
2729 return u'<%s => %s >' % (self.user, self.repository)
2724 return u'<%s => %s >' % (self.user, self.repository)
2730
2725
2731
2726
2732 class UserUserGroupToPerm(Base, BaseModel):
2727 class UserUserGroupToPerm(Base, BaseModel):
2733 __tablename__ = 'user_user_group_to_perm'
2728 __tablename__ = 'user_user_group_to_perm'
2734 __table_args__ = (
2729 __table_args__ = (
2735 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2730 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2736 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2731 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2737 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2732 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2738 )
2733 )
2739 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2734 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2740 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2735 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2741 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2736 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2742 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2737 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2743
2738
2744 user = relationship('User')
2739 user = relationship('User')
2745 user_group = relationship('UserGroup')
2740 user_group = relationship('UserGroup')
2746 permission = relationship('Permission')
2741 permission = relationship('Permission')
2747
2742
2748 @classmethod
2743 @classmethod
2749 def create(cls, user, user_group, permission):
2744 def create(cls, user, user_group, permission):
2750 n = cls()
2745 n = cls()
2751 n.user = user
2746 n.user = user
2752 n.user_group = user_group
2747 n.user_group = user_group
2753 n.permission = permission
2748 n.permission = permission
2754 Session().add(n)
2749 Session().add(n)
2755 return n
2750 return n
2756
2751
2757 def __unicode__(self):
2752 def __unicode__(self):
2758 return u'<%s => %s >' % (self.user, self.user_group)
2753 return u'<%s => %s >' % (self.user, self.user_group)
2759
2754
2760
2755
2761 class UserToPerm(Base, BaseModel):
2756 class UserToPerm(Base, BaseModel):
2762 __tablename__ = 'user_to_perm'
2757 __tablename__ = 'user_to_perm'
2763 __table_args__ = (
2758 __table_args__ = (
2764 UniqueConstraint('user_id', 'permission_id'),
2759 UniqueConstraint('user_id', 'permission_id'),
2765 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2760 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2766 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2761 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2767 )
2762 )
2768 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2763 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2769 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2764 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2770 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2765 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2771
2766
2772 user = relationship('User')
2767 user = relationship('User')
2773 permission = relationship('Permission', lazy='joined')
2768 permission = relationship('Permission', lazy='joined')
2774
2769
2775 def __unicode__(self):
2770 def __unicode__(self):
2776 return u'<%s => %s >' % (self.user, self.permission)
2771 return u'<%s => %s >' % (self.user, self.permission)
2777
2772
2778
2773
2779 class UserGroupRepoToPerm(Base, BaseModel):
2774 class UserGroupRepoToPerm(Base, BaseModel):
2780 __tablename__ = 'users_group_repo_to_perm'
2775 __tablename__ = 'users_group_repo_to_perm'
2781 __table_args__ = (
2776 __table_args__ = (
2782 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2777 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2783 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2778 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2784 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2779 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2785 )
2780 )
2786 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2781 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2787 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2782 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2788 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2783 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2789 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2784 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2790
2785
2791 users_group = relationship('UserGroup')
2786 users_group = relationship('UserGroup')
2792 permission = relationship('Permission')
2787 permission = relationship('Permission')
2793 repository = relationship('Repository')
2788 repository = relationship('Repository')
2794
2789
2795 @classmethod
2790 @classmethod
2796 def create(cls, users_group, repository, permission):
2791 def create(cls, users_group, repository, permission):
2797 n = cls()
2792 n = cls()
2798 n.users_group = users_group
2793 n.users_group = users_group
2799 n.repository = repository
2794 n.repository = repository
2800 n.permission = permission
2795 n.permission = permission
2801 Session().add(n)
2796 Session().add(n)
2802 return n
2797 return n
2803
2798
2804 def __unicode__(self):
2799 def __unicode__(self):
2805 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2800 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2806
2801
2807
2802
2808 class UserGroupUserGroupToPerm(Base, BaseModel):
2803 class UserGroupUserGroupToPerm(Base, BaseModel):
2809 __tablename__ = 'user_group_user_group_to_perm'
2804 __tablename__ = 'user_group_user_group_to_perm'
2810 __table_args__ = (
2805 __table_args__ = (
2811 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2806 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2812 CheckConstraint('target_user_group_id != user_group_id'),
2807 CheckConstraint('target_user_group_id != user_group_id'),
2813 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2808 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2814 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2809 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2815 )
2810 )
2816 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2811 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2817 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2812 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2818 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2813 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2819 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2814 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2820
2815
2821 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2816 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2822 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2817 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2823 permission = relationship('Permission')
2818 permission = relationship('Permission')
2824
2819
2825 @classmethod
2820 @classmethod
2826 def create(cls, target_user_group, user_group, permission):
2821 def create(cls, target_user_group, user_group, permission):
2827 n = cls()
2822 n = cls()
2828 n.target_user_group = target_user_group
2823 n.target_user_group = target_user_group
2829 n.user_group = user_group
2824 n.user_group = user_group
2830 n.permission = permission
2825 n.permission = permission
2831 Session().add(n)
2826 Session().add(n)
2832 return n
2827 return n
2833
2828
2834 def __unicode__(self):
2829 def __unicode__(self):
2835 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2830 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2836
2831
2837
2832
2838 class UserGroupToPerm(Base, BaseModel):
2833 class UserGroupToPerm(Base, BaseModel):
2839 __tablename__ = 'users_group_to_perm'
2834 __tablename__ = 'users_group_to_perm'
2840 __table_args__ = (
2835 __table_args__ = (
2841 UniqueConstraint('users_group_id', 'permission_id',),
2836 UniqueConstraint('users_group_id', 'permission_id',),
2842 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2837 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2843 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2838 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2844 )
2839 )
2845 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2840 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2846 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2841 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2847 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2842 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2848
2843
2849 users_group = relationship('UserGroup')
2844 users_group = relationship('UserGroup')
2850 permission = relationship('Permission')
2845 permission = relationship('Permission')
2851
2846
2852
2847
2853 class UserRepoGroupToPerm(Base, BaseModel):
2848 class UserRepoGroupToPerm(Base, BaseModel):
2854 __tablename__ = 'user_repo_group_to_perm'
2849 __tablename__ = 'user_repo_group_to_perm'
2855 __table_args__ = (
2850 __table_args__ = (
2856 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2851 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2857 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2852 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2858 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2853 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2859 )
2854 )
2860
2855
2861 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2856 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2862 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2857 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2863 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2858 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2864 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2859 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2865
2860
2866 user = relationship('User')
2861 user = relationship('User')
2867 group = relationship('RepoGroup')
2862 group = relationship('RepoGroup')
2868 permission = relationship('Permission')
2863 permission = relationship('Permission')
2869
2864
2870 @classmethod
2865 @classmethod
2871 def create(cls, user, repository_group, permission):
2866 def create(cls, user, repository_group, permission):
2872 n = cls()
2867 n = cls()
2873 n.user = user
2868 n.user = user
2874 n.group = repository_group
2869 n.group = repository_group
2875 n.permission = permission
2870 n.permission = permission
2876 Session().add(n)
2871 Session().add(n)
2877 return n
2872 return n
2878
2873
2879
2874
2880 class UserGroupRepoGroupToPerm(Base, BaseModel):
2875 class UserGroupRepoGroupToPerm(Base, BaseModel):
2881 __tablename__ = 'users_group_repo_group_to_perm'
2876 __tablename__ = 'users_group_repo_group_to_perm'
2882 __table_args__ = (
2877 __table_args__ = (
2883 UniqueConstraint('users_group_id', 'group_id'),
2878 UniqueConstraint('users_group_id', 'group_id'),
2884 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2879 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2885 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2880 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2886 )
2881 )
2887
2882
2888 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2883 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2889 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2884 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2890 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2885 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2891 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2886 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2892
2887
2893 users_group = relationship('UserGroup')
2888 users_group = relationship('UserGroup')
2894 permission = relationship('Permission')
2889 permission = relationship('Permission')
2895 group = relationship('RepoGroup')
2890 group = relationship('RepoGroup')
2896
2891
2897 @classmethod
2892 @classmethod
2898 def create(cls, user_group, repository_group, permission):
2893 def create(cls, user_group, repository_group, permission):
2899 n = cls()
2894 n = cls()
2900 n.users_group = user_group
2895 n.users_group = user_group
2901 n.group = repository_group
2896 n.group = repository_group
2902 n.permission = permission
2897 n.permission = permission
2903 Session().add(n)
2898 Session().add(n)
2904 return n
2899 return n
2905
2900
2906 def __unicode__(self):
2901 def __unicode__(self):
2907 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2902 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2908
2903
2909
2904
2910 class Statistics(Base, BaseModel):
2905 class Statistics(Base, BaseModel):
2911 __tablename__ = 'statistics'
2906 __tablename__ = 'statistics'
2912 __table_args__ = (
2907 __table_args__ = (
2913 UniqueConstraint('repository_id'),
2908 UniqueConstraint('repository_id'),
2914 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2909 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2915 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2910 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2916 )
2911 )
2917 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2912 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2918 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2913 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2919 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2914 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2920 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2915 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2921 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2916 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2922 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2917 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2923
2918
2924 repository = relationship('Repository', single_parent=True)
2919 repository = relationship('Repository', single_parent=True)
2925
2920
2926
2921
2927 class UserFollowing(Base, BaseModel):
2922 class UserFollowing(Base, BaseModel):
2928 __tablename__ = 'user_followings'
2923 __tablename__ = 'user_followings'
2929 __table_args__ = (
2924 __table_args__ = (
2930 UniqueConstraint('user_id', 'follows_repository_id'),
2925 UniqueConstraint('user_id', 'follows_repository_id'),
2931 UniqueConstraint('user_id', 'follows_user_id'),
2926 UniqueConstraint('user_id', 'follows_user_id'),
2932 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2927 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2933 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2928 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2934 )
2929 )
2935
2930
2936 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2931 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2937 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2932 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2938 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2933 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2939 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2934 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2940 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2935 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2941
2936
2942 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2937 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2943
2938
2944 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2939 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2945 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2940 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2946
2941
2947 @classmethod
2942 @classmethod
2948 def get_repo_followers(cls, repo_id):
2943 def get_repo_followers(cls, repo_id):
2949 return cls.query().filter(cls.follows_repo_id == repo_id)
2944 return cls.query().filter(cls.follows_repo_id == repo_id)
2950
2945
2951
2946
2952 class CacheKey(Base, BaseModel):
2947 class CacheKey(Base, BaseModel):
2953 __tablename__ = 'cache_invalidation'
2948 __tablename__ = 'cache_invalidation'
2954 __table_args__ = (
2949 __table_args__ = (
2955 UniqueConstraint('cache_key'),
2950 UniqueConstraint('cache_key'),
2956 Index('key_idx', 'cache_key'),
2951 Index('key_idx', 'cache_key'),
2957 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2952 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2958 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2953 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2959 )
2954 )
2960 CACHE_TYPE_ATOM = 'ATOM'
2955 CACHE_TYPE_ATOM = 'ATOM'
2961 CACHE_TYPE_RSS = 'RSS'
2956 CACHE_TYPE_RSS = 'RSS'
2962 CACHE_TYPE_README = 'README'
2957 CACHE_TYPE_README = 'README'
2963
2958
2964 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2959 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2965 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2960 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2966 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2961 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2967 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2962 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2968
2963
2969 def __init__(self, cache_key, cache_args=''):
2964 def __init__(self, cache_key, cache_args=''):
2970 self.cache_key = cache_key
2965 self.cache_key = cache_key
2971 self.cache_args = cache_args
2966 self.cache_args = cache_args
2972 self.cache_active = False
2967 self.cache_active = False
2973
2968
2974 def __unicode__(self):
2969 def __unicode__(self):
2975 return u"<%s('%s:%s[%s]')>" % (
2970 return u"<%s('%s:%s[%s]')>" % (
2976 self.__class__.__name__,
2971 self.__class__.__name__,
2977 self.cache_id, self.cache_key, self.cache_active)
2972 self.cache_id, self.cache_key, self.cache_active)
2978
2973
2979 def _cache_key_partition(self):
2974 def _cache_key_partition(self):
2980 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2975 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2981 return prefix, repo_name, suffix
2976 return prefix, repo_name, suffix
2982
2977
2983 def get_prefix(self):
2978 def get_prefix(self):
2984 """
2979 """
2985 Try to extract prefix from existing cache key. The key could consist
2980 Try to extract prefix from existing cache key. The key could consist
2986 of prefix, repo_name, suffix
2981 of prefix, repo_name, suffix
2987 """
2982 """
2988 # this returns prefix, repo_name, suffix
2983 # this returns prefix, repo_name, suffix
2989 return self._cache_key_partition()[0]
2984 return self._cache_key_partition()[0]
2990
2985
2991 def get_suffix(self):
2986 def get_suffix(self):
2992 """
2987 """
2993 get suffix that might have been used in _get_cache_key to
2988 get suffix that might have been used in _get_cache_key to
2994 generate self.cache_key. Only used for informational purposes
2989 generate self.cache_key. Only used for informational purposes
2995 in repo_edit.mako.
2990 in repo_edit.mako.
2996 """
2991 """
2997 # prefix, repo_name, suffix
2992 # prefix, repo_name, suffix
2998 return self._cache_key_partition()[2]
2993 return self._cache_key_partition()[2]
2999
2994
3000 @classmethod
2995 @classmethod
3001 def delete_all_cache(cls):
2996 def delete_all_cache(cls):
3002 """
2997 """
3003 Delete all cache keys from database.
2998 Delete all cache keys from database.
3004 Should only be run when all instances are down and all entries
2999 Should only be run when all instances are down and all entries
3005 thus stale.
3000 thus stale.
3006 """
3001 """
3007 cls.query().delete()
3002 cls.query().delete()
3008 Session().commit()
3003 Session().commit()
3009
3004
3010 @classmethod
3005 @classmethod
3011 def get_cache_key(cls, repo_name, cache_type):
3006 def get_cache_key(cls, repo_name, cache_type):
3012 """
3007 """
3013
3008
3014 Generate a cache key for this process of RhodeCode instance.
3009 Generate a cache key for this process of RhodeCode instance.
3015 Prefix most likely will be process id or maybe explicitly set
3010 Prefix most likely will be process id or maybe explicitly set
3016 instance_id from .ini file.
3011 instance_id from .ini file.
3017 """
3012 """
3018 import rhodecode
3013 import rhodecode
3019 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3014 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3020
3015
3021 repo_as_unicode = safe_unicode(repo_name)
3016 repo_as_unicode = safe_unicode(repo_name)
3022 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3017 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3023 if cache_type else repo_as_unicode
3018 if cache_type else repo_as_unicode
3024
3019
3025 return u'{}{}'.format(prefix, key)
3020 return u'{}{}'.format(prefix, key)
3026
3021
3027 @classmethod
3022 @classmethod
3028 def set_invalidate(cls, repo_name, delete=False):
3023 def set_invalidate(cls, repo_name, delete=False):
3029 """
3024 """
3030 Mark all caches of a repo as invalid in the database.
3025 Mark all caches of a repo as invalid in the database.
3031 """
3026 """
3032
3027
3033 try:
3028 try:
3034 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3029 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3035 if delete:
3030 if delete:
3036 log.debug('cache objects deleted for repo %s',
3031 log.debug('cache objects deleted for repo %s',
3037 safe_str(repo_name))
3032 safe_str(repo_name))
3038 qry.delete()
3033 qry.delete()
3039 else:
3034 else:
3040 log.debug('cache objects marked as invalid for repo %s',
3035 log.debug('cache objects marked as invalid for repo %s',
3041 safe_str(repo_name))
3036 safe_str(repo_name))
3042 qry.update({"cache_active": False})
3037 qry.update({"cache_active": False})
3043
3038
3044 Session().commit()
3039 Session().commit()
3045 except Exception:
3040 except Exception:
3046 log.exception(
3041 log.exception(
3047 'Cache key invalidation failed for repository %s',
3042 'Cache key invalidation failed for repository %s',
3048 safe_str(repo_name))
3043 safe_str(repo_name))
3049 Session().rollback()
3044 Session().rollback()
3050
3045
3051 @classmethod
3046 @classmethod
3052 def get_active_cache(cls, cache_key):
3047 def get_active_cache(cls, cache_key):
3053 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3048 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3054 if inv_obj:
3049 if inv_obj:
3055 return inv_obj
3050 return inv_obj
3056 return None
3051 return None
3057
3052
3058 @classmethod
3053 @classmethod
3059 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3054 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3060 thread_scoped=False):
3055 thread_scoped=False):
3061 """
3056 """
3062 @cache_region('long_term')
3057 @cache_region('long_term')
3063 def _heavy_calculation(cache_key):
3058 def _heavy_calculation(cache_key):
3064 return 'result'
3059 return 'result'
3065
3060
3066 cache_context = CacheKey.repo_context_cache(
3061 cache_context = CacheKey.repo_context_cache(
3067 _heavy_calculation, repo_name, cache_type)
3062 _heavy_calculation, repo_name, cache_type)
3068
3063
3069 with cache_context as context:
3064 with cache_context as context:
3070 context.invalidate()
3065 context.invalidate()
3071 computed = context.compute()
3066 computed = context.compute()
3072
3067
3073 assert computed == 'result'
3068 assert computed == 'result'
3074 """
3069 """
3075 from rhodecode.lib import caches
3070 from rhodecode.lib import caches
3076 return caches.InvalidationContext(
3071 return caches.InvalidationContext(
3077 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3072 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3078
3073
3079
3074
3080 class ChangesetComment(Base, BaseModel):
3075 class ChangesetComment(Base, BaseModel):
3081 __tablename__ = 'changeset_comments'
3076 __tablename__ = 'changeset_comments'
3082 __table_args__ = (
3077 __table_args__ = (
3083 Index('cc_revision_idx', 'revision'),
3078 Index('cc_revision_idx', 'revision'),
3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3079 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3080 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 )
3081 )
3087
3082
3088 COMMENT_OUTDATED = u'comment_outdated'
3083 COMMENT_OUTDATED = u'comment_outdated'
3089 COMMENT_TYPE_NOTE = u'note'
3084 COMMENT_TYPE_NOTE = u'note'
3090 COMMENT_TYPE_TODO = u'todo'
3085 COMMENT_TYPE_TODO = u'todo'
3091 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3086 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3092
3087
3093 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3088 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3094 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3089 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3095 revision = Column('revision', String(40), nullable=True)
3090 revision = Column('revision', String(40), nullable=True)
3096 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3091 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3097 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3092 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3098 line_no = Column('line_no', Unicode(10), nullable=True)
3093 line_no = Column('line_no', Unicode(10), nullable=True)
3099 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3094 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3100 f_path = Column('f_path', Unicode(1000), nullable=True)
3095 f_path = Column('f_path', Unicode(1000), nullable=True)
3101 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3096 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3102 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3097 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3103 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3098 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3104 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3099 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3105 renderer = Column('renderer', Unicode(64), nullable=True)
3100 renderer = Column('renderer', Unicode(64), nullable=True)
3106 display_state = Column('display_state', Unicode(128), nullable=True)
3101 display_state = Column('display_state', Unicode(128), nullable=True)
3107
3102
3108 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3103 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3109 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3104 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3110 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3105 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3111 author = relationship('User', lazy='joined')
3106 author = relationship('User', lazy='joined')
3112 repo = relationship('Repository')
3107 repo = relationship('Repository')
3113 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3108 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3114 pull_request = relationship('PullRequest', lazy='joined')
3109 pull_request = relationship('PullRequest', lazy='joined')
3115 pull_request_version = relationship('PullRequestVersion')
3110 pull_request_version = relationship('PullRequestVersion')
3116
3111
3117 @classmethod
3112 @classmethod
3118 def get_users(cls, revision=None, pull_request_id=None):
3113 def get_users(cls, revision=None, pull_request_id=None):
3119 """
3114 """
3120 Returns user associated with this ChangesetComment. ie those
3115 Returns user associated with this ChangesetComment. ie those
3121 who actually commented
3116 who actually commented
3122
3117
3123 :param cls:
3118 :param cls:
3124 :param revision:
3119 :param revision:
3125 """
3120 """
3126 q = Session().query(User)\
3121 q = Session().query(User)\
3127 .join(ChangesetComment.author)
3122 .join(ChangesetComment.author)
3128 if revision:
3123 if revision:
3129 q = q.filter(cls.revision == revision)
3124 q = q.filter(cls.revision == revision)
3130 elif pull_request_id:
3125 elif pull_request_id:
3131 q = q.filter(cls.pull_request_id == pull_request_id)
3126 q = q.filter(cls.pull_request_id == pull_request_id)
3132 return q.all()
3127 return q.all()
3133
3128
3134 @classmethod
3129 @classmethod
3135 def get_index_from_version(cls, pr_version, versions):
3130 def get_index_from_version(cls, pr_version, versions):
3136 num_versions = [x.pull_request_version_id for x in versions]
3131 num_versions = [x.pull_request_version_id for x in versions]
3137 try:
3132 try:
3138 return num_versions.index(pr_version) +1
3133 return num_versions.index(pr_version) +1
3139 except (IndexError, ValueError):
3134 except (IndexError, ValueError):
3140 return
3135 return
3141
3136
3142 @property
3137 @property
3143 def outdated(self):
3138 def outdated(self):
3144 return self.display_state == self.COMMENT_OUTDATED
3139 return self.display_state == self.COMMENT_OUTDATED
3145
3140
3146 def outdated_at_version(self, version):
3141 def outdated_at_version(self, version):
3147 """
3142 """
3148 Checks if comment is outdated for given pull request version
3143 Checks if comment is outdated for given pull request version
3149 """
3144 """
3150 return self.outdated and self.pull_request_version_id != version
3145 return self.outdated and self.pull_request_version_id != version
3151
3146
3152 def older_than_version(self, version):
3147 def older_than_version(self, version):
3153 """
3148 """
3154 Checks if comment is made from previous version than given
3149 Checks if comment is made from previous version than given
3155 """
3150 """
3156 if version is None:
3151 if version is None:
3157 return self.pull_request_version_id is not None
3152 return self.pull_request_version_id is not None
3158
3153
3159 return self.pull_request_version_id < version
3154 return self.pull_request_version_id < version
3160
3155
3161 @property
3156 @property
3162 def resolved(self):
3157 def resolved(self):
3163 return self.resolved_by[0] if self.resolved_by else None
3158 return self.resolved_by[0] if self.resolved_by else None
3164
3159
3165 @property
3160 @property
3166 def is_todo(self):
3161 def is_todo(self):
3167 return self.comment_type == self.COMMENT_TYPE_TODO
3162 return self.comment_type == self.COMMENT_TYPE_TODO
3168
3163
3169 @property
3164 @property
3170 def is_inline(self):
3165 def is_inline(self):
3171 return self.line_no and self.f_path
3166 return self.line_no and self.f_path
3172
3167
3173 def get_index_version(self, versions):
3168 def get_index_version(self, versions):
3174 return self.get_index_from_version(
3169 return self.get_index_from_version(
3175 self.pull_request_version_id, versions)
3170 self.pull_request_version_id, versions)
3176
3171
3177 def __repr__(self):
3172 def __repr__(self):
3178 if self.comment_id:
3173 if self.comment_id:
3179 return '<DB:Comment #%s>' % self.comment_id
3174 return '<DB:Comment #%s>' % self.comment_id
3180 else:
3175 else:
3181 return '<DB:Comment at %#x>' % id(self)
3176 return '<DB:Comment at %#x>' % id(self)
3182
3177
3183 def get_api_data(self):
3178 def get_api_data(self):
3184 comment = self
3179 comment = self
3185 data = {
3180 data = {
3186 'comment_id': comment.comment_id,
3181 'comment_id': comment.comment_id,
3187 'comment_type': comment.comment_type,
3182 'comment_type': comment.comment_type,
3188 'comment_text': comment.text,
3183 'comment_text': comment.text,
3189 'comment_status': comment.status_change,
3184 'comment_status': comment.status_change,
3190 'comment_f_path': comment.f_path,
3185 'comment_f_path': comment.f_path,
3191 'comment_lineno': comment.line_no,
3186 'comment_lineno': comment.line_no,
3192 'comment_author': comment.author,
3187 'comment_author': comment.author,
3193 'comment_created_on': comment.created_on
3188 'comment_created_on': comment.created_on
3194 }
3189 }
3195 return data
3190 return data
3196
3191
3197 def __json__(self):
3192 def __json__(self):
3198 data = dict()
3193 data = dict()
3199 data.update(self.get_api_data())
3194 data.update(self.get_api_data())
3200 return data
3195 return data
3201
3196
3202
3197
3203 class ChangesetStatus(Base, BaseModel):
3198 class ChangesetStatus(Base, BaseModel):
3204 __tablename__ = 'changeset_statuses'
3199 __tablename__ = 'changeset_statuses'
3205 __table_args__ = (
3200 __table_args__ = (
3206 Index('cs_revision_idx', 'revision'),
3201 Index('cs_revision_idx', 'revision'),
3207 Index('cs_version_idx', 'version'),
3202 Index('cs_version_idx', 'version'),
3208 UniqueConstraint('repo_id', 'revision', 'version'),
3203 UniqueConstraint('repo_id', 'revision', 'version'),
3209 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3210 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3211 )
3206 )
3212 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3207 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3213 STATUS_APPROVED = 'approved'
3208 STATUS_APPROVED = 'approved'
3214 STATUS_REJECTED = 'rejected'
3209 STATUS_REJECTED = 'rejected'
3215 STATUS_UNDER_REVIEW = 'under_review'
3210 STATUS_UNDER_REVIEW = 'under_review'
3216
3211
3217 STATUSES = [
3212 STATUSES = [
3218 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3213 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3219 (STATUS_APPROVED, _("Approved")),
3214 (STATUS_APPROVED, _("Approved")),
3220 (STATUS_REJECTED, _("Rejected")),
3215 (STATUS_REJECTED, _("Rejected")),
3221 (STATUS_UNDER_REVIEW, _("Under Review")),
3216 (STATUS_UNDER_REVIEW, _("Under Review")),
3222 ]
3217 ]
3223
3218
3224 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3219 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3225 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3220 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3226 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3221 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3227 revision = Column('revision', String(40), nullable=False)
3222 revision = Column('revision', String(40), nullable=False)
3228 status = Column('status', String(128), nullable=False, default=DEFAULT)
3223 status = Column('status', String(128), nullable=False, default=DEFAULT)
3229 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3224 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3230 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3225 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3231 version = Column('version', Integer(), nullable=False, default=0)
3226 version = Column('version', Integer(), nullable=False, default=0)
3232 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3227 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3233
3228
3234 author = relationship('User', lazy='joined')
3229 author = relationship('User', lazy='joined')
3235 repo = relationship('Repository')
3230 repo = relationship('Repository')
3236 comment = relationship('ChangesetComment', lazy='joined')
3231 comment = relationship('ChangesetComment', lazy='joined')
3237 pull_request = relationship('PullRequest', lazy='joined')
3232 pull_request = relationship('PullRequest', lazy='joined')
3238
3233
3239 def __unicode__(self):
3234 def __unicode__(self):
3240 return u"<%s('%s[v%s]:%s')>" % (
3235 return u"<%s('%s[v%s]:%s')>" % (
3241 self.__class__.__name__,
3236 self.__class__.__name__,
3242 self.status, self.version, self.author
3237 self.status, self.version, self.author
3243 )
3238 )
3244
3239
3245 @classmethod
3240 @classmethod
3246 def get_status_lbl(cls, value):
3241 def get_status_lbl(cls, value):
3247 return dict(cls.STATUSES).get(value)
3242 return dict(cls.STATUSES).get(value)
3248
3243
3249 @property
3244 @property
3250 def status_lbl(self):
3245 def status_lbl(self):
3251 return ChangesetStatus.get_status_lbl(self.status)
3246 return ChangesetStatus.get_status_lbl(self.status)
3252
3247
3253 def get_api_data(self):
3248 def get_api_data(self):
3254 status = self
3249 status = self
3255 data = {
3250 data = {
3256 'status_id': status.changeset_status_id,
3251 'status_id': status.changeset_status_id,
3257 'status': status.status,
3252 'status': status.status,
3258 }
3253 }
3259 return data
3254 return data
3260
3255
3261 def __json__(self):
3256 def __json__(self):
3262 data = dict()
3257 data = dict()
3263 data.update(self.get_api_data())
3258 data.update(self.get_api_data())
3264 return data
3259 return data
3265
3260
3266
3261
3267 class _PullRequestBase(BaseModel):
3262 class _PullRequestBase(BaseModel):
3268 """
3263 """
3269 Common attributes of pull request and version entries.
3264 Common attributes of pull request and version entries.
3270 """
3265 """
3271
3266
3272 # .status values
3267 # .status values
3273 STATUS_NEW = u'new'
3268 STATUS_NEW = u'new'
3274 STATUS_OPEN = u'open'
3269 STATUS_OPEN = u'open'
3275 STATUS_CLOSED = u'closed'
3270 STATUS_CLOSED = u'closed'
3276
3271
3277 title = Column('title', Unicode(255), nullable=True)
3272 title = Column('title', Unicode(255), nullable=True)
3278 description = Column(
3273 description = Column(
3279 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3274 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3280 nullable=True)
3275 nullable=True)
3281 # new/open/closed status of pull request (not approve/reject/etc)
3276 # new/open/closed status of pull request (not approve/reject/etc)
3282 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3277 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3283 created_on = Column(
3278 created_on = Column(
3284 'created_on', DateTime(timezone=False), nullable=False,
3279 'created_on', DateTime(timezone=False), nullable=False,
3285 default=datetime.datetime.now)
3280 default=datetime.datetime.now)
3286 updated_on = Column(
3281 updated_on = Column(
3287 'updated_on', DateTime(timezone=False), nullable=False,
3282 'updated_on', DateTime(timezone=False), nullable=False,
3288 default=datetime.datetime.now)
3283 default=datetime.datetime.now)
3289
3284
3290 @declared_attr
3285 @declared_attr
3291 def user_id(cls):
3286 def user_id(cls):
3292 return Column(
3287 return Column(
3293 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3288 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3294 unique=None)
3289 unique=None)
3295
3290
3296 # 500 revisions max
3291 # 500 revisions max
3297 _revisions = Column(
3292 _revisions = Column(
3298 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3293 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3299
3294
3300 @declared_attr
3295 @declared_attr
3301 def source_repo_id(cls):
3296 def source_repo_id(cls):
3302 # TODO: dan: rename column to source_repo_id
3297 # TODO: dan: rename column to source_repo_id
3303 return Column(
3298 return Column(
3304 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3299 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3305 nullable=False)
3300 nullable=False)
3306
3301
3307 source_ref = Column('org_ref', Unicode(255), nullable=False)
3302 source_ref = Column('org_ref', Unicode(255), nullable=False)
3308
3303
3309 @declared_attr
3304 @declared_attr
3310 def target_repo_id(cls):
3305 def target_repo_id(cls):
3311 # TODO: dan: rename column to target_repo_id
3306 # TODO: dan: rename column to target_repo_id
3312 return Column(
3307 return Column(
3313 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3308 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3314 nullable=False)
3309 nullable=False)
3315
3310
3316 target_ref = Column('other_ref', Unicode(255), nullable=False)
3311 target_ref = Column('other_ref', Unicode(255), nullable=False)
3317 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3312 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3318
3313
3319 # TODO: dan: rename column to last_merge_source_rev
3314 # TODO: dan: rename column to last_merge_source_rev
3320 _last_merge_source_rev = Column(
3315 _last_merge_source_rev = Column(
3321 'last_merge_org_rev', String(40), nullable=True)
3316 'last_merge_org_rev', String(40), nullable=True)
3322 # TODO: dan: rename column to last_merge_target_rev
3317 # TODO: dan: rename column to last_merge_target_rev
3323 _last_merge_target_rev = Column(
3318 _last_merge_target_rev = Column(
3324 'last_merge_other_rev', String(40), nullable=True)
3319 'last_merge_other_rev', String(40), nullable=True)
3325 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3320 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3326 merge_rev = Column('merge_rev', String(40), nullable=True)
3321 merge_rev = Column('merge_rev', String(40), nullable=True)
3327
3322
3328 reviewer_data = Column(
3323 reviewer_data = Column(
3329 'reviewer_data_json', MutationObj.as_mutable(
3324 'reviewer_data_json', MutationObj.as_mutable(
3330 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3325 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3331
3326
3332 @property
3327 @property
3333 def reviewer_data_json(self):
3328 def reviewer_data_json(self):
3334 return json.dumps(self.reviewer_data)
3329 return json.dumps(self.reviewer_data)
3335
3330
3336 @hybrid_property
3331 @hybrid_property
3337 def description_safe(self):
3332 def description_safe(self):
3338 from rhodecode.lib import helpers as h
3333 from rhodecode.lib import helpers as h
3339 return h.escape(self.description)
3334 return h.escape(self.description)
3340
3335
3341 @hybrid_property
3336 @hybrid_property
3342 def revisions(self):
3337 def revisions(self):
3343 return self._revisions.split(':') if self._revisions else []
3338 return self._revisions.split(':') if self._revisions else []
3344
3339
3345 @revisions.setter
3340 @revisions.setter
3346 def revisions(self, val):
3341 def revisions(self, val):
3347 self._revisions = ':'.join(val)
3342 self._revisions = ':'.join(val)
3348
3343
3349 @declared_attr
3344 @declared_attr
3350 def author(cls):
3345 def author(cls):
3351 return relationship('User', lazy='joined')
3346 return relationship('User', lazy='joined')
3352
3347
3353 @declared_attr
3348 @declared_attr
3354 def source_repo(cls):
3349 def source_repo(cls):
3355 return relationship(
3350 return relationship(
3356 'Repository',
3351 'Repository',
3357 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3352 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3358
3353
3359 @property
3354 @property
3360 def source_ref_parts(self):
3355 def source_ref_parts(self):
3361 return self.unicode_to_reference(self.source_ref)
3356 return self.unicode_to_reference(self.source_ref)
3362
3357
3363 @declared_attr
3358 @declared_attr
3364 def target_repo(cls):
3359 def target_repo(cls):
3365 return relationship(
3360 return relationship(
3366 'Repository',
3361 'Repository',
3367 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3362 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3368
3363
3369 @property
3364 @property
3370 def target_ref_parts(self):
3365 def target_ref_parts(self):
3371 return self.unicode_to_reference(self.target_ref)
3366 return self.unicode_to_reference(self.target_ref)
3372
3367
3373 @property
3368 @property
3374 def shadow_merge_ref(self):
3369 def shadow_merge_ref(self):
3375 return self.unicode_to_reference(self._shadow_merge_ref)
3370 return self.unicode_to_reference(self._shadow_merge_ref)
3376
3371
3377 @shadow_merge_ref.setter
3372 @shadow_merge_ref.setter
3378 def shadow_merge_ref(self, ref):
3373 def shadow_merge_ref(self, ref):
3379 self._shadow_merge_ref = self.reference_to_unicode(ref)
3374 self._shadow_merge_ref = self.reference_to_unicode(ref)
3380
3375
3381 def unicode_to_reference(self, raw):
3376 def unicode_to_reference(self, raw):
3382 """
3377 """
3383 Convert a unicode (or string) to a reference object.
3378 Convert a unicode (or string) to a reference object.
3384 If unicode evaluates to False it returns None.
3379 If unicode evaluates to False it returns None.
3385 """
3380 """
3386 if raw:
3381 if raw:
3387 refs = raw.split(':')
3382 refs = raw.split(':')
3388 return Reference(*refs)
3383 return Reference(*refs)
3389 else:
3384 else:
3390 return None
3385 return None
3391
3386
3392 def reference_to_unicode(self, ref):
3387 def reference_to_unicode(self, ref):
3393 """
3388 """
3394 Convert a reference object to unicode.
3389 Convert a reference object to unicode.
3395 If reference is None it returns None.
3390 If reference is None it returns None.
3396 """
3391 """
3397 if ref:
3392 if ref:
3398 return u':'.join(ref)
3393 return u':'.join(ref)
3399 else:
3394 else:
3400 return None
3395 return None
3401
3396
3402 def get_api_data(self, with_merge_state=True):
3397 def get_api_data(self, with_merge_state=True):
3403 from rhodecode.model.pull_request import PullRequestModel
3398 from rhodecode.model.pull_request import PullRequestModel
3404
3399
3405 pull_request = self
3400 pull_request = self
3406 if with_merge_state:
3401 if with_merge_state:
3407 merge_status = PullRequestModel().merge_status(pull_request)
3402 merge_status = PullRequestModel().merge_status(pull_request)
3408 merge_state = {
3403 merge_state = {
3409 'status': merge_status[0],
3404 'status': merge_status[0],
3410 'message': safe_unicode(merge_status[1]),
3405 'message': safe_unicode(merge_status[1]),
3411 }
3406 }
3412 else:
3407 else:
3413 merge_state = {'status': 'not_available',
3408 merge_state = {'status': 'not_available',
3414 'message': 'not_available'}
3409 'message': 'not_available'}
3415
3410
3416 merge_data = {
3411 merge_data = {
3417 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3412 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3418 'reference': (
3413 'reference': (
3419 pull_request.shadow_merge_ref._asdict()
3414 pull_request.shadow_merge_ref._asdict()
3420 if pull_request.shadow_merge_ref else None),
3415 if pull_request.shadow_merge_ref else None),
3421 }
3416 }
3422
3417
3423 data = {
3418 data = {
3424 'pull_request_id': pull_request.pull_request_id,
3419 'pull_request_id': pull_request.pull_request_id,
3425 'url': PullRequestModel().get_url(pull_request),
3420 'url': PullRequestModel().get_url(pull_request),
3426 'title': pull_request.title,
3421 'title': pull_request.title,
3427 'description': pull_request.description,
3422 'description': pull_request.description,
3428 'status': pull_request.status,
3423 'status': pull_request.status,
3429 'created_on': pull_request.created_on,
3424 'created_on': pull_request.created_on,
3430 'updated_on': pull_request.updated_on,
3425 'updated_on': pull_request.updated_on,
3431 'commit_ids': pull_request.revisions,
3426 'commit_ids': pull_request.revisions,
3432 'review_status': pull_request.calculated_review_status(),
3427 'review_status': pull_request.calculated_review_status(),
3433 'mergeable': merge_state,
3428 'mergeable': merge_state,
3434 'source': {
3429 'source': {
3435 'clone_url': pull_request.source_repo.clone_url(),
3430 'clone_url': pull_request.source_repo.clone_url(),
3436 'repository': pull_request.source_repo.repo_name,
3431 'repository': pull_request.source_repo.repo_name,
3437 'reference': {
3432 'reference': {
3438 'name': pull_request.source_ref_parts.name,
3433 'name': pull_request.source_ref_parts.name,
3439 'type': pull_request.source_ref_parts.type,
3434 'type': pull_request.source_ref_parts.type,
3440 'commit_id': pull_request.source_ref_parts.commit_id,
3435 'commit_id': pull_request.source_ref_parts.commit_id,
3441 },
3436 },
3442 },
3437 },
3443 'target': {
3438 'target': {
3444 'clone_url': pull_request.target_repo.clone_url(),
3439 'clone_url': pull_request.target_repo.clone_url(),
3445 'repository': pull_request.target_repo.repo_name,
3440 'repository': pull_request.target_repo.repo_name,
3446 'reference': {
3441 'reference': {
3447 'name': pull_request.target_ref_parts.name,
3442 'name': pull_request.target_ref_parts.name,
3448 'type': pull_request.target_ref_parts.type,
3443 'type': pull_request.target_ref_parts.type,
3449 'commit_id': pull_request.target_ref_parts.commit_id,
3444 'commit_id': pull_request.target_ref_parts.commit_id,
3450 },
3445 },
3451 },
3446 },
3452 'merge': merge_data,
3447 'merge': merge_data,
3453 'author': pull_request.author.get_api_data(include_secrets=False,
3448 'author': pull_request.author.get_api_data(include_secrets=False,
3454 details='basic'),
3449 details='basic'),
3455 'reviewers': [
3450 'reviewers': [
3456 {
3451 {
3457 'user': reviewer.get_api_data(include_secrets=False,
3452 'user': reviewer.get_api_data(include_secrets=False,
3458 details='basic'),
3453 details='basic'),
3459 'reasons': reasons,
3454 'reasons': reasons,
3460 'review_status': st[0][1].status if st else 'not_reviewed',
3455 'review_status': st[0][1].status if st else 'not_reviewed',
3461 }
3456 }
3462 for reviewer, reasons, mandatory, st in
3457 for reviewer, reasons, mandatory, st in
3463 pull_request.reviewers_statuses()
3458 pull_request.reviewers_statuses()
3464 ]
3459 ]
3465 }
3460 }
3466
3461
3467 return data
3462 return data
3468
3463
3469
3464
3470 class PullRequest(Base, _PullRequestBase):
3465 class PullRequest(Base, _PullRequestBase):
3471 __tablename__ = 'pull_requests'
3466 __tablename__ = 'pull_requests'
3472 __table_args__ = (
3467 __table_args__ = (
3473 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3468 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3474 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3469 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3475 )
3470 )
3476
3471
3477 pull_request_id = Column(
3472 pull_request_id = Column(
3478 'pull_request_id', Integer(), nullable=False, primary_key=True)
3473 'pull_request_id', Integer(), nullable=False, primary_key=True)
3479
3474
3480 def __repr__(self):
3475 def __repr__(self):
3481 if self.pull_request_id:
3476 if self.pull_request_id:
3482 return '<DB:PullRequest #%s>' % self.pull_request_id
3477 return '<DB:PullRequest #%s>' % self.pull_request_id
3483 else:
3478 else:
3484 return '<DB:PullRequest at %#x>' % id(self)
3479 return '<DB:PullRequest at %#x>' % id(self)
3485
3480
3486 reviewers = relationship('PullRequestReviewers',
3481 reviewers = relationship('PullRequestReviewers',
3487 cascade="all, delete, delete-orphan")
3482 cascade="all, delete, delete-orphan")
3488 statuses = relationship('ChangesetStatus',
3483 statuses = relationship('ChangesetStatus',
3489 cascade="all, delete, delete-orphan")
3484 cascade="all, delete, delete-orphan")
3490 comments = relationship('ChangesetComment',
3485 comments = relationship('ChangesetComment',
3491 cascade="all, delete, delete-orphan")
3486 cascade="all, delete, delete-orphan")
3492 versions = relationship('PullRequestVersion',
3487 versions = relationship('PullRequestVersion',
3493 cascade="all, delete, delete-orphan",
3488 cascade="all, delete, delete-orphan",
3494 lazy='dynamic')
3489 lazy='dynamic')
3495
3490
3496 @classmethod
3491 @classmethod
3497 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3492 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3498 internal_methods=None):
3493 internal_methods=None):
3499
3494
3500 class PullRequestDisplay(object):
3495 class PullRequestDisplay(object):
3501 """
3496 """
3502 Special object wrapper for showing PullRequest data via Versions
3497 Special object wrapper for showing PullRequest data via Versions
3503 It mimics PR object as close as possible. This is read only object
3498 It mimics PR object as close as possible. This is read only object
3504 just for display
3499 just for display
3505 """
3500 """
3506
3501
3507 def __init__(self, attrs, internal=None):
3502 def __init__(self, attrs, internal=None):
3508 self.attrs = attrs
3503 self.attrs = attrs
3509 # internal have priority over the given ones via attrs
3504 # internal have priority over the given ones via attrs
3510 self.internal = internal or ['versions']
3505 self.internal = internal or ['versions']
3511
3506
3512 def __getattr__(self, item):
3507 def __getattr__(self, item):
3513 if item in self.internal:
3508 if item in self.internal:
3514 return getattr(self, item)
3509 return getattr(self, item)
3515 try:
3510 try:
3516 return self.attrs[item]
3511 return self.attrs[item]
3517 except KeyError:
3512 except KeyError:
3518 raise AttributeError(
3513 raise AttributeError(
3519 '%s object has no attribute %s' % (self, item))
3514 '%s object has no attribute %s' % (self, item))
3520
3515
3521 def __repr__(self):
3516 def __repr__(self):
3522 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3517 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3523
3518
3524 def versions(self):
3519 def versions(self):
3525 return pull_request_obj.versions.order_by(
3520 return pull_request_obj.versions.order_by(
3526 PullRequestVersion.pull_request_version_id).all()
3521 PullRequestVersion.pull_request_version_id).all()
3527
3522
3528 def is_closed(self):
3523 def is_closed(self):
3529 return pull_request_obj.is_closed()
3524 return pull_request_obj.is_closed()
3530
3525
3531 @property
3526 @property
3532 def pull_request_version_id(self):
3527 def pull_request_version_id(self):
3533 return getattr(pull_request_obj, 'pull_request_version_id', None)
3528 return getattr(pull_request_obj, 'pull_request_version_id', None)
3534
3529
3535 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3530 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3536
3531
3537 attrs.author = StrictAttributeDict(
3532 attrs.author = StrictAttributeDict(
3538 pull_request_obj.author.get_api_data())
3533 pull_request_obj.author.get_api_data())
3539 if pull_request_obj.target_repo:
3534 if pull_request_obj.target_repo:
3540 attrs.target_repo = StrictAttributeDict(
3535 attrs.target_repo = StrictAttributeDict(
3541 pull_request_obj.target_repo.get_api_data())
3536 pull_request_obj.target_repo.get_api_data())
3542 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3537 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3543
3538
3544 if pull_request_obj.source_repo:
3539 if pull_request_obj.source_repo:
3545 attrs.source_repo = StrictAttributeDict(
3540 attrs.source_repo = StrictAttributeDict(
3546 pull_request_obj.source_repo.get_api_data())
3541 pull_request_obj.source_repo.get_api_data())
3547 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3542 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3548
3543
3549 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3544 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3550 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3545 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3551 attrs.revisions = pull_request_obj.revisions
3546 attrs.revisions = pull_request_obj.revisions
3552
3547
3553 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3548 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3554 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3549 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3555 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3550 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3556
3551
3557 return PullRequestDisplay(attrs, internal=internal_methods)
3552 return PullRequestDisplay(attrs, internal=internal_methods)
3558
3553
3559 def is_closed(self):
3554 def is_closed(self):
3560 return self.status == self.STATUS_CLOSED
3555 return self.status == self.STATUS_CLOSED
3561
3556
3562 def __json__(self):
3557 def __json__(self):
3563 return {
3558 return {
3564 'revisions': self.revisions,
3559 'revisions': self.revisions,
3565 }
3560 }
3566
3561
3567 def calculated_review_status(self):
3562 def calculated_review_status(self):
3568 from rhodecode.model.changeset_status import ChangesetStatusModel
3563 from rhodecode.model.changeset_status import ChangesetStatusModel
3569 return ChangesetStatusModel().calculated_review_status(self)
3564 return ChangesetStatusModel().calculated_review_status(self)
3570
3565
3571 def reviewers_statuses(self):
3566 def reviewers_statuses(self):
3572 from rhodecode.model.changeset_status import ChangesetStatusModel
3567 from rhodecode.model.changeset_status import ChangesetStatusModel
3573 return ChangesetStatusModel().reviewers_statuses(self)
3568 return ChangesetStatusModel().reviewers_statuses(self)
3574
3569
3575 @property
3570 @property
3576 def workspace_id(self):
3571 def workspace_id(self):
3577 from rhodecode.model.pull_request import PullRequestModel
3572 from rhodecode.model.pull_request import PullRequestModel
3578 return PullRequestModel()._workspace_id(self)
3573 return PullRequestModel()._workspace_id(self)
3579
3574
3580 def get_shadow_repo(self):
3575 def get_shadow_repo(self):
3581 workspace_id = self.workspace_id
3576 workspace_id = self.workspace_id
3582 vcs_obj = self.target_repo.scm_instance()
3577 vcs_obj = self.target_repo.scm_instance()
3583 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3578 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3584 workspace_id)
3579 workspace_id)
3585 return vcs_obj._get_shadow_instance(shadow_repository_path)
3580 return vcs_obj._get_shadow_instance(shadow_repository_path)
3586
3581
3587
3582
3588 class PullRequestVersion(Base, _PullRequestBase):
3583 class PullRequestVersion(Base, _PullRequestBase):
3589 __tablename__ = 'pull_request_versions'
3584 __tablename__ = 'pull_request_versions'
3590 __table_args__ = (
3585 __table_args__ = (
3591 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3586 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3592 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3587 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3593 )
3588 )
3594
3589
3595 pull_request_version_id = Column(
3590 pull_request_version_id = Column(
3596 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3591 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3597 pull_request_id = Column(
3592 pull_request_id = Column(
3598 'pull_request_id', Integer(),
3593 'pull_request_id', Integer(),
3599 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3594 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3600 pull_request = relationship('PullRequest')
3595 pull_request = relationship('PullRequest')
3601
3596
3602 def __repr__(self):
3597 def __repr__(self):
3603 if self.pull_request_version_id:
3598 if self.pull_request_version_id:
3604 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3599 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3605 else:
3600 else:
3606 return '<DB:PullRequestVersion at %#x>' % id(self)
3601 return '<DB:PullRequestVersion at %#x>' % id(self)
3607
3602
3608 @property
3603 @property
3609 def reviewers(self):
3604 def reviewers(self):
3610 return self.pull_request.reviewers
3605 return self.pull_request.reviewers
3611
3606
3612 @property
3607 @property
3613 def versions(self):
3608 def versions(self):
3614 return self.pull_request.versions
3609 return self.pull_request.versions
3615
3610
3616 def is_closed(self):
3611 def is_closed(self):
3617 # calculate from original
3612 # calculate from original
3618 return self.pull_request.status == self.STATUS_CLOSED
3613 return self.pull_request.status == self.STATUS_CLOSED
3619
3614
3620 def calculated_review_status(self):
3615 def calculated_review_status(self):
3621 return self.pull_request.calculated_review_status()
3616 return self.pull_request.calculated_review_status()
3622
3617
3623 def reviewers_statuses(self):
3618 def reviewers_statuses(self):
3624 return self.pull_request.reviewers_statuses()
3619 return self.pull_request.reviewers_statuses()
3625
3620
3626
3621
3627 class PullRequestReviewers(Base, BaseModel):
3622 class PullRequestReviewers(Base, BaseModel):
3628 __tablename__ = 'pull_request_reviewers'
3623 __tablename__ = 'pull_request_reviewers'
3629 __table_args__ = (
3624 __table_args__ = (
3630 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3625 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3631 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3626 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3632 )
3627 )
3633
3628
3634 @hybrid_property
3629 @hybrid_property
3635 def reasons(self):
3630 def reasons(self):
3636 if not self._reasons:
3631 if not self._reasons:
3637 return []
3632 return []
3638 return self._reasons
3633 return self._reasons
3639
3634
3640 @reasons.setter
3635 @reasons.setter
3641 def reasons(self, val):
3636 def reasons(self, val):
3642 val = val or []
3637 val = val or []
3643 if any(not isinstance(x, basestring) for x in val):
3638 if any(not isinstance(x, basestring) for x in val):
3644 raise Exception('invalid reasons type, must be list of strings')
3639 raise Exception('invalid reasons type, must be list of strings')
3645 self._reasons = val
3640 self._reasons = val
3646
3641
3647 pull_requests_reviewers_id = Column(
3642 pull_requests_reviewers_id = Column(
3648 'pull_requests_reviewers_id', Integer(), nullable=False,
3643 'pull_requests_reviewers_id', Integer(), nullable=False,
3649 primary_key=True)
3644 primary_key=True)
3650 pull_request_id = Column(
3645 pull_request_id = Column(
3651 "pull_request_id", Integer(),
3646 "pull_request_id", Integer(),
3652 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3647 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3653 user_id = Column(
3648 user_id = Column(
3654 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3649 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3655 _reasons = Column(
3650 _reasons = Column(
3656 'reason', MutationList.as_mutable(
3651 'reason', MutationList.as_mutable(
3657 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3652 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3658 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3653 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3659 user = relationship('User')
3654 user = relationship('User')
3660 pull_request = relationship('PullRequest')
3655 pull_request = relationship('PullRequest')
3661
3656
3662
3657
3663 class Notification(Base, BaseModel):
3658 class Notification(Base, BaseModel):
3664 __tablename__ = 'notifications'
3659 __tablename__ = 'notifications'
3665 __table_args__ = (
3660 __table_args__ = (
3666 Index('notification_type_idx', 'type'),
3661 Index('notification_type_idx', 'type'),
3667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3662 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3663 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3669 )
3664 )
3670
3665
3671 TYPE_CHANGESET_COMMENT = u'cs_comment'
3666 TYPE_CHANGESET_COMMENT = u'cs_comment'
3672 TYPE_MESSAGE = u'message'
3667 TYPE_MESSAGE = u'message'
3673 TYPE_MENTION = u'mention'
3668 TYPE_MENTION = u'mention'
3674 TYPE_REGISTRATION = u'registration'
3669 TYPE_REGISTRATION = u'registration'
3675 TYPE_PULL_REQUEST = u'pull_request'
3670 TYPE_PULL_REQUEST = u'pull_request'
3676 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3671 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3677
3672
3678 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3673 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3679 subject = Column('subject', Unicode(512), nullable=True)
3674 subject = Column('subject', Unicode(512), nullable=True)
3680 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3675 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3681 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3676 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3682 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3677 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3683 type_ = Column('type', Unicode(255))
3678 type_ = Column('type', Unicode(255))
3684
3679
3685 created_by_user = relationship('User')
3680 created_by_user = relationship('User')
3686 notifications_to_users = relationship('UserNotification', lazy='joined',
3681 notifications_to_users = relationship('UserNotification', lazy='joined',
3687 cascade="all, delete, delete-orphan")
3682 cascade="all, delete, delete-orphan")
3688
3683
3689 @property
3684 @property
3690 def recipients(self):
3685 def recipients(self):
3691 return [x.user for x in UserNotification.query()\
3686 return [x.user for x in UserNotification.query()\
3692 .filter(UserNotification.notification == self)\
3687 .filter(UserNotification.notification == self)\
3693 .order_by(UserNotification.user_id.asc()).all()]
3688 .order_by(UserNotification.user_id.asc()).all()]
3694
3689
3695 @classmethod
3690 @classmethod
3696 def create(cls, created_by, subject, body, recipients, type_=None):
3691 def create(cls, created_by, subject, body, recipients, type_=None):
3697 if type_ is None:
3692 if type_ is None:
3698 type_ = Notification.TYPE_MESSAGE
3693 type_ = Notification.TYPE_MESSAGE
3699
3694
3700 notification = cls()
3695 notification = cls()
3701 notification.created_by_user = created_by
3696 notification.created_by_user = created_by
3702 notification.subject = subject
3697 notification.subject = subject
3703 notification.body = body
3698 notification.body = body
3704 notification.type_ = type_
3699 notification.type_ = type_
3705 notification.created_on = datetime.datetime.now()
3700 notification.created_on = datetime.datetime.now()
3706
3701
3707 for u in recipients:
3702 for u in recipients:
3708 assoc = UserNotification()
3703 assoc = UserNotification()
3709 assoc.notification = notification
3704 assoc.notification = notification
3710
3705
3711 # if created_by is inside recipients mark his notification
3706 # if created_by is inside recipients mark his notification
3712 # as read
3707 # as read
3713 if u.user_id == created_by.user_id:
3708 if u.user_id == created_by.user_id:
3714 assoc.read = True
3709 assoc.read = True
3715
3710
3716 u.notifications.append(assoc)
3711 u.notifications.append(assoc)
3717 Session().add(notification)
3712 Session().add(notification)
3718
3713
3719 return notification
3714 return notification
3720
3715
3721
3716
3722 class UserNotification(Base, BaseModel):
3717 class UserNotification(Base, BaseModel):
3723 __tablename__ = 'user_to_notification'
3718 __tablename__ = 'user_to_notification'
3724 __table_args__ = (
3719 __table_args__ = (
3725 UniqueConstraint('user_id', 'notification_id'),
3720 UniqueConstraint('user_id', 'notification_id'),
3726 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3721 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3727 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3722 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3728 )
3723 )
3729 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3724 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3730 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3725 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3731 read = Column('read', Boolean, default=False)
3726 read = Column('read', Boolean, default=False)
3732 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3727 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3733
3728
3734 user = relationship('User', lazy="joined")
3729 user = relationship('User', lazy="joined")
3735 notification = relationship('Notification', lazy="joined",
3730 notification = relationship('Notification', lazy="joined",
3736 order_by=lambda: Notification.created_on.desc(),)
3731 order_by=lambda: Notification.created_on.desc(),)
3737
3732
3738 def mark_as_read(self):
3733 def mark_as_read(self):
3739 self.read = True
3734 self.read = True
3740 Session().add(self)
3735 Session().add(self)
3741
3736
3742
3737
3743 class Gist(Base, BaseModel):
3738 class Gist(Base, BaseModel):
3744 __tablename__ = 'gists'
3739 __tablename__ = 'gists'
3745 __table_args__ = (
3740 __table_args__ = (
3746 Index('g_gist_access_id_idx', 'gist_access_id'),
3741 Index('g_gist_access_id_idx', 'gist_access_id'),
3747 Index('g_created_on_idx', 'created_on'),
3742 Index('g_created_on_idx', 'created_on'),
3748 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3743 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3749 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3744 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3750 )
3745 )
3751 GIST_PUBLIC = u'public'
3746 GIST_PUBLIC = u'public'
3752 GIST_PRIVATE = u'private'
3747 GIST_PRIVATE = u'private'
3753 DEFAULT_FILENAME = u'gistfile1.txt'
3748 DEFAULT_FILENAME = u'gistfile1.txt'
3754
3749
3755 ACL_LEVEL_PUBLIC = u'acl_public'
3750 ACL_LEVEL_PUBLIC = u'acl_public'
3756 ACL_LEVEL_PRIVATE = u'acl_private'
3751 ACL_LEVEL_PRIVATE = u'acl_private'
3757
3752
3758 gist_id = Column('gist_id', Integer(), primary_key=True)
3753 gist_id = Column('gist_id', Integer(), primary_key=True)
3759 gist_access_id = Column('gist_access_id', Unicode(250))
3754 gist_access_id = Column('gist_access_id', Unicode(250))
3760 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3755 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3761 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3756 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3762 gist_expires = Column('gist_expires', Float(53), nullable=False)
3757 gist_expires = Column('gist_expires', Float(53), nullable=False)
3763 gist_type = Column('gist_type', Unicode(128), nullable=False)
3758 gist_type = Column('gist_type', Unicode(128), nullable=False)
3764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3759 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3765 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3760 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3766 acl_level = Column('acl_level', Unicode(128), nullable=True)
3761 acl_level = Column('acl_level', Unicode(128), nullable=True)
3767
3762
3768 owner = relationship('User')
3763 owner = relationship('User')
3769
3764
3770 def __repr__(self):
3765 def __repr__(self):
3771 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3766 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3772
3767
3773 @hybrid_property
3768 @hybrid_property
3774 def description_safe(self):
3769 def description_safe(self):
3775 from rhodecode.lib import helpers as h
3770 from rhodecode.lib import helpers as h
3776 return h.escape(self.gist_description)
3771 return h.escape(self.gist_description)
3777
3772
3778 @classmethod
3773 @classmethod
3779 def get_or_404(cls, id_, pyramid_exc=False):
3774 def get_or_404(cls, id_):
3780
3775 from pyramid.httpexceptions import HTTPNotFound
3781 if pyramid_exc:
3782 from pyramid.httpexceptions import HTTPNotFound
3783 else:
3784 from webob.exc import HTTPNotFound
3785
3776
3786 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3777 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3787 if not res:
3778 if not res:
3788 raise HTTPNotFound
3779 raise HTTPNotFound()
3789 return res
3780 return res
3790
3781
3791 @classmethod
3782 @classmethod
3792 def get_by_access_id(cls, gist_access_id):
3783 def get_by_access_id(cls, gist_access_id):
3793 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3784 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3794
3785
3795 def gist_url(self):
3786 def gist_url(self):
3796 from rhodecode.model.gist import GistModel
3787 from rhodecode.model.gist import GistModel
3797 return GistModel().get_url(self)
3788 return GistModel().get_url(self)
3798
3789
3799 @classmethod
3790 @classmethod
3800 def base_path(cls):
3791 def base_path(cls):
3801 """
3792 """
3802 Returns base path when all gists are stored
3793 Returns base path when all gists are stored
3803
3794
3804 :param cls:
3795 :param cls:
3805 """
3796 """
3806 from rhodecode.model.gist import GIST_STORE_LOC
3797 from rhodecode.model.gist import GIST_STORE_LOC
3807 q = Session().query(RhodeCodeUi)\
3798 q = Session().query(RhodeCodeUi)\
3808 .filter(RhodeCodeUi.ui_key == URL_SEP)
3799 .filter(RhodeCodeUi.ui_key == URL_SEP)
3809 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3800 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3810 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3801 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3811
3802
3812 def get_api_data(self):
3803 def get_api_data(self):
3813 """
3804 """
3814 Common function for generating gist related data for API
3805 Common function for generating gist related data for API
3815 """
3806 """
3816 gist = self
3807 gist = self
3817 data = {
3808 data = {
3818 'gist_id': gist.gist_id,
3809 'gist_id': gist.gist_id,
3819 'type': gist.gist_type,
3810 'type': gist.gist_type,
3820 'access_id': gist.gist_access_id,
3811 'access_id': gist.gist_access_id,
3821 'description': gist.gist_description,
3812 'description': gist.gist_description,
3822 'url': gist.gist_url(),
3813 'url': gist.gist_url(),
3823 'expires': gist.gist_expires,
3814 'expires': gist.gist_expires,
3824 'created_on': gist.created_on,
3815 'created_on': gist.created_on,
3825 'modified_at': gist.modified_at,
3816 'modified_at': gist.modified_at,
3826 'content': None,
3817 'content': None,
3827 'acl_level': gist.acl_level,
3818 'acl_level': gist.acl_level,
3828 }
3819 }
3829 return data
3820 return data
3830
3821
3831 def __json__(self):
3822 def __json__(self):
3832 data = dict(
3823 data = dict(
3833 )
3824 )
3834 data.update(self.get_api_data())
3825 data.update(self.get_api_data())
3835 return data
3826 return data
3836 # SCM functions
3827 # SCM functions
3837
3828
3838 def scm_instance(self, **kwargs):
3829 def scm_instance(self, **kwargs):
3839 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3830 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3840 return get_vcs_instance(
3831 return get_vcs_instance(
3841 repo_path=safe_str(full_repo_path), create=False)
3832 repo_path=safe_str(full_repo_path), create=False)
3842
3833
3843
3834
3844 class ExternalIdentity(Base, BaseModel):
3835 class ExternalIdentity(Base, BaseModel):
3845 __tablename__ = 'external_identities'
3836 __tablename__ = 'external_identities'
3846 __table_args__ = (
3837 __table_args__ = (
3847 Index('local_user_id_idx', 'local_user_id'),
3838 Index('local_user_id_idx', 'local_user_id'),
3848 Index('external_id_idx', 'external_id'),
3839 Index('external_id_idx', 'external_id'),
3849 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3840 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3850 'mysql_charset': 'utf8'})
3841 'mysql_charset': 'utf8'})
3851
3842
3852 external_id = Column('external_id', Unicode(255), default=u'',
3843 external_id = Column('external_id', Unicode(255), default=u'',
3853 primary_key=True)
3844 primary_key=True)
3854 external_username = Column('external_username', Unicode(1024), default=u'')
3845 external_username = Column('external_username', Unicode(1024), default=u'')
3855 local_user_id = Column('local_user_id', Integer(),
3846 local_user_id = Column('local_user_id', Integer(),
3856 ForeignKey('users.user_id'), primary_key=True)
3847 ForeignKey('users.user_id'), primary_key=True)
3857 provider_name = Column('provider_name', Unicode(255), default=u'',
3848 provider_name = Column('provider_name', Unicode(255), default=u'',
3858 primary_key=True)
3849 primary_key=True)
3859 access_token = Column('access_token', String(1024), default=u'')
3850 access_token = Column('access_token', String(1024), default=u'')
3860 alt_token = Column('alt_token', String(1024), default=u'')
3851 alt_token = Column('alt_token', String(1024), default=u'')
3861 token_secret = Column('token_secret', String(1024), default=u'')
3852 token_secret = Column('token_secret', String(1024), default=u'')
3862
3853
3863 @classmethod
3854 @classmethod
3864 def by_external_id_and_provider(cls, external_id, provider_name,
3855 def by_external_id_and_provider(cls, external_id, provider_name,
3865 local_user_id=None):
3856 local_user_id=None):
3866 """
3857 """
3867 Returns ExternalIdentity instance based on search params
3858 Returns ExternalIdentity instance based on search params
3868
3859
3869 :param external_id:
3860 :param external_id:
3870 :param provider_name:
3861 :param provider_name:
3871 :return: ExternalIdentity
3862 :return: ExternalIdentity
3872 """
3863 """
3873 query = cls.query()
3864 query = cls.query()
3874 query = query.filter(cls.external_id == external_id)
3865 query = query.filter(cls.external_id == external_id)
3875 query = query.filter(cls.provider_name == provider_name)
3866 query = query.filter(cls.provider_name == provider_name)
3876 if local_user_id:
3867 if local_user_id:
3877 query = query.filter(cls.local_user_id == local_user_id)
3868 query = query.filter(cls.local_user_id == local_user_id)
3878 return query.first()
3869 return query.first()
3879
3870
3880 @classmethod
3871 @classmethod
3881 def user_by_external_id_and_provider(cls, external_id, provider_name):
3872 def user_by_external_id_and_provider(cls, external_id, provider_name):
3882 """
3873 """
3883 Returns User instance based on search params
3874 Returns User instance based on search params
3884
3875
3885 :param external_id:
3876 :param external_id:
3886 :param provider_name:
3877 :param provider_name:
3887 :return: User
3878 :return: User
3888 """
3879 """
3889 query = User.query()
3880 query = User.query()
3890 query = query.filter(cls.external_id == external_id)
3881 query = query.filter(cls.external_id == external_id)
3891 query = query.filter(cls.provider_name == provider_name)
3882 query = query.filter(cls.provider_name == provider_name)
3892 query = query.filter(User.user_id == cls.local_user_id)
3883 query = query.filter(User.user_id == cls.local_user_id)
3893 return query.first()
3884 return query.first()
3894
3885
3895 @classmethod
3886 @classmethod
3896 def by_local_user_id(cls, local_user_id):
3887 def by_local_user_id(cls, local_user_id):
3897 """
3888 """
3898 Returns all tokens for user
3889 Returns all tokens for user
3899
3890
3900 :param local_user_id:
3891 :param local_user_id:
3901 :return: ExternalIdentity
3892 :return: ExternalIdentity
3902 """
3893 """
3903 query = cls.query()
3894 query = cls.query()
3904 query = query.filter(cls.local_user_id == local_user_id)
3895 query = query.filter(cls.local_user_id == local_user_id)
3905 return query
3896 return query
3906
3897
3907
3898
3908 class Integration(Base, BaseModel):
3899 class Integration(Base, BaseModel):
3909 __tablename__ = 'integrations'
3900 __tablename__ = 'integrations'
3910 __table_args__ = (
3901 __table_args__ = (
3911 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3902 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3912 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3903 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3913 )
3904 )
3914
3905
3915 integration_id = Column('integration_id', Integer(), primary_key=True)
3906 integration_id = Column('integration_id', Integer(), primary_key=True)
3916 integration_type = Column('integration_type', String(255))
3907 integration_type = Column('integration_type', String(255))
3917 enabled = Column('enabled', Boolean(), nullable=False)
3908 enabled = Column('enabled', Boolean(), nullable=False)
3918 name = Column('name', String(255), nullable=False)
3909 name = Column('name', String(255), nullable=False)
3919 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3910 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3920 default=False)
3911 default=False)
3921
3912
3922 settings = Column(
3913 settings = Column(
3923 'settings_json', MutationObj.as_mutable(
3914 'settings_json', MutationObj.as_mutable(
3924 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3915 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3925 repo_id = Column(
3916 repo_id = Column(
3926 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3917 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3927 nullable=True, unique=None, default=None)
3918 nullable=True, unique=None, default=None)
3928 repo = relationship('Repository', lazy='joined')
3919 repo = relationship('Repository', lazy='joined')
3929
3920
3930 repo_group_id = Column(
3921 repo_group_id = Column(
3931 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3922 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3932 nullable=True, unique=None, default=None)
3923 nullable=True, unique=None, default=None)
3933 repo_group = relationship('RepoGroup', lazy='joined')
3924 repo_group = relationship('RepoGroup', lazy='joined')
3934
3925
3935 @property
3926 @property
3936 def scope(self):
3927 def scope(self):
3937 if self.repo:
3928 if self.repo:
3938 return repr(self.repo)
3929 return repr(self.repo)
3939 if self.repo_group:
3930 if self.repo_group:
3940 if self.child_repos_only:
3931 if self.child_repos_only:
3941 return repr(self.repo_group) + ' (child repos only)'
3932 return repr(self.repo_group) + ' (child repos only)'
3942 else:
3933 else:
3943 return repr(self.repo_group) + ' (recursive)'
3934 return repr(self.repo_group) + ' (recursive)'
3944 if self.child_repos_only:
3935 if self.child_repos_only:
3945 return 'root_repos'
3936 return 'root_repos'
3946 return 'global'
3937 return 'global'
3947
3938
3948 def __repr__(self):
3939 def __repr__(self):
3949 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3940 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3950
3941
3951
3942
3952 class RepoReviewRuleUser(Base, BaseModel):
3943 class RepoReviewRuleUser(Base, BaseModel):
3953 __tablename__ = 'repo_review_rules_users'
3944 __tablename__ = 'repo_review_rules_users'
3954 __table_args__ = (
3945 __table_args__ = (
3955 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3946 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3956 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3947 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3957 )
3948 )
3958 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3949 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3959 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3950 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3960 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3951 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3961 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3952 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3962 user = relationship('User')
3953 user = relationship('User')
3963
3954
3964 def rule_data(self):
3955 def rule_data(self):
3965 return {
3956 return {
3966 'mandatory': self.mandatory
3957 'mandatory': self.mandatory
3967 }
3958 }
3968
3959
3969
3960
3970 class RepoReviewRuleUserGroup(Base, BaseModel):
3961 class RepoReviewRuleUserGroup(Base, BaseModel):
3971 __tablename__ = 'repo_review_rules_users_groups'
3962 __tablename__ = 'repo_review_rules_users_groups'
3972 __table_args__ = (
3963 __table_args__ = (
3973 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3964 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3974 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3965 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3975 )
3966 )
3976 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3967 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3977 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3968 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3978 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3969 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3979 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3970 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3980 users_group = relationship('UserGroup')
3971 users_group = relationship('UserGroup')
3981
3972
3982 def rule_data(self):
3973 def rule_data(self):
3983 return {
3974 return {
3984 'mandatory': self.mandatory
3975 'mandatory': self.mandatory
3985 }
3976 }
3986
3977
3987
3978
3988 class RepoReviewRule(Base, BaseModel):
3979 class RepoReviewRule(Base, BaseModel):
3989 __tablename__ = 'repo_review_rules'
3980 __tablename__ = 'repo_review_rules'
3990 __table_args__ = (
3981 __table_args__ = (
3991 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3982 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3992 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3983 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3993 )
3984 )
3994
3985
3995 repo_review_rule_id = Column(
3986 repo_review_rule_id = Column(
3996 'repo_review_rule_id', Integer(), primary_key=True)
3987 'repo_review_rule_id', Integer(), primary_key=True)
3997 repo_id = Column(
3988 repo_id = Column(
3998 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3989 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3999 repo = relationship('Repository', backref='review_rules')
3990 repo = relationship('Repository', backref='review_rules')
4000
3991
4001 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3992 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4002 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3993 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4003
3994
4004 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3995 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4005 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3996 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4006 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
3997 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4007 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3998 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4008
3999
4009 rule_users = relationship('RepoReviewRuleUser')
4000 rule_users = relationship('RepoReviewRuleUser')
4010 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4001 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4011
4002
4012 @hybrid_property
4003 @hybrid_property
4013 def branch_pattern(self):
4004 def branch_pattern(self):
4014 return self._branch_pattern or '*'
4005 return self._branch_pattern or '*'
4015
4006
4016 def _validate_glob(self, value):
4007 def _validate_glob(self, value):
4017 re.compile('^' + glob2re(value) + '$')
4008 re.compile('^' + glob2re(value) + '$')
4018
4009
4019 @branch_pattern.setter
4010 @branch_pattern.setter
4020 def branch_pattern(self, value):
4011 def branch_pattern(self, value):
4021 self._validate_glob(value)
4012 self._validate_glob(value)
4022 self._branch_pattern = value or '*'
4013 self._branch_pattern = value or '*'
4023
4014
4024 @hybrid_property
4015 @hybrid_property
4025 def file_pattern(self):
4016 def file_pattern(self):
4026 return self._file_pattern or '*'
4017 return self._file_pattern or '*'
4027
4018
4028 @file_pattern.setter
4019 @file_pattern.setter
4029 def file_pattern(self, value):
4020 def file_pattern(self, value):
4030 self._validate_glob(value)
4021 self._validate_glob(value)
4031 self._file_pattern = value or '*'
4022 self._file_pattern = value or '*'
4032
4023
4033 def matches(self, branch, files_changed):
4024 def matches(self, branch, files_changed):
4034 """
4025 """
4035 Check if this review rule matches a branch/files in a pull request
4026 Check if this review rule matches a branch/files in a pull request
4036
4027
4037 :param branch: branch name for the commit
4028 :param branch: branch name for the commit
4038 :param files_changed: list of file paths changed in the pull request
4029 :param files_changed: list of file paths changed in the pull request
4039 """
4030 """
4040
4031
4041 branch = branch or ''
4032 branch = branch or ''
4042 files_changed = files_changed or []
4033 files_changed = files_changed or []
4043
4034
4044 branch_matches = True
4035 branch_matches = True
4045 if branch:
4036 if branch:
4046 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4037 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4047 branch_matches = bool(branch_regex.search(branch))
4038 branch_matches = bool(branch_regex.search(branch))
4048
4039
4049 files_matches = True
4040 files_matches = True
4050 if self.file_pattern != '*':
4041 if self.file_pattern != '*':
4051 files_matches = False
4042 files_matches = False
4052 file_regex = re.compile(glob2re(self.file_pattern))
4043 file_regex = re.compile(glob2re(self.file_pattern))
4053 for filename in files_changed:
4044 for filename in files_changed:
4054 if file_regex.search(filename):
4045 if file_regex.search(filename):
4055 files_matches = True
4046 files_matches = True
4056 break
4047 break
4057
4048
4058 return branch_matches and files_matches
4049 return branch_matches and files_matches
4059
4050
4060 @property
4051 @property
4061 def review_users(self):
4052 def review_users(self):
4062 """ Returns the users which this rule applies to """
4053 """ Returns the users which this rule applies to """
4063
4054
4064 users = collections.OrderedDict()
4055 users = collections.OrderedDict()
4065
4056
4066 for rule_user in self.rule_users:
4057 for rule_user in self.rule_users:
4067 if rule_user.user.active:
4058 if rule_user.user.active:
4068 if rule_user.user not in users:
4059 if rule_user.user not in users:
4069 users[rule_user.user.username] = {
4060 users[rule_user.user.username] = {
4070 'user': rule_user.user,
4061 'user': rule_user.user,
4071 'source': 'user',
4062 'source': 'user',
4072 'source_data': {},
4063 'source_data': {},
4073 'data': rule_user.rule_data()
4064 'data': rule_user.rule_data()
4074 }
4065 }
4075
4066
4076 for rule_user_group in self.rule_user_groups:
4067 for rule_user_group in self.rule_user_groups:
4077 source_data = {
4068 source_data = {
4078 'name': rule_user_group.users_group.users_group_name,
4069 'name': rule_user_group.users_group.users_group_name,
4079 'members': len(rule_user_group.users_group.members)
4070 'members': len(rule_user_group.users_group.members)
4080 }
4071 }
4081 for member in rule_user_group.users_group.members:
4072 for member in rule_user_group.users_group.members:
4082 if member.user.active:
4073 if member.user.active:
4083 users[member.user.username] = {
4074 users[member.user.username] = {
4084 'user': member.user,
4075 'user': member.user,
4085 'source': 'user_group',
4076 'source': 'user_group',
4086 'source_data': source_data,
4077 'source_data': source_data,
4087 'data': rule_user_group.rule_data()
4078 'data': rule_user_group.rule_data()
4088 }
4079 }
4089
4080
4090 return users
4081 return users
4091
4082
4092 def __repr__(self):
4083 def __repr__(self):
4093 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4084 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4094 self.repo_review_rule_id, self.repo)
4085 self.repo_review_rule_id, self.repo)
4095
4086
4096
4087
4097 class DbMigrateVersion(Base, BaseModel):
4088 class DbMigrateVersion(Base, BaseModel):
4098 __tablename__ = 'db_migrate_version'
4089 __tablename__ = 'db_migrate_version'
4099 __table_args__ = (
4090 __table_args__ = (
4100 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4091 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4101 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4092 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4102 )
4093 )
4103 repository_id = Column('repository_id', String(250), primary_key=True)
4094 repository_id = Column('repository_id', String(250), primary_key=True)
4104 repository_path = Column('repository_path', Text)
4095 repository_path = Column('repository_path', Text)
4105 version = Column('version', Integer)
4096 version = Column('version', Integer)
4106
4097
4107
4098
4108 class DbSession(Base, BaseModel):
4099 class DbSession(Base, BaseModel):
4109 __tablename__ = 'db_session'
4100 __tablename__ = 'db_session'
4110 __table_args__ = (
4101 __table_args__ = (
4111 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4102 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4112 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4103 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4113 )
4104 )
4114
4105
4115 def __repr__(self):
4106 def __repr__(self):
4116 return '<DB:DbSession({})>'.format(self.id)
4107 return '<DB:DbSession({})>'.format(self.id)
4117
4108
4118 id = Column('id', Integer())
4109 id = Column('id', Integer())
4119 namespace = Column('namespace', String(255), primary_key=True)
4110 namespace = Column('namespace', String(255), primary_key=True)
4120 accessed = Column('accessed', DateTime, nullable=False)
4111 accessed = Column('accessed', DateTime, nullable=False)
4121 created = Column('created', DateTime, nullable=False)
4112 created = Column('created', DateTime, nullable=False)
4122 data = Column('data', PickleType, nullable=False)
4113 data = Column('data', PickleType, nullable=False)
General Comments 0
You need to be logged in to leave comments. Login now