##// 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 29 from rhodecode.model import repo
30 30 from rhodecode.model import repo_group
31 31 from rhodecode.model.db import User
32 32 from rhodecode.model.scm import ScmModel
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 ADMIN_PREFIX = '/_admin'
38 38 STATIC_FILE_PREFIX = '/_static'
39 39
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 def add_route_with_slash(config,name, pattern, **kw):
55 55 config.add_route(name, pattern, **kw)
56 56 if not pattern.endswith('/'):
57 57 config.add_route(name + '_slash', pattern + '/', **kw)
58 58
59 59
60 60 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
61 61 """
62 62 Adds regex requirements to pyramid routes using a mapping dict
63 63 e.g::
64 64 add_route_requirements('{repo_name}/settings')
65 65 """
66 66 for key, regex in requirements.items():
67 67 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
68 68 return route_path
69 69
70 70
71 71 def get_format_ref_id(repo):
72 72 """Returns a `repo` specific reference formatter function"""
73 73 if h.is_svn(repo):
74 74 return _format_ref_id_svn
75 75 else:
76 76 return _format_ref_id
77 77
78 78
79 79 def _format_ref_id(name, raw_id):
80 80 """Default formatting of a given reference `name`"""
81 81 return name
82 82
83 83
84 84 def _format_ref_id_svn(name, raw_id):
85 85 """Special way of formatting a reference for Subversion including path"""
86 86 return '%s@%s' % (name, raw_id)
87 87
88 88
89 89 class TemplateArgs(StrictAttributeDict):
90 90 pass
91 91
92 92
93 93 class BaseAppView(object):
94 94
95 95 def __init__(self, context, request):
96 96 self.request = request
97 97 self.context = context
98 98 self.session = request.session
99 99 self._rhodecode_user = request.user # auth user
100 100 self._rhodecode_db_user = self._rhodecode_user.get_instance()
101 101 self._maybe_needs_password_change(
102 102 request.matched_route.name, self._rhodecode_db_user)
103 103
104 104 def _maybe_needs_password_change(self, view_name, user_obj):
105 105 log.debug('Checking if user %s needs password change on view %s',
106 106 user_obj, view_name)
107 107 skip_user_views = [
108 108 'logout', 'login',
109 109 'my_account_password', 'my_account_password_update'
110 110 ]
111 111
112 112 if not user_obj:
113 113 return
114 114
115 115 if user_obj.username == User.DEFAULT_USER:
116 116 return
117 117
118 118 now = time.time()
119 119 should_change = user_obj.user_data.get('force_password_change')
120 120 change_after = safe_int(should_change) or 0
121 121 if should_change and now > change_after:
122 122 log.debug('User %s requires password change', user_obj)
123 123 h.flash('You are required to change your password', 'warning',
124 124 ignore_duplicate=True)
125 125
126 126 if view_name not in skip_user_views:
127 127 raise HTTPFound(
128 128 self.request.route_path('my_account_password'))
129 129
130 130 def _get_local_tmpl_context(self, include_app_defaults=False):
131 131 c = TemplateArgs()
132 132 c.auth_user = self.request.user
133 133 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
134 134 c.rhodecode_user = self.request.user
135 135
136 136 if include_app_defaults:
137 137 # NOTE(marcink): after full pyramid migration include_app_defaults
138 138 # should be turned on by default
139 139 from rhodecode.lib.base import attach_context_attributes
140 140 attach_context_attributes(c, self.request, self.request.user.user_id)
141 141
142 142 return c
143 143
144 144 def _register_global_c(self, tmpl_args):
145 145 """
146 146 Registers attributes to pylons global `c`
147 147 """
148 148
149 149 # TODO(marcink): remove once pyramid migration is finished
150 150 from pylons import tmpl_context as c
151 151 try:
152 152 for k, v in tmpl_args.items():
153 153 setattr(c, k, v)
154 154 except TypeError:
155 155 log.exception('Failed to register pylons C')
156 156 pass
157 157
158 158 def _get_template_context(self, tmpl_args):
159 159 self._register_global_c(tmpl_args)
160 160
161 161 local_tmpl_args = {
162 162 'defaults': {},
163 163 'errors': {},
164 164 # register a fake 'c' to be used in templates instead of global
165 165 # pylons c, after migration to pyramid we should rename it to 'c'
166 166 # make sure we replace usage of _c in templates too
167 167 '_c': tmpl_args
168 168 }
169 169 local_tmpl_args.update(tmpl_args)
170 170 return local_tmpl_args
171 171
172 172 def load_default_context(self):
173 173 """
174 174 example:
175 175
176 176 def load_default_context(self):
177 177 c = self._get_local_tmpl_context()
178 178 c.custom_var = 'foobar'
179 179 self._register_global_c(c)
180 180 return c
181 181 """
182 182 raise NotImplementedError('Needs implementation in view class')
183 183
184 184
185 185 class RepoAppView(BaseAppView):
186 186
187 187 def __init__(self, context, request):
188 188 super(RepoAppView, self).__init__(context, request)
189 189 self.db_repo = request.db_repo
190 190 self.db_repo_name = self.db_repo.repo_name
191 191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
192 192
193 193 def _handle_missing_requirements(self, error):
194 194 log.error(
195 195 'Requirements are missing for repository %s: %s',
196 196 self.db_repo_name, error.message)
197 197
198 198 def _get_local_tmpl_context(self, include_app_defaults=False):
199 199 c = super(RepoAppView, self)._get_local_tmpl_context(
200 200 include_app_defaults=include_app_defaults)
201 201
202 202 # register common vars for this type of view
203 203 c.rhodecode_db_repo = self.db_repo
204 204 c.repo_name = self.db_repo_name
205 205 c.repository_pull_requests = self.db_repo_pull_requests
206 206
207 207 c.repository_requirements_missing = False
208 208 try:
209 209 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
210 210 except RepositoryRequirementError as e:
211 211 c.repository_requirements_missing = True
212 212 self._handle_missing_requirements(e)
213 213
214 214 return c
215 215
216 216 def _get_f_path(self, matchdict, default=None):
217 217 f_path = matchdict.get('f_path')
218 218 if f_path:
219 219 # fix for multiple initial slashes that causes errors for GIT
220 220 return f_path.lstrip('/')
221 221
222 222 return default
223 223
224
224 225 class DataGridAppView(object):
225 226 """
226 227 Common class to have re-usable grid rendering components
227 228 """
228 229
229 230 def _extract_ordering(self, request, column_map=None):
230 231 column_map = column_map or {}
231 232 column_index = safe_int(request.GET.get('order[0][column]'))
232 233 order_dir = request.GET.get(
233 234 'order[0][dir]', 'desc')
234 235 order_by = request.GET.get(
235 236 'columns[%s][data][sort]' % column_index, 'name_raw')
236 237
237 238 # translate datatable to DB columns
238 239 order_by = column_map.get(order_by) or order_by
239 240
240 241 search_q = request.GET.get('search[value]')
241 242 return search_q, order_by, order_dir
242 243
243 244 def _extract_chunk(self, request):
244 245 start = safe_int(request.GET.get('start'), 0)
245 246 length = safe_int(request.GET.get('length'), 25)
246 247 draw = safe_int(request.GET.get('draw'))
247 248 return draw, start, length
248 249
249 250
250 251 class BaseReferencesView(RepoAppView):
251 252 """
252 253 Base for reference view for branches, tags and bookmarks.
253 254 """
254 255 def load_default_context(self):
255 256 c = self._get_local_tmpl_context()
256 257
257 258 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
258 259 c.repo_info = self.db_repo
259 260
260 261 self._register_global_c(c)
261 262 return c
262 263
263 264 def load_refs_context(self, ref_items, partials_template):
264 265 _render = self.request.get_partial_renderer(partials_template)
265 266 pre_load = ["author", "date", "message"]
266 267
267 268 is_svn = h.is_svn(self.rhodecode_vcs_repo)
268 269 is_hg = h.is_hg(self.rhodecode_vcs_repo)
269 270
270 271 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
271 272
272 273 closed_refs = {}
273 274 if is_hg:
274 275 closed_refs = self.rhodecode_vcs_repo.branches_closed
275 276
276 277 data = []
277 278 for ref_name, commit_id in ref_items:
278 279 commit = self.rhodecode_vcs_repo.get_commit(
279 280 commit_id=commit_id, pre_load=pre_load)
280 281 closed = ref_name in closed_refs
281 282
282 283 # TODO: johbo: Unify generation of reference links
283 284 use_commit_id = '/' in ref_name or is_svn
284 285
285 286 if use_commit_id:
286 287 files_url = h.route_path(
287 288 'repo_files',
288 289 repo_name=self.db_repo_name,
289 290 f_path=ref_name if is_svn else '',
290 291 commit_id=commit_id)
291 292
292 293 else:
293 294 files_url = h.route_path(
294 295 'repo_files',
295 296 repo_name=self.db_repo_name,
296 297 f_path=ref_name if is_svn else '',
297 298 commit_id=ref_name,
298 299 _query=dict(at=ref_name))
299 300
300 301 data.append({
301 302 "name": _render('name', ref_name, files_url, closed),
302 303 "name_raw": ref_name,
303 304 "date": _render('date', commit.date),
304 305 "date_raw": datetime_to_time(commit.date),
305 306 "author": _render('author', commit.author),
306 307 "commit": _render(
307 308 'commit', commit.message, commit.raw_id, commit.idx),
308 309 "commit_raw": commit.idx,
309 310 "compare": _render(
310 311 'compare', format_ref_id(ref_name, commit.raw_id)),
311 312 })
312 313
313 314 return data
314 315
315 316
316 317 class RepoRoutePredicate(object):
317 318 def __init__(self, val, config):
318 319 self.val = val
319 320
320 321 def text(self):
321 322 return 'repo_route = %s' % self.val
322 323
323 324 phash = text
324 325
325 326 def __call__(self, info, request):
326 327
327 328 if hasattr(request, 'vcs_call'):
328 329 # skip vcs calls
329 330 return
330 331
331 332 repo_name = info['match']['repo_name']
332 333 repo_model = repo.RepoModel()
333 334 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
334 335
335 336 if by_name_match:
336 337 # register this as request object we can re-use later
337 338 request.db_repo = by_name_match
338 339 return True
339 340
340 341 by_id_match = repo_model.get_repo_by_id(repo_name)
341 342 if by_id_match:
342 343 request.db_repo = by_id_match
343 344 return True
344 345
345 346 return False
346 347
347 348
348 349 class RepoTypeRoutePredicate(object):
349 350 def __init__(self, val, config):
350 351 self.val = val or ['hg', 'git', 'svn']
351 352
352 353 def text(self):
353 354 return 'repo_accepted_type = %s' % self.val
354 355
355 356 phash = text
356 357
357 358 def __call__(self, info, request):
358 359 if hasattr(request, 'vcs_call'):
359 360 # skip vcs calls
360 361 return
361 362
362 363 rhodecode_db_repo = request.db_repo
363 364
364 365 log.debug(
365 366 '%s checking repo type for %s in %s',
366 367 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
367 368
368 369 if rhodecode_db_repo.repo_type in self.val:
369 370 return True
370 371 else:
371 372 log.warning('Current view is not supported for repo type:%s',
372 373 rhodecode_db_repo.repo_type)
373 374 #
374 375 # h.flash(h.literal(
375 376 # _('Action not supported for %s.' % rhodecode_repo.alias)),
376 377 # category='warning')
377 378 # return redirect(
378 379 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
379 380
380 381 return False
381 382
382 383
383 384 class RepoGroupRoutePredicate(object):
384 385 def __init__(self, val, config):
385 386 self.val = val
386 387
387 388 def text(self):
388 389 return 'repo_group_route = %s' % self.val
389 390
390 391 phash = text
391 392
392 393 def __call__(self, info, request):
393 394 if hasattr(request, 'vcs_call'):
394 395 # skip vcs calls
395 396 return
396 397
397 398 repo_group_name = info['match']['repo_group_name']
398 399 repo_group_model = repo_group.RepoGroupModel()
399 400 by_name_match = repo_group_model.get_by_group_name(
400 401 repo_group_name, cache=True)
401 402
402 403 if by_name_match:
403 404 # register this as request object we can re-use later
404 405 request.db_repo_group = by_name_match
405 406 return True
406 407
407 408 return False
408 409
409 410
410 411 def includeme(config):
411 412 config.add_route_predicate(
412 413 'repo_route', RepoRoutePredicate)
413 414 config.add_route_predicate(
414 415 'repo_accepted_types', RepoTypeRoutePredicate)
415 416 config.add_route_predicate(
416 417 'repo_group_route', RepoGroupRoutePredicate)
@@ -1,63 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 30 from rhodecode.model.db import PullRequest
31 31
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class AdminMainView(BaseAppView):
37 37
38 38 @LoginRequired()
39 39 @HasPermissionAllDecorator('hg.admin')
40 40 @view_config(
41 41 route_name='admin_home', request_method='GET')
42 42 def admin_main(self):
43 43 # redirect _admin to audit logs...
44 44 raise HTTPFound(h.route_path('admin_audit_logs'))
45 45
46 46 @LoginRequired()
47 47 @view_config(route_name='pull_requests_global_0', request_method='GET')
48 48 @view_config(route_name='pull_requests_global_1', request_method='GET')
49 49 @view_config(route_name='pull_requests_global', request_method='GET')
50 50 def pull_requests(self):
51 51 """
52 52 Global redirect for Pull Requests
53 53
54 54 :param pull_request_id: id of pull requests in the system
55 55 """
56 56
57 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 59 repo_name = pull_request.target_repo.repo_name
60 60
61 61 raise HTTPFound(
62 62 h.route_path('pullrequest_show', repo_name=repo_name,
63 63 pull_request_id=pull_request_id))
@@ -1,505 +1,505 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27 from sqlalchemy.sql.functions import coalesce
28 28
29 29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 30
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 37 from rhodecode.model.auth_token import AuthTokenModel
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.user_group import UserGroupModel
40 40 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
41 41 from rhodecode.model.meta import Session
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class AdminUsersView(BaseAppView, DataGridAppView):
47 47 ALLOW_SCOPED_TOKENS = False
48 48 """
49 49 This view has alternative version inside EE, if modified please take a look
50 50 in there as well.
51 51 """
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
56 56 self._register_global_c(c)
57 57 return c
58 58
59 59 def _redirect_for_default_user(self, username):
60 60 _ = self.request.translate
61 61 if username == User.DEFAULT_USER:
62 62 h.flash(_("You can't edit this user"), category='warning')
63 63 # TODO(marcink): redirect to 'users' admin panel once this
64 64 # is a pyramid view
65 65 raise HTTPFound('/')
66 66
67 67 @HasPermissionAllDecorator('hg.admin')
68 68 @view_config(
69 69 route_name='users', request_method='GET',
70 70 renderer='rhodecode:templates/admin/users/users.mako')
71 71 def users_list(self):
72 72 c = self.load_default_context()
73 73 return self._get_template_context(c)
74 74
75 75 @HasPermissionAllDecorator('hg.admin')
76 76 @view_config(
77 77 # renderer defined below
78 78 route_name='users_data', request_method='GET',
79 79 renderer='json_ext', xhr=True)
80 80 def users_list_data(self):
81 81 draw, start, limit = self._extract_chunk(self.request)
82 82 search_q, order_by, order_dir = self._extract_ordering(self.request)
83 83
84 84 _render = self.request.get_partial_renderer(
85 85 'data_table/_dt_elements.mako')
86 86
87 87 def user_actions(user_id, username):
88 88 return _render("user_actions", user_id, username)
89 89
90 90 users_data_total_count = User.query()\
91 91 .filter(User.username != User.DEFAULT_USER) \
92 92 .count()
93 93
94 94 # json generate
95 95 base_q = User.query().filter(User.username != User.DEFAULT_USER)
96 96
97 97 if search_q:
98 98 like_expression = u'%{}%'.format(safe_unicode(search_q))
99 99 base_q = base_q.filter(or_(
100 100 User.username.ilike(like_expression),
101 101 User._email.ilike(like_expression),
102 102 User.name.ilike(like_expression),
103 103 User.lastname.ilike(like_expression),
104 104 ))
105 105
106 106 users_data_total_filtered_count = base_q.count()
107 107
108 108 sort_col = getattr(User, order_by, None)
109 109 if sort_col:
110 110 if order_dir == 'asc':
111 111 # handle null values properly to order by NULL last
112 112 if order_by in ['last_activity']:
113 113 sort_col = coalesce(sort_col, datetime.date.max)
114 114 sort_col = sort_col.asc()
115 115 else:
116 116 # handle null values properly to order by NULL last
117 117 if order_by in ['last_activity']:
118 118 sort_col = coalesce(sort_col, datetime.date.min)
119 119 sort_col = sort_col.desc()
120 120
121 121 base_q = base_q.order_by(sort_col)
122 122 base_q = base_q.offset(start).limit(limit)
123 123
124 124 users_list = base_q.all()
125 125
126 126 users_data = []
127 127 for user in users_list:
128 128 users_data.append({
129 129 "username": h.gravatar_with_user(self.request, user.username),
130 130 "email": user.email,
131 131 "first_name": user.first_name,
132 132 "last_name": user.last_name,
133 133 "last_login": h.format_date(user.last_login),
134 134 "last_activity": h.format_date(user.last_activity),
135 135 "active": h.bool2icon(user.active),
136 136 "active_raw": user.active,
137 137 "admin": h.bool2icon(user.admin),
138 138 "extern_type": user.extern_type,
139 139 "extern_name": user.extern_name,
140 140 "action": user_actions(user.user_id, user.username),
141 141 })
142 142
143 143 data = ({
144 144 'draw': draw,
145 145 'data': users_data,
146 146 'recordsTotal': users_data_total_count,
147 147 'recordsFiltered': users_data_total_filtered_count,
148 148 })
149 149
150 150 return data
151 151
152 152 @LoginRequired()
153 153 @HasPermissionAllDecorator('hg.admin')
154 154 @view_config(
155 155 route_name='edit_user_auth_tokens', request_method='GET',
156 156 renderer='rhodecode:templates/admin/users/user_edit.mako')
157 157 def auth_tokens(self):
158 158 _ = self.request.translate
159 159 c = self.load_default_context()
160 160
161 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 163 self._redirect_for_default_user(c.user.username)
164 164
165 165 c.active = 'auth_tokens'
166 166
167 167 c.lifetime_values = [
168 168 (str(-1), _('forever')),
169 169 (str(5), _('5 minutes')),
170 170 (str(60), _('1 hour')),
171 171 (str(60 * 24), _('1 day')),
172 172 (str(60 * 24 * 30), _('1 month')),
173 173 ]
174 174 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
175 175 c.role_values = [
176 176 (x, AuthTokenModel.cls._get_role_name(x))
177 177 for x in AuthTokenModel.cls.ROLES]
178 178 c.role_options = [(c.role_values, _("Role"))]
179 179 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
180 180 c.user.user_id, show_expired=True)
181 181 return self._get_template_context(c)
182 182
183 183 def maybe_attach_token_scope(self, token):
184 184 # implemented in EE edition
185 185 pass
186 186
187 187 @LoginRequired()
188 188 @HasPermissionAllDecorator('hg.admin')
189 189 @CSRFRequired()
190 190 @view_config(
191 191 route_name='edit_user_auth_tokens_add', request_method='POST')
192 192 def auth_tokens_add(self):
193 193 _ = self.request.translate
194 194 c = self.load_default_context()
195 195
196 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 199 self._redirect_for_default_user(c.user.username)
200 200
201 201 user_data = c.user.get_api_data()
202 202 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
203 203 description = self.request.POST.get('description')
204 204 role = self.request.POST.get('role')
205 205
206 206 token = AuthTokenModel().create(
207 207 c.user.user_id, description, lifetime, role)
208 208 token_data = token.get_api_data()
209 209
210 210 self.maybe_attach_token_scope(token)
211 211 audit_logger.store_web(
212 212 'user.edit.token.add', action_data={
213 213 'data': {'token': token_data, 'user': user_data}},
214 214 user=self._rhodecode_user, )
215 215 Session().commit()
216 216
217 217 h.flash(_("Auth token successfully created"), category='success')
218 218 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
219 219
220 220 @LoginRequired()
221 221 @HasPermissionAllDecorator('hg.admin')
222 222 @CSRFRequired()
223 223 @view_config(
224 224 route_name='edit_user_auth_tokens_delete', request_method='POST')
225 225 def auth_tokens_delete(self):
226 226 _ = self.request.translate
227 227 c = self.load_default_context()
228 228
229 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 231 self._redirect_for_default_user(c.user.username)
232 232 user_data = c.user.get_api_data()
233 233
234 234 del_auth_token = self.request.POST.get('del_auth_token')
235 235
236 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 238 token_data = token.get_api_data()
239 239
240 240 AuthTokenModel().delete(del_auth_token, c.user.user_id)
241 241 audit_logger.store_web(
242 242 'user.edit.token.delete', action_data={
243 243 'data': {'token': token_data, 'user': user_data}},
244 244 user=self._rhodecode_user,)
245 245 Session().commit()
246 246 h.flash(_("Auth token successfully deleted"), category='success')
247 247
248 248 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
249 249
250 250 @LoginRequired()
251 251 @HasPermissionAllDecorator('hg.admin')
252 252 @view_config(
253 253 route_name='edit_user_emails', request_method='GET',
254 254 renderer='rhodecode:templates/admin/users/user_edit.mako')
255 255 def emails(self):
256 256 _ = self.request.translate
257 257 c = self.load_default_context()
258 258
259 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 261 self._redirect_for_default_user(c.user.username)
262 262
263 263 c.active = 'emails'
264 264 c.user_email_map = UserEmailMap.query() \
265 265 .filter(UserEmailMap.user == c.user).all()
266 266
267 267 return self._get_template_context(c)
268 268
269 269 @LoginRequired()
270 270 @HasPermissionAllDecorator('hg.admin')
271 271 @CSRFRequired()
272 272 @view_config(
273 273 route_name='edit_user_emails_add', request_method='POST')
274 274 def emails_add(self):
275 275 _ = self.request.translate
276 276 c = self.load_default_context()
277 277
278 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 280 self._redirect_for_default_user(c.user.username)
281 281
282 282 email = self.request.POST.get('new_email')
283 283 user_data = c.user.get_api_data()
284 284 try:
285 285 UserModel().add_extra_email(c.user.user_id, email)
286 286 audit_logger.store_web(
287 287 'user.edit.email.add', action_data={'email': email, 'user': user_data},
288 288 user=self._rhodecode_user)
289 289 Session().commit()
290 290 h.flash(_("Added new email address `%s` for user account") % email,
291 291 category='success')
292 292 except formencode.Invalid as error:
293 293 h.flash(h.escape(error.error_dict['email']), category='error')
294 294 except Exception:
295 295 log.exception("Exception during email saving")
296 296 h.flash(_('An error occurred during email saving'),
297 297 category='error')
298 298 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
299 299
300 300 @LoginRequired()
301 301 @HasPermissionAllDecorator('hg.admin')
302 302 @CSRFRequired()
303 303 @view_config(
304 304 route_name='edit_user_emails_delete', request_method='POST')
305 305 def emails_delete(self):
306 306 _ = self.request.translate
307 307 c = self.load_default_context()
308 308
309 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 311 self._redirect_for_default_user(c.user.username)
312 312
313 313 email_id = self.request.POST.get('del_email_id')
314 314 user_model = UserModel()
315 315
316 316 email = UserEmailMap.query().get(email_id).email
317 317 user_data = c.user.get_api_data()
318 318 user_model.delete_extra_email(c.user.user_id, email_id)
319 319 audit_logger.store_web(
320 320 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
321 321 user=self._rhodecode_user)
322 322 Session().commit()
323 323 h.flash(_("Removed email address from user account"),
324 324 category='success')
325 325 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
326 326
327 327 @LoginRequired()
328 328 @HasPermissionAllDecorator('hg.admin')
329 329 @view_config(
330 330 route_name='edit_user_ips', request_method='GET',
331 331 renderer='rhodecode:templates/admin/users/user_edit.mako')
332 332 def ips(self):
333 333 _ = self.request.translate
334 334 c = self.load_default_context()
335 335
336 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 338 self._redirect_for_default_user(c.user.username)
339 339
340 340 c.active = 'ips'
341 341 c.user_ip_map = UserIpMap.query() \
342 342 .filter(UserIpMap.user == c.user).all()
343 343
344 344 c.inherit_default_ips = c.user.inherit_default_permissions
345 345 c.default_user_ip_map = UserIpMap.query() \
346 346 .filter(UserIpMap.user == User.get_default_user()).all()
347 347
348 348 return self._get_template_context(c)
349 349
350 350 @LoginRequired()
351 351 @HasPermissionAllDecorator('hg.admin')
352 352 @CSRFRequired()
353 353 @view_config(
354 354 route_name='edit_user_ips_add', request_method='POST')
355 355 def ips_add(self):
356 356 _ = self.request.translate
357 357 c = self.load_default_context()
358 358
359 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 361 # NOTE(marcink): this view is allowed for default users, as we can
362 362 # edit their IP white list
363 363
364 364 user_model = UserModel()
365 365 desc = self.request.POST.get('description')
366 366 try:
367 367 ip_list = user_model.parse_ip_range(
368 368 self.request.POST.get('new_ip'))
369 369 except Exception as e:
370 370 ip_list = []
371 371 log.exception("Exception during ip saving")
372 372 h.flash(_('An error occurred during ip saving:%s' % (e,)),
373 373 category='error')
374 374 added = []
375 375 user_data = c.user.get_api_data()
376 376 for ip in ip_list:
377 377 try:
378 378 user_model.add_extra_ip(c.user.user_id, ip, desc)
379 379 audit_logger.store_web(
380 380 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
381 381 user=self._rhodecode_user)
382 382 Session().commit()
383 383 added.append(ip)
384 384 except formencode.Invalid as error:
385 385 msg = error.error_dict['ip']
386 386 h.flash(msg, category='error')
387 387 except Exception:
388 388 log.exception("Exception during ip saving")
389 389 h.flash(_('An error occurred during ip saving'),
390 390 category='error')
391 391 if added:
392 392 h.flash(
393 393 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
394 394 category='success')
395 395 if 'default_user' in self.request.POST:
396 396 # case for editing global IP list we do it for 'DEFAULT' user
397 397 raise HTTPFound(h.route_path('admin_permissions_ips'))
398 398 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
399 399
400 400 @LoginRequired()
401 401 @HasPermissionAllDecorator('hg.admin')
402 402 @CSRFRequired()
403 403 @view_config(
404 404 route_name='edit_user_ips_delete', request_method='POST')
405 405 def ips_delete(self):
406 406 _ = self.request.translate
407 407 c = self.load_default_context()
408 408
409 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 411 # NOTE(marcink): this view is allowed for default users, as we can
412 412 # edit their IP white list
413 413
414 414 ip_id = self.request.POST.get('del_ip_id')
415 415 user_model = UserModel()
416 416 user_data = c.user.get_api_data()
417 417 ip = UserIpMap.query().get(ip_id).ip_addr
418 418 user_model.delete_extra_ip(c.user.user_id, ip_id)
419 419 audit_logger.store_web(
420 420 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
421 421 user=self._rhodecode_user)
422 422 Session().commit()
423 423 h.flash(_("Removed ip address from user whitelist"), category='success')
424 424
425 425 if 'default_user' in self.request.POST:
426 426 # case for editing global IP list we do it for 'DEFAULT' user
427 427 raise HTTPFound(h.route_path('admin_permissions_ips'))
428 428 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
429 429
430 430 @LoginRequired()
431 431 @HasPermissionAllDecorator('hg.admin')
432 432 @view_config(
433 433 route_name='edit_user_groups_management', request_method='GET',
434 434 renderer='rhodecode:templates/admin/users/user_edit.mako')
435 435 def groups_management(self):
436 436 c = self.load_default_context()
437 437
438 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 440 c.data = c.user.group_member
441 441 self._redirect_for_default_user(c.user.username)
442 442 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
443 443 for group in c.user.group_member]
444 444 c.groups = json.dumps(groups)
445 445 c.active = 'groups'
446 446
447 447 return self._get_template_context(c)
448 448
449 449 @LoginRequired()
450 450 @HasPermissionAllDecorator('hg.admin')
451 451 @CSRFRequired()
452 452 @view_config(
453 453 route_name='edit_user_groups_management_updates', request_method='POST')
454 454 def groups_management_updates(self):
455 455 _ = self.request.translate
456 456 c = self.load_default_context()
457 457
458 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 460 self._redirect_for_default_user(c.user.username)
461 461
462 462 users_groups = set(self.request.POST.getall('users_group_id'))
463 463 users_groups_model = []
464 464
465 465 for ugid in users_groups:
466 466 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
467 467 user_group_model = UserGroupModel()
468 468 user_group_model.change_groups(c.user, users_groups_model)
469 469
470 470 Session().commit()
471 471 c.active = 'user_groups_management'
472 472 h.flash(_("Groups successfully changed"), category='success')
473 473
474 474 return HTTPFound(h.route_path(
475 475 'edit_user_groups_management', user_id=user_id))
476 476
477 477 @LoginRequired()
478 478 @HasPermissionAllDecorator('hg.admin')
479 479 @view_config(
480 480 route_name='edit_user_audit_logs', request_method='GET',
481 481 renderer='rhodecode:templates/admin/users/user_edit.mako')
482 482 def user_audit_logs(self):
483 483 _ = self.request.translate
484 484 c = self.load_default_context()
485 485
486 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 488 self._redirect_for_default_user(c.user.username)
489 489 c.active = 'audit'
490 490
491 491 p = safe_int(self.request.GET.get('page', 1), 1)
492 492
493 493 filter_term = self.request.GET.get('filter')
494 494 user_log = UserModel().get_user_log(c.user, filter_term)
495 495
496 496 def url_generator(**kw):
497 497 if filter_term:
498 498 kw['filter'] = filter_term
499 499 return self.request.current_route_path(_query=kw)
500 500
501 501 c.audit_logs = h.Page(
502 502 user_log, page=p, items_per_page=10, url=url_generator)
503 503 c.filter_term = filter_term
504 504 return self._get_template_context(c)
505 505
@@ -1,586 +1,586 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 import formencode
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 31 from rhodecode import forms
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 36 from rhodecode.lib.channelstream import channelstream_request, \
37 37 ChannelstreamException
38 38 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 39 from rhodecode.model.auth_token import AuthTokenModel
40 40 from rhodecode.model.comment import CommentsModel
41 41 from rhodecode.model.db import (
42 42 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
43 43 PullRequest)
44 44 from rhodecode.model.forms import UserForm
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.pull_request import PullRequestModel
47 47 from rhodecode.model.scm import RepoList
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.repo import RepoModel
50 50 from rhodecode.model.validation_schema.schemas import user_schema
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountView(BaseAppView, DataGridAppView):
56 56 ALLOW_SCOPED_TOKENS = False
57 57 """
58 58 This view has alternative version inside EE, if modified please take a look
59 59 in there as well.
60 60 """
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context()
64 64 c.user = c.auth_user.get_instance()
65 65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 66 self._register_global_c(c)
67 67 return c
68 68
69 69 @LoginRequired()
70 70 @NotAnonymous()
71 71 @view_config(
72 72 route_name='my_account_profile', request_method='GET',
73 73 renderer='rhodecode:templates/admin/my_account/my_account.mako')
74 74 def my_account_profile(self):
75 75 c = self.load_default_context()
76 76 c.active = 'profile'
77 77 return self._get_template_context(c)
78 78
79 79 @LoginRequired()
80 80 @NotAnonymous()
81 81 @view_config(
82 82 route_name='my_account_password', request_method='GET',
83 83 renderer='rhodecode:templates/admin/my_account/my_account.mako')
84 84 def my_account_password(self):
85 85 c = self.load_default_context()
86 86 c.active = 'password'
87 87 c.extern_type = c.user.extern_type
88 88
89 89 schema = user_schema.ChangePasswordSchema().bind(
90 90 username=c.user.username)
91 91
92 92 form = forms.Form(
93 93 schema,
94 94 action=h.route_path('my_account_password_update'),
95 95 buttons=(forms.buttons.save, forms.buttons.reset))
96 96
97 97 c.form = form
98 98 return self._get_template_context(c)
99 99
100 100 @LoginRequired()
101 101 @NotAnonymous()
102 102 @CSRFRequired()
103 103 @view_config(
104 104 route_name='my_account_password_update', request_method='POST',
105 105 renderer='rhodecode:templates/admin/my_account/my_account.mako')
106 106 def my_account_password_update(self):
107 107 _ = self.request.translate
108 108 c = self.load_default_context()
109 109 c.active = 'password'
110 110 c.extern_type = c.user.extern_type
111 111
112 112 schema = user_schema.ChangePasswordSchema().bind(
113 113 username=c.user.username)
114 114
115 115 form = forms.Form(
116 116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117 117
118 118 if c.extern_type != 'rhodecode':
119 119 raise HTTPFound(self.request.route_path('my_account_password'))
120 120
121 121 controls = self.request.POST.items()
122 122 try:
123 123 valid_data = form.validate(controls)
124 124 UserModel().update_user(c.user.user_id, **valid_data)
125 125 c.user.update_userdata(force_password_change=False)
126 126 Session().commit()
127 127 except forms.ValidationFailure as e:
128 128 c.form = e
129 129 return self._get_template_context(c)
130 130
131 131 except Exception:
132 132 log.exception("Exception updating password")
133 133 h.flash(_('Error occurred during update of user password'),
134 134 category='error')
135 135 else:
136 136 instance = c.auth_user.get_instance()
137 137 self.session.setdefault('rhodecode_user', {}).update(
138 138 {'password': md5(instance.password)})
139 139 self.session.save()
140 140 h.flash(_("Successfully updated password"), category='success')
141 141
142 142 raise HTTPFound(self.request.route_path('my_account_password'))
143 143
144 144 @LoginRequired()
145 145 @NotAnonymous()
146 146 @view_config(
147 147 route_name='my_account_auth_tokens', request_method='GET',
148 148 renderer='rhodecode:templates/admin/my_account/my_account.mako')
149 149 def my_account_auth_tokens(self):
150 150 _ = self.request.translate
151 151
152 152 c = self.load_default_context()
153 153 c.active = 'auth_tokens'
154 154
155 155 c.lifetime_values = [
156 156 (str(-1), _('forever')),
157 157 (str(5), _('5 minutes')),
158 158 (str(60), _('1 hour')),
159 159 (str(60 * 24), _('1 day')),
160 160 (str(60 * 24 * 30), _('1 month')),
161 161 ]
162 162 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
163 163 c.role_values = [
164 164 (x, AuthTokenModel.cls._get_role_name(x))
165 165 for x in AuthTokenModel.cls.ROLES]
166 166 c.role_options = [(c.role_values, _("Role"))]
167 167 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
168 168 c.user.user_id, show_expired=True)
169 169 return self._get_template_context(c)
170 170
171 171 def maybe_attach_token_scope(self, token):
172 172 # implemented in EE edition
173 173 pass
174 174
175 175 @LoginRequired()
176 176 @NotAnonymous()
177 177 @CSRFRequired()
178 178 @view_config(
179 179 route_name='my_account_auth_tokens_add', request_method='POST',)
180 180 def my_account_auth_tokens_add(self):
181 181 _ = self.request.translate
182 182 c = self.load_default_context()
183 183
184 184 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
185 185 description = self.request.POST.get('description')
186 186 role = self.request.POST.get('role')
187 187
188 188 token = AuthTokenModel().create(
189 189 c.user.user_id, description, lifetime, role)
190 190 token_data = token.get_api_data()
191 191
192 192 self.maybe_attach_token_scope(token)
193 193 audit_logger.store_web(
194 194 'user.edit.token.add', action_data={
195 195 'data': {'token': token_data, 'user': 'self'}},
196 196 user=self._rhodecode_user, )
197 197 Session().commit()
198 198
199 199 h.flash(_("Auth token successfully created"), category='success')
200 200 return HTTPFound(h.route_path('my_account_auth_tokens'))
201 201
202 202 @LoginRequired()
203 203 @NotAnonymous()
204 204 @CSRFRequired()
205 205 @view_config(
206 206 route_name='my_account_auth_tokens_delete', request_method='POST')
207 207 def my_account_auth_tokens_delete(self):
208 208 _ = self.request.translate
209 209 c = self.load_default_context()
210 210
211 211 del_auth_token = self.request.POST.get('del_auth_token')
212 212
213 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 215 token_data = token.get_api_data()
216 216
217 217 AuthTokenModel().delete(del_auth_token, c.user.user_id)
218 218 audit_logger.store_web(
219 219 'user.edit.token.delete', action_data={
220 220 'data': {'token': token_data, 'user': 'self'}},
221 221 user=self._rhodecode_user,)
222 222 Session().commit()
223 223 h.flash(_("Auth token successfully deleted"), category='success')
224 224
225 225 return HTTPFound(h.route_path('my_account_auth_tokens'))
226 226
227 227 @LoginRequired()
228 228 @NotAnonymous()
229 229 @view_config(
230 230 route_name='my_account_emails', request_method='GET',
231 231 renderer='rhodecode:templates/admin/my_account/my_account.mako')
232 232 def my_account_emails(self):
233 233 _ = self.request.translate
234 234
235 235 c = self.load_default_context()
236 236 c.active = 'emails'
237 237
238 238 c.user_email_map = UserEmailMap.query()\
239 239 .filter(UserEmailMap.user == c.user).all()
240 240 return self._get_template_context(c)
241 241
242 242 @LoginRequired()
243 243 @NotAnonymous()
244 244 @CSRFRequired()
245 245 @view_config(
246 246 route_name='my_account_emails_add', request_method='POST')
247 247 def my_account_emails_add(self):
248 248 _ = self.request.translate
249 249 c = self.load_default_context()
250 250
251 251 email = self.request.POST.get('new_email')
252 252
253 253 try:
254 254 UserModel().add_extra_email(c.user.user_id, email)
255 255 audit_logger.store_web(
256 256 'user.edit.email.add', action_data={
257 257 'data': {'email': email, 'user': 'self'}},
258 258 user=self._rhodecode_user,)
259 259
260 260 Session().commit()
261 261 h.flash(_("Added new email address `%s` for user account") % email,
262 262 category='success')
263 263 except formencode.Invalid as error:
264 264 h.flash(h.escape(error.error_dict['email']), category='error')
265 265 except Exception:
266 266 log.exception("Exception in my_account_emails")
267 267 h.flash(_('An error occurred during email saving'),
268 268 category='error')
269 269 return HTTPFound(h.route_path('my_account_emails'))
270 270
271 271 @LoginRequired()
272 272 @NotAnonymous()
273 273 @CSRFRequired()
274 274 @view_config(
275 275 route_name='my_account_emails_delete', request_method='POST')
276 276 def my_account_emails_delete(self):
277 277 _ = self.request.translate
278 278 c = self.load_default_context()
279 279
280 280 del_email_id = self.request.POST.get('del_email_id')
281 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 283 UserModel().delete_extra_email(c.user.user_id, del_email_id)
284 284 audit_logger.store_web(
285 285 'user.edit.email.delete', action_data={
286 286 'data': {'email': email, 'user': 'self'}},
287 287 user=self._rhodecode_user,)
288 288 Session().commit()
289 289 h.flash(_("Email successfully deleted"),
290 290 category='success')
291 291 return HTTPFound(h.route_path('my_account_emails'))
292 292
293 293 @LoginRequired()
294 294 @NotAnonymous()
295 295 @CSRFRequired()
296 296 @view_config(
297 297 route_name='my_account_notifications_test_channelstream',
298 298 request_method='POST', renderer='json_ext')
299 299 def my_account_notifications_test_channelstream(self):
300 300 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
301 301 self._rhodecode_user.username, datetime.datetime.now())
302 302 payload = {
303 303 # 'channel': 'broadcast',
304 304 'type': 'message',
305 305 'timestamp': datetime.datetime.utcnow(),
306 306 'user': 'system',
307 307 'pm_users': [self._rhodecode_user.username],
308 308 'message': {
309 309 'message': message,
310 310 'level': 'info',
311 311 'topic': '/notifications'
312 312 }
313 313 }
314 314
315 315 registry = self.request.registry
316 316 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
317 317 channelstream_config = rhodecode_plugins.get('channelstream', {})
318 318
319 319 try:
320 320 channelstream_request(channelstream_config, [payload], '/message')
321 321 except ChannelstreamException as e:
322 322 log.exception('Failed to send channelstream data')
323 323 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
324 324 return {"response": 'Channelstream data sent. '
325 325 'You should see a new live message now.'}
326 326
327 327 def _load_my_repos_data(self, watched=False):
328 328 if watched:
329 329 admin = False
330 330 follows_repos = Session().query(UserFollowing)\
331 331 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
332 332 .options(joinedload(UserFollowing.follows_repository))\
333 333 .all()
334 334 repo_list = [x.follows_repository for x in follows_repos]
335 335 else:
336 336 admin = True
337 337 repo_list = Repository.get_all_repos(
338 338 user_id=self._rhodecode_user.user_id)
339 339 repo_list = RepoList(repo_list, perm_set=[
340 340 'repository.read', 'repository.write', 'repository.admin'])
341 341
342 342 repos_data = RepoModel().get_repos_as_dict(
343 343 repo_list=repo_list, admin=admin)
344 344 # json used to render the grid
345 345 return json.dumps(repos_data)
346 346
347 347 @LoginRequired()
348 348 @NotAnonymous()
349 349 @view_config(
350 350 route_name='my_account_repos', request_method='GET',
351 351 renderer='rhodecode:templates/admin/my_account/my_account.mako')
352 352 def my_account_repos(self):
353 353 c = self.load_default_context()
354 354 c.active = 'repos'
355 355
356 356 # json used to render the grid
357 357 c.data = self._load_my_repos_data()
358 358 return self._get_template_context(c)
359 359
360 360 @LoginRequired()
361 361 @NotAnonymous()
362 362 @view_config(
363 363 route_name='my_account_watched', request_method='GET',
364 364 renderer='rhodecode:templates/admin/my_account/my_account.mako')
365 365 def my_account_watched(self):
366 366 c = self.load_default_context()
367 367 c.active = 'watched'
368 368
369 369 # json used to render the grid
370 370 c.data = self._load_my_repos_data(watched=True)
371 371 return self._get_template_context(c)
372 372
373 373 @LoginRequired()
374 374 @NotAnonymous()
375 375 @view_config(
376 376 route_name='my_account_perms', request_method='GET',
377 377 renderer='rhodecode:templates/admin/my_account/my_account.mako')
378 378 def my_account_perms(self):
379 379 c = self.load_default_context()
380 380 c.active = 'perms'
381 381
382 382 c.perm_user = c.auth_user
383 383 return self._get_template_context(c)
384 384
385 385 @LoginRequired()
386 386 @NotAnonymous()
387 387 @view_config(
388 388 route_name='my_account_notifications', request_method='GET',
389 389 renderer='rhodecode:templates/admin/my_account/my_account.mako')
390 390 def my_notifications(self):
391 391 c = self.load_default_context()
392 392 c.active = 'notifications'
393 393
394 394 return self._get_template_context(c)
395 395
396 396 @LoginRequired()
397 397 @NotAnonymous()
398 398 @CSRFRequired()
399 399 @view_config(
400 400 route_name='my_account_notifications_toggle_visibility',
401 401 request_method='POST', renderer='json_ext')
402 402 def my_notifications_toggle_visibility(self):
403 403 user = self._rhodecode_db_user
404 404 new_status = not user.user_data.get('notification_status', True)
405 405 user.update_userdata(notification_status=new_status)
406 406 Session().commit()
407 407 return user.user_data['notification_status']
408 408
409 409 @LoginRequired()
410 410 @NotAnonymous()
411 411 @view_config(
412 412 route_name='my_account_edit',
413 413 request_method='GET',
414 414 renderer='rhodecode:templates/admin/my_account/my_account.mako')
415 415 def my_account_edit(self):
416 416 c = self.load_default_context()
417 417 c.active = 'profile_edit'
418 418
419 419 c.perm_user = c.auth_user
420 420 c.extern_type = c.user.extern_type
421 421 c.extern_name = c.user.extern_name
422 422
423 423 defaults = c.user.get_dict()
424 424
425 425 data = render('rhodecode:templates/admin/my_account/my_account.mako',
426 426 self._get_template_context(c), self.request)
427 427 html = formencode.htmlfill.render(
428 428 data,
429 429 defaults=defaults,
430 430 encoding="UTF-8",
431 431 force_defaults=False
432 432 )
433 433 return Response(html)
434 434
435 435 @LoginRequired()
436 436 @NotAnonymous()
437 437 @CSRFRequired()
438 438 @view_config(
439 439 route_name='my_account_update',
440 440 request_method='POST',
441 441 renderer='rhodecode:templates/admin/my_account/my_account.mako')
442 442 def my_account_update(self):
443 443 _ = self.request.translate
444 444 c = self.load_default_context()
445 445 c.active = 'profile_edit'
446 446
447 447 c.perm_user = c.auth_user
448 448 c.extern_type = c.user.extern_type
449 449 c.extern_name = c.user.extern_name
450 450
451 451 _form = UserForm(edit=True,
452 452 old_data={'user_id': self._rhodecode_user.user_id,
453 453 'email': self._rhodecode_user.email})()
454 454 form_result = {}
455 455 try:
456 456 post_data = dict(self.request.POST)
457 457 post_data['new_password'] = ''
458 458 post_data['password_confirmation'] = ''
459 459 form_result = _form.to_python(post_data)
460 460 # skip updating those attrs for my account
461 461 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
462 462 'new_password', 'password_confirmation']
463 463 # TODO: plugin should define if username can be updated
464 464 if c.extern_type != "rhodecode":
465 465 # forbid updating username for external accounts
466 466 skip_attrs.append('username')
467 467
468 468 UserModel().update_user(
469 469 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
470 470 **form_result)
471 471 h.flash(_('Your account was updated successfully'),
472 472 category='success')
473 473 Session().commit()
474 474
475 475 except formencode.Invalid as errors:
476 476 data = render(
477 477 'rhodecode:templates/admin/my_account/my_account.mako',
478 478 self._get_template_context(c), self.request)
479 479
480 480 html = formencode.htmlfill.render(
481 481 data,
482 482 defaults=errors.value,
483 483 errors=errors.error_dict or {},
484 484 prefix_error=False,
485 485 encoding="UTF-8",
486 486 force_defaults=False)
487 487 return Response(html)
488 488
489 489 except Exception:
490 490 log.exception("Exception updating user")
491 491 h.flash(_('Error occurred during update of user %s')
492 492 % form_result.get('username'), category='error')
493 493 raise HTTPFound(h.route_path('my_account_profile'))
494 494
495 495 raise HTTPFound(h.route_path('my_account_profile'))
496 496
497 497 def _get_pull_requests_list(self, statuses):
498 498 draw, start, limit = self._extract_chunk(self.request)
499 499 search_q, order_by, order_dir = self._extract_ordering(self.request)
500 500 _render = self.request.get_partial_renderer(
501 501 'data_table/_dt_elements.mako')
502 502
503 503 pull_requests = PullRequestModel().get_im_participating_in(
504 504 user_id=self._rhodecode_user.user_id,
505 505 statuses=statuses,
506 506 offset=start, length=limit, order_by=order_by,
507 507 order_dir=order_dir)
508 508
509 509 pull_requests_total_count = PullRequestModel().count_im_participating_in(
510 510 user_id=self._rhodecode_user.user_id, statuses=statuses)
511 511
512 512 data = []
513 513 comments_model = CommentsModel()
514 514 for pr in pull_requests:
515 515 repo_id = pr.target_repo_id
516 516 comments = comments_model.get_all_comments(
517 517 repo_id, pull_request=pr)
518 518 owned = pr.user_id == self._rhodecode_user.user_id
519 519
520 520 data.append({
521 521 'target_repo': _render('pullrequest_target_repo',
522 522 pr.target_repo.repo_name),
523 523 'name': _render('pullrequest_name',
524 524 pr.pull_request_id, pr.target_repo.repo_name,
525 525 short=True),
526 526 'name_raw': pr.pull_request_id,
527 527 'status': _render('pullrequest_status',
528 528 pr.calculated_review_status()),
529 529 'title': _render(
530 530 'pullrequest_title', pr.title, pr.description),
531 531 'description': h.escape(pr.description),
532 532 'updated_on': _render('pullrequest_updated_on',
533 533 h.datetime_to_time(pr.updated_on)),
534 534 'updated_on_raw': h.datetime_to_time(pr.updated_on),
535 535 'created_on': _render('pullrequest_updated_on',
536 536 h.datetime_to_time(pr.created_on)),
537 537 'created_on_raw': h.datetime_to_time(pr.created_on),
538 538 'author': _render('pullrequest_author',
539 539 pr.author.full_contact, ),
540 540 'author_raw': pr.author.full_name,
541 541 'comments': _render('pullrequest_comments', len(comments)),
542 542 'comments_raw': len(comments),
543 543 'closed': pr.is_closed(),
544 544 'owned': owned
545 545 })
546 546
547 547 # json used to render the grid
548 548 data = ({
549 549 'draw': draw,
550 550 'data': data,
551 551 'recordsTotal': pull_requests_total_count,
552 552 'recordsFiltered': pull_requests_total_count,
553 553 })
554 554 return data
555 555
556 556 @LoginRequired()
557 557 @NotAnonymous()
558 558 @view_config(
559 559 route_name='my_account_pullrequests',
560 560 request_method='GET',
561 561 renderer='rhodecode:templates/admin/my_account/my_account.mako')
562 562 def my_account_pullrequests(self):
563 563 c = self.load_default_context()
564 564 c.active = 'pullrequests'
565 565 req_get = self.request.GET
566 566
567 567 c.closed = str2bool(req_get.get('pr_show_closed'))
568 568
569 569 return self._get_template_context(c)
570 570
571 571 @LoginRequired()
572 572 @NotAnonymous()
573 573 @view_config(
574 574 route_name='my_account_pullrequests_data',
575 575 request_method='GET', renderer='json_ext')
576 576 def my_account_pullrequests_data(self):
577 577 req_get = self.request.GET
578 578 closed = str2bool(req_get.get('closed'))
579 579
580 580 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
581 581 if closed:
582 582 statuses += [PullRequest.STATUS_CLOSED]
583 583
584 584 data = self._get_pull_requests_list(statuses=statuses)
585 585 return data
586 586
@@ -1,557 +1,557 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import RepoAppView
31 31
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 35
36 36 from rhodecode.lib.compat import OrderedDict
37 37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 38 import rhodecode.lib.helpers as h
39 39 from rhodecode.lib.utils2 import safe_unicode, safe_int
40 40 from rhodecode.lib.vcs.backends.base import EmptyCommit
41 41 from rhodecode.lib.vcs.exceptions import (
42 42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
43 43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
44 44 from rhodecode.model.changeset_status import ChangesetStatusModel
45 45 from rhodecode.model.comment import CommentsModel
46 46 from rhodecode.model.meta import Session
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 def _update_with_GET(params, request):
53 53 for k in ['diff1', 'diff2', 'diff']:
54 54 params[k] += request.GET.getall(k)
55 55
56 56
57 57 def get_ignore_ws(fid, request):
58 58 ig_ws_global = request.GET.get('ignorews')
59 59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
60 60 if ig_ws:
61 61 try:
62 62 return int(ig_ws[0].split(':')[-1])
63 63 except Exception:
64 64 pass
65 65 return ig_ws_global
66 66
67 67
68 68 def _ignorews_url(request, fileid=None):
69 69 _ = request.translate
70 70 fileid = str(fileid) if fileid else None
71 71 params = collections.defaultdict(list)
72 72 _update_with_GET(params, request)
73 73 label = _('Show whitespace')
74 74 tooltiplbl = _('Show whitespace for all diffs')
75 75 ig_ws = get_ignore_ws(fileid, request)
76 76 ln_ctx = get_line_ctx(fileid, request)
77 77
78 78 if ig_ws is None:
79 79 params['ignorews'] += [1]
80 80 label = _('Ignore whitespace')
81 81 tooltiplbl = _('Ignore whitespace for all diffs')
82 82 ctx_key = 'context'
83 83 ctx_val = ln_ctx
84 84
85 85 # if we have passed in ln_ctx pass it along to our params
86 86 if ln_ctx:
87 87 params[ctx_key] += [ctx_val]
88 88
89 89 if fileid:
90 90 params['anchor'] = 'a_' + fileid
91 91 return h.link_to(label, request.current_route_path(_query=params),
92 92 title=tooltiplbl, class_='tooltip')
93 93
94 94
95 95 def get_line_ctx(fid, request):
96 96 ln_ctx_global = request.GET.get('context')
97 97 if fid:
98 98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
99 99 else:
100 100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
101 101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
102 102 if ln_ctx:
103 103 ln_ctx = [ln_ctx]
104 104
105 105 if ln_ctx:
106 106 retval = ln_ctx[0].split(':')[-1]
107 107 else:
108 108 retval = ln_ctx_global
109 109
110 110 try:
111 111 return int(retval)
112 112 except Exception:
113 113 return 3
114 114
115 115
116 116 def _context_url(request, fileid=None):
117 117 """
118 118 Generates a url for context lines.
119 119
120 120 :param fileid:
121 121 """
122 122
123 123 _ = request.translate
124 124 fileid = str(fileid) if fileid else None
125 125 ig_ws = get_ignore_ws(fileid, request)
126 126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
127 127
128 128 params = collections.defaultdict(list)
129 129 _update_with_GET(params, request)
130 130
131 131 if ln_ctx > 0:
132 132 params['context'] += [ln_ctx]
133 133
134 134 if ig_ws:
135 135 ig_ws_key = 'ignorews'
136 136 ig_ws_val = 1
137 137 params[ig_ws_key] += [ig_ws_val]
138 138
139 139 lbl = _('Increase context')
140 140 tooltiplbl = _('Increase context for all diffs')
141 141
142 142 if fileid:
143 143 params['anchor'] = 'a_' + fileid
144 144 return h.link_to(lbl, request.current_route_path(_query=params),
145 145 title=tooltiplbl, class_='tooltip')
146 146
147 147
148 148 class RepoCommitsView(RepoAppView):
149 149 def load_default_context(self):
150 150 c = self._get_local_tmpl_context(include_app_defaults=True)
151 151
152 152 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
153 153 c.repo_info = self.db_repo
154 154 c.rhodecode_repo = self.rhodecode_vcs_repo
155 155
156 156 self._register_global_c(c)
157 157 return c
158 158
159 159 def _commit(self, commit_id_range, method):
160 160 _ = self.request.translate
161 161 c = self.load_default_context()
162 162 c.ignorews_url = _ignorews_url
163 163 c.context_url = _context_url
164 164 c.fulldiff = self.request.GET.get('fulldiff')
165 165
166 166 # fetch global flags of ignore ws or context lines
167 167 context_lcl = get_line_ctx('', self.request)
168 168 ign_whitespace_lcl = get_ignore_ws('', self.request)
169 169
170 170 # diff_limit will cut off the whole diff if the limit is applied
171 171 # otherwise it will just hide the big files from the front-end
172 172 diff_limit = c.visual.cut_off_limit_diff
173 173 file_limit = c.visual.cut_off_limit_file
174 174
175 175 # get ranges of commit ids if preset
176 176 commit_range = commit_id_range.split('...')[:2]
177 177
178 178 try:
179 179 pre_load = ['affected_files', 'author', 'branch', 'date',
180 180 'message', 'parents']
181 181
182 182 if len(commit_range) == 2:
183 183 commits = self.rhodecode_vcs_repo.get_commits(
184 184 start_id=commit_range[0], end_id=commit_range[1],
185 185 pre_load=pre_load)
186 186 commits = list(commits)
187 187 else:
188 188 commits = [self.rhodecode_vcs_repo.get_commit(
189 189 commit_id=commit_id_range, pre_load=pre_load)]
190 190
191 191 c.commit_ranges = commits
192 192 if not c.commit_ranges:
193 193 raise RepositoryError(
194 194 'The commit range returned an empty result')
195 195 except CommitDoesNotExistError:
196 196 msg = _('No such commit exists for this repository')
197 197 h.flash(msg, category='error')
198 198 raise HTTPNotFound()
199 199 except Exception:
200 200 log.exception("General failure")
201 201 raise HTTPNotFound()
202 202
203 203 c.changes = OrderedDict()
204 204 c.lines_added = 0
205 205 c.lines_deleted = 0
206 206
207 207 # auto collapse if we have more than limit
208 208 collapse_limit = diffs.DiffProcessor._collapse_commits_over
209 209 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
210 210
211 211 c.commit_statuses = ChangesetStatus.STATUSES
212 212 c.inline_comments = []
213 213 c.files = []
214 214
215 215 c.statuses = []
216 216 c.comments = []
217 217 c.unresolved_comments = []
218 218 if len(c.commit_ranges) == 1:
219 219 commit = c.commit_ranges[0]
220 220 c.comments = CommentsModel().get_comments(
221 221 self.db_repo.repo_id,
222 222 revision=commit.raw_id)
223 223 c.statuses.append(ChangesetStatusModel().get_status(
224 224 self.db_repo.repo_id, commit.raw_id))
225 225 # comments from PR
226 226 statuses = ChangesetStatusModel().get_statuses(
227 227 self.db_repo.repo_id, commit.raw_id,
228 228 with_revisions=True)
229 229 prs = set(st.pull_request for st in statuses
230 230 if st.pull_request is not None)
231 231 # from associated statuses, check the pull requests, and
232 232 # show comments from them
233 233 for pr in prs:
234 234 c.comments.extend(pr.comments)
235 235
236 236 c.unresolved_comments = CommentsModel()\
237 237 .get_commit_unresolved_todos(commit.raw_id)
238 238
239 239 diff = None
240 240 # Iterate over ranges (default commit view is always one commit)
241 241 for commit in c.commit_ranges:
242 242 c.changes[commit.raw_id] = []
243 243
244 244 commit2 = commit
245 245 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
246 246
247 247 _diff = self.rhodecode_vcs_repo.get_diff(
248 248 commit1, commit2,
249 249 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
250 250 diff_processor = diffs.DiffProcessor(
251 251 _diff, format='newdiff', diff_limit=diff_limit,
252 252 file_limit=file_limit, show_full_diff=c.fulldiff)
253 253
254 254 commit_changes = OrderedDict()
255 255 if method == 'show':
256 256 _parsed = diff_processor.prepare()
257 257 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
258 258
259 259 _parsed = diff_processor.prepare()
260 260
261 261 def _node_getter(commit):
262 262 def get_node(fname):
263 263 try:
264 264 return commit.get_node(fname)
265 265 except NodeDoesNotExistError:
266 266 return None
267 267 return get_node
268 268
269 269 inline_comments = CommentsModel().get_inline_comments(
270 270 self.db_repo.repo_id, revision=commit.raw_id)
271 271 c.inline_cnt = CommentsModel().get_inline_comments_count(
272 272 inline_comments)
273 273
274 274 diffset = codeblocks.DiffSet(
275 275 repo_name=self.db_repo_name,
276 276 source_node_getter=_node_getter(commit1),
277 277 target_node_getter=_node_getter(commit2),
278 278 comments=inline_comments)
279 279 diffset = diffset.render_patchset(
280 280 _parsed, commit1.raw_id, commit2.raw_id)
281 281
282 282 c.changes[commit.raw_id] = diffset
283 283 else:
284 284 # downloads/raw we only need RAW diff nothing else
285 285 diff = diff_processor.as_raw()
286 286 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
287 287
288 288 # sort comments by how they were generated
289 289 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
290 290
291 291 if len(c.commit_ranges) == 1:
292 292 c.commit = c.commit_ranges[0]
293 293 c.parent_tmpl = ''.join(
294 294 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
295 295
296 296 if method == 'download':
297 297 response = Response(diff)
298 298 response.content_type = 'text/plain'
299 299 response.content_disposition = (
300 300 'attachment; filename=%s.diff' % commit_id_range[:12])
301 301 return response
302 302 elif method == 'patch':
303 303 c.diff = safe_unicode(diff)
304 304 patch = render(
305 305 'rhodecode:templates/changeset/patch_changeset.mako',
306 306 self._get_template_context(c), self.request)
307 307 response = Response(patch)
308 308 response.content_type = 'text/plain'
309 309 return response
310 310 elif method == 'raw':
311 311 response = Response(diff)
312 312 response.content_type = 'text/plain'
313 313 return response
314 314 elif method == 'show':
315 315 if len(c.commit_ranges) == 1:
316 316 html = render(
317 317 'rhodecode:templates/changeset/changeset.mako',
318 318 self._get_template_context(c), self.request)
319 319 return Response(html)
320 320 else:
321 321 c.ancestor = None
322 322 c.target_repo = self.db_repo
323 323 html = render(
324 324 'rhodecode:templates/changeset/changeset_range.mako',
325 325 self._get_template_context(c), self.request)
326 326 return Response(html)
327 327
328 328 raise HTTPBadRequest()
329 329
330 330 @LoginRequired()
331 331 @HasRepoPermissionAnyDecorator(
332 332 'repository.read', 'repository.write', 'repository.admin')
333 333 @view_config(
334 334 route_name='repo_commit', request_method='GET',
335 335 renderer=None)
336 336 def repo_commit_show(self):
337 337 commit_id = self.request.matchdict['commit_id']
338 338 return self._commit(commit_id, method='show')
339 339
340 340 @LoginRequired()
341 341 @HasRepoPermissionAnyDecorator(
342 342 'repository.read', 'repository.write', 'repository.admin')
343 343 @view_config(
344 344 route_name='repo_commit_raw', request_method='GET',
345 345 renderer=None)
346 346 @view_config(
347 347 route_name='repo_commit_raw_deprecated', request_method='GET',
348 348 renderer=None)
349 349 def repo_commit_raw(self):
350 350 commit_id = self.request.matchdict['commit_id']
351 351 return self._commit(commit_id, method='raw')
352 352
353 353 @LoginRequired()
354 354 @HasRepoPermissionAnyDecorator(
355 355 'repository.read', 'repository.write', 'repository.admin')
356 356 @view_config(
357 357 route_name='repo_commit_patch', request_method='GET',
358 358 renderer=None)
359 359 def repo_commit_patch(self):
360 360 commit_id = self.request.matchdict['commit_id']
361 361 return self._commit(commit_id, method='patch')
362 362
363 363 @LoginRequired()
364 364 @HasRepoPermissionAnyDecorator(
365 365 'repository.read', 'repository.write', 'repository.admin')
366 366 @view_config(
367 367 route_name='repo_commit_download', request_method='GET',
368 368 renderer=None)
369 369 def repo_commit_download(self):
370 370 commit_id = self.request.matchdict['commit_id']
371 371 return self._commit(commit_id, method='download')
372 372
373 373 @LoginRequired()
374 374 @NotAnonymous()
375 375 @HasRepoPermissionAnyDecorator(
376 376 'repository.read', 'repository.write', 'repository.admin')
377 377 @CSRFRequired()
378 378 @view_config(
379 379 route_name='repo_commit_comment_create', request_method='POST',
380 380 renderer='json_ext')
381 381 def repo_commit_comment_create(self):
382 382 _ = self.request.translate
383 383 commit_id = self.request.matchdict['commit_id']
384 384
385 385 c = self.load_default_context()
386 386 status = self.request.POST.get('changeset_status', None)
387 387 text = self.request.POST.get('text')
388 388 comment_type = self.request.POST.get('comment_type')
389 389 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
390 390
391 391 if status:
392 392 text = text or (_('Status change %(transition_icon)s %(status)s')
393 393 % {'transition_icon': '>',
394 394 'status': ChangesetStatus.get_status_lbl(status)})
395 395
396 396 multi_commit_ids = []
397 397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
398 398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
399 399 if _commit_id not in multi_commit_ids:
400 400 multi_commit_ids.append(_commit_id)
401 401
402 402 commit_ids = multi_commit_ids or [commit_id]
403 403
404 404 comment = None
405 405 for current_id in filter(None, commit_ids):
406 406 comment = CommentsModel().create(
407 407 text=text,
408 408 repo=self.db_repo.repo_id,
409 409 user=self._rhodecode_db_user.user_id,
410 410 commit_id=current_id,
411 411 f_path=self.request.POST.get('f_path'),
412 412 line_no=self.request.POST.get('line'),
413 413 status_change=(ChangesetStatus.get_status_lbl(status)
414 414 if status else None),
415 415 status_change_type=status,
416 416 comment_type=comment_type,
417 417 resolves_comment_id=resolves_comment_id
418 418 )
419 419
420 420 # get status if set !
421 421 if status:
422 422 # if latest status was from pull request and it's closed
423 423 # disallow changing status !
424 424 # dont_allow_on_closed_pull_request = True !
425 425
426 426 try:
427 427 ChangesetStatusModel().set_status(
428 428 self.db_repo.repo_id,
429 429 status,
430 430 self._rhodecode_db_user.user_id,
431 431 comment,
432 432 revision=current_id,
433 433 dont_allow_on_closed_pull_request=True
434 434 )
435 435 except StatusChangeOnClosedPullRequestError:
436 436 msg = _('Changing the status of a commit associated with '
437 437 'a closed pull request is not allowed')
438 438 log.exception(msg)
439 439 h.flash(msg, category='warning')
440 440 raise HTTPFound(h.route_path(
441 441 'repo_commit', repo_name=self.db_repo_name,
442 442 commit_id=current_id))
443 443
444 444 # finalize, commit and redirect
445 445 Session().commit()
446 446
447 447 data = {
448 448 'target_id': h.safeid(h.safe_unicode(
449 449 self.request.POST.get('f_path'))),
450 450 }
451 451 if comment:
452 452 c.co = comment
453 453 rendered_comment = render(
454 454 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 455 self._get_template_context(c), self.request)
456 456
457 457 data.update(comment.get_dict())
458 458 data.update({'rendered_text': rendered_comment})
459 459
460 460 return data
461 461
462 462 @LoginRequired()
463 463 @NotAnonymous()
464 464 @HasRepoPermissionAnyDecorator(
465 465 'repository.read', 'repository.write', 'repository.admin')
466 466 @CSRFRequired()
467 467 @view_config(
468 468 route_name='repo_commit_comment_preview', request_method='POST',
469 469 renderer='string', xhr=True)
470 470 def repo_commit_comment_preview(self):
471 471 # Technically a CSRF token is not needed as no state changes with this
472 472 # call. However, as this is a POST is better to have it, so automated
473 473 # tools don't flag it as potential CSRF.
474 474 # Post is required because the payload could be bigger than the maximum
475 475 # allowed by GET.
476 476
477 477 text = self.request.POST.get('text')
478 478 renderer = self.request.POST.get('renderer') or 'rst'
479 479 if text:
480 480 return h.render(text, renderer=renderer, mentions=True)
481 481 return ''
482 482
483 483 @LoginRequired()
484 484 @NotAnonymous()
485 485 @HasRepoPermissionAnyDecorator(
486 486 'repository.read', 'repository.write', 'repository.admin')
487 487 @CSRFRequired()
488 488 @view_config(
489 489 route_name='repo_commit_comment_delete', request_method='POST',
490 490 renderer='json_ext')
491 491 def repo_commit_comment_delete(self):
492 492 commit_id = self.request.matchdict['commit_id']
493 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 496 if not comment:
497 497 log.debug('Comment with id:%s not found, skipping', comment_id)
498 498 # comment already deleted in another call probably
499 499 return True
500 500
501 501 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
502 502 super_admin = h.HasPermissionAny('hg.admin')()
503 503 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
504 504 is_repo_comment = comment.repo.repo_name == self.db_repo_name
505 505 comment_repo_admin = is_repo_admin and is_repo_comment
506 506
507 507 if super_admin or comment_owner or comment_repo_admin:
508 508 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
509 509 Session().commit()
510 510 return True
511 511 else:
512 512 log.warning('No permissions for user %s to delete comment_id: %s',
513 513 self._rhodecode_db_user, comment_id)
514 514 raise HTTPNotFound()
515 515
516 516 @LoginRequired()
517 517 @HasRepoPermissionAnyDecorator(
518 518 'repository.read', 'repository.write', 'repository.admin')
519 519 @view_config(
520 520 route_name='repo_commit_data', request_method='GET',
521 521 renderer='json_ext', xhr=True)
522 522 def repo_commit_data(self):
523 523 commit_id = self.request.matchdict['commit_id']
524 524 self.load_default_context()
525 525
526 526 try:
527 527 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
528 528 except CommitDoesNotExistError as e:
529 529 return EmptyCommit(message=str(e))
530 530
531 531 @LoginRequired()
532 532 @HasRepoPermissionAnyDecorator(
533 533 'repository.read', 'repository.write', 'repository.admin')
534 534 @view_config(
535 535 route_name='repo_commit_children', request_method='GET',
536 536 renderer='json_ext', xhr=True)
537 537 def repo_commit_children(self):
538 538 commit_id = self.request.matchdict['commit_id']
539 539 self.load_default_context()
540 540
541 541 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
542 542 result = {"results": commit.children}
543 543 return result
544 544
545 545 @LoginRequired()
546 546 @HasRepoPermissionAnyDecorator(
547 547 'repository.read', 'repository.write', 'repository.admin')
548 548 @view_config(
549 549 route_name='repo_commit_parents', request_method='GET',
550 550 renderer='json_ext')
551 551 def repo_commit_parents(self):
552 552 commit_id = self.request.matchdict['commit_id']
553 553 self.load_default_context()
554 554
555 555 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
556 556 result = {"results": commit.parents}
557 557 return result
@@ -1,1018 +1,1018 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import peppercorn
25 25 import formencode
26 26 import logging
27 27 import collections
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 30 from pylons import request, tmpl_context as c, url
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from pyramid.threadlocal import get_current_registry
34 34 from pyramid.httpexceptions import HTTPFound
35 35 from sqlalchemy.sql import func
36 36 from sqlalchemy.sql.expression import or_
37 37
38 38 from rhodecode import events
39 39 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.base import (
42 42 BaseRepoController, render, vcs_operation_context)
43 43 from rhodecode.lib.auth import (
44 44 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 45 HasAcceptedRepoType, XHRRequired)
46 46 from rhodecode.lib.channelstream import channelstream_request
47 47 from rhodecode.lib.utils import jsonify
48 48 from rhodecode.lib.utils2 import (
49 49 safe_int, safe_str, str2bool, safe_unicode)
50 50 from rhodecode.lib.vcs.backends.base import (
51 51 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 52 from rhodecode.lib.vcs.exceptions import (
53 53 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 54 NodeDoesNotExistError)
55 55
56 56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 57 from rhodecode.model.comment import CommentsModel
58 58 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 59 Repository, PullRequestVersion)
60 60 from rhodecode.model.forms import PullRequestForm
61 61 from rhodecode.model.meta import Session
62 62 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class PullrequestsController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 72 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 73
74 74 @LoginRequired()
75 75 @NotAnonymous()
76 76 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 77 'repository.admin')
78 78 @HasAcceptedRepoType('git', 'hg')
79 79 def index(self):
80 80 source_repo = c.rhodecode_db_repo
81 81
82 82 try:
83 83 source_repo.scm_instance().get_commit()
84 84 except EmptyRepositoryError:
85 85 h.flash(h.literal(_('There are no commits yet')),
86 86 category='warning')
87 87 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88 88
89 89 commit_id = request.GET.get('commit')
90 90 branch_ref = request.GET.get('branch')
91 91 bookmark_ref = request.GET.get('bookmark')
92 92
93 93 try:
94 94 source_repo_data = PullRequestModel().generate_repo_data(
95 95 source_repo, commit_id=commit_id,
96 96 branch=branch_ref, bookmark=bookmark_ref)
97 97 except CommitDoesNotExistError as e:
98 98 log.exception(e)
99 99 h.flash(_('Commit does not exist'), 'error')
100 100 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 101
102 102 default_target_repo = source_repo
103 103
104 104 if source_repo.parent:
105 105 parent_vcs_obj = source_repo.parent.scm_instance()
106 106 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 107 # change default if we have a parent repo
108 108 default_target_repo = source_repo.parent
109 109
110 110 target_repo_data = PullRequestModel().generate_repo_data(
111 111 default_target_repo)
112 112
113 113 selected_source_ref = source_repo_data['refs']['selected_ref']
114 114
115 115 title_source_ref = selected_source_ref.split(':', 2)[1]
116 116 c.default_title = PullRequestModel().generate_pullrequest_title(
117 117 source=source_repo.repo_name,
118 118 source_ref=title_source_ref,
119 119 target=default_target_repo.repo_name
120 120 )
121 121
122 122 c.default_repo_data = {
123 123 'source_repo_name': source_repo.repo_name,
124 124 'source_refs_json': json.dumps(source_repo_data),
125 125 'target_repo_name': default_target_repo.repo_name,
126 126 'target_refs_json': json.dumps(target_repo_data),
127 127 }
128 128 c.default_source_ref = selected_source_ref
129 129
130 130 return render('/pullrequests/pullrequest.mako')
131 131
132 132 @LoginRequired()
133 133 @NotAnonymous()
134 134 @XHRRequired()
135 135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 136 'repository.admin')
137 137 @jsonify
138 138 def get_repo_refs(self, repo_name, target_repo_name):
139 139 repo = Repository.get_by_repo_name(target_repo_name)
140 140 if not repo:
141 141 raise HTTPNotFound
142 142 return PullRequestModel().generate_repo_data(repo)
143 143
144 144 @LoginRequired()
145 145 @NotAnonymous()
146 146 @XHRRequired()
147 147 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 148 'repository.admin')
149 149 @jsonify
150 150 def get_repo_destinations(self, repo_name):
151 151 repo = Repository.get_by_repo_name(repo_name)
152 152 if not repo:
153 153 raise HTTPNotFound
154 154 filter_query = request.GET.get('query')
155 155
156 156 query = Repository.query() \
157 157 .order_by(func.length(Repository.repo_name)) \
158 158 .filter(or_(
159 159 Repository.repo_name == repo.repo_name,
160 160 Repository.fork_id == repo.repo_id))
161 161
162 162 if filter_query:
163 163 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 164 query = query.filter(
165 165 Repository.repo_name.ilike(ilike_expression))
166 166
167 167 add_parent = False
168 168 if repo.parent:
169 169 if filter_query in repo.parent.repo_name:
170 170 parent_vcs_obj = repo.parent.scm_instance()
171 171 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 172 add_parent = True
173 173
174 174 limit = 20 - 1 if add_parent else 20
175 175 all_repos = query.limit(limit).all()
176 176 if add_parent:
177 177 all_repos += [repo.parent]
178 178
179 179 repos = []
180 180 for obj in self.scm_model.get_repos(all_repos):
181 181 repos.append({
182 182 'id': obj['name'],
183 183 'text': obj['name'],
184 184 'type': 'repo',
185 185 'obj': obj['dbrepo']
186 186 })
187 187
188 188 data = {
189 189 'more': False,
190 190 'results': [{
191 191 'text': _('Repositories'),
192 192 'children': repos
193 193 }] if repos else []
194 194 }
195 195 return data
196 196
197 197 @LoginRequired()
198 198 @NotAnonymous()
199 199 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 200 'repository.admin')
201 201 @HasAcceptedRepoType('git', 'hg')
202 202 @auth.CSRFRequired()
203 203 def create(self, repo_name):
204 204 repo = Repository.get_by_repo_name(repo_name)
205 205 if not repo:
206 206 raise HTTPNotFound
207 207
208 208 controls = peppercorn.parse(request.POST.items())
209 209
210 210 try:
211 211 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 212 except formencode.Invalid as errors:
213 213 if errors.error_dict.get('revisions'):
214 214 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 215 elif errors.error_dict.get('pullrequest_title'):
216 216 msg = _('Pull request requires a title with min. 3 chars')
217 217 else:
218 218 msg = _('Error creating pull request: {}').format(errors)
219 219 log.exception(msg)
220 220 h.flash(msg, 'error')
221 221
222 222 # would rather just go back to form ...
223 223 return redirect(url('pullrequest_home', repo_name=repo_name))
224 224
225 225 source_repo = _form['source_repo']
226 226 source_ref = _form['source_ref']
227 227 target_repo = _form['target_repo']
228 228 target_ref = _form['target_ref']
229 229 commit_ids = _form['revisions'][::-1]
230 230
231 231 # find the ancestor for this pr
232 232 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 233 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 234
235 235 source_scm = source_db_repo.scm_instance()
236 236 target_scm = target_db_repo.scm_instance()
237 237
238 238 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 239 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 240
241 241 ancestor = source_scm.get_common_ancestor(
242 242 source_commit.raw_id, target_commit.raw_id, target_scm)
243 243
244 244 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 245 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 246
247 247 pullrequest_title = _form['pullrequest_title']
248 248 title_source_ref = source_ref.split(':', 2)[1]
249 249 if not pullrequest_title:
250 250 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 251 source=source_repo,
252 252 source_ref=title_source_ref,
253 253 target=target_repo
254 254 )
255 255
256 256 description = _form['pullrequest_desc']
257 257
258 258 get_default_reviewers_data, validate_default_reviewers = \
259 259 PullRequestModel().get_reviewer_functions()
260 260
261 261 # recalculate reviewers logic, to make sure we can validate this
262 262 reviewer_rules = get_default_reviewers_data(
263 263 c.rhodecode_user.get_instance(), source_db_repo,
264 264 source_commit, target_db_repo, target_commit)
265 265
266 266 given_reviewers = _form['review_members']
267 267 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
268 268
269 269 try:
270 270 pull_request = PullRequestModel().create(
271 271 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 272 target_ref, commit_ids, reviewers, pullrequest_title,
273 273 description, reviewer_rules
274 274 )
275 275 Session().commit()
276 276 h.flash(_('Successfully opened new pull request'),
277 277 category='success')
278 278 except Exception as e:
279 279 msg = _('Error occurred during creation of this pull request.')
280 280 log.exception(msg)
281 281 h.flash(msg, category='error')
282 282 return redirect(url('pullrequest_home', repo_name=repo_name))
283 283
284 284 raise HTTPFound(
285 285 h.route_path('pullrequest_show', repo_name=target_repo,
286 286 pull_request_id=pull_request.pull_request_id))
287 287
288 288 @LoginRequired()
289 289 @NotAnonymous()
290 290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 291 'repository.admin')
292 292 @auth.CSRFRequired()
293 293 @jsonify
294 294 def update(self, repo_name, pull_request_id):
295 295 pull_request_id = safe_int(pull_request_id)
296 296 pull_request = PullRequest.get_or_404(pull_request_id)
297 297 # only owner or admin can update it
298 298 allowed_to_update = PullRequestModel().check_user_update(
299 299 pull_request, c.rhodecode_user)
300 300 if allowed_to_update:
301 301 controls = peppercorn.parse(request.POST.items())
302 302
303 303 if 'review_members' in controls:
304 304 self._update_reviewers(
305 305 pull_request_id, controls['review_members'],
306 306 pull_request.reviewer_data)
307 307 elif str2bool(request.POST.get('update_commits', 'false')):
308 308 self._update_commits(pull_request)
309 309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
310 310 self._edit_pull_request(pull_request)
311 311 else:
312 312 raise HTTPBadRequest()
313 313 return True
314 314 raise HTTPForbidden()
315 315
316 316 def _edit_pull_request(self, pull_request):
317 317 try:
318 318 PullRequestModel().edit(
319 319 pull_request, request.POST.get('title'),
320 320 request.POST.get('description'), c.rhodecode_user)
321 321 except ValueError:
322 322 msg = _(u'Cannot update closed pull requests.')
323 323 h.flash(msg, category='error')
324 324 return
325 325 else:
326 326 Session().commit()
327 327
328 328 msg = _(u'Pull request title & description updated.')
329 329 h.flash(msg, category='success')
330 330 return
331 331
332 332 def _update_commits(self, pull_request):
333 333 resp = PullRequestModel().update_commits(pull_request)
334 334
335 335 if resp.executed:
336 336
337 337 if resp.target_changed and resp.source_changed:
338 338 changed = 'target and source repositories'
339 339 elif resp.target_changed and not resp.source_changed:
340 340 changed = 'target repository'
341 341 elif not resp.target_changed and resp.source_changed:
342 342 changed = 'source repository'
343 343 else:
344 344 changed = 'nothing'
345 345
346 346 msg = _(
347 347 u'Pull request updated to "{source_commit_id}" with '
348 348 u'{count_added} added, {count_removed} removed commits. '
349 349 u'Source of changes: {change_source}')
350 350 msg = msg.format(
351 351 source_commit_id=pull_request.source_ref_parts.commit_id,
352 352 count_added=len(resp.changes.added),
353 353 count_removed=len(resp.changes.removed),
354 354 change_source=changed)
355 355 h.flash(msg, category='success')
356 356
357 357 registry = get_current_registry()
358 358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 360 if channelstream_config.get('enabled'):
361 361 message = msg + (
362 362 ' - <a onclick="window.location.reload()">'
363 363 '<strong>{}</strong></a>'.format(_('Reload page')))
364 364 channel = '/repo${}$/pr/{}'.format(
365 365 pull_request.target_repo.repo_name,
366 366 pull_request.pull_request_id
367 367 )
368 368 payload = {
369 369 'type': 'message',
370 370 'user': 'system',
371 371 'exclude_users': [request.user.username],
372 372 'channel': channel,
373 373 'message': {
374 374 'message': message,
375 375 'level': 'success',
376 376 'topic': '/notifications'
377 377 }
378 378 }
379 379 channelstream_request(
380 380 channelstream_config, [payload], '/message',
381 381 raise_exc=False)
382 382 else:
383 383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
384 384 warning_reasons = [
385 385 UpdateFailureReason.NO_CHANGE,
386 386 UpdateFailureReason.WRONG_REF_TYPE,
387 387 ]
388 388 category = 'warning' if resp.reason in warning_reasons else 'error'
389 389 h.flash(msg, category=category)
390 390
391 391 @auth.CSRFRequired()
392 392 @LoginRequired()
393 393 @NotAnonymous()
394 394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 395 'repository.admin')
396 396 def merge(self, repo_name, pull_request_id):
397 397 """
398 398 POST /{repo_name}/pull-request/{pull_request_id}
399 399
400 400 Merge will perform a server-side merge of the specified
401 401 pull request, if the pull request is approved and mergeable.
402 402 After successful merging, the pull request is automatically
403 403 closed, with a relevant comment.
404 404 """
405 405 pull_request_id = safe_int(pull_request_id)
406 406 pull_request = PullRequest.get_or_404(pull_request_id)
407 407 user = c.rhodecode_user
408 408
409 409 check = MergeCheck.validate(pull_request, user)
410 410 merge_possible = not check.failed
411 411
412 412 for err_type, error_msg in check.errors:
413 413 h.flash(error_msg, category=err_type)
414 414
415 415 if merge_possible:
416 416 log.debug("Pre-conditions checked, trying to merge.")
417 417 extras = vcs_operation_context(
418 418 request.environ, repo_name=pull_request.target_repo.repo_name,
419 419 username=user.username, action='push',
420 420 scm=pull_request.target_repo.repo_type)
421 421 self._merge_pull_request(pull_request, user, extras)
422 422
423 423 raise HTTPFound(
424 424 h.route_path('pullrequest_show',
425 425 repo_name=pull_request.target_repo.repo_name,
426 426 pull_request_id=pull_request.pull_request_id))
427 427
428 428 def _merge_pull_request(self, pull_request, user, extras):
429 429 merge_resp = PullRequestModel().merge(
430 430 pull_request, user, extras=extras)
431 431
432 432 if merge_resp.executed:
433 433 log.debug("The merge was successful, closing the pull request.")
434 434 PullRequestModel().close_pull_request(
435 435 pull_request.pull_request_id, user)
436 436 Session().commit()
437 437 msg = _('Pull request was successfully merged and closed.')
438 438 h.flash(msg, category='success')
439 439 else:
440 440 log.debug(
441 441 "The merge was not successful. Merge response: %s",
442 442 merge_resp)
443 443 msg = PullRequestModel().merge_status_message(
444 444 merge_resp.failure_reason)
445 445 h.flash(msg, category='error')
446 446
447 447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
448 448
449 449 get_default_reviewers_data, validate_default_reviewers = \
450 450 PullRequestModel().get_reviewer_functions()
451 451
452 452 try:
453 453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
454 454 except ValueError as e:
455 455 log.error('Reviewers Validation: {}'.format(e))
456 456 h.flash(e, category='error')
457 457 return
458 458
459 459 PullRequestModel().update_reviewers(
460 460 pull_request_id, reviewers, c.rhodecode_user)
461 461 h.flash(_('Pull request reviewers updated.'), category='success')
462 462 Session().commit()
463 463
464 464 @LoginRequired()
465 465 @NotAnonymous()
466 466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 467 'repository.admin')
468 468 @auth.CSRFRequired()
469 469 @jsonify
470 470 def delete(self, repo_name, pull_request_id):
471 471 pull_request_id = safe_int(pull_request_id)
472 472 pull_request = PullRequest.get_or_404(pull_request_id)
473 473
474 474 pr_closed = pull_request.is_closed()
475 475 allowed_to_delete = PullRequestModel().check_user_delete(
476 476 pull_request, c.rhodecode_user) and not pr_closed
477 477
478 478 # only owner can delete it !
479 479 if allowed_to_delete:
480 480 PullRequestModel().delete(pull_request, c.rhodecode_user)
481 481 Session().commit()
482 482 h.flash(_('Successfully deleted pull request'),
483 483 category='success')
484 484 return redirect(url('my_account_pullrequests'))
485 485
486 486 h.flash(_('Your are not allowed to delete this pull request'),
487 487 category='error')
488 488 raise HTTPForbidden()
489 489
490 490 def _get_pr_version(self, pull_request_id, version=None):
491 491 pull_request_id = safe_int(pull_request_id)
492 492 at_version = None
493 493
494 494 if version and version == 'latest':
495 495 pull_request_ver = PullRequest.get(pull_request_id)
496 496 pull_request_obj = pull_request_ver
497 497 _org_pull_request_obj = pull_request_obj
498 498 at_version = 'latest'
499 499 elif version:
500 500 pull_request_ver = PullRequestVersion.get_or_404(version)
501 501 pull_request_obj = pull_request_ver
502 502 _org_pull_request_obj = pull_request_ver.pull_request
503 503 at_version = pull_request_ver.pull_request_version_id
504 504 else:
505 505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
506 506 pull_request_id)
507 507
508 508 pull_request_display_obj = PullRequest.get_pr_display_object(
509 509 pull_request_obj, _org_pull_request_obj)
510 510
511 511 return _org_pull_request_obj, pull_request_obj, \
512 512 pull_request_display_obj, at_version
513 513
514 514 def _get_diffset(
515 515 self, source_repo, source_ref_id, target_ref_id, target_commit,
516 516 source_commit, diff_limit, file_limit, display_inline_comments):
517 517 vcs_diff = PullRequestModel().get_diff(
518 518 source_repo, source_ref_id, target_ref_id)
519 519
520 520 diff_processor = diffs.DiffProcessor(
521 521 vcs_diff, format='newdiff', diff_limit=diff_limit,
522 522 file_limit=file_limit, show_full_diff=c.fulldiff)
523 523
524 524 _parsed = diff_processor.prepare()
525 525
526 526 def _node_getter(commit):
527 527 def get_node(fname):
528 528 try:
529 529 return commit.get_node(fname)
530 530 except NodeDoesNotExistError:
531 531 return None
532 532
533 533 return get_node
534 534
535 535 diffset = codeblocks.DiffSet(
536 536 repo_name=c.repo_name,
537 537 source_repo_name=c.source_repo.repo_name,
538 538 source_node_getter=_node_getter(target_commit),
539 539 target_node_getter=_node_getter(source_commit),
540 540 comments=display_inline_comments
541 541 )
542 542 diffset = diffset.render_patchset(
543 543 _parsed, target_commit.raw_id, source_commit.raw_id)
544 544
545 545 return diffset
546 546
547 547 @LoginRequired()
548 548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
549 549 'repository.admin')
550 550 def show(self, repo_name, pull_request_id):
551 551 pull_request_id = safe_int(pull_request_id)
552 552 version = request.GET.get('version')
553 553 from_version = request.GET.get('from_version') or version
554 554 merge_checks = request.GET.get('merge_checks')
555 555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
556 556
557 557 (pull_request_latest,
558 558 pull_request_at_ver,
559 559 pull_request_display_obj,
560 560 at_version) = self._get_pr_version(
561 561 pull_request_id, version=version)
562 562 pr_closed = pull_request_latest.is_closed()
563 563
564 564 if pr_closed and (version or from_version):
565 565 # not allow to browse versions
566 566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
567 567 pull_request_id=pull_request_id))
568 568
569 569 versions = pull_request_display_obj.versions()
570 570
571 571 c.at_version = at_version
572 572 c.at_version_num = (at_version
573 573 if at_version and at_version != 'latest'
574 574 else None)
575 575 c.at_version_pos = ChangesetComment.get_index_from_version(
576 576 c.at_version_num, versions)
577 577
578 578 (prev_pull_request_latest,
579 579 prev_pull_request_at_ver,
580 580 prev_pull_request_display_obj,
581 581 prev_at_version) = self._get_pr_version(
582 582 pull_request_id, version=from_version)
583 583
584 584 c.from_version = prev_at_version
585 585 c.from_version_num = (prev_at_version
586 586 if prev_at_version and prev_at_version != 'latest'
587 587 else None)
588 588 c.from_version_pos = ChangesetComment.get_index_from_version(
589 589 c.from_version_num, versions)
590 590
591 591 # define if we're in COMPARE mode or VIEW at version mode
592 592 compare = at_version != prev_at_version
593 593
594 594 # pull_requests repo_name we opened it against
595 595 # ie. target_repo must match
596 596 if repo_name != pull_request_at_ver.target_repo.repo_name:
597 597 raise HTTPNotFound
598 598
599 599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
600 600 pull_request_at_ver)
601 601
602 602 c.pull_request = pull_request_display_obj
603 603 c.pull_request_latest = pull_request_latest
604 604
605 605 if compare or (at_version and not at_version == 'latest'):
606 606 c.allowed_to_change_status = False
607 607 c.allowed_to_update = False
608 608 c.allowed_to_merge = False
609 609 c.allowed_to_delete = False
610 610 c.allowed_to_comment = False
611 611 c.allowed_to_close = False
612 612 else:
613 613 can_change_status = PullRequestModel().check_user_change_status(
614 614 pull_request_at_ver, c.rhodecode_user)
615 615 c.allowed_to_change_status = can_change_status and not pr_closed
616 616
617 617 c.allowed_to_update = PullRequestModel().check_user_update(
618 618 pull_request_latest, c.rhodecode_user) and not pr_closed
619 619 c.allowed_to_merge = PullRequestModel().check_user_merge(
620 620 pull_request_latest, c.rhodecode_user) and not pr_closed
621 621 c.allowed_to_delete = PullRequestModel().check_user_delete(
622 622 pull_request_latest, c.rhodecode_user) and not pr_closed
623 623 c.allowed_to_comment = not pr_closed
624 624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
625 625
626 626 c.forbid_adding_reviewers = False
627 627 c.forbid_author_to_review = False
628 628 c.forbid_commit_author_to_review = False
629 629
630 630 if pull_request_latest.reviewer_data and \
631 631 'rules' in pull_request_latest.reviewer_data:
632 632 rules = pull_request_latest.reviewer_data['rules'] or {}
633 633 try:
634 634 c.forbid_adding_reviewers = rules.get(
635 635 'forbid_adding_reviewers')
636 636 c.forbid_author_to_review = rules.get(
637 637 'forbid_author_to_review')
638 638 c.forbid_commit_author_to_review = rules.get(
639 639 'forbid_commit_author_to_review')
640 640 except Exception:
641 641 pass
642 642
643 643 # check merge capabilities
644 644 _merge_check = MergeCheck.validate(
645 645 pull_request_latest, user=c.rhodecode_user)
646 646 c.pr_merge_errors = _merge_check.error_details
647 647 c.pr_merge_possible = not _merge_check.failed
648 648 c.pr_merge_message = _merge_check.merge_msg
649 649
650 650 c.pull_request_review_status = _merge_check.review_status
651 651 if merge_checks:
652 652 return render('/pullrequests/pullrequest_merge_checks.mako')
653 653
654 654 comments_model = CommentsModel()
655 655
656 656 # reviewers and statuses
657 657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
658 658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
659 659
660 660 # GENERAL COMMENTS with versions #
661 661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
662 662 q = q.order_by(ChangesetComment.comment_id.asc())
663 663 general_comments = q
664 664
665 665 # pick comments we want to render at current version
666 666 c.comment_versions = comments_model.aggregate_comments(
667 667 general_comments, versions, c.at_version_num)
668 668 c.comments = c.comment_versions[c.at_version_num]['until']
669 669
670 670 # INLINE COMMENTS with versions #
671 671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
672 672 q = q.order_by(ChangesetComment.comment_id.asc())
673 673 inline_comments = q
674 674
675 675 c.inline_versions = comments_model.aggregate_comments(
676 676 inline_comments, versions, c.at_version_num, inline=True)
677 677
678 678 # inject latest version
679 679 latest_ver = PullRequest.get_pr_display_object(
680 680 pull_request_latest, pull_request_latest)
681 681
682 682 c.versions = versions + [latest_ver]
683 683
684 684 # if we use version, then do not show later comments
685 685 # than current version
686 686 display_inline_comments = collections.defaultdict(
687 687 lambda: collections.defaultdict(list))
688 688 for co in inline_comments:
689 689 if c.at_version_num:
690 690 # pick comments that are at least UPTO given version, so we
691 691 # don't render comments for higher version
692 692 should_render = co.pull_request_version_id and \
693 693 co.pull_request_version_id <= c.at_version_num
694 694 else:
695 695 # showing all, for 'latest'
696 696 should_render = True
697 697
698 698 if should_render:
699 699 display_inline_comments[co.f_path][co.line_no].append(co)
700 700
701 701 # load diff data into template context, if we use compare mode then
702 702 # diff is calculated based on changes between versions of PR
703 703
704 704 source_repo = pull_request_at_ver.source_repo
705 705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
706 706
707 707 target_repo = pull_request_at_ver.target_repo
708 708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
709 709
710 710 if compare:
711 711 # in compare switch the diff base to latest commit from prev version
712 712 target_ref_id = prev_pull_request_display_obj.revisions[0]
713 713
714 714 # despite opening commits for bookmarks/branches/tags, we always
715 715 # convert this to rev to prevent changes after bookmark or branch change
716 716 c.source_ref_type = 'rev'
717 717 c.source_ref = source_ref_id
718 718
719 719 c.target_ref_type = 'rev'
720 720 c.target_ref = target_ref_id
721 721
722 722 c.source_repo = source_repo
723 723 c.target_repo = target_repo
724 724
725 725 # diff_limit is the old behavior, will cut off the whole diff
726 726 # if the limit is applied otherwise will just hide the
727 727 # big files from the front-end
728 728 diff_limit = self.cut_off_limit_diff
729 729 file_limit = self.cut_off_limit_file
730 730
731 731 c.commit_ranges = []
732 732 source_commit = EmptyCommit()
733 733 target_commit = EmptyCommit()
734 734 c.missing_requirements = False
735 735
736 736 source_scm = source_repo.scm_instance()
737 737 target_scm = target_repo.scm_instance()
738 738
739 739 # try first shadow repo, fallback to regular repo
740 740 try:
741 741 commits_source_repo = pull_request_latest.get_shadow_repo()
742 742 except Exception:
743 743 log.debug('Failed to get shadow repo', exc_info=True)
744 744 commits_source_repo = source_scm
745 745
746 746 c.commits_source_repo = commits_source_repo
747 747 commit_cache = {}
748 748 try:
749 749 pre_load = ["author", "branch", "date", "message"]
750 750 show_revs = pull_request_at_ver.revisions
751 751 for rev in show_revs:
752 752 comm = commits_source_repo.get_commit(
753 753 commit_id=rev, pre_load=pre_load)
754 754 c.commit_ranges.append(comm)
755 755 commit_cache[comm.raw_id] = comm
756 756
757 757 # Order here matters, we first need to get target, and then
758 758 # the source
759 759 target_commit = commits_source_repo.get_commit(
760 760 commit_id=safe_str(target_ref_id))
761 761
762 762 source_commit = commits_source_repo.get_commit(
763 763 commit_id=safe_str(source_ref_id))
764 764
765 765 except CommitDoesNotExistError:
766 766 log.warning(
767 767 'Failed to get commit from `{}` repo'.format(
768 768 commits_source_repo), exc_info=True)
769 769 except RepositoryRequirementError:
770 770 log.warning(
771 771 'Failed to get all required data from repo', exc_info=True)
772 772 c.missing_requirements = True
773 773
774 774 c.ancestor = None # set it to None, to hide it from PR view
775 775
776 776 try:
777 777 ancestor_id = source_scm.get_common_ancestor(
778 778 source_commit.raw_id, target_commit.raw_id, target_scm)
779 779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
780 780 except Exception:
781 781 c.ancestor_commit = None
782 782
783 783 c.statuses = source_repo.statuses(
784 784 [x.raw_id for x in c.commit_ranges])
785 785
786 786 # auto collapse if we have more than limit
787 787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
788 788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
789 789 c.compare_mode = compare
790 790
791 791 c.missing_commits = False
792 792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
793 793 or source_commit == target_commit):
794 794
795 795 c.missing_commits = True
796 796 else:
797 797
798 798 c.diffset = self._get_diffset(
799 799 commits_source_repo, source_ref_id, target_ref_id,
800 800 target_commit, source_commit,
801 801 diff_limit, file_limit, display_inline_comments)
802 802
803 803 c.limited_diff = c.diffset.limited_diff
804 804
805 805 # calculate removed files that are bound to comments
806 806 comment_deleted_files = [
807 807 fname for fname in display_inline_comments
808 808 if fname not in c.diffset.file_stats]
809 809
810 810 c.deleted_files_comments = collections.defaultdict(dict)
811 811 for fname, per_line_comments in display_inline_comments.items():
812 812 if fname in comment_deleted_files:
813 813 c.deleted_files_comments[fname]['stats'] = 0
814 814 c.deleted_files_comments[fname]['comments'] = list()
815 815 for lno, comments in per_line_comments.items():
816 816 c.deleted_files_comments[fname]['comments'].extend(
817 817 comments)
818 818
819 819 # this is a hack to properly display links, when creating PR, the
820 820 # compare view and others uses different notation, and
821 821 # compare_commits.mako renders links based on the target_repo.
822 822 # We need to swap that here to generate it properly on the html side
823 823 c.target_repo = c.source_repo
824 824
825 825 c.commit_statuses = ChangesetStatus.STATUSES
826 826
827 827 c.show_version_changes = not pr_closed
828 828 if c.show_version_changes:
829 829 cur_obj = pull_request_at_ver
830 830 prev_obj = prev_pull_request_at_ver
831 831
832 832 old_commit_ids = prev_obj.revisions
833 833 new_commit_ids = cur_obj.revisions
834 834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
835 835 old_commit_ids, new_commit_ids)
836 836 c.commit_changes_summary = commit_changes
837 837
838 838 # calculate the diff for commits between versions
839 839 c.commit_changes = []
840 840 mark = lambda cs, fw: list(
841 841 h.itertools.izip_longest([], cs, fillvalue=fw))
842 842 for c_type, raw_id in mark(commit_changes.added, 'a') \
843 843 + mark(commit_changes.removed, 'r') \
844 844 + mark(commit_changes.common, 'c'):
845 845
846 846 if raw_id in commit_cache:
847 847 commit = commit_cache[raw_id]
848 848 else:
849 849 try:
850 850 commit = commits_source_repo.get_commit(raw_id)
851 851 except CommitDoesNotExistError:
852 852 # in case we fail extracting still use "dummy" commit
853 853 # for display in commit diff
854 854 commit = h.AttributeDict(
855 855 {'raw_id': raw_id,
856 856 'message': 'EMPTY or MISSING COMMIT'})
857 857 c.commit_changes.append([c_type, commit])
858 858
859 859 # current user review statuses for each version
860 860 c.review_versions = {}
861 861 if c.rhodecode_user.user_id in allowed_reviewers:
862 862 for co in general_comments:
863 863 if co.author.user_id == c.rhodecode_user.user_id:
864 864 # each comment has a status change
865 865 status = co.status_change
866 866 if status:
867 867 _ver_pr = status[0].comment.pull_request_version_id
868 868 c.review_versions[_ver_pr] = status[0]
869 869
870 870 return render('/pullrequests/pullrequest_show.mako')
871 871
872 872 @LoginRequired()
873 873 @NotAnonymous()
874 874 @HasRepoPermissionAnyDecorator(
875 875 'repository.read', 'repository.write', 'repository.admin')
876 876 @auth.CSRFRequired()
877 877 @jsonify
878 878 def comment(self, repo_name, pull_request_id):
879 879 pull_request_id = safe_int(pull_request_id)
880 880 pull_request = PullRequest.get_or_404(pull_request_id)
881 881 if pull_request.is_closed():
882 882 log.debug('comment: forbidden because pull request is closed')
883 883 raise HTTPForbidden()
884 884
885 885 status = request.POST.get('changeset_status', None)
886 886 text = request.POST.get('text')
887 887 comment_type = request.POST.get('comment_type')
888 888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
889 889 close_pull_request = request.POST.get('close_pull_request')
890 890
891 891 # the logic here should work like following, if we submit close
892 892 # pr comment, use `close_pull_request_with_comment` function
893 893 # else handle regular comment logic
894 894 user = c.rhodecode_user
895 895 repo = c.rhodecode_db_repo
896 896
897 897 if close_pull_request:
898 898 # only owner or admin or person with write permissions
899 899 allowed_to_close = PullRequestModel().check_user_update(
900 900 pull_request, c.rhodecode_user)
901 901 if not allowed_to_close:
902 902 log.debug('comment: forbidden because not allowed to close '
903 903 'pull request %s', pull_request_id)
904 904 raise HTTPForbidden()
905 905 comment, status = PullRequestModel().close_pull_request_with_comment(
906 906 pull_request, user, repo, message=text)
907 907 Session().flush()
908 908 events.trigger(
909 909 events.PullRequestCommentEvent(pull_request, comment))
910 910
911 911 else:
912 912 # regular comment case, could be inline, or one with status.
913 913 # for that one we check also permissions
914 914
915 915 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 916 pull_request, c.rhodecode_user)
917 917
918 918 if status and allowed_to_change_status:
919 919 message = (_('Status change %(transition_icon)s %(status)s')
920 920 % {'transition_icon': '>',
921 921 'status': ChangesetStatus.get_status_lbl(status)})
922 922 text = text or message
923 923
924 924 comment = CommentsModel().create(
925 925 text=text,
926 926 repo=c.rhodecode_db_repo.repo_id,
927 927 user=c.rhodecode_user.user_id,
928 928 pull_request=pull_request_id,
929 929 f_path=request.POST.get('f_path'),
930 930 line_no=request.POST.get('line'),
931 931 status_change=(ChangesetStatus.get_status_lbl(status)
932 932 if status and allowed_to_change_status else None),
933 933 status_change_type=(status
934 934 if status and allowed_to_change_status else None),
935 935 comment_type=comment_type,
936 936 resolves_comment_id=resolves_comment_id
937 937 )
938 938
939 939 if allowed_to_change_status:
940 940 # calculate old status before we change it
941 941 old_calculated_status = pull_request.calculated_review_status()
942 942
943 943 # get status if set !
944 944 if status:
945 945 ChangesetStatusModel().set_status(
946 946 c.rhodecode_db_repo.repo_id,
947 947 status,
948 948 c.rhodecode_user.user_id,
949 949 comment,
950 950 pull_request=pull_request_id
951 951 )
952 952
953 953 Session().flush()
954 954 events.trigger(
955 955 events.PullRequestCommentEvent(pull_request, comment))
956 956
957 957 # we now calculate the status of pull request, and based on that
958 958 # calculation we set the commits status
959 959 calculated_status = pull_request.calculated_review_status()
960 960 if old_calculated_status != calculated_status:
961 961 PullRequestModel()._trigger_pull_request_hook(
962 962 pull_request, c.rhodecode_user, 'review_status_change')
963 963
964 964 Session().commit()
965 965
966 966 if not request.is_xhr:
967 967 raise HTTPFound(
968 968 h.route_path('pullrequest_show',
969 969 repo_name=repo_name,
970 970 pull_request_id=pull_request_id))
971 971
972 972 data = {
973 973 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
974 974 }
975 975 if comment:
976 976 c.co = comment
977 977 rendered_comment = render('changeset/changeset_comment_block.mako')
978 978 data.update(comment.get_dict())
979 979 data.update({'rendered_text': rendered_comment})
980 980
981 981 return data
982 982
983 983 @LoginRequired()
984 984 @NotAnonymous()
985 985 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
986 986 'repository.admin')
987 987 @auth.CSRFRequired()
988 988 @jsonify
989 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 991 if not comment:
992 992 log.debug('Comment with id:%s not found, skipping', comment_id)
993 993 # comment already deleted in another call probably
994 994 return True
995 995
996 996 if comment.pull_request.is_closed():
997 997 # don't allow deleting comments on closed pull request
998 998 raise HTTPForbidden()
999 999
1000 1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1001 1001 super_admin = h.HasPermissionAny('hg.admin')()
1002 1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1003 1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1004 1004 comment_repo_admin = is_repo_admin and is_repo_comment
1005 1005
1006 1006 if super_admin or comment_owner or comment_repo_admin:
1007 1007 old_calculated_status = comment.pull_request.calculated_review_status()
1008 1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1009 1009 Session().commit()
1010 1010 calculated_status = comment.pull_request.calculated_review_status()
1011 1011 if old_calculated_status != calculated_status:
1012 1012 PullRequestModel()._trigger_pull_request_hook(
1013 1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1014 1014 return True
1015 1015 else:
1016 1016 log.warning('No permissions for user %s to delete comment_id: %s',
1017 1017 c.rhodecode_user, comment_id)
1018 1018 raise HTTPNotFound()
@@ -1,4122 +1,4113 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from zope.cachedescriptors.property import Lazy as LazyProperty
46 46
47 47 from pyramid.threadlocal import get_current_request
48 48
49 49 from rhodecode.translation import _
50 50 from rhodecode.lib.vcs import get_vcs_instance
51 51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 52 from rhodecode.lib.utils2 import (
53 53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 55 glob2re, StrictAttributeDict, cleaned_uri)
56 56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 57 from rhodecode.lib.ext_json import json
58 58 from rhodecode.lib.caching_query import FromCache
59 59 from rhodecode.lib.encrypt import AESCipher
60 60
61 61 from rhodecode.model.meta import Base, Session
62 62
63 63 URL_SEP = '/'
64 64 log = logging.getLogger(__name__)
65 65
66 66 # =============================================================================
67 67 # BASE CLASSES
68 68 # =============================================================================
69 69
70 70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 71 # beaker.session.secret if first is not set.
72 72 # and initialized at environment.py
73 73 ENCRYPTION_KEY = None
74 74
75 75 # used to sort permissions by types, '#' used here is not allowed to be in
76 76 # usernames, and it's very early in sorted string.printable table.
77 77 PERMISSION_TYPE_SORT = {
78 78 'admin': '####',
79 79 'write': '###',
80 80 'read': '##',
81 81 'none': '#',
82 82 }
83 83
84 84
85 85 def display_sort(obj):
86 86 """
87 87 Sort function used to sort permissions in .permissions() function of
88 88 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 89 of all other resources
90 90 """
91 91
92 92 if obj.username == User.DEFAULT_USER:
93 93 return '#####'
94 94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 95 return prefix + obj.username
96 96
97 97
98 98 def _hash_key(k):
99 99 return md5_safe(k)
100 100
101 101
102 102 class EncryptedTextValue(TypeDecorator):
103 103 """
104 104 Special column for encrypted long text data, use like::
105 105
106 106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 107
108 108 This column is intelligent so if value is in unencrypted form it return
109 109 unencrypted form, but on save it always encrypts
110 110 """
111 111 impl = Text
112 112
113 113 def process_bind_param(self, value, dialect):
114 114 if not value:
115 115 return value
116 116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 117 # protect against double encrypting if someone manually starts
118 118 # doing
119 119 raise ValueError('value needs to be in unencrypted format, ie. '
120 120 'not starting with enc$aes')
121 121 return 'enc$aes_hmac$%s' % AESCipher(
122 122 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 123
124 124 def process_result_value(self, value, dialect):
125 125 import rhodecode
126 126
127 127 if not value:
128 128 return value
129 129
130 130 parts = value.split('$', 3)
131 131 if not len(parts) == 3:
132 132 # probably not encrypted values
133 133 return value
134 134 else:
135 135 if parts[0] != 'enc':
136 136 # parts ok but without our header ?
137 137 return value
138 138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 139 'rhodecode.encrypted_values.strict') or True)
140 140 # at that stage we know it's our encryption
141 141 if parts[1] == 'aes':
142 142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 143 elif parts[1] == 'aes_hmac':
144 144 decrypted_data = AESCipher(
145 145 ENCRYPTION_KEY, hmac=True,
146 146 strict_verification=enc_strict_mode).decrypt(parts[2])
147 147 else:
148 148 raise ValueError(
149 149 'Encryption type part is wrong, must be `aes` '
150 150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 151 return decrypted_data
152 152
153 153
154 154 class BaseModel(object):
155 155 """
156 156 Base Model for all classes
157 157 """
158 158
159 159 @classmethod
160 160 def _get_keys(cls):
161 161 """return column names for this model """
162 162 return class_mapper(cls).c.keys()
163 163
164 164 def get_dict(self):
165 165 """
166 166 return dict with keys and values corresponding
167 167 to this model data """
168 168
169 169 d = {}
170 170 for k in self._get_keys():
171 171 d[k] = getattr(self, k)
172 172
173 173 # also use __json__() if present to get additional fields
174 174 _json_attr = getattr(self, '__json__', None)
175 175 if _json_attr:
176 176 # update with attributes from __json__
177 177 if callable(_json_attr):
178 178 _json_attr = _json_attr()
179 179 for k, val in _json_attr.iteritems():
180 180 d[k] = val
181 181 return d
182 182
183 183 def get_appstruct(self):
184 184 """return list with keys and values tuples corresponding
185 185 to this model data """
186 186
187 187 l = []
188 188 for k in self._get_keys():
189 189 l.append((k, getattr(self, k),))
190 190 return l
191 191
192 192 def populate_obj(self, populate_dict):
193 193 """populate model with data from given populate_dict"""
194 194
195 195 for k in self._get_keys():
196 196 if k in populate_dict:
197 197 setattr(self, k, populate_dict[k])
198 198
199 199 @classmethod
200 200 def query(cls):
201 201 return Session().query(cls)
202 202
203 203 @classmethod
204 204 def get(cls, id_):
205 205 if id_:
206 206 return cls.query().get(id_)
207 207
208 208 @classmethod
209 def get_or_404(cls, id_, pyramid_exc=False):
210 if pyramid_exc:
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
209 def get_or_404(cls, id_):
210 from pyramid.httpexceptions import HTTPNotFound
216 211
217 212 try:
218 213 id_ = int(id_)
219 214 except (TypeError, ValueError):
220 raise HTTPNotFound
215 raise HTTPNotFound()
221 216
222 217 res = cls.query().get(id_)
223 218 if not res:
224 raise HTTPNotFound
219 raise HTTPNotFound()
225 220 return res
226 221
227 222 @classmethod
228 223 def getAll(cls):
229 224 # deprecated and left for backward compatibility
230 225 return cls.get_all()
231 226
232 227 @classmethod
233 228 def get_all(cls):
234 229 return cls.query().all()
235 230
236 231 @classmethod
237 232 def delete(cls, id_):
238 233 obj = cls.query().get(id_)
239 234 Session().delete(obj)
240 235
241 236 @classmethod
242 237 def identity_cache(cls, session, attr_name, value):
243 238 exist_in_session = []
244 239 for (item_cls, pkey), instance in session.identity_map.items():
245 240 if cls == item_cls and getattr(instance, attr_name) == value:
246 241 exist_in_session.append(instance)
247 242 if exist_in_session:
248 243 if len(exist_in_session) == 1:
249 244 return exist_in_session[0]
250 245 log.exception(
251 246 'multiple objects with attr %s and '
252 247 'value %s found with same name: %r',
253 248 attr_name, value, exist_in_session)
254 249
255 250 def __repr__(self):
256 251 if hasattr(self, '__unicode__'):
257 252 # python repr needs to return str
258 253 try:
259 254 return safe_str(self.__unicode__())
260 255 except UnicodeDecodeError:
261 256 pass
262 257 return '<DB:%s>' % (self.__class__.__name__)
263 258
264 259
265 260 class RhodeCodeSetting(Base, BaseModel):
266 261 __tablename__ = 'rhodecode_settings'
267 262 __table_args__ = (
268 263 UniqueConstraint('app_settings_name'),
269 264 {'extend_existing': True, 'mysql_engine': 'InnoDB',
270 265 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
271 266 )
272 267
273 268 SETTINGS_TYPES = {
274 269 'str': safe_str,
275 270 'int': safe_int,
276 271 'unicode': safe_unicode,
277 272 'bool': str2bool,
278 273 'list': functools.partial(aslist, sep=',')
279 274 }
280 275 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
281 276 GLOBAL_CONF_KEY = 'app_settings'
282 277
283 278 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
284 279 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
285 280 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
286 281 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
287 282
288 283 def __init__(self, key='', val='', type='unicode'):
289 284 self.app_settings_name = key
290 285 self.app_settings_type = type
291 286 self.app_settings_value = val
292 287
293 288 @validates('_app_settings_value')
294 289 def validate_settings_value(self, key, val):
295 290 assert type(val) == unicode
296 291 return val
297 292
298 293 @hybrid_property
299 294 def app_settings_value(self):
300 295 v = self._app_settings_value
301 296 _type = self.app_settings_type
302 297 if _type:
303 298 _type = self.app_settings_type.split('.')[0]
304 299 # decode the encrypted value
305 300 if 'encrypted' in self.app_settings_type:
306 301 cipher = EncryptedTextValue()
307 302 v = safe_unicode(cipher.process_result_value(v, None))
308 303
309 304 converter = self.SETTINGS_TYPES.get(_type) or \
310 305 self.SETTINGS_TYPES['unicode']
311 306 return converter(v)
312 307
313 308 @app_settings_value.setter
314 309 def app_settings_value(self, val):
315 310 """
316 311 Setter that will always make sure we use unicode in app_settings_value
317 312
318 313 :param val:
319 314 """
320 315 val = safe_unicode(val)
321 316 # encode the encrypted value
322 317 if 'encrypted' in self.app_settings_type:
323 318 cipher = EncryptedTextValue()
324 319 val = safe_unicode(cipher.process_bind_param(val, None))
325 320 self._app_settings_value = val
326 321
327 322 @hybrid_property
328 323 def app_settings_type(self):
329 324 return self._app_settings_type
330 325
331 326 @app_settings_type.setter
332 327 def app_settings_type(self, val):
333 328 if val.split('.')[0] not in self.SETTINGS_TYPES:
334 329 raise Exception('type must be one of %s got %s'
335 330 % (self.SETTINGS_TYPES.keys(), val))
336 331 self._app_settings_type = val
337 332
338 333 def __unicode__(self):
339 334 return u"<%s('%s:%s[%s]')>" % (
340 335 self.__class__.__name__,
341 336 self.app_settings_name, self.app_settings_value,
342 337 self.app_settings_type
343 338 )
344 339
345 340
346 341 class RhodeCodeUi(Base, BaseModel):
347 342 __tablename__ = 'rhodecode_ui'
348 343 __table_args__ = (
349 344 UniqueConstraint('ui_key'),
350 345 {'extend_existing': True, 'mysql_engine': 'InnoDB',
351 346 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
352 347 )
353 348
354 349 HOOK_REPO_SIZE = 'changegroup.repo_size'
355 350 # HG
356 351 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
357 352 HOOK_PULL = 'outgoing.pull_logger'
358 353 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
359 354 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
360 355 HOOK_PUSH = 'changegroup.push_logger'
361 356 HOOK_PUSH_KEY = 'pushkey.key_push'
362 357
363 358 # TODO: johbo: Unify way how hooks are configured for git and hg,
364 359 # git part is currently hardcoded.
365 360
366 361 # SVN PATTERNS
367 362 SVN_BRANCH_ID = 'vcs_svn_branch'
368 363 SVN_TAG_ID = 'vcs_svn_tag'
369 364
370 365 ui_id = Column(
371 366 "ui_id", Integer(), nullable=False, unique=True, default=None,
372 367 primary_key=True)
373 368 ui_section = Column(
374 369 "ui_section", String(255), nullable=True, unique=None, default=None)
375 370 ui_key = Column(
376 371 "ui_key", String(255), nullable=True, unique=None, default=None)
377 372 ui_value = Column(
378 373 "ui_value", String(255), nullable=True, unique=None, default=None)
379 374 ui_active = Column(
380 375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
381 376
382 377 def __repr__(self):
383 378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
384 379 self.ui_key, self.ui_value)
385 380
386 381
387 382 class RepoRhodeCodeSetting(Base, BaseModel):
388 383 __tablename__ = 'repo_rhodecode_settings'
389 384 __table_args__ = (
390 385 UniqueConstraint(
391 386 'app_settings_name', 'repository_id',
392 387 name='uq_repo_rhodecode_setting_name_repo_id'),
393 388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
394 389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
395 390 )
396 391
397 392 repository_id = Column(
398 393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
399 394 nullable=False)
400 395 app_settings_id = Column(
401 396 "app_settings_id", Integer(), nullable=False, unique=True,
402 397 default=None, primary_key=True)
403 398 app_settings_name = Column(
404 399 "app_settings_name", String(255), nullable=True, unique=None,
405 400 default=None)
406 401 _app_settings_value = Column(
407 402 "app_settings_value", String(4096), nullable=True, unique=None,
408 403 default=None)
409 404 _app_settings_type = Column(
410 405 "app_settings_type", String(255), nullable=True, unique=None,
411 406 default=None)
412 407
413 408 repository = relationship('Repository')
414 409
415 410 def __init__(self, repository_id, key='', val='', type='unicode'):
416 411 self.repository_id = repository_id
417 412 self.app_settings_name = key
418 413 self.app_settings_type = type
419 414 self.app_settings_value = val
420 415
421 416 @validates('_app_settings_value')
422 417 def validate_settings_value(self, key, val):
423 418 assert type(val) == unicode
424 419 return val
425 420
426 421 @hybrid_property
427 422 def app_settings_value(self):
428 423 v = self._app_settings_value
429 424 type_ = self.app_settings_type
430 425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
431 426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
432 427 return converter(v)
433 428
434 429 @app_settings_value.setter
435 430 def app_settings_value(self, val):
436 431 """
437 432 Setter that will always make sure we use unicode in app_settings_value
438 433
439 434 :param val:
440 435 """
441 436 self._app_settings_value = safe_unicode(val)
442 437
443 438 @hybrid_property
444 439 def app_settings_type(self):
445 440 return self._app_settings_type
446 441
447 442 @app_settings_type.setter
448 443 def app_settings_type(self, val):
449 444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
450 445 if val not in SETTINGS_TYPES:
451 446 raise Exception('type must be one of %s got %s'
452 447 % (SETTINGS_TYPES.keys(), val))
453 448 self._app_settings_type = val
454 449
455 450 def __unicode__(self):
456 451 return u"<%s('%s:%s:%s[%s]')>" % (
457 452 self.__class__.__name__, self.repository.repo_name,
458 453 self.app_settings_name, self.app_settings_value,
459 454 self.app_settings_type
460 455 )
461 456
462 457
463 458 class RepoRhodeCodeUi(Base, BaseModel):
464 459 __tablename__ = 'repo_rhodecode_ui'
465 460 __table_args__ = (
466 461 UniqueConstraint(
467 462 'repository_id', 'ui_section', 'ui_key',
468 463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
469 464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
470 465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
471 466 )
472 467
473 468 repository_id = Column(
474 469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
475 470 nullable=False)
476 471 ui_id = Column(
477 472 "ui_id", Integer(), nullable=False, unique=True, default=None,
478 473 primary_key=True)
479 474 ui_section = Column(
480 475 "ui_section", String(255), nullable=True, unique=None, default=None)
481 476 ui_key = Column(
482 477 "ui_key", String(255), nullable=True, unique=None, default=None)
483 478 ui_value = Column(
484 479 "ui_value", String(255), nullable=True, unique=None, default=None)
485 480 ui_active = Column(
486 481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
487 482
488 483 repository = relationship('Repository')
489 484
490 485 def __repr__(self):
491 486 return '<%s[%s:%s]%s=>%s]>' % (
492 487 self.__class__.__name__, self.repository.repo_name,
493 488 self.ui_section, self.ui_key, self.ui_value)
494 489
495 490
496 491 class User(Base, BaseModel):
497 492 __tablename__ = 'users'
498 493 __table_args__ = (
499 494 UniqueConstraint('username'), UniqueConstraint('email'),
500 495 Index('u_username_idx', 'username'),
501 496 Index('u_email_idx', 'email'),
502 497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
503 498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
504 499 )
505 500 DEFAULT_USER = 'default'
506 501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
507 502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
508 503
509 504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
510 505 username = Column("username", String(255), nullable=True, unique=None, default=None)
511 506 password = Column("password", String(255), nullable=True, unique=None, default=None)
512 507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
513 508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
514 509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
515 510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
516 511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
517 512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
518 513 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
519 514
520 515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
521 516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
522 517 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
523 518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
524 519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
525 520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
526 521
527 522 user_log = relationship('UserLog')
528 523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
529 524
530 525 repositories = relationship('Repository')
531 526 repository_groups = relationship('RepoGroup')
532 527 user_groups = relationship('UserGroup')
533 528
534 529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
535 530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
536 531
537 532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
538 533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
539 534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
540 535
541 536 group_member = relationship('UserGroupMember', cascade='all')
542 537
543 538 notifications = relationship('UserNotification', cascade='all')
544 539 # notifications assigned to this user
545 540 user_created_notifications = relationship('Notification', cascade='all')
546 541 # comments created by this user
547 542 user_comments = relationship('ChangesetComment', cascade='all')
548 543 # user profile extra info
549 544 user_emails = relationship('UserEmailMap', cascade='all')
550 545 user_ip_map = relationship('UserIpMap', cascade='all')
551 546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
552 547 # gists
553 548 user_gists = relationship('Gist', cascade='all')
554 549 # user pull requests
555 550 user_pull_requests = relationship('PullRequest', cascade='all')
556 551 # external identities
557 552 extenal_identities = relationship(
558 553 'ExternalIdentity',
559 554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
560 555 cascade='all')
561 556
562 557 def __unicode__(self):
563 558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
564 559 self.user_id, self.username)
565 560
566 561 @hybrid_property
567 562 def email(self):
568 563 return self._email
569 564
570 565 @email.setter
571 566 def email(self, val):
572 567 self._email = val.lower() if val else None
573 568
574 569 @hybrid_property
575 570 def first_name(self):
576 571 from rhodecode.lib import helpers as h
577 572 if self.name:
578 573 return h.escape(self.name)
579 574 return self.name
580 575
581 576 @hybrid_property
582 577 def last_name(self):
583 578 from rhodecode.lib import helpers as h
584 579 if self.lastname:
585 580 return h.escape(self.lastname)
586 581 return self.lastname
587 582
588 583 @hybrid_property
589 584 def api_key(self):
590 585 """
591 586 Fetch if exist an auth-token with role ALL connected to this user
592 587 """
593 588 user_auth_token = UserApiKeys.query()\
594 589 .filter(UserApiKeys.user_id == self.user_id)\
595 590 .filter(or_(UserApiKeys.expires == -1,
596 591 UserApiKeys.expires >= time.time()))\
597 592 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
598 593 if user_auth_token:
599 594 user_auth_token = user_auth_token.api_key
600 595
601 596 return user_auth_token
602 597
603 598 @api_key.setter
604 599 def api_key(self, val):
605 600 # don't allow to set API key this is deprecated for now
606 601 self._api_key = None
607 602
608 603 @property
609 604 def reviewer_pull_requests(self):
610 605 return PullRequestReviewers.query() \
611 606 .options(joinedload(PullRequestReviewers.pull_request)) \
612 607 .filter(PullRequestReviewers.user_id == self.user_id) \
613 608 .all()
614 609
615 610 @property
616 611 def firstname(self):
617 612 # alias for future
618 613 return self.name
619 614
620 615 @property
621 616 def emails(self):
622 617 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
623 618 return [self.email] + [x.email for x in other]
624 619
625 620 @property
626 621 def auth_tokens(self):
627 622 auth_tokens = self.get_auth_tokens()
628 623 return [x.api_key for x in auth_tokens]
629 624
630 625 def get_auth_tokens(self):
631 626 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
632 627
633 628 @property
634 629 def feed_token(self):
635 630 return self.get_feed_token()
636 631
637 632 def get_feed_token(self):
638 633 feed_tokens = UserApiKeys.query()\
639 634 .filter(UserApiKeys.user == self)\
640 635 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
641 636 .all()
642 637 if feed_tokens:
643 638 return feed_tokens[0].api_key
644 639 return 'NO_FEED_TOKEN_AVAILABLE'
645 640
646 641 @classmethod
647 642 def extra_valid_auth_tokens(cls, user, role=None):
648 643 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
649 644 .filter(or_(UserApiKeys.expires == -1,
650 645 UserApiKeys.expires >= time.time()))
651 646 if role:
652 647 tokens = tokens.filter(or_(UserApiKeys.role == role,
653 648 UserApiKeys.role == UserApiKeys.ROLE_ALL))
654 649 return tokens.all()
655 650
656 651 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
657 652 from rhodecode.lib import auth
658 653
659 654 log.debug('Trying to authenticate user: %s via auth-token, '
660 655 'and roles: %s', self, roles)
661 656
662 657 if not auth_token:
663 658 return False
664 659
665 660 crypto_backend = auth.crypto_backend()
666 661
667 662 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
668 663 tokens_q = UserApiKeys.query()\
669 664 .filter(UserApiKeys.user_id == self.user_id)\
670 665 .filter(or_(UserApiKeys.expires == -1,
671 666 UserApiKeys.expires >= time.time()))
672 667
673 668 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
674 669
675 670 plain_tokens = []
676 671 hash_tokens = []
677 672
678 673 for token in tokens_q.all():
679 674 # verify scope first
680 675 if token.repo_id:
681 676 # token has a scope, we need to verify it
682 677 if scope_repo_id != token.repo_id:
683 678 log.debug(
684 679 'Scope mismatch: token has a set repo scope: %s, '
685 680 'and calling scope is:%s, skipping further checks',
686 681 token.repo, scope_repo_id)
687 682 # token has a scope, and it doesn't match, skip token
688 683 continue
689 684
690 685 if token.api_key.startswith(crypto_backend.ENC_PREF):
691 686 hash_tokens.append(token.api_key)
692 687 else:
693 688 plain_tokens.append(token.api_key)
694 689
695 690 is_plain_match = auth_token in plain_tokens
696 691 if is_plain_match:
697 692 return True
698 693
699 694 for hashed in hash_tokens:
700 695 # TODO(marcink): this is expensive to calculate, but most secure
701 696 match = crypto_backend.hash_check(auth_token, hashed)
702 697 if match:
703 698 return True
704 699
705 700 return False
706 701
707 702 @property
708 703 def ip_addresses(self):
709 704 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
710 705 return [x.ip_addr for x in ret]
711 706
712 707 @property
713 708 def username_and_name(self):
714 709 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
715 710
716 711 @property
717 712 def username_or_name_or_email(self):
718 713 full_name = self.full_name if self.full_name is not ' ' else None
719 714 return self.username or full_name or self.email
720 715
721 716 @property
722 717 def full_name(self):
723 718 return '%s %s' % (self.first_name, self.last_name)
724 719
725 720 @property
726 721 def full_name_or_username(self):
727 722 return ('%s %s' % (self.first_name, self.last_name)
728 723 if (self.first_name and self.last_name) else self.username)
729 724
730 725 @property
731 726 def full_contact(self):
732 727 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
733 728
734 729 @property
735 730 def short_contact(self):
736 731 return '%s %s' % (self.first_name, self.last_name)
737 732
738 733 @property
739 734 def is_admin(self):
740 735 return self.admin
741 736
742 737 @property
743 738 def AuthUser(self):
744 739 """
745 740 Returns instance of AuthUser for this user
746 741 """
747 742 from rhodecode.lib.auth import AuthUser
748 743 return AuthUser(user_id=self.user_id, username=self.username)
749 744
750 745 @hybrid_property
751 746 def user_data(self):
752 747 if not self._user_data:
753 748 return {}
754 749
755 750 try:
756 751 return json.loads(self._user_data)
757 752 except TypeError:
758 753 return {}
759 754
760 755 @user_data.setter
761 756 def user_data(self, val):
762 757 if not isinstance(val, dict):
763 758 raise Exception('user_data must be dict, got %s' % type(val))
764 759 try:
765 760 self._user_data = json.dumps(val)
766 761 except Exception:
767 762 log.error(traceback.format_exc())
768 763
769 764 @classmethod
770 765 def get_by_username(cls, username, case_insensitive=False,
771 766 cache=False, identity_cache=False):
772 767 session = Session()
773 768
774 769 if case_insensitive:
775 770 q = cls.query().filter(
776 771 func.lower(cls.username) == func.lower(username))
777 772 else:
778 773 q = cls.query().filter(cls.username == username)
779 774
780 775 if cache:
781 776 if identity_cache:
782 777 val = cls.identity_cache(session, 'username', username)
783 778 if val:
784 779 return val
785 780 else:
786 781 cache_key = "get_user_by_name_%s" % _hash_key(username)
787 782 q = q.options(
788 783 FromCache("sql_cache_short", cache_key))
789 784
790 785 return q.scalar()
791 786
792 787 @classmethod
793 788 def get_by_auth_token(cls, auth_token, cache=False):
794 789 q = UserApiKeys.query()\
795 790 .filter(UserApiKeys.api_key == auth_token)\
796 791 .filter(or_(UserApiKeys.expires == -1,
797 792 UserApiKeys.expires >= time.time()))
798 793 if cache:
799 794 q = q.options(
800 795 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
801 796
802 797 match = q.first()
803 798 if match:
804 799 return match.user
805 800
806 801 @classmethod
807 802 def get_by_email(cls, email, case_insensitive=False, cache=False):
808 803
809 804 if case_insensitive:
810 805 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
811 806
812 807 else:
813 808 q = cls.query().filter(cls.email == email)
814 809
815 810 email_key = _hash_key(email)
816 811 if cache:
817 812 q = q.options(
818 813 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
819 814
820 815 ret = q.scalar()
821 816 if ret is None:
822 817 q = UserEmailMap.query()
823 818 # try fetching in alternate email map
824 819 if case_insensitive:
825 820 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
826 821 else:
827 822 q = q.filter(UserEmailMap.email == email)
828 823 q = q.options(joinedload(UserEmailMap.user))
829 824 if cache:
830 825 q = q.options(
831 826 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
832 827 ret = getattr(q.scalar(), 'user', None)
833 828
834 829 return ret
835 830
836 831 @classmethod
837 832 def get_from_cs_author(cls, author):
838 833 """
839 834 Tries to get User objects out of commit author string
840 835
841 836 :param author:
842 837 """
843 838 from rhodecode.lib.helpers import email, author_name
844 839 # Valid email in the attribute passed, see if they're in the system
845 840 _email = email(author)
846 841 if _email:
847 842 user = cls.get_by_email(_email, case_insensitive=True)
848 843 if user:
849 844 return user
850 845 # Maybe we can match by username?
851 846 _author = author_name(author)
852 847 user = cls.get_by_username(_author, case_insensitive=True)
853 848 if user:
854 849 return user
855 850
856 851 def update_userdata(self, **kwargs):
857 852 usr = self
858 853 old = usr.user_data
859 854 old.update(**kwargs)
860 855 usr.user_data = old
861 856 Session().add(usr)
862 857 log.debug('updated userdata with ', kwargs)
863 858
864 859 def update_lastlogin(self):
865 860 """Update user lastlogin"""
866 861 self.last_login = datetime.datetime.now()
867 862 Session().add(self)
868 863 log.debug('updated user %s lastlogin', self.username)
869 864
870 865 def update_lastactivity(self):
871 866 """Update user lastactivity"""
872 867 self.last_activity = datetime.datetime.now()
873 868 Session().add(self)
874 869 log.debug('updated user %s lastactivity', self.username)
875 870
876 871 def update_password(self, new_password):
877 872 from rhodecode.lib.auth import get_crypt_password
878 873
879 874 self.password = get_crypt_password(new_password)
880 875 Session().add(self)
881 876
882 877 @classmethod
883 878 def get_first_super_admin(cls):
884 879 user = User.query().filter(User.admin == true()).first()
885 880 if user is None:
886 881 raise Exception('FATAL: Missing administrative account!')
887 882 return user
888 883
889 884 @classmethod
890 885 def get_all_super_admins(cls):
891 886 """
892 887 Returns all admin accounts sorted by username
893 888 """
894 889 return User.query().filter(User.admin == true())\
895 890 .order_by(User.username.asc()).all()
896 891
897 892 @classmethod
898 893 def get_default_user(cls, cache=False, refresh=False):
899 894 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
900 895 if user is None:
901 896 raise Exception('FATAL: Missing default account!')
902 897 if refresh:
903 898 # The default user might be based on outdated state which
904 899 # has been loaded from the cache.
905 900 # A call to refresh() ensures that the
906 901 # latest state from the database is used.
907 902 Session().refresh(user)
908 903 return user
909 904
910 905 def _get_default_perms(self, user, suffix=''):
911 906 from rhodecode.model.permission import PermissionModel
912 907 return PermissionModel().get_default_perms(user.user_perms, suffix)
913 908
914 909 def get_default_perms(self, suffix=''):
915 910 return self._get_default_perms(self, suffix)
916 911
917 912 def get_api_data(self, include_secrets=False, details='full'):
918 913 """
919 914 Common function for generating user related data for API
920 915
921 916 :param include_secrets: By default secrets in the API data will be replaced
922 917 by a placeholder value to prevent exposing this data by accident. In case
923 918 this data shall be exposed, set this flag to ``True``.
924 919
925 920 :param details: details can be 'basic|full' basic gives only a subset of
926 921 the available user information that includes user_id, name and emails.
927 922 """
928 923 user = self
929 924 user_data = self.user_data
930 925 data = {
931 926 'user_id': user.user_id,
932 927 'username': user.username,
933 928 'firstname': user.name,
934 929 'lastname': user.lastname,
935 930 'email': user.email,
936 931 'emails': user.emails,
937 932 }
938 933 if details == 'basic':
939 934 return data
940 935
941 936 auth_token_length = 40
942 937 auth_token_replacement = '*' * auth_token_length
943 938
944 939 extras = {
945 940 'auth_tokens': [auth_token_replacement],
946 941 'active': user.active,
947 942 'admin': user.admin,
948 943 'extern_type': user.extern_type,
949 944 'extern_name': user.extern_name,
950 945 'last_login': user.last_login,
951 946 'last_activity': user.last_activity,
952 947 'ip_addresses': user.ip_addresses,
953 948 'language': user_data.get('language')
954 949 }
955 950 data.update(extras)
956 951
957 952 if include_secrets:
958 953 data['auth_tokens'] = user.auth_tokens
959 954 return data
960 955
961 956 def __json__(self):
962 957 data = {
963 958 'full_name': self.full_name,
964 959 'full_name_or_username': self.full_name_or_username,
965 960 'short_contact': self.short_contact,
966 961 'full_contact': self.full_contact,
967 962 }
968 963 data.update(self.get_api_data())
969 964 return data
970 965
971 966
972 967 class UserApiKeys(Base, BaseModel):
973 968 __tablename__ = 'user_api_keys'
974 969 __table_args__ = (
975 970 Index('uak_api_key_idx', 'api_key'),
976 971 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
977 972 UniqueConstraint('api_key'),
978 973 {'extend_existing': True, 'mysql_engine': 'InnoDB',
979 974 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
980 975 )
981 976 __mapper_args__ = {}
982 977
983 978 # ApiKey role
984 979 ROLE_ALL = 'token_role_all'
985 980 ROLE_HTTP = 'token_role_http'
986 981 ROLE_VCS = 'token_role_vcs'
987 982 ROLE_API = 'token_role_api'
988 983 ROLE_FEED = 'token_role_feed'
989 984 ROLE_PASSWORD_RESET = 'token_password_reset'
990 985
991 986 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
992 987
993 988 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
994 989 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
995 990 api_key = Column("api_key", String(255), nullable=False, unique=True)
996 991 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
997 992 expires = Column('expires', Float(53), nullable=False)
998 993 role = Column('role', String(255), nullable=True)
999 994 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1000 995
1001 996 # scope columns
1002 997 repo_id = Column(
1003 998 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1004 999 nullable=True, unique=None, default=None)
1005 1000 repo = relationship('Repository', lazy='joined')
1006 1001
1007 1002 repo_group_id = Column(
1008 1003 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1009 1004 nullable=True, unique=None, default=None)
1010 1005 repo_group = relationship('RepoGroup', lazy='joined')
1011 1006
1012 1007 user = relationship('User', lazy='joined')
1013 1008
1014 1009 def __unicode__(self):
1015 1010 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1016 1011
1017 1012 def __json__(self):
1018 1013 data = {
1019 1014 'auth_token': self.api_key,
1020 1015 'role': self.role,
1021 1016 'scope': self.scope_humanized,
1022 1017 'expired': self.expired
1023 1018 }
1024 1019 return data
1025 1020
1026 1021 def get_api_data(self, include_secrets=False):
1027 1022 data = self.__json__()
1028 1023 if include_secrets:
1029 1024 return data
1030 1025 else:
1031 1026 data['auth_token'] = self.token_obfuscated
1032 1027 return data
1033 1028
1034 1029 @hybrid_property
1035 1030 def description_safe(self):
1036 1031 from rhodecode.lib import helpers as h
1037 1032 return h.escape(self.description)
1038 1033
1039 1034 @property
1040 1035 def expired(self):
1041 1036 if self.expires == -1:
1042 1037 return False
1043 1038 return time.time() > self.expires
1044 1039
1045 1040 @classmethod
1046 1041 def _get_role_name(cls, role):
1047 1042 return {
1048 1043 cls.ROLE_ALL: _('all'),
1049 1044 cls.ROLE_HTTP: _('http/web interface'),
1050 1045 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1051 1046 cls.ROLE_API: _('api calls'),
1052 1047 cls.ROLE_FEED: _('feed access'),
1053 1048 }.get(role, role)
1054 1049
1055 1050 @property
1056 1051 def role_humanized(self):
1057 1052 return self._get_role_name(self.role)
1058 1053
1059 1054 def _get_scope(self):
1060 1055 if self.repo:
1061 1056 return repr(self.repo)
1062 1057 if self.repo_group:
1063 1058 return repr(self.repo_group) + ' (recursive)'
1064 1059 return 'global'
1065 1060
1066 1061 @property
1067 1062 def scope_humanized(self):
1068 1063 return self._get_scope()
1069 1064
1070 1065 @property
1071 1066 def token_obfuscated(self):
1072 1067 if self.api_key:
1073 1068 return self.api_key[:4] + "****"
1074 1069
1075 1070
1076 1071 class UserEmailMap(Base, BaseModel):
1077 1072 __tablename__ = 'user_email_map'
1078 1073 __table_args__ = (
1079 1074 Index('uem_email_idx', 'email'),
1080 1075 UniqueConstraint('email'),
1081 1076 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1082 1077 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1083 1078 )
1084 1079 __mapper_args__ = {}
1085 1080
1086 1081 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1087 1082 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1088 1083 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1089 1084 user = relationship('User', lazy='joined')
1090 1085
1091 1086 @validates('_email')
1092 1087 def validate_email(self, key, email):
1093 1088 # check if this email is not main one
1094 1089 main_email = Session().query(User).filter(User.email == email).scalar()
1095 1090 if main_email is not None:
1096 1091 raise AttributeError('email %s is present is user table' % email)
1097 1092 return email
1098 1093
1099 1094 @hybrid_property
1100 1095 def email(self):
1101 1096 return self._email
1102 1097
1103 1098 @email.setter
1104 1099 def email(self, val):
1105 1100 self._email = val.lower() if val else None
1106 1101
1107 1102
1108 1103 class UserIpMap(Base, BaseModel):
1109 1104 __tablename__ = 'user_ip_map'
1110 1105 __table_args__ = (
1111 1106 UniqueConstraint('user_id', 'ip_addr'),
1112 1107 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1113 1108 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1114 1109 )
1115 1110 __mapper_args__ = {}
1116 1111
1117 1112 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1118 1113 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1119 1114 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1120 1115 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1121 1116 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1122 1117 user = relationship('User', lazy='joined')
1123 1118
1124 1119 @hybrid_property
1125 1120 def description_safe(self):
1126 1121 from rhodecode.lib import helpers as h
1127 1122 return h.escape(self.description)
1128 1123
1129 1124 @classmethod
1130 1125 def _get_ip_range(cls, ip_addr):
1131 1126 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1132 1127 return [str(net.network_address), str(net.broadcast_address)]
1133 1128
1134 1129 def __json__(self):
1135 1130 return {
1136 1131 'ip_addr': self.ip_addr,
1137 1132 'ip_range': self._get_ip_range(self.ip_addr),
1138 1133 }
1139 1134
1140 1135 def __unicode__(self):
1141 1136 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1142 1137 self.user_id, self.ip_addr)
1143 1138
1144 1139
1145 1140 class UserLog(Base, BaseModel):
1146 1141 __tablename__ = 'user_logs'
1147 1142 __table_args__ = (
1148 1143 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1149 1144 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1150 1145 )
1151 1146 VERSION_1 = 'v1'
1152 1147 VERSION_2 = 'v2'
1153 1148 VERSIONS = [VERSION_1, VERSION_2]
1154 1149
1155 1150 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1156 1151 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1157 1152 username = Column("username", String(255), nullable=True, unique=None, default=None)
1158 1153 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1159 1154 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1160 1155 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1161 1156 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1162 1157 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1163 1158
1164 1159 version = Column("version", String(255), nullable=True, default=VERSION_1)
1165 1160 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1166 1161 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1167 1162
1168 1163 def __unicode__(self):
1169 1164 return u"<%s('id:%s:%s')>" % (
1170 1165 self.__class__.__name__, self.repository_name, self.action)
1171 1166
1172 1167 def __json__(self):
1173 1168 return {
1174 1169 'user_id': self.user_id,
1175 1170 'username': self.username,
1176 1171 'repository_id': self.repository_id,
1177 1172 'repository_name': self.repository_name,
1178 1173 'user_ip': self.user_ip,
1179 1174 'action_date': self.action_date,
1180 1175 'action': self.action,
1181 1176 }
1182 1177
1183 1178 @property
1184 1179 def action_as_day(self):
1185 1180 return datetime.date(*self.action_date.timetuple()[:3])
1186 1181
1187 1182 user = relationship('User')
1188 1183 repository = relationship('Repository', cascade='')
1189 1184
1190 1185
1191 1186 class UserGroup(Base, BaseModel):
1192 1187 __tablename__ = 'users_groups'
1193 1188 __table_args__ = (
1194 1189 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1195 1190 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1196 1191 )
1197 1192
1198 1193 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1199 1194 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1200 1195 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1201 1196 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1202 1197 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1203 1198 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1204 1199 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1205 1200 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1206 1201
1207 1202 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1208 1203 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1209 1204 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1210 1205 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1211 1206 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1212 1207 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1213 1208
1214 1209 user = relationship('User')
1215 1210
1216 1211 @hybrid_property
1217 1212 def description_safe(self):
1218 1213 from rhodecode.lib import helpers as h
1219 1214 return h.escape(self.description)
1220 1215
1221 1216 @hybrid_property
1222 1217 def group_data(self):
1223 1218 if not self._group_data:
1224 1219 return {}
1225 1220
1226 1221 try:
1227 1222 return json.loads(self._group_data)
1228 1223 except TypeError:
1229 1224 return {}
1230 1225
1231 1226 @group_data.setter
1232 1227 def group_data(self, val):
1233 1228 try:
1234 1229 self._group_data = json.dumps(val)
1235 1230 except Exception:
1236 1231 log.error(traceback.format_exc())
1237 1232
1238 1233 def __unicode__(self):
1239 1234 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1240 1235 self.users_group_id,
1241 1236 self.users_group_name)
1242 1237
1243 1238 @classmethod
1244 1239 def get_by_group_name(cls, group_name, cache=False,
1245 1240 case_insensitive=False):
1246 1241 if case_insensitive:
1247 1242 q = cls.query().filter(func.lower(cls.users_group_name) ==
1248 1243 func.lower(group_name))
1249 1244
1250 1245 else:
1251 1246 q = cls.query().filter(cls.users_group_name == group_name)
1252 1247 if cache:
1253 1248 q = q.options(
1254 1249 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1255 1250 return q.scalar()
1256 1251
1257 1252 @classmethod
1258 1253 def get(cls, user_group_id, cache=False):
1259 1254 user_group = cls.query()
1260 1255 if cache:
1261 1256 user_group = user_group.options(
1262 1257 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1263 1258 return user_group.get(user_group_id)
1264 1259
1265 1260 def permissions(self, with_admins=True, with_owner=True):
1266 1261 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1267 1262 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1268 1263 joinedload(UserUserGroupToPerm.user),
1269 1264 joinedload(UserUserGroupToPerm.permission),)
1270 1265
1271 1266 # get owners and admins and permissions. We do a trick of re-writing
1272 1267 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1273 1268 # has a global reference and changing one object propagates to all
1274 1269 # others. This means if admin is also an owner admin_row that change
1275 1270 # would propagate to both objects
1276 1271 perm_rows = []
1277 1272 for _usr in q.all():
1278 1273 usr = AttributeDict(_usr.user.get_dict())
1279 1274 usr.permission = _usr.permission.permission_name
1280 1275 perm_rows.append(usr)
1281 1276
1282 1277 # filter the perm rows by 'default' first and then sort them by
1283 1278 # admin,write,read,none permissions sorted again alphabetically in
1284 1279 # each group
1285 1280 perm_rows = sorted(perm_rows, key=display_sort)
1286 1281
1287 1282 _admin_perm = 'usergroup.admin'
1288 1283 owner_row = []
1289 1284 if with_owner:
1290 1285 usr = AttributeDict(self.user.get_dict())
1291 1286 usr.owner_row = True
1292 1287 usr.permission = _admin_perm
1293 1288 owner_row.append(usr)
1294 1289
1295 1290 super_admin_rows = []
1296 1291 if with_admins:
1297 1292 for usr in User.get_all_super_admins():
1298 1293 # if this admin is also owner, don't double the record
1299 1294 if usr.user_id == owner_row[0].user_id:
1300 1295 owner_row[0].admin_row = True
1301 1296 else:
1302 1297 usr = AttributeDict(usr.get_dict())
1303 1298 usr.admin_row = True
1304 1299 usr.permission = _admin_perm
1305 1300 super_admin_rows.append(usr)
1306 1301
1307 1302 return super_admin_rows + owner_row + perm_rows
1308 1303
1309 1304 def permission_user_groups(self):
1310 1305 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1311 1306 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1312 1307 joinedload(UserGroupUserGroupToPerm.target_user_group),
1313 1308 joinedload(UserGroupUserGroupToPerm.permission),)
1314 1309
1315 1310 perm_rows = []
1316 1311 for _user_group in q.all():
1317 1312 usr = AttributeDict(_user_group.user_group.get_dict())
1318 1313 usr.permission = _user_group.permission.permission_name
1319 1314 perm_rows.append(usr)
1320 1315
1321 1316 return perm_rows
1322 1317
1323 1318 def _get_default_perms(self, user_group, suffix=''):
1324 1319 from rhodecode.model.permission import PermissionModel
1325 1320 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1326 1321
1327 1322 def get_default_perms(self, suffix=''):
1328 1323 return self._get_default_perms(self, suffix)
1329 1324
1330 1325 def get_api_data(self, with_group_members=True, include_secrets=False):
1331 1326 """
1332 1327 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1333 1328 basically forwarded.
1334 1329
1335 1330 """
1336 1331 user_group = self
1337 1332 data = {
1338 1333 'users_group_id': user_group.users_group_id,
1339 1334 'group_name': user_group.users_group_name,
1340 1335 'group_description': user_group.user_group_description,
1341 1336 'active': user_group.users_group_active,
1342 1337 'owner': user_group.user.username,
1343 1338 'owner_email': user_group.user.email,
1344 1339 }
1345 1340
1346 1341 if with_group_members:
1347 1342 users = []
1348 1343 for user in user_group.members:
1349 1344 user = user.user
1350 1345 users.append(user.get_api_data(include_secrets=include_secrets))
1351 1346 data['users'] = users
1352 1347
1353 1348 return data
1354 1349
1355 1350
1356 1351 class UserGroupMember(Base, BaseModel):
1357 1352 __tablename__ = 'users_groups_members'
1358 1353 __table_args__ = (
1359 1354 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1360 1355 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1361 1356 )
1362 1357
1363 1358 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1364 1359 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1365 1360 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1366 1361
1367 1362 user = relationship('User', lazy='joined')
1368 1363 users_group = relationship('UserGroup')
1369 1364
1370 1365 def __init__(self, gr_id='', u_id=''):
1371 1366 self.users_group_id = gr_id
1372 1367 self.user_id = u_id
1373 1368
1374 1369
1375 1370 class RepositoryField(Base, BaseModel):
1376 1371 __tablename__ = 'repositories_fields'
1377 1372 __table_args__ = (
1378 1373 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1379 1374 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1380 1375 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1381 1376 )
1382 1377 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1383 1378
1384 1379 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1385 1380 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1386 1381 field_key = Column("field_key", String(250))
1387 1382 field_label = Column("field_label", String(1024), nullable=False)
1388 1383 field_value = Column("field_value", String(10000), nullable=False)
1389 1384 field_desc = Column("field_desc", String(1024), nullable=False)
1390 1385 field_type = Column("field_type", String(255), nullable=False, unique=None)
1391 1386 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1392 1387
1393 1388 repository = relationship('Repository')
1394 1389
1395 1390 @property
1396 1391 def field_key_prefixed(self):
1397 1392 return 'ex_%s' % self.field_key
1398 1393
1399 1394 @classmethod
1400 1395 def un_prefix_key(cls, key):
1401 1396 if key.startswith(cls.PREFIX):
1402 1397 return key[len(cls.PREFIX):]
1403 1398 return key
1404 1399
1405 1400 @classmethod
1406 1401 def get_by_key_name(cls, key, repo):
1407 1402 row = cls.query()\
1408 1403 .filter(cls.repository == repo)\
1409 1404 .filter(cls.field_key == key).scalar()
1410 1405 return row
1411 1406
1412 1407
1413 1408 class Repository(Base, BaseModel):
1414 1409 __tablename__ = 'repositories'
1415 1410 __table_args__ = (
1416 1411 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1417 1412 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1418 1413 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1419 1414 )
1420 1415 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1421 1416 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1422 1417
1423 1418 STATE_CREATED = 'repo_state_created'
1424 1419 STATE_PENDING = 'repo_state_pending'
1425 1420 STATE_ERROR = 'repo_state_error'
1426 1421
1427 1422 LOCK_AUTOMATIC = 'lock_auto'
1428 1423 LOCK_API = 'lock_api'
1429 1424 LOCK_WEB = 'lock_web'
1430 1425 LOCK_PULL = 'lock_pull'
1431 1426
1432 1427 NAME_SEP = URL_SEP
1433 1428
1434 1429 repo_id = Column(
1435 1430 "repo_id", Integer(), nullable=False, unique=True, default=None,
1436 1431 primary_key=True)
1437 1432 _repo_name = Column(
1438 1433 "repo_name", Text(), nullable=False, default=None)
1439 1434 _repo_name_hash = Column(
1440 1435 "repo_name_hash", String(255), nullable=False, unique=True)
1441 1436 repo_state = Column("repo_state", String(255), nullable=True)
1442 1437
1443 1438 clone_uri = Column(
1444 1439 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1445 1440 default=None)
1446 1441 repo_type = Column(
1447 1442 "repo_type", String(255), nullable=False, unique=False, default=None)
1448 1443 user_id = Column(
1449 1444 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1450 1445 unique=False, default=None)
1451 1446 private = Column(
1452 1447 "private", Boolean(), nullable=True, unique=None, default=None)
1453 1448 enable_statistics = Column(
1454 1449 "statistics", Boolean(), nullable=True, unique=None, default=True)
1455 1450 enable_downloads = Column(
1456 1451 "downloads", Boolean(), nullable=True, unique=None, default=True)
1457 1452 description = Column(
1458 1453 "description", String(10000), nullable=True, unique=None, default=None)
1459 1454 created_on = Column(
1460 1455 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1461 1456 default=datetime.datetime.now)
1462 1457 updated_on = Column(
1463 1458 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1464 1459 default=datetime.datetime.now)
1465 1460 _landing_revision = Column(
1466 1461 "landing_revision", String(255), nullable=False, unique=False,
1467 1462 default=None)
1468 1463 enable_locking = Column(
1469 1464 "enable_locking", Boolean(), nullable=False, unique=None,
1470 1465 default=False)
1471 1466 _locked = Column(
1472 1467 "locked", String(255), nullable=True, unique=False, default=None)
1473 1468 _changeset_cache = Column(
1474 1469 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1475 1470
1476 1471 fork_id = Column(
1477 1472 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1478 1473 nullable=True, unique=False, default=None)
1479 1474 group_id = Column(
1480 1475 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1481 1476 unique=False, default=None)
1482 1477
1483 1478 user = relationship('User', lazy='joined')
1484 1479 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1485 1480 group = relationship('RepoGroup', lazy='joined')
1486 1481 repo_to_perm = relationship(
1487 1482 'UserRepoToPerm', cascade='all',
1488 1483 order_by='UserRepoToPerm.repo_to_perm_id')
1489 1484 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1490 1485 stats = relationship('Statistics', cascade='all', uselist=False)
1491 1486
1492 1487 followers = relationship(
1493 1488 'UserFollowing',
1494 1489 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1495 1490 cascade='all')
1496 1491 extra_fields = relationship(
1497 1492 'RepositoryField', cascade="all, delete, delete-orphan")
1498 1493 logs = relationship('UserLog')
1499 1494 comments = relationship(
1500 1495 'ChangesetComment', cascade="all, delete, delete-orphan")
1501 1496 pull_requests_source = relationship(
1502 1497 'PullRequest',
1503 1498 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1504 1499 cascade="all, delete, delete-orphan")
1505 1500 pull_requests_target = relationship(
1506 1501 'PullRequest',
1507 1502 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1508 1503 cascade="all, delete, delete-orphan")
1509 1504 ui = relationship('RepoRhodeCodeUi', cascade="all")
1510 1505 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1511 1506 integrations = relationship('Integration',
1512 1507 cascade="all, delete, delete-orphan")
1513 1508
1514 1509 def __unicode__(self):
1515 1510 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1516 1511 safe_unicode(self.repo_name))
1517 1512
1518 1513 @hybrid_property
1519 1514 def description_safe(self):
1520 1515 from rhodecode.lib import helpers as h
1521 1516 return h.escape(self.description)
1522 1517
1523 1518 @hybrid_property
1524 1519 def landing_rev(self):
1525 1520 # always should return [rev_type, rev]
1526 1521 if self._landing_revision:
1527 1522 _rev_info = self._landing_revision.split(':')
1528 1523 if len(_rev_info) < 2:
1529 1524 _rev_info.insert(0, 'rev')
1530 1525 return [_rev_info[0], _rev_info[1]]
1531 1526 return [None, None]
1532 1527
1533 1528 @landing_rev.setter
1534 1529 def landing_rev(self, val):
1535 1530 if ':' not in val:
1536 1531 raise ValueError('value must be delimited with `:` and consist '
1537 1532 'of <rev_type>:<rev>, got %s instead' % val)
1538 1533 self._landing_revision = val
1539 1534
1540 1535 @hybrid_property
1541 1536 def locked(self):
1542 1537 if self._locked:
1543 1538 user_id, timelocked, reason = self._locked.split(':')
1544 1539 lock_values = int(user_id), timelocked, reason
1545 1540 else:
1546 1541 lock_values = [None, None, None]
1547 1542 return lock_values
1548 1543
1549 1544 @locked.setter
1550 1545 def locked(self, val):
1551 1546 if val and isinstance(val, (list, tuple)):
1552 1547 self._locked = ':'.join(map(str, val))
1553 1548 else:
1554 1549 self._locked = None
1555 1550
1556 1551 @hybrid_property
1557 1552 def changeset_cache(self):
1558 1553 from rhodecode.lib.vcs.backends.base import EmptyCommit
1559 1554 dummy = EmptyCommit().__json__()
1560 1555 if not self._changeset_cache:
1561 1556 return dummy
1562 1557 try:
1563 1558 return json.loads(self._changeset_cache)
1564 1559 except TypeError:
1565 1560 return dummy
1566 1561 except Exception:
1567 1562 log.error(traceback.format_exc())
1568 1563 return dummy
1569 1564
1570 1565 @changeset_cache.setter
1571 1566 def changeset_cache(self, val):
1572 1567 try:
1573 1568 self._changeset_cache = json.dumps(val)
1574 1569 except Exception:
1575 1570 log.error(traceback.format_exc())
1576 1571
1577 1572 @hybrid_property
1578 1573 def repo_name(self):
1579 1574 return self._repo_name
1580 1575
1581 1576 @repo_name.setter
1582 1577 def repo_name(self, value):
1583 1578 self._repo_name = value
1584 1579 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1585 1580
1586 1581 @classmethod
1587 1582 def normalize_repo_name(cls, repo_name):
1588 1583 """
1589 1584 Normalizes os specific repo_name to the format internally stored inside
1590 1585 database using URL_SEP
1591 1586
1592 1587 :param cls:
1593 1588 :param repo_name:
1594 1589 """
1595 1590 return cls.NAME_SEP.join(repo_name.split(os.sep))
1596 1591
1597 1592 @classmethod
1598 1593 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1599 1594 session = Session()
1600 1595 q = session.query(cls).filter(cls.repo_name == repo_name)
1601 1596
1602 1597 if cache:
1603 1598 if identity_cache:
1604 1599 val = cls.identity_cache(session, 'repo_name', repo_name)
1605 1600 if val:
1606 1601 return val
1607 1602 else:
1608 1603 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1609 1604 q = q.options(
1610 1605 FromCache("sql_cache_short", cache_key))
1611 1606
1612 1607 return q.scalar()
1613 1608
1614 1609 @classmethod
1615 1610 def get_by_full_path(cls, repo_full_path):
1616 1611 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1617 1612 repo_name = cls.normalize_repo_name(repo_name)
1618 1613 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1619 1614
1620 1615 @classmethod
1621 1616 def get_repo_forks(cls, repo_id):
1622 1617 return cls.query().filter(Repository.fork_id == repo_id)
1623 1618
1624 1619 @classmethod
1625 1620 def base_path(cls):
1626 1621 """
1627 1622 Returns base path when all repos are stored
1628 1623
1629 1624 :param cls:
1630 1625 """
1631 1626 q = Session().query(RhodeCodeUi)\
1632 1627 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1633 1628 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1634 1629 return q.one().ui_value
1635 1630
1636 1631 @classmethod
1637 1632 def is_valid(cls, repo_name):
1638 1633 """
1639 1634 returns True if given repo name is a valid filesystem repository
1640 1635
1641 1636 :param cls:
1642 1637 :param repo_name:
1643 1638 """
1644 1639 from rhodecode.lib.utils import is_valid_repo
1645 1640
1646 1641 return is_valid_repo(repo_name, cls.base_path())
1647 1642
1648 1643 @classmethod
1649 1644 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1650 1645 case_insensitive=True):
1651 1646 q = Repository.query()
1652 1647
1653 1648 if not isinstance(user_id, Optional):
1654 1649 q = q.filter(Repository.user_id == user_id)
1655 1650
1656 1651 if not isinstance(group_id, Optional):
1657 1652 q = q.filter(Repository.group_id == group_id)
1658 1653
1659 1654 if case_insensitive:
1660 1655 q = q.order_by(func.lower(Repository.repo_name))
1661 1656 else:
1662 1657 q = q.order_by(Repository.repo_name)
1663 1658 return q.all()
1664 1659
1665 1660 @property
1666 1661 def forks(self):
1667 1662 """
1668 1663 Return forks of this repo
1669 1664 """
1670 1665 return Repository.get_repo_forks(self.repo_id)
1671 1666
1672 1667 @property
1673 1668 def parent(self):
1674 1669 """
1675 1670 Returns fork parent
1676 1671 """
1677 1672 return self.fork
1678 1673
1679 1674 @property
1680 1675 def just_name(self):
1681 1676 return self.repo_name.split(self.NAME_SEP)[-1]
1682 1677
1683 1678 @property
1684 1679 def groups_with_parents(self):
1685 1680 groups = []
1686 1681 if self.group is None:
1687 1682 return groups
1688 1683
1689 1684 cur_gr = self.group
1690 1685 groups.insert(0, cur_gr)
1691 1686 while 1:
1692 1687 gr = getattr(cur_gr, 'parent_group', None)
1693 1688 cur_gr = cur_gr.parent_group
1694 1689 if gr is None:
1695 1690 break
1696 1691 groups.insert(0, gr)
1697 1692
1698 1693 return groups
1699 1694
1700 1695 @property
1701 1696 def groups_and_repo(self):
1702 1697 return self.groups_with_parents, self
1703 1698
1704 1699 @LazyProperty
1705 1700 def repo_path(self):
1706 1701 """
1707 1702 Returns base full path for that repository means where it actually
1708 1703 exists on a filesystem
1709 1704 """
1710 1705 q = Session().query(RhodeCodeUi).filter(
1711 1706 RhodeCodeUi.ui_key == self.NAME_SEP)
1712 1707 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1713 1708 return q.one().ui_value
1714 1709
1715 1710 @property
1716 1711 def repo_full_path(self):
1717 1712 p = [self.repo_path]
1718 1713 # we need to split the name by / since this is how we store the
1719 1714 # names in the database, but that eventually needs to be converted
1720 1715 # into a valid system path
1721 1716 p += self.repo_name.split(self.NAME_SEP)
1722 1717 return os.path.join(*map(safe_unicode, p))
1723 1718
1724 1719 @property
1725 1720 def cache_keys(self):
1726 1721 """
1727 1722 Returns associated cache keys for that repo
1728 1723 """
1729 1724 return CacheKey.query()\
1730 1725 .filter(CacheKey.cache_args == self.repo_name)\
1731 1726 .order_by(CacheKey.cache_key)\
1732 1727 .all()
1733 1728
1734 1729 def get_new_name(self, repo_name):
1735 1730 """
1736 1731 returns new full repository name based on assigned group and new new
1737 1732
1738 1733 :param group_name:
1739 1734 """
1740 1735 path_prefix = self.group.full_path_splitted if self.group else []
1741 1736 return self.NAME_SEP.join(path_prefix + [repo_name])
1742 1737
1743 1738 @property
1744 1739 def _config(self):
1745 1740 """
1746 1741 Returns db based config object.
1747 1742 """
1748 1743 from rhodecode.lib.utils import make_db_config
1749 1744 return make_db_config(clear_session=False, repo=self)
1750 1745
1751 1746 def permissions(self, with_admins=True, with_owner=True):
1752 1747 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1753 1748 q = q.options(joinedload(UserRepoToPerm.repository),
1754 1749 joinedload(UserRepoToPerm.user),
1755 1750 joinedload(UserRepoToPerm.permission),)
1756 1751
1757 1752 # get owners and admins and permissions. We do a trick of re-writing
1758 1753 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1759 1754 # has a global reference and changing one object propagates to all
1760 1755 # others. This means if admin is also an owner admin_row that change
1761 1756 # would propagate to both objects
1762 1757 perm_rows = []
1763 1758 for _usr in q.all():
1764 1759 usr = AttributeDict(_usr.user.get_dict())
1765 1760 usr.permission = _usr.permission.permission_name
1766 1761 perm_rows.append(usr)
1767 1762
1768 1763 # filter the perm rows by 'default' first and then sort them by
1769 1764 # admin,write,read,none permissions sorted again alphabetically in
1770 1765 # each group
1771 1766 perm_rows = sorted(perm_rows, key=display_sort)
1772 1767
1773 1768 _admin_perm = 'repository.admin'
1774 1769 owner_row = []
1775 1770 if with_owner:
1776 1771 usr = AttributeDict(self.user.get_dict())
1777 1772 usr.owner_row = True
1778 1773 usr.permission = _admin_perm
1779 1774 owner_row.append(usr)
1780 1775
1781 1776 super_admin_rows = []
1782 1777 if with_admins:
1783 1778 for usr in User.get_all_super_admins():
1784 1779 # if this admin is also owner, don't double the record
1785 1780 if usr.user_id == owner_row[0].user_id:
1786 1781 owner_row[0].admin_row = True
1787 1782 else:
1788 1783 usr = AttributeDict(usr.get_dict())
1789 1784 usr.admin_row = True
1790 1785 usr.permission = _admin_perm
1791 1786 super_admin_rows.append(usr)
1792 1787
1793 1788 return super_admin_rows + owner_row + perm_rows
1794 1789
1795 1790 def permission_user_groups(self):
1796 1791 q = UserGroupRepoToPerm.query().filter(
1797 1792 UserGroupRepoToPerm.repository == self)
1798 1793 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1799 1794 joinedload(UserGroupRepoToPerm.users_group),
1800 1795 joinedload(UserGroupRepoToPerm.permission),)
1801 1796
1802 1797 perm_rows = []
1803 1798 for _user_group in q.all():
1804 1799 usr = AttributeDict(_user_group.users_group.get_dict())
1805 1800 usr.permission = _user_group.permission.permission_name
1806 1801 perm_rows.append(usr)
1807 1802
1808 1803 return perm_rows
1809 1804
1810 1805 def get_api_data(self, include_secrets=False):
1811 1806 """
1812 1807 Common function for generating repo api data
1813 1808
1814 1809 :param include_secrets: See :meth:`User.get_api_data`.
1815 1810
1816 1811 """
1817 1812 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1818 1813 # move this methods on models level.
1819 1814 from rhodecode.model.settings import SettingsModel
1820 1815 from rhodecode.model.repo import RepoModel
1821 1816
1822 1817 repo = self
1823 1818 _user_id, _time, _reason = self.locked
1824 1819
1825 1820 data = {
1826 1821 'repo_id': repo.repo_id,
1827 1822 'repo_name': repo.repo_name,
1828 1823 'repo_type': repo.repo_type,
1829 1824 'clone_uri': repo.clone_uri or '',
1830 1825 'url': RepoModel().get_url(self),
1831 1826 'private': repo.private,
1832 1827 'created_on': repo.created_on,
1833 1828 'description': repo.description_safe,
1834 1829 'landing_rev': repo.landing_rev,
1835 1830 'owner': repo.user.username,
1836 1831 'fork_of': repo.fork.repo_name if repo.fork else None,
1837 1832 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1838 1833 'enable_statistics': repo.enable_statistics,
1839 1834 'enable_locking': repo.enable_locking,
1840 1835 'enable_downloads': repo.enable_downloads,
1841 1836 'last_changeset': repo.changeset_cache,
1842 1837 'locked_by': User.get(_user_id).get_api_data(
1843 1838 include_secrets=include_secrets) if _user_id else None,
1844 1839 'locked_date': time_to_datetime(_time) if _time else None,
1845 1840 'lock_reason': _reason if _reason else None,
1846 1841 }
1847 1842
1848 1843 # TODO: mikhail: should be per-repo settings here
1849 1844 rc_config = SettingsModel().get_all_settings()
1850 1845 repository_fields = str2bool(
1851 1846 rc_config.get('rhodecode_repository_fields'))
1852 1847 if repository_fields:
1853 1848 for f in self.extra_fields:
1854 1849 data[f.field_key_prefixed] = f.field_value
1855 1850
1856 1851 return data
1857 1852
1858 1853 @classmethod
1859 1854 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1860 1855 if not lock_time:
1861 1856 lock_time = time.time()
1862 1857 if not lock_reason:
1863 1858 lock_reason = cls.LOCK_AUTOMATIC
1864 1859 repo.locked = [user_id, lock_time, lock_reason]
1865 1860 Session().add(repo)
1866 1861 Session().commit()
1867 1862
1868 1863 @classmethod
1869 1864 def unlock(cls, repo):
1870 1865 repo.locked = None
1871 1866 Session().add(repo)
1872 1867 Session().commit()
1873 1868
1874 1869 @classmethod
1875 1870 def getlock(cls, repo):
1876 1871 return repo.locked
1877 1872
1878 1873 def is_user_lock(self, user_id):
1879 1874 if self.lock[0]:
1880 1875 lock_user_id = safe_int(self.lock[0])
1881 1876 user_id = safe_int(user_id)
1882 1877 # both are ints, and they are equal
1883 1878 return all([lock_user_id, user_id]) and lock_user_id == user_id
1884 1879
1885 1880 return False
1886 1881
1887 1882 def get_locking_state(self, action, user_id, only_when_enabled=True):
1888 1883 """
1889 1884 Checks locking on this repository, if locking is enabled and lock is
1890 1885 present returns a tuple of make_lock, locked, locked_by.
1891 1886 make_lock can have 3 states None (do nothing) True, make lock
1892 1887 False release lock, This value is later propagated to hooks, which
1893 1888 do the locking. Think about this as signals passed to hooks what to do.
1894 1889
1895 1890 """
1896 1891 # TODO: johbo: This is part of the business logic and should be moved
1897 1892 # into the RepositoryModel.
1898 1893
1899 1894 if action not in ('push', 'pull'):
1900 1895 raise ValueError("Invalid action value: %s" % repr(action))
1901 1896
1902 1897 # defines if locked error should be thrown to user
1903 1898 currently_locked = False
1904 1899 # defines if new lock should be made, tri-state
1905 1900 make_lock = None
1906 1901 repo = self
1907 1902 user = User.get(user_id)
1908 1903
1909 1904 lock_info = repo.locked
1910 1905
1911 1906 if repo and (repo.enable_locking or not only_when_enabled):
1912 1907 if action == 'push':
1913 1908 # check if it's already locked !, if it is compare users
1914 1909 locked_by_user_id = lock_info[0]
1915 1910 if user.user_id == locked_by_user_id:
1916 1911 log.debug(
1917 1912 'Got `push` action from user %s, now unlocking', user)
1918 1913 # unlock if we have push from user who locked
1919 1914 make_lock = False
1920 1915 else:
1921 1916 # we're not the same user who locked, ban with
1922 1917 # code defined in settings (default is 423 HTTP Locked) !
1923 1918 log.debug('Repo %s is currently locked by %s', repo, user)
1924 1919 currently_locked = True
1925 1920 elif action == 'pull':
1926 1921 # [0] user [1] date
1927 1922 if lock_info[0] and lock_info[1]:
1928 1923 log.debug('Repo %s is currently locked by %s', repo, user)
1929 1924 currently_locked = True
1930 1925 else:
1931 1926 log.debug('Setting lock on repo %s by %s', repo, user)
1932 1927 make_lock = True
1933 1928
1934 1929 else:
1935 1930 log.debug('Repository %s do not have locking enabled', repo)
1936 1931
1937 1932 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1938 1933 make_lock, currently_locked, lock_info)
1939 1934
1940 1935 from rhodecode.lib.auth import HasRepoPermissionAny
1941 1936 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1942 1937 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1943 1938 # if we don't have at least write permission we cannot make a lock
1944 1939 log.debug('lock state reset back to FALSE due to lack '
1945 1940 'of at least read permission')
1946 1941 make_lock = False
1947 1942
1948 1943 return make_lock, currently_locked, lock_info
1949 1944
1950 1945 @property
1951 1946 def last_db_change(self):
1952 1947 return self.updated_on
1953 1948
1954 1949 @property
1955 1950 def clone_uri_hidden(self):
1956 1951 clone_uri = self.clone_uri
1957 1952 if clone_uri:
1958 1953 import urlobject
1959 1954 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1960 1955 if url_obj.password:
1961 1956 clone_uri = url_obj.with_password('*****')
1962 1957 return clone_uri
1963 1958
1964 1959 def clone_url(self, **override):
1965 1960 from rhodecode.model.settings import SettingsModel
1966 1961
1967 1962 uri_tmpl = None
1968 1963 if 'with_id' in override:
1969 1964 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1970 1965 del override['with_id']
1971 1966
1972 1967 if 'uri_tmpl' in override:
1973 1968 uri_tmpl = override['uri_tmpl']
1974 1969 del override['uri_tmpl']
1975 1970
1976 1971 # we didn't override our tmpl from **overrides
1977 1972 if not uri_tmpl:
1978 1973 rc_config = SettingsModel().get_all_settings(cache=True)
1979 1974 uri_tmpl = rc_config.get(
1980 1975 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
1981 1976
1982 1977 request = get_current_request()
1983 1978 return get_clone_url(request=request,
1984 1979 uri_tmpl=uri_tmpl,
1985 1980 repo_name=self.repo_name,
1986 1981 repo_id=self.repo_id, **override)
1987 1982
1988 1983 def set_state(self, state):
1989 1984 self.repo_state = state
1990 1985 Session().add(self)
1991 1986 #==========================================================================
1992 1987 # SCM PROPERTIES
1993 1988 #==========================================================================
1994 1989
1995 1990 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1996 1991 return get_commit_safe(
1997 1992 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1998 1993
1999 1994 def get_changeset(self, rev=None, pre_load=None):
2000 1995 warnings.warn("Use get_commit", DeprecationWarning)
2001 1996 commit_id = None
2002 1997 commit_idx = None
2003 1998 if isinstance(rev, basestring):
2004 1999 commit_id = rev
2005 2000 else:
2006 2001 commit_idx = rev
2007 2002 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2008 2003 pre_load=pre_load)
2009 2004
2010 2005 def get_landing_commit(self):
2011 2006 """
2012 2007 Returns landing commit, or if that doesn't exist returns the tip
2013 2008 """
2014 2009 _rev_type, _rev = self.landing_rev
2015 2010 commit = self.get_commit(_rev)
2016 2011 if isinstance(commit, EmptyCommit):
2017 2012 return self.get_commit()
2018 2013 return commit
2019 2014
2020 2015 def update_commit_cache(self, cs_cache=None, config=None):
2021 2016 """
2022 2017 Update cache of last changeset for repository, keys should be::
2023 2018
2024 2019 short_id
2025 2020 raw_id
2026 2021 revision
2027 2022 parents
2028 2023 message
2029 2024 date
2030 2025 author
2031 2026
2032 2027 :param cs_cache:
2033 2028 """
2034 2029 from rhodecode.lib.vcs.backends.base import BaseChangeset
2035 2030 if cs_cache is None:
2036 2031 # use no-cache version here
2037 2032 scm_repo = self.scm_instance(cache=False, config=config)
2038 2033 if scm_repo:
2039 2034 cs_cache = scm_repo.get_commit(
2040 2035 pre_load=["author", "date", "message", "parents"])
2041 2036 else:
2042 2037 cs_cache = EmptyCommit()
2043 2038
2044 2039 if isinstance(cs_cache, BaseChangeset):
2045 2040 cs_cache = cs_cache.__json__()
2046 2041
2047 2042 def is_outdated(new_cs_cache):
2048 2043 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2049 2044 new_cs_cache['revision'] != self.changeset_cache['revision']):
2050 2045 return True
2051 2046 return False
2052 2047
2053 2048 # check if we have maybe already latest cached revision
2054 2049 if is_outdated(cs_cache) or not self.changeset_cache:
2055 2050 _default = datetime.datetime.fromtimestamp(0)
2056 2051 last_change = cs_cache.get('date') or _default
2057 2052 log.debug('updated repo %s with new cs cache %s',
2058 2053 self.repo_name, cs_cache)
2059 2054 self.updated_on = last_change
2060 2055 self.changeset_cache = cs_cache
2061 2056 Session().add(self)
2062 2057 Session().commit()
2063 2058 else:
2064 2059 log.debug('Skipping update_commit_cache for repo:`%s` '
2065 2060 'commit already with latest changes', self.repo_name)
2066 2061
2067 2062 @property
2068 2063 def tip(self):
2069 2064 return self.get_commit('tip')
2070 2065
2071 2066 @property
2072 2067 def author(self):
2073 2068 return self.tip.author
2074 2069
2075 2070 @property
2076 2071 def last_change(self):
2077 2072 return self.scm_instance().last_change
2078 2073
2079 2074 def get_comments(self, revisions=None):
2080 2075 """
2081 2076 Returns comments for this repository grouped by revisions
2082 2077
2083 2078 :param revisions: filter query by revisions only
2084 2079 """
2085 2080 cmts = ChangesetComment.query()\
2086 2081 .filter(ChangesetComment.repo == self)
2087 2082 if revisions:
2088 2083 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2089 2084 grouped = collections.defaultdict(list)
2090 2085 for cmt in cmts.all():
2091 2086 grouped[cmt.revision].append(cmt)
2092 2087 return grouped
2093 2088
2094 2089 def statuses(self, revisions=None):
2095 2090 """
2096 2091 Returns statuses for this repository
2097 2092
2098 2093 :param revisions: list of revisions to get statuses for
2099 2094 """
2100 2095 statuses = ChangesetStatus.query()\
2101 2096 .filter(ChangesetStatus.repo == self)\
2102 2097 .filter(ChangesetStatus.version == 0)
2103 2098
2104 2099 if revisions:
2105 2100 # Try doing the filtering in chunks to avoid hitting limits
2106 2101 size = 500
2107 2102 status_results = []
2108 2103 for chunk in xrange(0, len(revisions), size):
2109 2104 status_results += statuses.filter(
2110 2105 ChangesetStatus.revision.in_(
2111 2106 revisions[chunk: chunk+size])
2112 2107 ).all()
2113 2108 else:
2114 2109 status_results = statuses.all()
2115 2110
2116 2111 grouped = {}
2117 2112
2118 2113 # maybe we have open new pullrequest without a status?
2119 2114 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2120 2115 status_lbl = ChangesetStatus.get_status_lbl(stat)
2121 2116 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2122 2117 for rev in pr.revisions:
2123 2118 pr_id = pr.pull_request_id
2124 2119 pr_repo = pr.target_repo.repo_name
2125 2120 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2126 2121
2127 2122 for stat in status_results:
2128 2123 pr_id = pr_repo = None
2129 2124 if stat.pull_request:
2130 2125 pr_id = stat.pull_request.pull_request_id
2131 2126 pr_repo = stat.pull_request.target_repo.repo_name
2132 2127 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2133 2128 pr_id, pr_repo]
2134 2129 return grouped
2135 2130
2136 2131 # ==========================================================================
2137 2132 # SCM CACHE INSTANCE
2138 2133 # ==========================================================================
2139 2134
2140 2135 def scm_instance(self, **kwargs):
2141 2136 import rhodecode
2142 2137
2143 2138 # Passing a config will not hit the cache currently only used
2144 2139 # for repo2dbmapper
2145 2140 config = kwargs.pop('config', None)
2146 2141 cache = kwargs.pop('cache', None)
2147 2142 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2148 2143 # if cache is NOT defined use default global, else we have a full
2149 2144 # control over cache behaviour
2150 2145 if cache is None and full_cache and not config:
2151 2146 return self._get_instance_cached()
2152 2147 return self._get_instance(cache=bool(cache), config=config)
2153 2148
2154 2149 def _get_instance_cached(self):
2155 2150 @cache_region('long_term')
2156 2151 def _get_repo(cache_key):
2157 2152 return self._get_instance()
2158 2153
2159 2154 invalidator_context = CacheKey.repo_context_cache(
2160 2155 _get_repo, self.repo_name, None, thread_scoped=True)
2161 2156
2162 2157 with invalidator_context as context:
2163 2158 context.invalidate()
2164 2159 repo = context.compute()
2165 2160
2166 2161 return repo
2167 2162
2168 2163 def _get_instance(self, cache=True, config=None):
2169 2164 config = config or self._config
2170 2165 custom_wire = {
2171 2166 'cache': cache # controls the vcs.remote cache
2172 2167 }
2173 2168 repo = get_vcs_instance(
2174 2169 repo_path=safe_str(self.repo_full_path),
2175 2170 config=config,
2176 2171 with_wire=custom_wire,
2177 2172 create=False,
2178 2173 _vcs_alias=self.repo_type)
2179 2174
2180 2175 return repo
2181 2176
2182 2177 def __json__(self):
2183 2178 return {'landing_rev': self.landing_rev}
2184 2179
2185 2180 def get_dict(self):
2186 2181
2187 2182 # Since we transformed `repo_name` to a hybrid property, we need to
2188 2183 # keep compatibility with the code which uses `repo_name` field.
2189 2184
2190 2185 result = super(Repository, self).get_dict()
2191 2186 result['repo_name'] = result.pop('_repo_name', None)
2192 2187 return result
2193 2188
2194 2189
2195 2190 class RepoGroup(Base, BaseModel):
2196 2191 __tablename__ = 'groups'
2197 2192 __table_args__ = (
2198 2193 UniqueConstraint('group_name', 'group_parent_id'),
2199 2194 CheckConstraint('group_id != group_parent_id'),
2200 2195 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2201 2196 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2202 2197 )
2203 2198 __mapper_args__ = {'order_by': 'group_name'}
2204 2199
2205 2200 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2206 2201
2207 2202 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2208 2203 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2209 2204 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2210 2205 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2211 2206 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2212 2207 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2213 2208 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2214 2209 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2215 2210 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2216 2211
2217 2212 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2218 2213 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2219 2214 parent_group = relationship('RepoGroup', remote_side=group_id)
2220 2215 user = relationship('User')
2221 2216 integrations = relationship('Integration',
2222 2217 cascade="all, delete, delete-orphan")
2223 2218
2224 2219 def __init__(self, group_name='', parent_group=None):
2225 2220 self.group_name = group_name
2226 2221 self.parent_group = parent_group
2227 2222
2228 2223 def __unicode__(self):
2229 2224 return u"<%s('id:%s:%s')>" % (
2230 2225 self.__class__.__name__, self.group_id, self.group_name)
2231 2226
2232 2227 @hybrid_property
2233 2228 def description_safe(self):
2234 2229 from rhodecode.lib import helpers as h
2235 2230 return h.escape(self.group_description)
2236 2231
2237 2232 @classmethod
2238 2233 def _generate_choice(cls, repo_group):
2239 2234 from webhelpers.html import literal as _literal
2240 2235 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2241 2236 return repo_group.group_id, _name(repo_group.full_path_splitted)
2242 2237
2243 2238 @classmethod
2244 2239 def groups_choices(cls, groups=None, show_empty_group=True):
2245 2240 if not groups:
2246 2241 groups = cls.query().all()
2247 2242
2248 2243 repo_groups = []
2249 2244 if show_empty_group:
2250 2245 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2251 2246
2252 2247 repo_groups.extend([cls._generate_choice(x) for x in groups])
2253 2248
2254 2249 repo_groups = sorted(
2255 2250 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2256 2251 return repo_groups
2257 2252
2258 2253 @classmethod
2259 2254 def url_sep(cls):
2260 2255 return URL_SEP
2261 2256
2262 2257 @classmethod
2263 2258 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2264 2259 if case_insensitive:
2265 2260 gr = cls.query().filter(func.lower(cls.group_name)
2266 2261 == func.lower(group_name))
2267 2262 else:
2268 2263 gr = cls.query().filter(cls.group_name == group_name)
2269 2264 if cache:
2270 2265 name_key = _hash_key(group_name)
2271 2266 gr = gr.options(
2272 2267 FromCache("sql_cache_short", "get_group_%s" % name_key))
2273 2268 return gr.scalar()
2274 2269
2275 2270 @classmethod
2276 2271 def get_user_personal_repo_group(cls, user_id):
2277 2272 user = User.get(user_id)
2278 2273 if user.username == User.DEFAULT_USER:
2279 2274 return None
2280 2275
2281 2276 return cls.query()\
2282 2277 .filter(cls.personal == true()) \
2283 2278 .filter(cls.user == user).scalar()
2284 2279
2285 2280 @classmethod
2286 2281 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2287 2282 case_insensitive=True):
2288 2283 q = RepoGroup.query()
2289 2284
2290 2285 if not isinstance(user_id, Optional):
2291 2286 q = q.filter(RepoGroup.user_id == user_id)
2292 2287
2293 2288 if not isinstance(group_id, Optional):
2294 2289 q = q.filter(RepoGroup.group_parent_id == group_id)
2295 2290
2296 2291 if case_insensitive:
2297 2292 q = q.order_by(func.lower(RepoGroup.group_name))
2298 2293 else:
2299 2294 q = q.order_by(RepoGroup.group_name)
2300 2295 return q.all()
2301 2296
2302 2297 @property
2303 2298 def parents(self):
2304 2299 parents_recursion_limit = 10
2305 2300 groups = []
2306 2301 if self.parent_group is None:
2307 2302 return groups
2308 2303 cur_gr = self.parent_group
2309 2304 groups.insert(0, cur_gr)
2310 2305 cnt = 0
2311 2306 while 1:
2312 2307 cnt += 1
2313 2308 gr = getattr(cur_gr, 'parent_group', None)
2314 2309 cur_gr = cur_gr.parent_group
2315 2310 if gr is None:
2316 2311 break
2317 2312 if cnt == parents_recursion_limit:
2318 2313 # this will prevent accidental infinit loops
2319 2314 log.error(('more than %s parents found for group %s, stopping '
2320 2315 'recursive parent fetching' % (parents_recursion_limit, self)))
2321 2316 break
2322 2317
2323 2318 groups.insert(0, gr)
2324 2319 return groups
2325 2320
2326 2321 @property
2327 2322 def last_db_change(self):
2328 2323 return self.updated_on
2329 2324
2330 2325 @property
2331 2326 def children(self):
2332 2327 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2333 2328
2334 2329 @property
2335 2330 def name(self):
2336 2331 return self.group_name.split(RepoGroup.url_sep())[-1]
2337 2332
2338 2333 @property
2339 2334 def full_path(self):
2340 2335 return self.group_name
2341 2336
2342 2337 @property
2343 2338 def full_path_splitted(self):
2344 2339 return self.group_name.split(RepoGroup.url_sep())
2345 2340
2346 2341 @property
2347 2342 def repositories(self):
2348 2343 return Repository.query()\
2349 2344 .filter(Repository.group == self)\
2350 2345 .order_by(Repository.repo_name)
2351 2346
2352 2347 @property
2353 2348 def repositories_recursive_count(self):
2354 2349 cnt = self.repositories.count()
2355 2350
2356 2351 def children_count(group):
2357 2352 cnt = 0
2358 2353 for child in group.children:
2359 2354 cnt += child.repositories.count()
2360 2355 cnt += children_count(child)
2361 2356 return cnt
2362 2357
2363 2358 return cnt + children_count(self)
2364 2359
2365 2360 def _recursive_objects(self, include_repos=True):
2366 2361 all_ = []
2367 2362
2368 2363 def _get_members(root_gr):
2369 2364 if include_repos:
2370 2365 for r in root_gr.repositories:
2371 2366 all_.append(r)
2372 2367 childs = root_gr.children.all()
2373 2368 if childs:
2374 2369 for gr in childs:
2375 2370 all_.append(gr)
2376 2371 _get_members(gr)
2377 2372
2378 2373 _get_members(self)
2379 2374 return [self] + all_
2380 2375
2381 2376 def recursive_groups_and_repos(self):
2382 2377 """
2383 2378 Recursive return all groups, with repositories in those groups
2384 2379 """
2385 2380 return self._recursive_objects()
2386 2381
2387 2382 def recursive_groups(self):
2388 2383 """
2389 2384 Returns all children groups for this group including children of children
2390 2385 """
2391 2386 return self._recursive_objects(include_repos=False)
2392 2387
2393 2388 def get_new_name(self, group_name):
2394 2389 """
2395 2390 returns new full group name based on parent and new name
2396 2391
2397 2392 :param group_name:
2398 2393 """
2399 2394 path_prefix = (self.parent_group.full_path_splitted if
2400 2395 self.parent_group else [])
2401 2396 return RepoGroup.url_sep().join(path_prefix + [group_name])
2402 2397
2403 2398 def permissions(self, with_admins=True, with_owner=True):
2404 2399 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2405 2400 q = q.options(joinedload(UserRepoGroupToPerm.group),
2406 2401 joinedload(UserRepoGroupToPerm.user),
2407 2402 joinedload(UserRepoGroupToPerm.permission),)
2408 2403
2409 2404 # get owners and admins and permissions. We do a trick of re-writing
2410 2405 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2411 2406 # has a global reference and changing one object propagates to all
2412 2407 # others. This means if admin is also an owner admin_row that change
2413 2408 # would propagate to both objects
2414 2409 perm_rows = []
2415 2410 for _usr in q.all():
2416 2411 usr = AttributeDict(_usr.user.get_dict())
2417 2412 usr.permission = _usr.permission.permission_name
2418 2413 perm_rows.append(usr)
2419 2414
2420 2415 # filter the perm rows by 'default' first and then sort them by
2421 2416 # admin,write,read,none permissions sorted again alphabetically in
2422 2417 # each group
2423 2418 perm_rows = sorted(perm_rows, key=display_sort)
2424 2419
2425 2420 _admin_perm = 'group.admin'
2426 2421 owner_row = []
2427 2422 if with_owner:
2428 2423 usr = AttributeDict(self.user.get_dict())
2429 2424 usr.owner_row = True
2430 2425 usr.permission = _admin_perm
2431 2426 owner_row.append(usr)
2432 2427
2433 2428 super_admin_rows = []
2434 2429 if with_admins:
2435 2430 for usr in User.get_all_super_admins():
2436 2431 # if this admin is also owner, don't double the record
2437 2432 if usr.user_id == owner_row[0].user_id:
2438 2433 owner_row[0].admin_row = True
2439 2434 else:
2440 2435 usr = AttributeDict(usr.get_dict())
2441 2436 usr.admin_row = True
2442 2437 usr.permission = _admin_perm
2443 2438 super_admin_rows.append(usr)
2444 2439
2445 2440 return super_admin_rows + owner_row + perm_rows
2446 2441
2447 2442 def permission_user_groups(self):
2448 2443 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2449 2444 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2450 2445 joinedload(UserGroupRepoGroupToPerm.users_group),
2451 2446 joinedload(UserGroupRepoGroupToPerm.permission),)
2452 2447
2453 2448 perm_rows = []
2454 2449 for _user_group in q.all():
2455 2450 usr = AttributeDict(_user_group.users_group.get_dict())
2456 2451 usr.permission = _user_group.permission.permission_name
2457 2452 perm_rows.append(usr)
2458 2453
2459 2454 return perm_rows
2460 2455
2461 2456 def get_api_data(self):
2462 2457 """
2463 2458 Common function for generating api data
2464 2459
2465 2460 """
2466 2461 group = self
2467 2462 data = {
2468 2463 'group_id': group.group_id,
2469 2464 'group_name': group.group_name,
2470 2465 'group_description': group.description_safe,
2471 2466 'parent_group': group.parent_group.group_name if group.parent_group else None,
2472 2467 'repositories': [x.repo_name for x in group.repositories],
2473 2468 'owner': group.user.username,
2474 2469 }
2475 2470 return data
2476 2471
2477 2472
2478 2473 class Permission(Base, BaseModel):
2479 2474 __tablename__ = 'permissions'
2480 2475 __table_args__ = (
2481 2476 Index('p_perm_name_idx', 'permission_name'),
2482 2477 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2483 2478 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2484 2479 )
2485 2480 PERMS = [
2486 2481 ('hg.admin', _('RhodeCode Super Administrator')),
2487 2482
2488 2483 ('repository.none', _('Repository no access')),
2489 2484 ('repository.read', _('Repository read access')),
2490 2485 ('repository.write', _('Repository write access')),
2491 2486 ('repository.admin', _('Repository admin access')),
2492 2487
2493 2488 ('group.none', _('Repository group no access')),
2494 2489 ('group.read', _('Repository group read access')),
2495 2490 ('group.write', _('Repository group write access')),
2496 2491 ('group.admin', _('Repository group admin access')),
2497 2492
2498 2493 ('usergroup.none', _('User group no access')),
2499 2494 ('usergroup.read', _('User group read access')),
2500 2495 ('usergroup.write', _('User group write access')),
2501 2496 ('usergroup.admin', _('User group admin access')),
2502 2497
2503 2498 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2504 2499 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2505 2500
2506 2501 ('hg.usergroup.create.false', _('User Group creation disabled')),
2507 2502 ('hg.usergroup.create.true', _('User Group creation enabled')),
2508 2503
2509 2504 ('hg.create.none', _('Repository creation disabled')),
2510 2505 ('hg.create.repository', _('Repository creation enabled')),
2511 2506 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2512 2507 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2513 2508
2514 2509 ('hg.fork.none', _('Repository forking disabled')),
2515 2510 ('hg.fork.repository', _('Repository forking enabled')),
2516 2511
2517 2512 ('hg.register.none', _('Registration disabled')),
2518 2513 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2519 2514 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2520 2515
2521 2516 ('hg.password_reset.enabled', _('Password reset enabled')),
2522 2517 ('hg.password_reset.hidden', _('Password reset hidden')),
2523 2518 ('hg.password_reset.disabled', _('Password reset disabled')),
2524 2519
2525 2520 ('hg.extern_activate.manual', _('Manual activation of external account')),
2526 2521 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2527 2522
2528 2523 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2529 2524 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2530 2525 ]
2531 2526
2532 2527 # definition of system default permissions for DEFAULT user
2533 2528 DEFAULT_USER_PERMISSIONS = [
2534 2529 'repository.read',
2535 2530 'group.read',
2536 2531 'usergroup.read',
2537 2532 'hg.create.repository',
2538 2533 'hg.repogroup.create.false',
2539 2534 'hg.usergroup.create.false',
2540 2535 'hg.create.write_on_repogroup.true',
2541 2536 'hg.fork.repository',
2542 2537 'hg.register.manual_activate',
2543 2538 'hg.password_reset.enabled',
2544 2539 'hg.extern_activate.auto',
2545 2540 'hg.inherit_default_perms.true',
2546 2541 ]
2547 2542
2548 2543 # defines which permissions are more important higher the more important
2549 2544 # Weight defines which permissions are more important.
2550 2545 # The higher number the more important.
2551 2546 PERM_WEIGHTS = {
2552 2547 'repository.none': 0,
2553 2548 'repository.read': 1,
2554 2549 'repository.write': 3,
2555 2550 'repository.admin': 4,
2556 2551
2557 2552 'group.none': 0,
2558 2553 'group.read': 1,
2559 2554 'group.write': 3,
2560 2555 'group.admin': 4,
2561 2556
2562 2557 'usergroup.none': 0,
2563 2558 'usergroup.read': 1,
2564 2559 'usergroup.write': 3,
2565 2560 'usergroup.admin': 4,
2566 2561
2567 2562 'hg.repogroup.create.false': 0,
2568 2563 'hg.repogroup.create.true': 1,
2569 2564
2570 2565 'hg.usergroup.create.false': 0,
2571 2566 'hg.usergroup.create.true': 1,
2572 2567
2573 2568 'hg.fork.none': 0,
2574 2569 'hg.fork.repository': 1,
2575 2570 'hg.create.none': 0,
2576 2571 'hg.create.repository': 1
2577 2572 }
2578 2573
2579 2574 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2580 2575 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2581 2576 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2582 2577
2583 2578 def __unicode__(self):
2584 2579 return u"<%s('%s:%s')>" % (
2585 2580 self.__class__.__name__, self.permission_id, self.permission_name
2586 2581 )
2587 2582
2588 2583 @classmethod
2589 2584 def get_by_key(cls, key):
2590 2585 return cls.query().filter(cls.permission_name == key).scalar()
2591 2586
2592 2587 @classmethod
2593 2588 def get_default_repo_perms(cls, user_id, repo_id=None):
2594 2589 q = Session().query(UserRepoToPerm, Repository, Permission)\
2595 2590 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2596 2591 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2597 2592 .filter(UserRepoToPerm.user_id == user_id)
2598 2593 if repo_id:
2599 2594 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2600 2595 return q.all()
2601 2596
2602 2597 @classmethod
2603 2598 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2604 2599 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2605 2600 .join(
2606 2601 Permission,
2607 2602 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2608 2603 .join(
2609 2604 Repository,
2610 2605 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2611 2606 .join(
2612 2607 UserGroup,
2613 2608 UserGroupRepoToPerm.users_group_id ==
2614 2609 UserGroup.users_group_id)\
2615 2610 .join(
2616 2611 UserGroupMember,
2617 2612 UserGroupRepoToPerm.users_group_id ==
2618 2613 UserGroupMember.users_group_id)\
2619 2614 .filter(
2620 2615 UserGroupMember.user_id == user_id,
2621 2616 UserGroup.users_group_active == true())
2622 2617 if repo_id:
2623 2618 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2624 2619 return q.all()
2625 2620
2626 2621 @classmethod
2627 2622 def get_default_group_perms(cls, user_id, repo_group_id=None):
2628 2623 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2629 2624 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2630 2625 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2631 2626 .filter(UserRepoGroupToPerm.user_id == user_id)
2632 2627 if repo_group_id:
2633 2628 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2634 2629 return q.all()
2635 2630
2636 2631 @classmethod
2637 2632 def get_default_group_perms_from_user_group(
2638 2633 cls, user_id, repo_group_id=None):
2639 2634 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2640 2635 .join(
2641 2636 Permission,
2642 2637 UserGroupRepoGroupToPerm.permission_id ==
2643 2638 Permission.permission_id)\
2644 2639 .join(
2645 2640 RepoGroup,
2646 2641 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2647 2642 .join(
2648 2643 UserGroup,
2649 2644 UserGroupRepoGroupToPerm.users_group_id ==
2650 2645 UserGroup.users_group_id)\
2651 2646 .join(
2652 2647 UserGroupMember,
2653 2648 UserGroupRepoGroupToPerm.users_group_id ==
2654 2649 UserGroupMember.users_group_id)\
2655 2650 .filter(
2656 2651 UserGroupMember.user_id == user_id,
2657 2652 UserGroup.users_group_active == true())
2658 2653 if repo_group_id:
2659 2654 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2660 2655 return q.all()
2661 2656
2662 2657 @classmethod
2663 2658 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2664 2659 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2665 2660 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2666 2661 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2667 2662 .filter(UserUserGroupToPerm.user_id == user_id)
2668 2663 if user_group_id:
2669 2664 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2670 2665 return q.all()
2671 2666
2672 2667 @classmethod
2673 2668 def get_default_user_group_perms_from_user_group(
2674 2669 cls, user_id, user_group_id=None):
2675 2670 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2676 2671 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2677 2672 .join(
2678 2673 Permission,
2679 2674 UserGroupUserGroupToPerm.permission_id ==
2680 2675 Permission.permission_id)\
2681 2676 .join(
2682 2677 TargetUserGroup,
2683 2678 UserGroupUserGroupToPerm.target_user_group_id ==
2684 2679 TargetUserGroup.users_group_id)\
2685 2680 .join(
2686 2681 UserGroup,
2687 2682 UserGroupUserGroupToPerm.user_group_id ==
2688 2683 UserGroup.users_group_id)\
2689 2684 .join(
2690 2685 UserGroupMember,
2691 2686 UserGroupUserGroupToPerm.user_group_id ==
2692 2687 UserGroupMember.users_group_id)\
2693 2688 .filter(
2694 2689 UserGroupMember.user_id == user_id,
2695 2690 UserGroup.users_group_active == true())
2696 2691 if user_group_id:
2697 2692 q = q.filter(
2698 2693 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2699 2694
2700 2695 return q.all()
2701 2696
2702 2697
2703 2698 class UserRepoToPerm(Base, BaseModel):
2704 2699 __tablename__ = 'repo_to_perm'
2705 2700 __table_args__ = (
2706 2701 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2707 2702 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2708 2703 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2709 2704 )
2710 2705 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2711 2706 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2712 2707 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2713 2708 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2714 2709
2715 2710 user = relationship('User')
2716 2711 repository = relationship('Repository')
2717 2712 permission = relationship('Permission')
2718 2713
2719 2714 @classmethod
2720 2715 def create(cls, user, repository, permission):
2721 2716 n = cls()
2722 2717 n.user = user
2723 2718 n.repository = repository
2724 2719 n.permission = permission
2725 2720 Session().add(n)
2726 2721 return n
2727 2722
2728 2723 def __unicode__(self):
2729 2724 return u'<%s => %s >' % (self.user, self.repository)
2730 2725
2731 2726
2732 2727 class UserUserGroupToPerm(Base, BaseModel):
2733 2728 __tablename__ = 'user_user_group_to_perm'
2734 2729 __table_args__ = (
2735 2730 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2736 2731 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2737 2732 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2738 2733 )
2739 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 2735 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2741 2736 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2742 2737 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2743 2738
2744 2739 user = relationship('User')
2745 2740 user_group = relationship('UserGroup')
2746 2741 permission = relationship('Permission')
2747 2742
2748 2743 @classmethod
2749 2744 def create(cls, user, user_group, permission):
2750 2745 n = cls()
2751 2746 n.user = user
2752 2747 n.user_group = user_group
2753 2748 n.permission = permission
2754 2749 Session().add(n)
2755 2750 return n
2756 2751
2757 2752 def __unicode__(self):
2758 2753 return u'<%s => %s >' % (self.user, self.user_group)
2759 2754
2760 2755
2761 2756 class UserToPerm(Base, BaseModel):
2762 2757 __tablename__ = 'user_to_perm'
2763 2758 __table_args__ = (
2764 2759 UniqueConstraint('user_id', 'permission_id'),
2765 2760 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2766 2761 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2767 2762 )
2768 2763 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2769 2764 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2770 2765 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2771 2766
2772 2767 user = relationship('User')
2773 2768 permission = relationship('Permission', lazy='joined')
2774 2769
2775 2770 def __unicode__(self):
2776 2771 return u'<%s => %s >' % (self.user, self.permission)
2777 2772
2778 2773
2779 2774 class UserGroupRepoToPerm(Base, BaseModel):
2780 2775 __tablename__ = 'users_group_repo_to_perm'
2781 2776 __table_args__ = (
2782 2777 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2783 2778 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2784 2779 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2785 2780 )
2786 2781 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2787 2782 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2788 2783 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2789 2784 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2790 2785
2791 2786 users_group = relationship('UserGroup')
2792 2787 permission = relationship('Permission')
2793 2788 repository = relationship('Repository')
2794 2789
2795 2790 @classmethod
2796 2791 def create(cls, users_group, repository, permission):
2797 2792 n = cls()
2798 2793 n.users_group = users_group
2799 2794 n.repository = repository
2800 2795 n.permission = permission
2801 2796 Session().add(n)
2802 2797 return n
2803 2798
2804 2799 def __unicode__(self):
2805 2800 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2806 2801
2807 2802
2808 2803 class UserGroupUserGroupToPerm(Base, BaseModel):
2809 2804 __tablename__ = 'user_group_user_group_to_perm'
2810 2805 __table_args__ = (
2811 2806 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2812 2807 CheckConstraint('target_user_group_id != user_group_id'),
2813 2808 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2814 2809 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2815 2810 )
2816 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 2812 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2818 2813 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2819 2814 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2820 2815
2821 2816 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2822 2817 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2823 2818 permission = relationship('Permission')
2824 2819
2825 2820 @classmethod
2826 2821 def create(cls, target_user_group, user_group, permission):
2827 2822 n = cls()
2828 2823 n.target_user_group = target_user_group
2829 2824 n.user_group = user_group
2830 2825 n.permission = permission
2831 2826 Session().add(n)
2832 2827 return n
2833 2828
2834 2829 def __unicode__(self):
2835 2830 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2836 2831
2837 2832
2838 2833 class UserGroupToPerm(Base, BaseModel):
2839 2834 __tablename__ = 'users_group_to_perm'
2840 2835 __table_args__ = (
2841 2836 UniqueConstraint('users_group_id', 'permission_id',),
2842 2837 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2843 2838 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2844 2839 )
2845 2840 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2846 2841 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2847 2842 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2848 2843
2849 2844 users_group = relationship('UserGroup')
2850 2845 permission = relationship('Permission')
2851 2846
2852 2847
2853 2848 class UserRepoGroupToPerm(Base, BaseModel):
2854 2849 __tablename__ = 'user_repo_group_to_perm'
2855 2850 __table_args__ = (
2856 2851 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2857 2852 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2858 2853 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2859 2854 )
2860 2855
2861 2856 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2862 2857 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2863 2858 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2864 2859 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2865 2860
2866 2861 user = relationship('User')
2867 2862 group = relationship('RepoGroup')
2868 2863 permission = relationship('Permission')
2869 2864
2870 2865 @classmethod
2871 2866 def create(cls, user, repository_group, permission):
2872 2867 n = cls()
2873 2868 n.user = user
2874 2869 n.group = repository_group
2875 2870 n.permission = permission
2876 2871 Session().add(n)
2877 2872 return n
2878 2873
2879 2874
2880 2875 class UserGroupRepoGroupToPerm(Base, BaseModel):
2881 2876 __tablename__ = 'users_group_repo_group_to_perm'
2882 2877 __table_args__ = (
2883 2878 UniqueConstraint('users_group_id', 'group_id'),
2884 2879 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2885 2880 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2886 2881 )
2887 2882
2888 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 2884 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2890 2885 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2891 2886 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2892 2887
2893 2888 users_group = relationship('UserGroup')
2894 2889 permission = relationship('Permission')
2895 2890 group = relationship('RepoGroup')
2896 2891
2897 2892 @classmethod
2898 2893 def create(cls, user_group, repository_group, permission):
2899 2894 n = cls()
2900 2895 n.users_group = user_group
2901 2896 n.group = repository_group
2902 2897 n.permission = permission
2903 2898 Session().add(n)
2904 2899 return n
2905 2900
2906 2901 def __unicode__(self):
2907 2902 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2908 2903
2909 2904
2910 2905 class Statistics(Base, BaseModel):
2911 2906 __tablename__ = 'statistics'
2912 2907 __table_args__ = (
2913 2908 UniqueConstraint('repository_id'),
2914 2909 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2915 2910 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2916 2911 )
2917 2912 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2918 2913 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2919 2914 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2920 2915 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2921 2916 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2922 2917 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2923 2918
2924 2919 repository = relationship('Repository', single_parent=True)
2925 2920
2926 2921
2927 2922 class UserFollowing(Base, BaseModel):
2928 2923 __tablename__ = 'user_followings'
2929 2924 __table_args__ = (
2930 2925 UniqueConstraint('user_id', 'follows_repository_id'),
2931 2926 UniqueConstraint('user_id', 'follows_user_id'),
2932 2927 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2933 2928 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2934 2929 )
2935 2930
2936 2931 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2937 2932 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2938 2933 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2939 2934 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2940 2935 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2941 2936
2942 2937 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2943 2938
2944 2939 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2945 2940 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2946 2941
2947 2942 @classmethod
2948 2943 def get_repo_followers(cls, repo_id):
2949 2944 return cls.query().filter(cls.follows_repo_id == repo_id)
2950 2945
2951 2946
2952 2947 class CacheKey(Base, BaseModel):
2953 2948 __tablename__ = 'cache_invalidation'
2954 2949 __table_args__ = (
2955 2950 UniqueConstraint('cache_key'),
2956 2951 Index('key_idx', 'cache_key'),
2957 2952 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2958 2953 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2959 2954 )
2960 2955 CACHE_TYPE_ATOM = 'ATOM'
2961 2956 CACHE_TYPE_RSS = 'RSS'
2962 2957 CACHE_TYPE_README = 'README'
2963 2958
2964 2959 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2965 2960 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2966 2961 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2967 2962 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2968 2963
2969 2964 def __init__(self, cache_key, cache_args=''):
2970 2965 self.cache_key = cache_key
2971 2966 self.cache_args = cache_args
2972 2967 self.cache_active = False
2973 2968
2974 2969 def __unicode__(self):
2975 2970 return u"<%s('%s:%s[%s]')>" % (
2976 2971 self.__class__.__name__,
2977 2972 self.cache_id, self.cache_key, self.cache_active)
2978 2973
2979 2974 def _cache_key_partition(self):
2980 2975 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2981 2976 return prefix, repo_name, suffix
2982 2977
2983 2978 def get_prefix(self):
2984 2979 """
2985 2980 Try to extract prefix from existing cache key. The key could consist
2986 2981 of prefix, repo_name, suffix
2987 2982 """
2988 2983 # this returns prefix, repo_name, suffix
2989 2984 return self._cache_key_partition()[0]
2990 2985
2991 2986 def get_suffix(self):
2992 2987 """
2993 2988 get suffix that might have been used in _get_cache_key to
2994 2989 generate self.cache_key. Only used for informational purposes
2995 2990 in repo_edit.mako.
2996 2991 """
2997 2992 # prefix, repo_name, suffix
2998 2993 return self._cache_key_partition()[2]
2999 2994
3000 2995 @classmethod
3001 2996 def delete_all_cache(cls):
3002 2997 """
3003 2998 Delete all cache keys from database.
3004 2999 Should only be run when all instances are down and all entries
3005 3000 thus stale.
3006 3001 """
3007 3002 cls.query().delete()
3008 3003 Session().commit()
3009 3004
3010 3005 @classmethod
3011 3006 def get_cache_key(cls, repo_name, cache_type):
3012 3007 """
3013 3008
3014 3009 Generate a cache key for this process of RhodeCode instance.
3015 3010 Prefix most likely will be process id or maybe explicitly set
3016 3011 instance_id from .ini file.
3017 3012 """
3018 3013 import rhodecode
3019 3014 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3020 3015
3021 3016 repo_as_unicode = safe_unicode(repo_name)
3022 3017 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3023 3018 if cache_type else repo_as_unicode
3024 3019
3025 3020 return u'{}{}'.format(prefix, key)
3026 3021
3027 3022 @classmethod
3028 3023 def set_invalidate(cls, repo_name, delete=False):
3029 3024 """
3030 3025 Mark all caches of a repo as invalid in the database.
3031 3026 """
3032 3027
3033 3028 try:
3034 3029 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3035 3030 if delete:
3036 3031 log.debug('cache objects deleted for repo %s',
3037 3032 safe_str(repo_name))
3038 3033 qry.delete()
3039 3034 else:
3040 3035 log.debug('cache objects marked as invalid for repo %s',
3041 3036 safe_str(repo_name))
3042 3037 qry.update({"cache_active": False})
3043 3038
3044 3039 Session().commit()
3045 3040 except Exception:
3046 3041 log.exception(
3047 3042 'Cache key invalidation failed for repository %s',
3048 3043 safe_str(repo_name))
3049 3044 Session().rollback()
3050 3045
3051 3046 @classmethod
3052 3047 def get_active_cache(cls, cache_key):
3053 3048 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3054 3049 if inv_obj:
3055 3050 return inv_obj
3056 3051 return None
3057 3052
3058 3053 @classmethod
3059 3054 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3060 3055 thread_scoped=False):
3061 3056 """
3062 3057 @cache_region('long_term')
3063 3058 def _heavy_calculation(cache_key):
3064 3059 return 'result'
3065 3060
3066 3061 cache_context = CacheKey.repo_context_cache(
3067 3062 _heavy_calculation, repo_name, cache_type)
3068 3063
3069 3064 with cache_context as context:
3070 3065 context.invalidate()
3071 3066 computed = context.compute()
3072 3067
3073 3068 assert computed == 'result'
3074 3069 """
3075 3070 from rhodecode.lib import caches
3076 3071 return caches.InvalidationContext(
3077 3072 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3078 3073
3079 3074
3080 3075 class ChangesetComment(Base, BaseModel):
3081 3076 __tablename__ = 'changeset_comments'
3082 3077 __table_args__ = (
3083 3078 Index('cc_revision_idx', 'revision'),
3084 3079 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 3080 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 3081 )
3087 3082
3088 3083 COMMENT_OUTDATED = u'comment_outdated'
3089 3084 COMMENT_TYPE_NOTE = u'note'
3090 3085 COMMENT_TYPE_TODO = u'todo'
3091 3086 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3092 3087
3093 3088 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3094 3089 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3095 3090 revision = Column('revision', String(40), nullable=True)
3096 3091 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3097 3092 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3098 3093 line_no = Column('line_no', Unicode(10), nullable=True)
3099 3094 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3100 3095 f_path = Column('f_path', Unicode(1000), nullable=True)
3101 3096 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3102 3097 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3103 3098 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3104 3099 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3105 3100 renderer = Column('renderer', Unicode(64), nullable=True)
3106 3101 display_state = Column('display_state', Unicode(128), nullable=True)
3107 3102
3108 3103 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3109 3104 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3110 3105 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3111 3106 author = relationship('User', lazy='joined')
3112 3107 repo = relationship('Repository')
3113 3108 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3114 3109 pull_request = relationship('PullRequest', lazy='joined')
3115 3110 pull_request_version = relationship('PullRequestVersion')
3116 3111
3117 3112 @classmethod
3118 3113 def get_users(cls, revision=None, pull_request_id=None):
3119 3114 """
3120 3115 Returns user associated with this ChangesetComment. ie those
3121 3116 who actually commented
3122 3117
3123 3118 :param cls:
3124 3119 :param revision:
3125 3120 """
3126 3121 q = Session().query(User)\
3127 3122 .join(ChangesetComment.author)
3128 3123 if revision:
3129 3124 q = q.filter(cls.revision == revision)
3130 3125 elif pull_request_id:
3131 3126 q = q.filter(cls.pull_request_id == pull_request_id)
3132 3127 return q.all()
3133 3128
3134 3129 @classmethod
3135 3130 def get_index_from_version(cls, pr_version, versions):
3136 3131 num_versions = [x.pull_request_version_id for x in versions]
3137 3132 try:
3138 3133 return num_versions.index(pr_version) +1
3139 3134 except (IndexError, ValueError):
3140 3135 return
3141 3136
3142 3137 @property
3143 3138 def outdated(self):
3144 3139 return self.display_state == self.COMMENT_OUTDATED
3145 3140
3146 3141 def outdated_at_version(self, version):
3147 3142 """
3148 3143 Checks if comment is outdated for given pull request version
3149 3144 """
3150 3145 return self.outdated and self.pull_request_version_id != version
3151 3146
3152 3147 def older_than_version(self, version):
3153 3148 """
3154 3149 Checks if comment is made from previous version than given
3155 3150 """
3156 3151 if version is None:
3157 3152 return self.pull_request_version_id is not None
3158 3153
3159 3154 return self.pull_request_version_id < version
3160 3155
3161 3156 @property
3162 3157 def resolved(self):
3163 3158 return self.resolved_by[0] if self.resolved_by else None
3164 3159
3165 3160 @property
3166 3161 def is_todo(self):
3167 3162 return self.comment_type == self.COMMENT_TYPE_TODO
3168 3163
3169 3164 @property
3170 3165 def is_inline(self):
3171 3166 return self.line_no and self.f_path
3172 3167
3173 3168 def get_index_version(self, versions):
3174 3169 return self.get_index_from_version(
3175 3170 self.pull_request_version_id, versions)
3176 3171
3177 3172 def __repr__(self):
3178 3173 if self.comment_id:
3179 3174 return '<DB:Comment #%s>' % self.comment_id
3180 3175 else:
3181 3176 return '<DB:Comment at %#x>' % id(self)
3182 3177
3183 3178 def get_api_data(self):
3184 3179 comment = self
3185 3180 data = {
3186 3181 'comment_id': comment.comment_id,
3187 3182 'comment_type': comment.comment_type,
3188 3183 'comment_text': comment.text,
3189 3184 'comment_status': comment.status_change,
3190 3185 'comment_f_path': comment.f_path,
3191 3186 'comment_lineno': comment.line_no,
3192 3187 'comment_author': comment.author,
3193 3188 'comment_created_on': comment.created_on
3194 3189 }
3195 3190 return data
3196 3191
3197 3192 def __json__(self):
3198 3193 data = dict()
3199 3194 data.update(self.get_api_data())
3200 3195 return data
3201 3196
3202 3197
3203 3198 class ChangesetStatus(Base, BaseModel):
3204 3199 __tablename__ = 'changeset_statuses'
3205 3200 __table_args__ = (
3206 3201 Index('cs_revision_idx', 'revision'),
3207 3202 Index('cs_version_idx', 'version'),
3208 3203 UniqueConstraint('repo_id', 'revision', 'version'),
3209 3204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3210 3205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3211 3206 )
3212 3207 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3213 3208 STATUS_APPROVED = 'approved'
3214 3209 STATUS_REJECTED = 'rejected'
3215 3210 STATUS_UNDER_REVIEW = 'under_review'
3216 3211
3217 3212 STATUSES = [
3218 3213 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3219 3214 (STATUS_APPROVED, _("Approved")),
3220 3215 (STATUS_REJECTED, _("Rejected")),
3221 3216 (STATUS_UNDER_REVIEW, _("Under Review")),
3222 3217 ]
3223 3218
3224 3219 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3225 3220 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3226 3221 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3227 3222 revision = Column('revision', String(40), nullable=False)
3228 3223 status = Column('status', String(128), nullable=False, default=DEFAULT)
3229 3224 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3230 3225 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3231 3226 version = Column('version', Integer(), nullable=False, default=0)
3232 3227 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3233 3228
3234 3229 author = relationship('User', lazy='joined')
3235 3230 repo = relationship('Repository')
3236 3231 comment = relationship('ChangesetComment', lazy='joined')
3237 3232 pull_request = relationship('PullRequest', lazy='joined')
3238 3233
3239 3234 def __unicode__(self):
3240 3235 return u"<%s('%s[v%s]:%s')>" % (
3241 3236 self.__class__.__name__,
3242 3237 self.status, self.version, self.author
3243 3238 )
3244 3239
3245 3240 @classmethod
3246 3241 def get_status_lbl(cls, value):
3247 3242 return dict(cls.STATUSES).get(value)
3248 3243
3249 3244 @property
3250 3245 def status_lbl(self):
3251 3246 return ChangesetStatus.get_status_lbl(self.status)
3252 3247
3253 3248 def get_api_data(self):
3254 3249 status = self
3255 3250 data = {
3256 3251 'status_id': status.changeset_status_id,
3257 3252 'status': status.status,
3258 3253 }
3259 3254 return data
3260 3255
3261 3256 def __json__(self):
3262 3257 data = dict()
3263 3258 data.update(self.get_api_data())
3264 3259 return data
3265 3260
3266 3261
3267 3262 class _PullRequestBase(BaseModel):
3268 3263 """
3269 3264 Common attributes of pull request and version entries.
3270 3265 """
3271 3266
3272 3267 # .status values
3273 3268 STATUS_NEW = u'new'
3274 3269 STATUS_OPEN = u'open'
3275 3270 STATUS_CLOSED = u'closed'
3276 3271
3277 3272 title = Column('title', Unicode(255), nullable=True)
3278 3273 description = Column(
3279 3274 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3280 3275 nullable=True)
3281 3276 # new/open/closed status of pull request (not approve/reject/etc)
3282 3277 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3283 3278 created_on = Column(
3284 3279 'created_on', DateTime(timezone=False), nullable=False,
3285 3280 default=datetime.datetime.now)
3286 3281 updated_on = Column(
3287 3282 'updated_on', DateTime(timezone=False), nullable=False,
3288 3283 default=datetime.datetime.now)
3289 3284
3290 3285 @declared_attr
3291 3286 def user_id(cls):
3292 3287 return Column(
3293 3288 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3294 3289 unique=None)
3295 3290
3296 3291 # 500 revisions max
3297 3292 _revisions = Column(
3298 3293 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3299 3294
3300 3295 @declared_attr
3301 3296 def source_repo_id(cls):
3302 3297 # TODO: dan: rename column to source_repo_id
3303 3298 return Column(
3304 3299 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3305 3300 nullable=False)
3306 3301
3307 3302 source_ref = Column('org_ref', Unicode(255), nullable=False)
3308 3303
3309 3304 @declared_attr
3310 3305 def target_repo_id(cls):
3311 3306 # TODO: dan: rename column to target_repo_id
3312 3307 return Column(
3313 3308 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3314 3309 nullable=False)
3315 3310
3316 3311 target_ref = Column('other_ref', Unicode(255), nullable=False)
3317 3312 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3318 3313
3319 3314 # TODO: dan: rename column to last_merge_source_rev
3320 3315 _last_merge_source_rev = Column(
3321 3316 'last_merge_org_rev', String(40), nullable=True)
3322 3317 # TODO: dan: rename column to last_merge_target_rev
3323 3318 _last_merge_target_rev = Column(
3324 3319 'last_merge_other_rev', String(40), nullable=True)
3325 3320 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3326 3321 merge_rev = Column('merge_rev', String(40), nullable=True)
3327 3322
3328 3323 reviewer_data = Column(
3329 3324 'reviewer_data_json', MutationObj.as_mutable(
3330 3325 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3331 3326
3332 3327 @property
3333 3328 def reviewer_data_json(self):
3334 3329 return json.dumps(self.reviewer_data)
3335 3330
3336 3331 @hybrid_property
3337 3332 def description_safe(self):
3338 3333 from rhodecode.lib import helpers as h
3339 3334 return h.escape(self.description)
3340 3335
3341 3336 @hybrid_property
3342 3337 def revisions(self):
3343 3338 return self._revisions.split(':') if self._revisions else []
3344 3339
3345 3340 @revisions.setter
3346 3341 def revisions(self, val):
3347 3342 self._revisions = ':'.join(val)
3348 3343
3349 3344 @declared_attr
3350 3345 def author(cls):
3351 3346 return relationship('User', lazy='joined')
3352 3347
3353 3348 @declared_attr
3354 3349 def source_repo(cls):
3355 3350 return relationship(
3356 3351 'Repository',
3357 3352 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3358 3353
3359 3354 @property
3360 3355 def source_ref_parts(self):
3361 3356 return self.unicode_to_reference(self.source_ref)
3362 3357
3363 3358 @declared_attr
3364 3359 def target_repo(cls):
3365 3360 return relationship(
3366 3361 'Repository',
3367 3362 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3368 3363
3369 3364 @property
3370 3365 def target_ref_parts(self):
3371 3366 return self.unicode_to_reference(self.target_ref)
3372 3367
3373 3368 @property
3374 3369 def shadow_merge_ref(self):
3375 3370 return self.unicode_to_reference(self._shadow_merge_ref)
3376 3371
3377 3372 @shadow_merge_ref.setter
3378 3373 def shadow_merge_ref(self, ref):
3379 3374 self._shadow_merge_ref = self.reference_to_unicode(ref)
3380 3375
3381 3376 def unicode_to_reference(self, raw):
3382 3377 """
3383 3378 Convert a unicode (or string) to a reference object.
3384 3379 If unicode evaluates to False it returns None.
3385 3380 """
3386 3381 if raw:
3387 3382 refs = raw.split(':')
3388 3383 return Reference(*refs)
3389 3384 else:
3390 3385 return None
3391 3386
3392 3387 def reference_to_unicode(self, ref):
3393 3388 """
3394 3389 Convert a reference object to unicode.
3395 3390 If reference is None it returns None.
3396 3391 """
3397 3392 if ref:
3398 3393 return u':'.join(ref)
3399 3394 else:
3400 3395 return None
3401 3396
3402 3397 def get_api_data(self, with_merge_state=True):
3403 3398 from rhodecode.model.pull_request import PullRequestModel
3404 3399
3405 3400 pull_request = self
3406 3401 if with_merge_state:
3407 3402 merge_status = PullRequestModel().merge_status(pull_request)
3408 3403 merge_state = {
3409 3404 'status': merge_status[0],
3410 3405 'message': safe_unicode(merge_status[1]),
3411 3406 }
3412 3407 else:
3413 3408 merge_state = {'status': 'not_available',
3414 3409 'message': 'not_available'}
3415 3410
3416 3411 merge_data = {
3417 3412 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3418 3413 'reference': (
3419 3414 pull_request.shadow_merge_ref._asdict()
3420 3415 if pull_request.shadow_merge_ref else None),
3421 3416 }
3422 3417
3423 3418 data = {
3424 3419 'pull_request_id': pull_request.pull_request_id,
3425 3420 'url': PullRequestModel().get_url(pull_request),
3426 3421 'title': pull_request.title,
3427 3422 'description': pull_request.description,
3428 3423 'status': pull_request.status,
3429 3424 'created_on': pull_request.created_on,
3430 3425 'updated_on': pull_request.updated_on,
3431 3426 'commit_ids': pull_request.revisions,
3432 3427 'review_status': pull_request.calculated_review_status(),
3433 3428 'mergeable': merge_state,
3434 3429 'source': {
3435 3430 'clone_url': pull_request.source_repo.clone_url(),
3436 3431 'repository': pull_request.source_repo.repo_name,
3437 3432 'reference': {
3438 3433 'name': pull_request.source_ref_parts.name,
3439 3434 'type': pull_request.source_ref_parts.type,
3440 3435 'commit_id': pull_request.source_ref_parts.commit_id,
3441 3436 },
3442 3437 },
3443 3438 'target': {
3444 3439 'clone_url': pull_request.target_repo.clone_url(),
3445 3440 'repository': pull_request.target_repo.repo_name,
3446 3441 'reference': {
3447 3442 'name': pull_request.target_ref_parts.name,
3448 3443 'type': pull_request.target_ref_parts.type,
3449 3444 'commit_id': pull_request.target_ref_parts.commit_id,
3450 3445 },
3451 3446 },
3452 3447 'merge': merge_data,
3453 3448 'author': pull_request.author.get_api_data(include_secrets=False,
3454 3449 details='basic'),
3455 3450 'reviewers': [
3456 3451 {
3457 3452 'user': reviewer.get_api_data(include_secrets=False,
3458 3453 details='basic'),
3459 3454 'reasons': reasons,
3460 3455 'review_status': st[0][1].status if st else 'not_reviewed',
3461 3456 }
3462 3457 for reviewer, reasons, mandatory, st in
3463 3458 pull_request.reviewers_statuses()
3464 3459 ]
3465 3460 }
3466 3461
3467 3462 return data
3468 3463
3469 3464
3470 3465 class PullRequest(Base, _PullRequestBase):
3471 3466 __tablename__ = 'pull_requests'
3472 3467 __table_args__ = (
3473 3468 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3474 3469 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3475 3470 )
3476 3471
3477 3472 pull_request_id = Column(
3478 3473 'pull_request_id', Integer(), nullable=False, primary_key=True)
3479 3474
3480 3475 def __repr__(self):
3481 3476 if self.pull_request_id:
3482 3477 return '<DB:PullRequest #%s>' % self.pull_request_id
3483 3478 else:
3484 3479 return '<DB:PullRequest at %#x>' % id(self)
3485 3480
3486 3481 reviewers = relationship('PullRequestReviewers',
3487 3482 cascade="all, delete, delete-orphan")
3488 3483 statuses = relationship('ChangesetStatus',
3489 3484 cascade="all, delete, delete-orphan")
3490 3485 comments = relationship('ChangesetComment',
3491 3486 cascade="all, delete, delete-orphan")
3492 3487 versions = relationship('PullRequestVersion',
3493 3488 cascade="all, delete, delete-orphan",
3494 3489 lazy='dynamic')
3495 3490
3496 3491 @classmethod
3497 3492 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3498 3493 internal_methods=None):
3499 3494
3500 3495 class PullRequestDisplay(object):
3501 3496 """
3502 3497 Special object wrapper for showing PullRequest data via Versions
3503 3498 It mimics PR object as close as possible. This is read only object
3504 3499 just for display
3505 3500 """
3506 3501
3507 3502 def __init__(self, attrs, internal=None):
3508 3503 self.attrs = attrs
3509 3504 # internal have priority over the given ones via attrs
3510 3505 self.internal = internal or ['versions']
3511 3506
3512 3507 def __getattr__(self, item):
3513 3508 if item in self.internal:
3514 3509 return getattr(self, item)
3515 3510 try:
3516 3511 return self.attrs[item]
3517 3512 except KeyError:
3518 3513 raise AttributeError(
3519 3514 '%s object has no attribute %s' % (self, item))
3520 3515
3521 3516 def __repr__(self):
3522 3517 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3523 3518
3524 3519 def versions(self):
3525 3520 return pull_request_obj.versions.order_by(
3526 3521 PullRequestVersion.pull_request_version_id).all()
3527 3522
3528 3523 def is_closed(self):
3529 3524 return pull_request_obj.is_closed()
3530 3525
3531 3526 @property
3532 3527 def pull_request_version_id(self):
3533 3528 return getattr(pull_request_obj, 'pull_request_version_id', None)
3534 3529
3535 3530 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3536 3531
3537 3532 attrs.author = StrictAttributeDict(
3538 3533 pull_request_obj.author.get_api_data())
3539 3534 if pull_request_obj.target_repo:
3540 3535 attrs.target_repo = StrictAttributeDict(
3541 3536 pull_request_obj.target_repo.get_api_data())
3542 3537 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3543 3538
3544 3539 if pull_request_obj.source_repo:
3545 3540 attrs.source_repo = StrictAttributeDict(
3546 3541 pull_request_obj.source_repo.get_api_data())
3547 3542 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3548 3543
3549 3544 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3550 3545 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3551 3546 attrs.revisions = pull_request_obj.revisions
3552 3547
3553 3548 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3554 3549 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3555 3550 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3556 3551
3557 3552 return PullRequestDisplay(attrs, internal=internal_methods)
3558 3553
3559 3554 def is_closed(self):
3560 3555 return self.status == self.STATUS_CLOSED
3561 3556
3562 3557 def __json__(self):
3563 3558 return {
3564 3559 'revisions': self.revisions,
3565 3560 }
3566 3561
3567 3562 def calculated_review_status(self):
3568 3563 from rhodecode.model.changeset_status import ChangesetStatusModel
3569 3564 return ChangesetStatusModel().calculated_review_status(self)
3570 3565
3571 3566 def reviewers_statuses(self):
3572 3567 from rhodecode.model.changeset_status import ChangesetStatusModel
3573 3568 return ChangesetStatusModel().reviewers_statuses(self)
3574 3569
3575 3570 @property
3576 3571 def workspace_id(self):
3577 3572 from rhodecode.model.pull_request import PullRequestModel
3578 3573 return PullRequestModel()._workspace_id(self)
3579 3574
3580 3575 def get_shadow_repo(self):
3581 3576 workspace_id = self.workspace_id
3582 3577 vcs_obj = self.target_repo.scm_instance()
3583 3578 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3584 3579 workspace_id)
3585 3580 return vcs_obj._get_shadow_instance(shadow_repository_path)
3586 3581
3587 3582
3588 3583 class PullRequestVersion(Base, _PullRequestBase):
3589 3584 __tablename__ = 'pull_request_versions'
3590 3585 __table_args__ = (
3591 3586 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3592 3587 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3593 3588 )
3594 3589
3595 3590 pull_request_version_id = Column(
3596 3591 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3597 3592 pull_request_id = Column(
3598 3593 'pull_request_id', Integer(),
3599 3594 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3600 3595 pull_request = relationship('PullRequest')
3601 3596
3602 3597 def __repr__(self):
3603 3598 if self.pull_request_version_id:
3604 3599 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3605 3600 else:
3606 3601 return '<DB:PullRequestVersion at %#x>' % id(self)
3607 3602
3608 3603 @property
3609 3604 def reviewers(self):
3610 3605 return self.pull_request.reviewers
3611 3606
3612 3607 @property
3613 3608 def versions(self):
3614 3609 return self.pull_request.versions
3615 3610
3616 3611 def is_closed(self):
3617 3612 # calculate from original
3618 3613 return self.pull_request.status == self.STATUS_CLOSED
3619 3614
3620 3615 def calculated_review_status(self):
3621 3616 return self.pull_request.calculated_review_status()
3622 3617
3623 3618 def reviewers_statuses(self):
3624 3619 return self.pull_request.reviewers_statuses()
3625 3620
3626 3621
3627 3622 class PullRequestReviewers(Base, BaseModel):
3628 3623 __tablename__ = 'pull_request_reviewers'
3629 3624 __table_args__ = (
3630 3625 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3631 3626 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3632 3627 )
3633 3628
3634 3629 @hybrid_property
3635 3630 def reasons(self):
3636 3631 if not self._reasons:
3637 3632 return []
3638 3633 return self._reasons
3639 3634
3640 3635 @reasons.setter
3641 3636 def reasons(self, val):
3642 3637 val = val or []
3643 3638 if any(not isinstance(x, basestring) for x in val):
3644 3639 raise Exception('invalid reasons type, must be list of strings')
3645 3640 self._reasons = val
3646 3641
3647 3642 pull_requests_reviewers_id = Column(
3648 3643 'pull_requests_reviewers_id', Integer(), nullable=False,
3649 3644 primary_key=True)
3650 3645 pull_request_id = Column(
3651 3646 "pull_request_id", Integer(),
3652 3647 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3653 3648 user_id = Column(
3654 3649 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3655 3650 _reasons = Column(
3656 3651 'reason', MutationList.as_mutable(
3657 3652 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3658 3653 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3659 3654 user = relationship('User')
3660 3655 pull_request = relationship('PullRequest')
3661 3656
3662 3657
3663 3658 class Notification(Base, BaseModel):
3664 3659 __tablename__ = 'notifications'
3665 3660 __table_args__ = (
3666 3661 Index('notification_type_idx', 'type'),
3667 3662 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3668 3663 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3669 3664 )
3670 3665
3671 3666 TYPE_CHANGESET_COMMENT = u'cs_comment'
3672 3667 TYPE_MESSAGE = u'message'
3673 3668 TYPE_MENTION = u'mention'
3674 3669 TYPE_REGISTRATION = u'registration'
3675 3670 TYPE_PULL_REQUEST = u'pull_request'
3676 3671 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3677 3672
3678 3673 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3679 3674 subject = Column('subject', Unicode(512), nullable=True)
3680 3675 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3681 3676 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3682 3677 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3683 3678 type_ = Column('type', Unicode(255))
3684 3679
3685 3680 created_by_user = relationship('User')
3686 3681 notifications_to_users = relationship('UserNotification', lazy='joined',
3687 3682 cascade="all, delete, delete-orphan")
3688 3683
3689 3684 @property
3690 3685 def recipients(self):
3691 3686 return [x.user for x in UserNotification.query()\
3692 3687 .filter(UserNotification.notification == self)\
3693 3688 .order_by(UserNotification.user_id.asc()).all()]
3694 3689
3695 3690 @classmethod
3696 3691 def create(cls, created_by, subject, body, recipients, type_=None):
3697 3692 if type_ is None:
3698 3693 type_ = Notification.TYPE_MESSAGE
3699 3694
3700 3695 notification = cls()
3701 3696 notification.created_by_user = created_by
3702 3697 notification.subject = subject
3703 3698 notification.body = body
3704 3699 notification.type_ = type_
3705 3700 notification.created_on = datetime.datetime.now()
3706 3701
3707 3702 for u in recipients:
3708 3703 assoc = UserNotification()
3709 3704 assoc.notification = notification
3710 3705
3711 3706 # if created_by is inside recipients mark his notification
3712 3707 # as read
3713 3708 if u.user_id == created_by.user_id:
3714 3709 assoc.read = True
3715 3710
3716 3711 u.notifications.append(assoc)
3717 3712 Session().add(notification)
3718 3713
3719 3714 return notification
3720 3715
3721 3716
3722 3717 class UserNotification(Base, BaseModel):
3723 3718 __tablename__ = 'user_to_notification'
3724 3719 __table_args__ = (
3725 3720 UniqueConstraint('user_id', 'notification_id'),
3726 3721 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3727 3722 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3728 3723 )
3729 3724 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3730 3725 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3731 3726 read = Column('read', Boolean, default=False)
3732 3727 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3733 3728
3734 3729 user = relationship('User', lazy="joined")
3735 3730 notification = relationship('Notification', lazy="joined",
3736 3731 order_by=lambda: Notification.created_on.desc(),)
3737 3732
3738 3733 def mark_as_read(self):
3739 3734 self.read = True
3740 3735 Session().add(self)
3741 3736
3742 3737
3743 3738 class Gist(Base, BaseModel):
3744 3739 __tablename__ = 'gists'
3745 3740 __table_args__ = (
3746 3741 Index('g_gist_access_id_idx', 'gist_access_id'),
3747 3742 Index('g_created_on_idx', 'created_on'),
3748 3743 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3749 3744 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3750 3745 )
3751 3746 GIST_PUBLIC = u'public'
3752 3747 GIST_PRIVATE = u'private'
3753 3748 DEFAULT_FILENAME = u'gistfile1.txt'
3754 3749
3755 3750 ACL_LEVEL_PUBLIC = u'acl_public'
3756 3751 ACL_LEVEL_PRIVATE = u'acl_private'
3757 3752
3758 3753 gist_id = Column('gist_id', Integer(), primary_key=True)
3759 3754 gist_access_id = Column('gist_access_id', Unicode(250))
3760 3755 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3761 3756 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3762 3757 gist_expires = Column('gist_expires', Float(53), nullable=False)
3763 3758 gist_type = Column('gist_type', Unicode(128), nullable=False)
3764 3759 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3765 3760 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3766 3761 acl_level = Column('acl_level', Unicode(128), nullable=True)
3767 3762
3768 3763 owner = relationship('User')
3769 3764
3770 3765 def __repr__(self):
3771 3766 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3772 3767
3773 3768 @hybrid_property
3774 3769 def description_safe(self):
3775 3770 from rhodecode.lib import helpers as h
3776 3771 return h.escape(self.gist_description)
3777 3772
3778 3773 @classmethod
3779 def get_or_404(cls, id_, pyramid_exc=False):
3780
3781 if pyramid_exc:
3782 from pyramid.httpexceptions import HTTPNotFound
3783 else:
3784 from webob.exc import HTTPNotFound
3774 def get_or_404(cls, id_):
3775 from pyramid.httpexceptions import HTTPNotFound
3785 3776
3786 3777 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3787 3778 if not res:
3788 raise HTTPNotFound
3779 raise HTTPNotFound()
3789 3780 return res
3790 3781
3791 3782 @classmethod
3792 3783 def get_by_access_id(cls, gist_access_id):
3793 3784 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3794 3785
3795 3786 def gist_url(self):
3796 3787 from rhodecode.model.gist import GistModel
3797 3788 return GistModel().get_url(self)
3798 3789
3799 3790 @classmethod
3800 3791 def base_path(cls):
3801 3792 """
3802 3793 Returns base path when all gists are stored
3803 3794
3804 3795 :param cls:
3805 3796 """
3806 3797 from rhodecode.model.gist import GIST_STORE_LOC
3807 3798 q = Session().query(RhodeCodeUi)\
3808 3799 .filter(RhodeCodeUi.ui_key == URL_SEP)
3809 3800 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3810 3801 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3811 3802
3812 3803 def get_api_data(self):
3813 3804 """
3814 3805 Common function for generating gist related data for API
3815 3806 """
3816 3807 gist = self
3817 3808 data = {
3818 3809 'gist_id': gist.gist_id,
3819 3810 'type': gist.gist_type,
3820 3811 'access_id': gist.gist_access_id,
3821 3812 'description': gist.gist_description,
3822 3813 'url': gist.gist_url(),
3823 3814 'expires': gist.gist_expires,
3824 3815 'created_on': gist.created_on,
3825 3816 'modified_at': gist.modified_at,
3826 3817 'content': None,
3827 3818 'acl_level': gist.acl_level,
3828 3819 }
3829 3820 return data
3830 3821
3831 3822 def __json__(self):
3832 3823 data = dict(
3833 3824 )
3834 3825 data.update(self.get_api_data())
3835 3826 return data
3836 3827 # SCM functions
3837 3828
3838 3829 def scm_instance(self, **kwargs):
3839 3830 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3840 3831 return get_vcs_instance(
3841 3832 repo_path=safe_str(full_repo_path), create=False)
3842 3833
3843 3834
3844 3835 class ExternalIdentity(Base, BaseModel):
3845 3836 __tablename__ = 'external_identities'
3846 3837 __table_args__ = (
3847 3838 Index('local_user_id_idx', 'local_user_id'),
3848 3839 Index('external_id_idx', 'external_id'),
3849 3840 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3850 3841 'mysql_charset': 'utf8'})
3851 3842
3852 3843 external_id = Column('external_id', Unicode(255), default=u'',
3853 3844 primary_key=True)
3854 3845 external_username = Column('external_username', Unicode(1024), default=u'')
3855 3846 local_user_id = Column('local_user_id', Integer(),
3856 3847 ForeignKey('users.user_id'), primary_key=True)
3857 3848 provider_name = Column('provider_name', Unicode(255), default=u'',
3858 3849 primary_key=True)
3859 3850 access_token = Column('access_token', String(1024), default=u'')
3860 3851 alt_token = Column('alt_token', String(1024), default=u'')
3861 3852 token_secret = Column('token_secret', String(1024), default=u'')
3862 3853
3863 3854 @classmethod
3864 3855 def by_external_id_and_provider(cls, external_id, provider_name,
3865 3856 local_user_id=None):
3866 3857 """
3867 3858 Returns ExternalIdentity instance based on search params
3868 3859
3869 3860 :param external_id:
3870 3861 :param provider_name:
3871 3862 :return: ExternalIdentity
3872 3863 """
3873 3864 query = cls.query()
3874 3865 query = query.filter(cls.external_id == external_id)
3875 3866 query = query.filter(cls.provider_name == provider_name)
3876 3867 if local_user_id:
3877 3868 query = query.filter(cls.local_user_id == local_user_id)
3878 3869 return query.first()
3879 3870
3880 3871 @classmethod
3881 3872 def user_by_external_id_and_provider(cls, external_id, provider_name):
3882 3873 """
3883 3874 Returns User instance based on search params
3884 3875
3885 3876 :param external_id:
3886 3877 :param provider_name:
3887 3878 :return: User
3888 3879 """
3889 3880 query = User.query()
3890 3881 query = query.filter(cls.external_id == external_id)
3891 3882 query = query.filter(cls.provider_name == provider_name)
3892 3883 query = query.filter(User.user_id == cls.local_user_id)
3893 3884 return query.first()
3894 3885
3895 3886 @classmethod
3896 3887 def by_local_user_id(cls, local_user_id):
3897 3888 """
3898 3889 Returns all tokens for user
3899 3890
3900 3891 :param local_user_id:
3901 3892 :return: ExternalIdentity
3902 3893 """
3903 3894 query = cls.query()
3904 3895 query = query.filter(cls.local_user_id == local_user_id)
3905 3896 return query
3906 3897
3907 3898
3908 3899 class Integration(Base, BaseModel):
3909 3900 __tablename__ = 'integrations'
3910 3901 __table_args__ = (
3911 3902 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3912 3903 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3913 3904 )
3914 3905
3915 3906 integration_id = Column('integration_id', Integer(), primary_key=True)
3916 3907 integration_type = Column('integration_type', String(255))
3917 3908 enabled = Column('enabled', Boolean(), nullable=False)
3918 3909 name = Column('name', String(255), nullable=False)
3919 3910 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3920 3911 default=False)
3921 3912
3922 3913 settings = Column(
3923 3914 'settings_json', MutationObj.as_mutable(
3924 3915 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3925 3916 repo_id = Column(
3926 3917 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3927 3918 nullable=True, unique=None, default=None)
3928 3919 repo = relationship('Repository', lazy='joined')
3929 3920
3930 3921 repo_group_id = Column(
3931 3922 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3932 3923 nullable=True, unique=None, default=None)
3933 3924 repo_group = relationship('RepoGroup', lazy='joined')
3934 3925
3935 3926 @property
3936 3927 def scope(self):
3937 3928 if self.repo:
3938 3929 return repr(self.repo)
3939 3930 if self.repo_group:
3940 3931 if self.child_repos_only:
3941 3932 return repr(self.repo_group) + ' (child repos only)'
3942 3933 else:
3943 3934 return repr(self.repo_group) + ' (recursive)'
3944 3935 if self.child_repos_only:
3945 3936 return 'root_repos'
3946 3937 return 'global'
3947 3938
3948 3939 def __repr__(self):
3949 3940 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3950 3941
3951 3942
3952 3943 class RepoReviewRuleUser(Base, BaseModel):
3953 3944 __tablename__ = 'repo_review_rules_users'
3954 3945 __table_args__ = (
3955 3946 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3956 3947 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3957 3948 )
3958 3949 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3959 3950 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3960 3951 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3961 3952 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3962 3953 user = relationship('User')
3963 3954
3964 3955 def rule_data(self):
3965 3956 return {
3966 3957 'mandatory': self.mandatory
3967 3958 }
3968 3959
3969 3960
3970 3961 class RepoReviewRuleUserGroup(Base, BaseModel):
3971 3962 __tablename__ = 'repo_review_rules_users_groups'
3972 3963 __table_args__ = (
3973 3964 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3974 3965 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3975 3966 )
3976 3967 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3977 3968 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3978 3969 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3979 3970 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3980 3971 users_group = relationship('UserGroup')
3981 3972
3982 3973 def rule_data(self):
3983 3974 return {
3984 3975 'mandatory': self.mandatory
3985 3976 }
3986 3977
3987 3978
3988 3979 class RepoReviewRule(Base, BaseModel):
3989 3980 __tablename__ = 'repo_review_rules'
3990 3981 __table_args__ = (
3991 3982 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3992 3983 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3993 3984 )
3994 3985
3995 3986 repo_review_rule_id = Column(
3996 3987 'repo_review_rule_id', Integer(), primary_key=True)
3997 3988 repo_id = Column(
3998 3989 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3999 3990 repo = relationship('Repository', backref='review_rules')
4000 3991
4001 3992 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4002 3993 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4003 3994
4004 3995 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4005 3996 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4006 3997 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4007 3998 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4008 3999
4009 4000 rule_users = relationship('RepoReviewRuleUser')
4010 4001 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4011 4002
4012 4003 @hybrid_property
4013 4004 def branch_pattern(self):
4014 4005 return self._branch_pattern or '*'
4015 4006
4016 4007 def _validate_glob(self, value):
4017 4008 re.compile('^' + glob2re(value) + '$')
4018 4009
4019 4010 @branch_pattern.setter
4020 4011 def branch_pattern(self, value):
4021 4012 self._validate_glob(value)
4022 4013 self._branch_pattern = value or '*'
4023 4014
4024 4015 @hybrid_property
4025 4016 def file_pattern(self):
4026 4017 return self._file_pattern or '*'
4027 4018
4028 4019 @file_pattern.setter
4029 4020 def file_pattern(self, value):
4030 4021 self._validate_glob(value)
4031 4022 self._file_pattern = value or '*'
4032 4023
4033 4024 def matches(self, branch, files_changed):
4034 4025 """
4035 4026 Check if this review rule matches a branch/files in a pull request
4036 4027
4037 4028 :param branch: branch name for the commit
4038 4029 :param files_changed: list of file paths changed in the pull request
4039 4030 """
4040 4031
4041 4032 branch = branch or ''
4042 4033 files_changed = files_changed or []
4043 4034
4044 4035 branch_matches = True
4045 4036 if branch:
4046 4037 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4047 4038 branch_matches = bool(branch_regex.search(branch))
4048 4039
4049 4040 files_matches = True
4050 4041 if self.file_pattern != '*':
4051 4042 files_matches = False
4052 4043 file_regex = re.compile(glob2re(self.file_pattern))
4053 4044 for filename in files_changed:
4054 4045 if file_regex.search(filename):
4055 4046 files_matches = True
4056 4047 break
4057 4048
4058 4049 return branch_matches and files_matches
4059 4050
4060 4051 @property
4061 4052 def review_users(self):
4062 4053 """ Returns the users which this rule applies to """
4063 4054
4064 4055 users = collections.OrderedDict()
4065 4056
4066 4057 for rule_user in self.rule_users:
4067 4058 if rule_user.user.active:
4068 4059 if rule_user.user not in users:
4069 4060 users[rule_user.user.username] = {
4070 4061 'user': rule_user.user,
4071 4062 'source': 'user',
4072 4063 'source_data': {},
4073 4064 'data': rule_user.rule_data()
4074 4065 }
4075 4066
4076 4067 for rule_user_group in self.rule_user_groups:
4077 4068 source_data = {
4078 4069 'name': rule_user_group.users_group.users_group_name,
4079 4070 'members': len(rule_user_group.users_group.members)
4080 4071 }
4081 4072 for member in rule_user_group.users_group.members:
4082 4073 if member.user.active:
4083 4074 users[member.user.username] = {
4084 4075 'user': member.user,
4085 4076 'source': 'user_group',
4086 4077 'source_data': source_data,
4087 4078 'data': rule_user_group.rule_data()
4088 4079 }
4089 4080
4090 4081 return users
4091 4082
4092 4083 def __repr__(self):
4093 4084 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4094 4085 self.repo_review_rule_id, self.repo)
4095 4086
4096 4087
4097 4088 class DbMigrateVersion(Base, BaseModel):
4098 4089 __tablename__ = 'db_migrate_version'
4099 4090 __table_args__ = (
4100 4091 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4101 4092 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4102 4093 )
4103 4094 repository_id = Column('repository_id', String(250), primary_key=True)
4104 4095 repository_path = Column('repository_path', Text)
4105 4096 version = Column('version', Integer)
4106 4097
4107 4098
4108 4099 class DbSession(Base, BaseModel):
4109 4100 __tablename__ = 'db_session'
4110 4101 __table_args__ = (
4111 4102 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4112 4103 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4113 4104 )
4114 4105
4115 4106 def __repr__(self):
4116 4107 return '<DB:DbSession({})>'.format(self.id)
4117 4108
4118 4109 id = Column('id', Integer())
4119 4110 namespace = Column('namespace', String(255), primary_key=True)
4120 4111 accessed = Column('accessed', DateTime, nullable=False)
4121 4112 created = Column('created', DateTime, nullable=False)
4122 4113 data = Column('data', PickleType, nullable=False)
General Comments 0
You need to be logged in to leave comments. Login now