##// END OF EJS Templates
pull-requests: redo my account pull request page with datagrid. Fixes #4297...
marcink -
r1084:84545e70 default
parent child Browse files
Show More
@@ -1,371 +1,432 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 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 """
23 23 my account controller for RhodeCode admin
24 24 """
25 25
26 26 import logging
27 27
28 28 import formencode
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, session
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from sqlalchemy.orm import joinedload
34 34
35 35 from rhodecode import forms
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib import auth
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
40 40 from rhodecode.lib.base import BaseController, render
41 41 from rhodecode.lib.utils import jsonify
42 from rhodecode.lib.utils2 import safe_int, md5
42 from rhodecode.lib.utils2 import safe_int, md5, str2bool
43 43 from rhodecode.lib.ext_json import json
44 44
45 45 from rhodecode.model.validation_schema.schemas import user_schema
46 46 from rhodecode.model.db import (
47 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
48 UserFollowing)
47 Repository, PullRequest, UserEmailMap, User, UserFollowing)
49 48 from rhodecode.model.forms import UserForm
50 49 from rhodecode.model.scm import RepoList
51 50 from rhodecode.model.user import UserModel
52 51 from rhodecode.model.repo import RepoModel
53 52 from rhodecode.model.auth_token import AuthTokenModel
54 53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel
55 from rhodecode.model.comment import ChangesetCommentsModel
55 56
56 57 log = logging.getLogger(__name__)
57 58
58 59
59 60 class MyAccountController(BaseController):
60 61 """REST Controller styled on the Atom Publishing Protocol"""
61 62 # To properly map this controller, ensure your config/routing.py
62 63 # file has a resource setup:
63 64 # map.resource('setting', 'settings', controller='admin/settings',
64 65 # path_prefix='/admin', name_prefix='admin_')
65 66
66 67 @LoginRequired()
67 68 @NotAnonymous()
68 69 def __before__(self):
69 70 super(MyAccountController, self).__before__()
70 71
71 72 def __load_data(self):
72 73 c.user = User.get(c.rhodecode_user.user_id)
73 74 if c.user.username == User.DEFAULT_USER:
74 75 h.flash(_("You can't edit this user since it's"
75 76 " crucial for entire application"), category='warning')
76 77 return redirect(url('users'))
77 78
78 79 def _load_my_repos_data(self, watched=False):
79 80 if watched:
80 81 admin = False
81 82 follows_repos = Session().query(UserFollowing)\
82 83 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
83 84 .options(joinedload(UserFollowing.follows_repository))\
84 85 .all()
85 86 repo_list = [x.follows_repository for x in follows_repos]
86 87 else:
87 88 admin = True
88 89 repo_list = Repository.get_all_repos(
89 90 user_id=c.rhodecode_user.user_id)
90 91 repo_list = RepoList(repo_list, perm_set=[
91 92 'repository.read', 'repository.write', 'repository.admin'])
92 93
93 94 repos_data = RepoModel().get_repos_as_dict(
94 95 repo_list=repo_list, admin=admin)
95 96 # json used to render the grid
96 97 return json.dumps(repos_data)
97 98
98 99 @auth.CSRFRequired()
99 100 def my_account_update(self):
100 101 """
101 102 POST /_admin/my_account Updates info of my account
102 103 """
103 104 # url('my_account')
104 105 c.active = 'profile_edit'
105 106 self.__load_data()
106 107 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
107 108 ip_addr=self.ip_addr)
108 109 c.extern_type = c.user.extern_type
109 110 c.extern_name = c.user.extern_name
110 111
111 112 defaults = c.user.get_dict()
112 113 update = False
113 114 _form = UserForm(edit=True,
114 115 old_data={'user_id': c.rhodecode_user.user_id,
115 116 'email': c.rhodecode_user.email})()
116 117 form_result = {}
117 118 try:
118 119 post_data = dict(request.POST)
119 120 post_data['new_password'] = ''
120 121 post_data['password_confirmation'] = ''
121 122 form_result = _form.to_python(post_data)
122 123 # skip updating those attrs for my account
123 124 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
124 125 'new_password', 'password_confirmation']
125 126 # TODO: plugin should define if username can be updated
126 127 if c.extern_type != "rhodecode":
127 128 # forbid updating username for external accounts
128 129 skip_attrs.append('username')
129 130
130 131 UserModel().update_user(
131 132 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
132 133 h.flash(_('Your account was updated successfully'),
133 134 category='success')
134 135 Session().commit()
135 136 update = True
136 137
137 138 except formencode.Invalid as errors:
138 139 return htmlfill.render(
139 140 render('admin/my_account/my_account.html'),
140 141 defaults=errors.value,
141 142 errors=errors.error_dict or {},
142 143 prefix_error=False,
143 144 encoding="UTF-8",
144 145 force_defaults=False)
145 146 except Exception:
146 147 log.exception("Exception updating user")
147 148 h.flash(_('Error occurred during update of user %s')
148 149 % form_result.get('username'), category='error')
149 150
150 151 if update:
151 152 return redirect('my_account')
152 153
153 154 return htmlfill.render(
154 155 render('admin/my_account/my_account.html'),
155 156 defaults=defaults,
156 157 encoding="UTF-8",
157 158 force_defaults=False
158 159 )
159 160
160 161 def my_account(self):
161 162 """
162 163 GET /_admin/my_account Displays info about my account
163 164 """
164 165 # url('my_account')
165 166 c.active = 'profile'
166 167 self.__load_data()
167 168
168 169 defaults = c.user.get_dict()
169 170 return htmlfill.render(
170 171 render('admin/my_account/my_account.html'),
171 172 defaults=defaults, encoding="UTF-8", force_defaults=False)
172 173
173 174 def my_account_edit(self):
174 175 """
175 176 GET /_admin/my_account/edit Displays edit form of my account
176 177 """
177 178 c.active = 'profile_edit'
178 179 self.__load_data()
179 180 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
180 181 ip_addr=self.ip_addr)
181 182 c.extern_type = c.user.extern_type
182 183 c.extern_name = c.user.extern_name
183 184
184 185 defaults = c.user.get_dict()
185 186 return htmlfill.render(
186 187 render('admin/my_account/my_account.html'),
187 188 defaults=defaults,
188 189 encoding="UTF-8",
189 190 force_defaults=False
190 191 )
191 192
192 193 @auth.CSRFRequired(except_methods=['GET'])
193 194 def my_account_password(self):
194 195 c.active = 'password'
195 196 self.__load_data()
196 197
197 198 schema = user_schema.ChangePasswordSchema().bind(
198 199 username=c.rhodecode_user.username)
199 200
200 201 form = forms.Form(schema,
201 202 buttons=(forms.buttons.save, forms.buttons.reset))
202 203
203 204 if request.method == 'POST':
204 205 controls = request.POST.items()
205 206 try:
206 207 valid_data = form.validate(controls)
207 208 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
208 209 instance = c.rhodecode_user.get_instance()
209 210 instance.update_userdata(force_password_change=False)
210 211 Session().commit()
211 212 except forms.ValidationFailure as e:
212 213 request.session.flash(
213 214 _('Error occurred during update of user password'),
214 215 queue='error')
215 216 form = e
216 217 except Exception:
217 218 log.exception("Exception updating password")
218 219 request.session.flash(
219 220 _('Error occurred during update of user password'),
220 221 queue='error')
221 222 else:
222 223 session.setdefault('rhodecode_user', {}).update(
223 224 {'password': md5(instance.password)})
224 225 session.save()
225 226 request.session.flash(
226 227 _("Successfully updated password"), queue='success')
227 228 return redirect(url('my_account_password'))
228 229
229 230 c.form = form
230 231 return render('admin/my_account/my_account.html')
231 232
232 233 def my_account_repos(self):
233 234 c.active = 'repos'
234 235 self.__load_data()
235 236
236 237 # json used to render the grid
237 238 c.data = self._load_my_repos_data()
238 239 return render('admin/my_account/my_account.html')
239 240
240 241 def my_account_watched(self):
241 242 c.active = 'watched'
242 243 self.__load_data()
243 244
244 245 # json used to render the grid
245 246 c.data = self._load_my_repos_data(watched=True)
246 247 return render('admin/my_account/my_account.html')
247 248
248 249 def my_account_perms(self):
249 250 c.active = 'perms'
250 251 self.__load_data()
251 252 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
252 253 ip_addr=self.ip_addr)
253 254
254 255 return render('admin/my_account/my_account.html')
255 256
256 257 def my_account_emails(self):
257 258 c.active = 'emails'
258 259 self.__load_data()
259 260
260 261 c.user_email_map = UserEmailMap.query()\
261 262 .filter(UserEmailMap.user == c.user).all()
262 263 return render('admin/my_account/my_account.html')
263 264
264 265 @auth.CSRFRequired()
265 266 def my_account_emails_add(self):
266 267 email = request.POST.get('new_email')
267 268
268 269 try:
269 270 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
270 271 Session().commit()
271 272 h.flash(_("Added new email address `%s` for user account") % email,
272 273 category='success')
273 274 except formencode.Invalid as error:
274 275 msg = error.error_dict['email']
275 276 h.flash(msg, category='error')
276 277 except Exception:
277 278 log.exception("Exception in my_account_emails")
278 279 h.flash(_('An error occurred during email saving'),
279 280 category='error')
280 281 return redirect(url('my_account_emails'))
281 282
282 283 @auth.CSRFRequired()
283 284 def my_account_emails_delete(self):
284 285 email_id = request.POST.get('del_email_id')
285 286 user_model = UserModel()
286 287 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
287 288 Session().commit()
288 289 h.flash(_("Removed email address from user account"),
289 290 category='success')
290 291 return redirect(url('my_account_emails'))
291 292
293 def _extract_ordering(self, request):
294 column_index = safe_int(request.GET.get('order[0][column]'))
295 order_dir = request.GET.get('order[0][dir]', 'desc')
296 order_by = request.GET.get(
297 'columns[%s][data][sort]' % column_index, 'name_raw')
298 return order_by, order_dir
299
300 def _get_pull_requests_list(self, statuses):
301 start = safe_int(request.GET.get('start'), 0)
302 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
303 order_by, order_dir = self._extract_ordering(request)
304
305 pull_requests = PullRequestModel().get_im_participating_in(
306 user_id=c.rhodecode_user.user_id,
307 statuses=statuses,
308 offset=start, length=length, order_by=order_by,
309 order_dir=order_dir)
310
311 pull_requests_total_count = PullRequestModel().count_im_participating_in(
312 user_id=c.rhodecode_user.user_id, statuses=statuses)
313
314 from rhodecode.lib.utils import PartialRenderer
315 _render = PartialRenderer('data_table/_dt_elements.html')
316 data = []
317 for pr in pull_requests:
318 repo_id = pr.target_repo_id
319 comments = ChangesetCommentsModel().get_all_comments(
320 repo_id, pull_request=pr)
321 owned = pr.user_id == c.rhodecode_user.user_id
322 status = pr.calculated_review_status()
323
324 data.append({
325 'target_repo': _render('pullrequest_target_repo',
326 pr.target_repo.repo_name),
327 'name': _render('pullrequest_name',
328 pr.pull_request_id, pr.target_repo.repo_name,
329 short=True),
330 'name_raw': pr.pull_request_id,
331 'status': _render('pullrequest_status', status),
332 'title': _render(
333 'pullrequest_title', pr.title, pr.description),
334 'description': h.escape(pr.description),
335 'updated_on': _render('pullrequest_updated_on',
336 h.datetime_to_time(pr.updated_on)),
337 'updated_on_raw': h.datetime_to_time(pr.updated_on),
338 'created_on': _render('pullrequest_updated_on',
339 h.datetime_to_time(pr.created_on)),
340 'created_on_raw': h.datetime_to_time(pr.created_on),
341 'author': _render('pullrequest_author',
342 pr.author.full_contact, ),
343 'author_raw': pr.author.full_name,
344 'comments': _render('pullrequest_comments', len(comments)),
345 'comments_raw': len(comments),
346 'closed': pr.is_closed(),
347 'owned': owned
348 })
349 # json used to render the grid
350 data = ({
351 'data': data,
352 'recordsTotal': pull_requests_total_count,
353 'recordsFiltered': pull_requests_total_count,
354 })
355 return data
356
292 357 def my_account_pullrequests(self):
293 358 c.active = 'pullrequests'
294 359 self.__load_data()
295 c.show_closed = request.GET.get('pr_show_closed')
296
297 def _filter(pr):
298 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
299 if not c.show_closed:
300 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
301 return s
360 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
302 361
303 c.my_pull_requests = _filter(
304 PullRequest.query().filter(
305 PullRequest.user_id == c.rhodecode_user.user_id).all())
306 my_prs = [
307 x.pull_request for x in PullRequestReviewers.query().filter(
308 PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
309 c.participate_in_pull_requests = _filter(my_prs)
310 return render('admin/my_account/my_account.html')
362 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
363 if c.show_closed:
364 statuses += [PullRequest.STATUS_CLOSED]
365 data = self._get_pull_requests_list(statuses)
366 if not request.is_xhr:
367 c.data_participate = json.dumps(data['data'])
368 c.records_total_participate = data['recordsTotal']
369 return render('admin/my_account/my_account.html')
370 else:
371 return json.dumps(data)
311 372
312 373 def my_account_auth_tokens(self):
313 374 c.active = 'auth_tokens'
314 375 self.__load_data()
315 376 show_expired = True
316 377 c.lifetime_values = [
317 378 (str(-1), _('forever')),
318 379 (str(5), _('5 minutes')),
319 380 (str(60), _('1 hour')),
320 381 (str(60 * 24), _('1 day')),
321 382 (str(60 * 24 * 30), _('1 month')),
322 383 ]
323 384 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
324 385 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
325 386 for x in AuthTokenModel.cls.ROLES]
326 387 c.role_options = [(c.role_values, _("Role"))]
327 388 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
328 389 c.rhodecode_user.user_id, show_expired=show_expired)
329 390 return render('admin/my_account/my_account.html')
330 391
331 392 @auth.CSRFRequired()
332 393 def my_account_auth_tokens_add(self):
333 394 lifetime = safe_int(request.POST.get('lifetime'), -1)
334 395 description = request.POST.get('description')
335 396 role = request.POST.get('role')
336 397 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
337 398 role)
338 399 Session().commit()
339 400 h.flash(_("Auth token successfully created"), category='success')
340 401 return redirect(url('my_account_auth_tokens'))
341 402
342 403 @auth.CSRFRequired()
343 404 def my_account_auth_tokens_delete(self):
344 405 auth_token = request.POST.get('del_auth_token')
345 406 user_id = c.rhodecode_user.user_id
346 407 if request.POST.get('del_auth_token_builtin'):
347 408 user = User.get(user_id)
348 409 if user:
349 410 user.api_key = generate_auth_token(user.username)
350 411 Session().add(user)
351 412 Session().commit()
352 413 h.flash(_("Auth token successfully reset"), category='success')
353 414 elif auth_token:
354 415 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
355 416 Session().commit()
356 417 h.flash(_("Auth token successfully deleted"), category='success')
357 418
358 419 return redirect(url('my_account_auth_tokens'))
359 420
360 421 def my_notifications(self):
361 422 c.active = 'notifications'
362 423 return render('admin/my_account/my_account.html')
363 424
364 425 @auth.CSRFRequired()
365 426 @jsonify
366 427 def my_notifications_toggle_visibility(self):
367 428 user = c.rhodecode_user.get_instance()
368 429 new_status = not user.user_data.get('notification_status', True)
369 430 user.update_userdata(notification_status=new_status)
370 431 Session().commit()
371 432 return user.user_data['notification_status']
@@ -1,3697 +1,3693 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PUSH = 'changegroup.push_logger'
354 354
355 355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 356 # git part is currently hardcoded.
357 357
358 358 # SVN PATTERNS
359 359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 360 SVN_TAG_ID = 'vcs_svn_tag'
361 361
362 362 ui_id = Column(
363 363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 364 primary_key=True)
365 365 ui_section = Column(
366 366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 367 ui_key = Column(
368 368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 369 ui_value = Column(
370 370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 371 ui_active = Column(
372 372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373 373
374 374 def __repr__(self):
375 375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 376 self.ui_key, self.ui_value)
377 377
378 378
379 379 class RepoRhodeCodeSetting(Base, BaseModel):
380 380 __tablename__ = 'repo_rhodecode_settings'
381 381 __table_args__ = (
382 382 UniqueConstraint(
383 383 'app_settings_name', 'repository_id',
384 384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 387 )
388 388
389 389 repository_id = Column(
390 390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 391 nullable=False)
392 392 app_settings_id = Column(
393 393 "app_settings_id", Integer(), nullable=False, unique=True,
394 394 default=None, primary_key=True)
395 395 app_settings_name = Column(
396 396 "app_settings_name", String(255), nullable=True, unique=None,
397 397 default=None)
398 398 _app_settings_value = Column(
399 399 "app_settings_value", String(4096), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_type = Column(
402 402 "app_settings_type", String(255), nullable=True, unique=None,
403 403 default=None)
404 404
405 405 repository = relationship('Repository')
406 406
407 407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 408 self.repository_id = repository_id
409 409 self.app_settings_name = key
410 410 self.app_settings_type = type
411 411 self.app_settings_value = val
412 412
413 413 @validates('_app_settings_value')
414 414 def validate_settings_value(self, key, val):
415 415 assert type(val) == unicode
416 416 return val
417 417
418 418 @hybrid_property
419 419 def app_settings_value(self):
420 420 v = self._app_settings_value
421 421 type_ = self.app_settings_type
422 422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 424 return converter(v)
425 425
426 426 @app_settings_value.setter
427 427 def app_settings_value(self, val):
428 428 """
429 429 Setter that will always make sure we use unicode in app_settings_value
430 430
431 431 :param val:
432 432 """
433 433 self._app_settings_value = safe_unicode(val)
434 434
435 435 @hybrid_property
436 436 def app_settings_type(self):
437 437 return self._app_settings_type
438 438
439 439 @app_settings_type.setter
440 440 def app_settings_type(self, val):
441 441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 442 if val not in SETTINGS_TYPES:
443 443 raise Exception('type must be one of %s got %s'
444 444 % (SETTINGS_TYPES.keys(), val))
445 445 self._app_settings_type = val
446 446
447 447 def __unicode__(self):
448 448 return u"<%s('%s:%s:%s[%s]')>" % (
449 449 self.__class__.__name__, self.repository.repo_name,
450 450 self.app_settings_name, self.app_settings_value,
451 451 self.app_settings_type
452 452 )
453 453
454 454
455 455 class RepoRhodeCodeUi(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_ui'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'repository_id', 'ui_section', 'ui_key',
460 460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 463 )
464 464
465 465 repository_id = Column(
466 466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 467 nullable=False)
468 468 ui_id = Column(
469 469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 470 primary_key=True)
471 471 ui_section = Column(
472 472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 473 ui_key = Column(
474 474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 475 ui_value = Column(
476 476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 477 ui_active = Column(
478 478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __repr__(self):
483 483 return '<%s[%s:%s]%s=>%s]>' % (
484 484 self.__class__.__name__, self.repository.repo_name,
485 485 self.ui_section, self.ui_key, self.ui_value)
486 486
487 487
488 488 class User(Base, BaseModel):
489 489 __tablename__ = 'users'
490 490 __table_args__ = (
491 491 UniqueConstraint('username'), UniqueConstraint('email'),
492 492 Index('u_username_idx', 'username'),
493 493 Index('u_email_idx', 'email'),
494 494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 496 )
497 497 DEFAULT_USER = 'default'
498 498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500 500
501 501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516 516
517 517 user_log = relationship('UserLog')
518 518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519 519
520 520 repositories = relationship('Repository')
521 521 repository_groups = relationship('RepoGroup')
522 522 user_groups = relationship('UserGroup')
523 523
524 524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526 526
527 527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530 530
531 531 group_member = relationship('UserGroupMember', cascade='all')
532 532
533 533 notifications = relationship('UserNotification', cascade='all')
534 534 # notifications assigned to this user
535 535 user_created_notifications = relationship('Notification', cascade='all')
536 536 # comments created by this user
537 537 user_comments = relationship('ChangesetComment', cascade='all')
538 538 # user profile extra info
539 539 user_emails = relationship('UserEmailMap', cascade='all')
540 540 user_ip_map = relationship('UserIpMap', cascade='all')
541 541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 542 # gists
543 543 user_gists = relationship('Gist', cascade='all')
544 544 # user pull requests
545 545 user_pull_requests = relationship('PullRequest', cascade='all')
546 546 # external identities
547 547 extenal_identities = relationship(
548 548 'ExternalIdentity',
549 549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 550 cascade='all')
551 551
552 552 def __unicode__(self):
553 553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 554 self.user_id, self.username)
555 555
556 556 @hybrid_property
557 557 def email(self):
558 558 return self._email
559 559
560 560 @email.setter
561 561 def email(self, val):
562 562 self._email = val.lower() if val else None
563 563
564 564 @property
565 565 def firstname(self):
566 566 # alias for future
567 567 return self.name
568 568
569 569 @property
570 570 def emails(self):
571 571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 572 return [self.email] + [x.email for x in other]
573 573
574 574 @property
575 575 def auth_tokens(self):
576 576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577 577
578 578 @property
579 579 def extra_auth_tokens(self):
580 580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581 581
582 582 @property
583 583 def feed_token(self):
584 584 feed_tokens = UserApiKeys.query()\
585 585 .filter(UserApiKeys.user == self)\
586 586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 587 .all()
588 588 if feed_tokens:
589 589 return feed_tokens[0].api_key
590 590 else:
591 591 # use the main token so we don't end up with nothing...
592 592 return self.api_key
593 593
594 594 @classmethod
595 595 def extra_valid_auth_tokens(cls, user, role=None):
596 596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 597 .filter(or_(UserApiKeys.expires == -1,
598 598 UserApiKeys.expires >= time.time()))
599 599 if role:
600 600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 602 return tokens.all()
603 603
604 604 @property
605 605 def ip_addresses(self):
606 606 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
607 607 return [x.ip_addr for x in ret]
608 608
609 609 @property
610 610 def username_and_name(self):
611 611 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
612 612
613 613 @property
614 614 def username_or_name_or_email(self):
615 615 full_name = self.full_name if self.full_name is not ' ' else None
616 616 return self.username or full_name or self.email
617 617
618 618 @property
619 619 def full_name(self):
620 620 return '%s %s' % (self.firstname, self.lastname)
621 621
622 622 @property
623 623 def full_name_or_username(self):
624 624 return ('%s %s' % (self.firstname, self.lastname)
625 625 if (self.firstname and self.lastname) else self.username)
626 626
627 627 @property
628 628 def full_contact(self):
629 629 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
630 630
631 631 @property
632 632 def short_contact(self):
633 633 return '%s %s' % (self.firstname, self.lastname)
634 634
635 635 @property
636 636 def is_admin(self):
637 637 return self.admin
638 638
639 639 @property
640 640 def AuthUser(self):
641 641 """
642 642 Returns instance of AuthUser for this user
643 643 """
644 644 from rhodecode.lib.auth import AuthUser
645 645 return AuthUser(user_id=self.user_id, api_key=self.api_key,
646 646 username=self.username)
647 647
648 648 @hybrid_property
649 649 def user_data(self):
650 650 if not self._user_data:
651 651 return {}
652 652
653 653 try:
654 654 return json.loads(self._user_data)
655 655 except TypeError:
656 656 return {}
657 657
658 658 @user_data.setter
659 659 def user_data(self, val):
660 660 if not isinstance(val, dict):
661 661 raise Exception('user_data must be dict, got %s' % type(val))
662 662 try:
663 663 self._user_data = json.dumps(val)
664 664 except Exception:
665 665 log.error(traceback.format_exc())
666 666
667 667 @classmethod
668 668 def get_by_username(cls, username, case_insensitive=False,
669 669 cache=False, identity_cache=False):
670 670 session = Session()
671 671
672 672 if case_insensitive:
673 673 q = cls.query().filter(
674 674 func.lower(cls.username) == func.lower(username))
675 675 else:
676 676 q = cls.query().filter(cls.username == username)
677 677
678 678 if cache:
679 679 if identity_cache:
680 680 val = cls.identity_cache(session, 'username', username)
681 681 if val:
682 682 return val
683 683 else:
684 684 q = q.options(
685 685 FromCache("sql_cache_short",
686 686 "get_user_by_name_%s" % _hash_key(username)))
687 687
688 688 return q.scalar()
689 689
690 690 @classmethod
691 691 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
692 692 q = cls.query().filter(cls.api_key == auth_token)
693 693
694 694 if cache:
695 695 q = q.options(FromCache("sql_cache_short",
696 696 "get_auth_token_%s" % auth_token))
697 697 res = q.scalar()
698 698
699 699 if fallback and not res:
700 700 #fallback to additional keys
701 701 _res = UserApiKeys.query()\
702 702 .filter(UserApiKeys.api_key == auth_token)\
703 703 .filter(or_(UserApiKeys.expires == -1,
704 704 UserApiKeys.expires >= time.time()))\
705 705 .first()
706 706 if _res:
707 707 res = _res.user
708 708 return res
709 709
710 710 @classmethod
711 711 def get_by_email(cls, email, case_insensitive=False, cache=False):
712 712
713 713 if case_insensitive:
714 714 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
715 715
716 716 else:
717 717 q = cls.query().filter(cls.email == email)
718 718
719 719 if cache:
720 720 q = q.options(FromCache("sql_cache_short",
721 721 "get_email_key_%s" % _hash_key(email)))
722 722
723 723 ret = q.scalar()
724 724 if ret is None:
725 725 q = UserEmailMap.query()
726 726 # try fetching in alternate email map
727 727 if case_insensitive:
728 728 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
729 729 else:
730 730 q = q.filter(UserEmailMap.email == email)
731 731 q = q.options(joinedload(UserEmailMap.user))
732 732 if cache:
733 733 q = q.options(FromCache("sql_cache_short",
734 734 "get_email_map_key_%s" % email))
735 735 ret = getattr(q.scalar(), 'user', None)
736 736
737 737 return ret
738 738
739 739 @classmethod
740 740 def get_from_cs_author(cls, author):
741 741 """
742 742 Tries to get User objects out of commit author string
743 743
744 744 :param author:
745 745 """
746 746 from rhodecode.lib.helpers import email, author_name
747 747 # Valid email in the attribute passed, see if they're in the system
748 748 _email = email(author)
749 749 if _email:
750 750 user = cls.get_by_email(_email, case_insensitive=True)
751 751 if user:
752 752 return user
753 753 # Maybe we can match by username?
754 754 _author = author_name(author)
755 755 user = cls.get_by_username(_author, case_insensitive=True)
756 756 if user:
757 757 return user
758 758
759 759 def update_userdata(self, **kwargs):
760 760 usr = self
761 761 old = usr.user_data
762 762 old.update(**kwargs)
763 763 usr.user_data = old
764 764 Session().add(usr)
765 765 log.debug('updated userdata with ', kwargs)
766 766
767 767 def update_lastlogin(self):
768 768 """Update user lastlogin"""
769 769 self.last_login = datetime.datetime.now()
770 770 Session().add(self)
771 771 log.debug('updated user %s lastlogin', self.username)
772 772
773 773 def update_lastactivity(self):
774 774 """Update user lastactivity"""
775 775 usr = self
776 776 old = usr.user_data
777 777 old.update({'last_activity': time.time()})
778 778 usr.user_data = old
779 779 Session().add(usr)
780 780 log.debug('updated user %s lastactivity', usr.username)
781 781
782 782 def update_password(self, new_password, change_api_key=False):
783 783 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
784 784
785 785 self.password = get_crypt_password(new_password)
786 786 if change_api_key:
787 787 self.api_key = generate_auth_token(self.username)
788 788 Session().add(self)
789 789
790 790 @classmethod
791 791 def get_first_super_admin(cls):
792 792 user = User.query().filter(User.admin == true()).first()
793 793 if user is None:
794 794 raise Exception('FATAL: Missing administrative account!')
795 795 return user
796 796
797 797 @classmethod
798 798 def get_all_super_admins(cls):
799 799 """
800 800 Returns all admin accounts sorted by username
801 801 """
802 802 return User.query().filter(User.admin == true())\
803 803 .order_by(User.username.asc()).all()
804 804
805 805 @classmethod
806 806 def get_default_user(cls, cache=False):
807 807 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
808 808 if user is None:
809 809 raise Exception('FATAL: Missing default account!')
810 810 return user
811 811
812 812 def _get_default_perms(self, user, suffix=''):
813 813 from rhodecode.model.permission import PermissionModel
814 814 return PermissionModel().get_default_perms(user.user_perms, suffix)
815 815
816 816 def get_default_perms(self, suffix=''):
817 817 return self._get_default_perms(self, suffix)
818 818
819 819 def get_api_data(self, include_secrets=False, details='full'):
820 820 """
821 821 Common function for generating user related data for API
822 822
823 823 :param include_secrets: By default secrets in the API data will be replaced
824 824 by a placeholder value to prevent exposing this data by accident. In case
825 825 this data shall be exposed, set this flag to ``True``.
826 826
827 827 :param details: details can be 'basic|full' basic gives only a subset of
828 828 the available user information that includes user_id, name and emails.
829 829 """
830 830 user = self
831 831 user_data = self.user_data
832 832 data = {
833 833 'user_id': user.user_id,
834 834 'username': user.username,
835 835 'firstname': user.name,
836 836 'lastname': user.lastname,
837 837 'email': user.email,
838 838 'emails': user.emails,
839 839 }
840 840 if details == 'basic':
841 841 return data
842 842
843 843 api_key_length = 40
844 844 api_key_replacement = '*' * api_key_length
845 845
846 846 extras = {
847 847 'api_key': api_key_replacement,
848 848 'api_keys': [api_key_replacement],
849 849 'active': user.active,
850 850 'admin': user.admin,
851 851 'extern_type': user.extern_type,
852 852 'extern_name': user.extern_name,
853 853 'last_login': user.last_login,
854 854 'ip_addresses': user.ip_addresses,
855 855 'language': user_data.get('language')
856 856 }
857 857 data.update(extras)
858 858
859 859 if include_secrets:
860 860 data['api_key'] = user.api_key
861 861 data['api_keys'] = user.auth_tokens
862 862 return data
863 863
864 864 def __json__(self):
865 865 data = {
866 866 'full_name': self.full_name,
867 867 'full_name_or_username': self.full_name_or_username,
868 868 'short_contact': self.short_contact,
869 869 'full_contact': self.full_contact,
870 870 }
871 871 data.update(self.get_api_data())
872 872 return data
873 873
874 874
875 875 class UserApiKeys(Base, BaseModel):
876 876 __tablename__ = 'user_api_keys'
877 877 __table_args__ = (
878 878 Index('uak_api_key_idx', 'api_key'),
879 879 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
880 880 UniqueConstraint('api_key'),
881 881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
882 882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
883 883 )
884 884 __mapper_args__ = {}
885 885
886 886 # ApiKey role
887 887 ROLE_ALL = 'token_role_all'
888 888 ROLE_HTTP = 'token_role_http'
889 889 ROLE_VCS = 'token_role_vcs'
890 890 ROLE_API = 'token_role_api'
891 891 ROLE_FEED = 'token_role_feed'
892 892 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
893 893
894 894 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
895 895 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
896 896 api_key = Column("api_key", String(255), nullable=False, unique=True)
897 897 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
898 898 expires = Column('expires', Float(53), nullable=False)
899 899 role = Column('role', String(255), nullable=True)
900 900 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
901 901
902 902 user = relationship('User', lazy='joined')
903 903
904 904 @classmethod
905 905 def _get_role_name(cls, role):
906 906 return {
907 907 cls.ROLE_ALL: _('all'),
908 908 cls.ROLE_HTTP: _('http/web interface'),
909 909 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
910 910 cls.ROLE_API: _('api calls'),
911 911 cls.ROLE_FEED: _('feed access'),
912 912 }.get(role, role)
913 913
914 914 @property
915 915 def expired(self):
916 916 if self.expires == -1:
917 917 return False
918 918 return time.time() > self.expires
919 919
920 920 @property
921 921 def role_humanized(self):
922 922 return self._get_role_name(self.role)
923 923
924 924
925 925 class UserEmailMap(Base, BaseModel):
926 926 __tablename__ = 'user_email_map'
927 927 __table_args__ = (
928 928 Index('uem_email_idx', 'email'),
929 929 UniqueConstraint('email'),
930 930 {'extend_existing': True, 'mysql_engine': 'InnoDB',
931 931 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
932 932 )
933 933 __mapper_args__ = {}
934 934
935 935 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
936 936 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
937 937 _email = Column("email", String(255), nullable=True, unique=False, default=None)
938 938 user = relationship('User', lazy='joined')
939 939
940 940 @validates('_email')
941 941 def validate_email(self, key, email):
942 942 # check if this email is not main one
943 943 main_email = Session().query(User).filter(User.email == email).scalar()
944 944 if main_email is not None:
945 945 raise AttributeError('email %s is present is user table' % email)
946 946 return email
947 947
948 948 @hybrid_property
949 949 def email(self):
950 950 return self._email
951 951
952 952 @email.setter
953 953 def email(self, val):
954 954 self._email = val.lower() if val else None
955 955
956 956
957 957 class UserIpMap(Base, BaseModel):
958 958 __tablename__ = 'user_ip_map'
959 959 __table_args__ = (
960 960 UniqueConstraint('user_id', 'ip_addr'),
961 961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
962 962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
963 963 )
964 964 __mapper_args__ = {}
965 965
966 966 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
967 967 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
968 968 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
969 969 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
970 970 description = Column("description", String(10000), nullable=True, unique=None, default=None)
971 971 user = relationship('User', lazy='joined')
972 972
973 973 @classmethod
974 974 def _get_ip_range(cls, ip_addr):
975 975 net = ipaddress.ip_network(ip_addr, strict=False)
976 976 return [str(net.network_address), str(net.broadcast_address)]
977 977
978 978 def __json__(self):
979 979 return {
980 980 'ip_addr': self.ip_addr,
981 981 'ip_range': self._get_ip_range(self.ip_addr),
982 982 }
983 983
984 984 def __unicode__(self):
985 985 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
986 986 self.user_id, self.ip_addr)
987 987
988 988 class UserLog(Base, BaseModel):
989 989 __tablename__ = 'user_logs'
990 990 __table_args__ = (
991 991 {'extend_existing': True, 'mysql_engine': 'InnoDB',
992 992 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
993 993 )
994 994 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
995 995 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
996 996 username = Column("username", String(255), nullable=True, unique=None, default=None)
997 997 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
998 998 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
999 999 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1000 1000 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1001 1001 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1002 1002
1003 1003 def __unicode__(self):
1004 1004 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1005 1005 self.repository_name,
1006 1006 self.action)
1007 1007
1008 1008 @property
1009 1009 def action_as_day(self):
1010 1010 return datetime.date(*self.action_date.timetuple()[:3])
1011 1011
1012 1012 user = relationship('User')
1013 1013 repository = relationship('Repository', cascade='')
1014 1014
1015 1015
1016 1016 class UserGroup(Base, BaseModel):
1017 1017 __tablename__ = 'users_groups'
1018 1018 __table_args__ = (
1019 1019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1020 1020 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1021 1021 )
1022 1022
1023 1023 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1024 1024 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1025 1025 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1026 1026 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1027 1027 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1028 1028 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1029 1029 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1030 1030 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1031 1031
1032 1032 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1033 1033 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1034 1034 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1035 1035 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1036 1036 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1037 1037 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1038 1038
1039 1039 user = relationship('User')
1040 1040
1041 1041 @hybrid_property
1042 1042 def group_data(self):
1043 1043 if not self._group_data:
1044 1044 return {}
1045 1045
1046 1046 try:
1047 1047 return json.loads(self._group_data)
1048 1048 except TypeError:
1049 1049 return {}
1050 1050
1051 1051 @group_data.setter
1052 1052 def group_data(self, val):
1053 1053 try:
1054 1054 self._group_data = json.dumps(val)
1055 1055 except Exception:
1056 1056 log.error(traceback.format_exc())
1057 1057
1058 1058 def __unicode__(self):
1059 1059 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1060 1060 self.users_group_id,
1061 1061 self.users_group_name)
1062 1062
1063 1063 @classmethod
1064 1064 def get_by_group_name(cls, group_name, cache=False,
1065 1065 case_insensitive=False):
1066 1066 if case_insensitive:
1067 1067 q = cls.query().filter(func.lower(cls.users_group_name) ==
1068 1068 func.lower(group_name))
1069 1069
1070 1070 else:
1071 1071 q = cls.query().filter(cls.users_group_name == group_name)
1072 1072 if cache:
1073 1073 q = q.options(FromCache(
1074 1074 "sql_cache_short",
1075 1075 "get_group_%s" % _hash_key(group_name)))
1076 1076 return q.scalar()
1077 1077
1078 1078 @classmethod
1079 1079 def get(cls, user_group_id, cache=False):
1080 1080 user_group = cls.query()
1081 1081 if cache:
1082 1082 user_group = user_group.options(FromCache("sql_cache_short",
1083 1083 "get_users_group_%s" % user_group_id))
1084 1084 return user_group.get(user_group_id)
1085 1085
1086 1086 def permissions(self, with_admins=True, with_owner=True):
1087 1087 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1088 1088 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1089 1089 joinedload(UserUserGroupToPerm.user),
1090 1090 joinedload(UserUserGroupToPerm.permission),)
1091 1091
1092 1092 # get owners and admins and permissions. We do a trick of re-writing
1093 1093 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1094 1094 # has a global reference and changing one object propagates to all
1095 1095 # others. This means if admin is also an owner admin_row that change
1096 1096 # would propagate to both objects
1097 1097 perm_rows = []
1098 1098 for _usr in q.all():
1099 1099 usr = AttributeDict(_usr.user.get_dict())
1100 1100 usr.permission = _usr.permission.permission_name
1101 1101 perm_rows.append(usr)
1102 1102
1103 1103 # filter the perm rows by 'default' first and then sort them by
1104 1104 # admin,write,read,none permissions sorted again alphabetically in
1105 1105 # each group
1106 1106 perm_rows = sorted(perm_rows, key=display_sort)
1107 1107
1108 1108 _admin_perm = 'usergroup.admin'
1109 1109 owner_row = []
1110 1110 if with_owner:
1111 1111 usr = AttributeDict(self.user.get_dict())
1112 1112 usr.owner_row = True
1113 1113 usr.permission = _admin_perm
1114 1114 owner_row.append(usr)
1115 1115
1116 1116 super_admin_rows = []
1117 1117 if with_admins:
1118 1118 for usr in User.get_all_super_admins():
1119 1119 # if this admin is also owner, don't double the record
1120 1120 if usr.user_id == owner_row[0].user_id:
1121 1121 owner_row[0].admin_row = True
1122 1122 else:
1123 1123 usr = AttributeDict(usr.get_dict())
1124 1124 usr.admin_row = True
1125 1125 usr.permission = _admin_perm
1126 1126 super_admin_rows.append(usr)
1127 1127
1128 1128 return super_admin_rows + owner_row + perm_rows
1129 1129
1130 1130 def permission_user_groups(self):
1131 1131 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1132 1132 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1133 1133 joinedload(UserGroupUserGroupToPerm.target_user_group),
1134 1134 joinedload(UserGroupUserGroupToPerm.permission),)
1135 1135
1136 1136 perm_rows = []
1137 1137 for _user_group in q.all():
1138 1138 usr = AttributeDict(_user_group.user_group.get_dict())
1139 1139 usr.permission = _user_group.permission.permission_name
1140 1140 perm_rows.append(usr)
1141 1141
1142 1142 return perm_rows
1143 1143
1144 1144 def _get_default_perms(self, user_group, suffix=''):
1145 1145 from rhodecode.model.permission import PermissionModel
1146 1146 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1147 1147
1148 1148 def get_default_perms(self, suffix=''):
1149 1149 return self._get_default_perms(self, suffix)
1150 1150
1151 1151 def get_api_data(self, with_group_members=True, include_secrets=False):
1152 1152 """
1153 1153 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1154 1154 basically forwarded.
1155 1155
1156 1156 """
1157 1157 user_group = self
1158 1158
1159 1159 data = {
1160 1160 'users_group_id': user_group.users_group_id,
1161 1161 'group_name': user_group.users_group_name,
1162 1162 'group_description': user_group.user_group_description,
1163 1163 'active': user_group.users_group_active,
1164 1164 'owner': user_group.user.username,
1165 1165 }
1166 1166 if with_group_members:
1167 1167 users = []
1168 1168 for user in user_group.members:
1169 1169 user = user.user
1170 1170 users.append(user.get_api_data(include_secrets=include_secrets))
1171 1171 data['users'] = users
1172 1172
1173 1173 return data
1174 1174
1175 1175
1176 1176 class UserGroupMember(Base, BaseModel):
1177 1177 __tablename__ = 'users_groups_members'
1178 1178 __table_args__ = (
1179 1179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1180 1180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1181 1181 )
1182 1182
1183 1183 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1184 1184 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1185 1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1186 1186
1187 1187 user = relationship('User', lazy='joined')
1188 1188 users_group = relationship('UserGroup')
1189 1189
1190 1190 def __init__(self, gr_id='', u_id=''):
1191 1191 self.users_group_id = gr_id
1192 1192 self.user_id = u_id
1193 1193
1194 1194
1195 1195 class RepositoryField(Base, BaseModel):
1196 1196 __tablename__ = 'repositories_fields'
1197 1197 __table_args__ = (
1198 1198 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1199 1199 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1200 1200 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1201 1201 )
1202 1202 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1203 1203
1204 1204 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1205 1205 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1206 1206 field_key = Column("field_key", String(250))
1207 1207 field_label = Column("field_label", String(1024), nullable=False)
1208 1208 field_value = Column("field_value", String(10000), nullable=False)
1209 1209 field_desc = Column("field_desc", String(1024), nullable=False)
1210 1210 field_type = Column("field_type", String(255), nullable=False, unique=None)
1211 1211 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1212 1212
1213 1213 repository = relationship('Repository')
1214 1214
1215 1215 @property
1216 1216 def field_key_prefixed(self):
1217 1217 return 'ex_%s' % self.field_key
1218 1218
1219 1219 @classmethod
1220 1220 def un_prefix_key(cls, key):
1221 1221 if key.startswith(cls.PREFIX):
1222 1222 return key[len(cls.PREFIX):]
1223 1223 return key
1224 1224
1225 1225 @classmethod
1226 1226 def get_by_key_name(cls, key, repo):
1227 1227 row = cls.query()\
1228 1228 .filter(cls.repository == repo)\
1229 1229 .filter(cls.field_key == key).scalar()
1230 1230 return row
1231 1231
1232 1232
1233 1233 class Repository(Base, BaseModel):
1234 1234 __tablename__ = 'repositories'
1235 1235 __table_args__ = (
1236 1236 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1237 1237 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1238 1238 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1239 1239 )
1240 1240 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1241 1241 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1242 1242
1243 1243 STATE_CREATED = 'repo_state_created'
1244 1244 STATE_PENDING = 'repo_state_pending'
1245 1245 STATE_ERROR = 'repo_state_error'
1246 1246
1247 1247 LOCK_AUTOMATIC = 'lock_auto'
1248 1248 LOCK_API = 'lock_api'
1249 1249 LOCK_WEB = 'lock_web'
1250 1250 LOCK_PULL = 'lock_pull'
1251 1251
1252 1252 NAME_SEP = URL_SEP
1253 1253
1254 1254 repo_id = Column(
1255 1255 "repo_id", Integer(), nullable=False, unique=True, default=None,
1256 1256 primary_key=True)
1257 1257 _repo_name = Column(
1258 1258 "repo_name", Text(), nullable=False, default=None)
1259 1259 _repo_name_hash = Column(
1260 1260 "repo_name_hash", String(255), nullable=False, unique=True)
1261 1261 repo_state = Column("repo_state", String(255), nullable=True)
1262 1262
1263 1263 clone_uri = Column(
1264 1264 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1265 1265 default=None)
1266 1266 repo_type = Column(
1267 1267 "repo_type", String(255), nullable=False, unique=False, default=None)
1268 1268 user_id = Column(
1269 1269 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1270 1270 unique=False, default=None)
1271 1271 private = Column(
1272 1272 "private", Boolean(), nullable=True, unique=None, default=None)
1273 1273 enable_statistics = Column(
1274 1274 "statistics", Boolean(), nullable=True, unique=None, default=True)
1275 1275 enable_downloads = Column(
1276 1276 "downloads", Boolean(), nullable=True, unique=None, default=True)
1277 1277 description = Column(
1278 1278 "description", String(10000), nullable=True, unique=None, default=None)
1279 1279 created_on = Column(
1280 1280 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1281 1281 default=datetime.datetime.now)
1282 1282 updated_on = Column(
1283 1283 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1284 1284 default=datetime.datetime.now)
1285 1285 _landing_revision = Column(
1286 1286 "landing_revision", String(255), nullable=False, unique=False,
1287 1287 default=None)
1288 1288 enable_locking = Column(
1289 1289 "enable_locking", Boolean(), nullable=False, unique=None,
1290 1290 default=False)
1291 1291 _locked = Column(
1292 1292 "locked", String(255), nullable=True, unique=False, default=None)
1293 1293 _changeset_cache = Column(
1294 1294 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1295 1295
1296 1296 fork_id = Column(
1297 1297 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1298 1298 nullable=True, unique=False, default=None)
1299 1299 group_id = Column(
1300 1300 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1301 1301 unique=False, default=None)
1302 1302
1303 1303 user = relationship('User', lazy='joined')
1304 1304 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1305 1305 group = relationship('RepoGroup', lazy='joined')
1306 1306 repo_to_perm = relationship(
1307 1307 'UserRepoToPerm', cascade='all',
1308 1308 order_by='UserRepoToPerm.repo_to_perm_id')
1309 1309 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1310 1310 stats = relationship('Statistics', cascade='all', uselist=False)
1311 1311
1312 1312 followers = relationship(
1313 1313 'UserFollowing',
1314 1314 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1315 1315 cascade='all')
1316 1316 extra_fields = relationship(
1317 1317 'RepositoryField', cascade="all, delete, delete-orphan")
1318 1318 logs = relationship('UserLog')
1319 1319 comments = relationship(
1320 1320 'ChangesetComment', cascade="all, delete, delete-orphan")
1321 1321 pull_requests_source = relationship(
1322 1322 'PullRequest',
1323 1323 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1324 1324 cascade="all, delete, delete-orphan")
1325 1325 pull_requests_target = relationship(
1326 1326 'PullRequest',
1327 1327 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1328 1328 cascade="all, delete, delete-orphan")
1329 1329 ui = relationship('RepoRhodeCodeUi', cascade="all")
1330 1330 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1331 1331 integrations = relationship('Integration',
1332 1332 cascade="all, delete, delete-orphan")
1333 1333
1334 1334 def __unicode__(self):
1335 1335 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1336 1336 safe_unicode(self.repo_name))
1337 1337
1338 1338 @hybrid_property
1339 1339 def landing_rev(self):
1340 1340 # always should return [rev_type, rev]
1341 1341 if self._landing_revision:
1342 1342 _rev_info = self._landing_revision.split(':')
1343 1343 if len(_rev_info) < 2:
1344 1344 _rev_info.insert(0, 'rev')
1345 1345 return [_rev_info[0], _rev_info[1]]
1346 1346 return [None, None]
1347 1347
1348 1348 @landing_rev.setter
1349 1349 def landing_rev(self, val):
1350 1350 if ':' not in val:
1351 1351 raise ValueError('value must be delimited with `:` and consist '
1352 1352 'of <rev_type>:<rev>, got %s instead' % val)
1353 1353 self._landing_revision = val
1354 1354
1355 1355 @hybrid_property
1356 1356 def locked(self):
1357 1357 if self._locked:
1358 1358 user_id, timelocked, reason = self._locked.split(':')
1359 1359 lock_values = int(user_id), timelocked, reason
1360 1360 else:
1361 1361 lock_values = [None, None, None]
1362 1362 return lock_values
1363 1363
1364 1364 @locked.setter
1365 1365 def locked(self, val):
1366 1366 if val and isinstance(val, (list, tuple)):
1367 1367 self._locked = ':'.join(map(str, val))
1368 1368 else:
1369 1369 self._locked = None
1370 1370
1371 1371 @hybrid_property
1372 1372 def changeset_cache(self):
1373 1373 from rhodecode.lib.vcs.backends.base import EmptyCommit
1374 1374 dummy = EmptyCommit().__json__()
1375 1375 if not self._changeset_cache:
1376 1376 return dummy
1377 1377 try:
1378 1378 return json.loads(self._changeset_cache)
1379 1379 except TypeError:
1380 1380 return dummy
1381 1381 except Exception:
1382 1382 log.error(traceback.format_exc())
1383 1383 return dummy
1384 1384
1385 1385 @changeset_cache.setter
1386 1386 def changeset_cache(self, val):
1387 1387 try:
1388 1388 self._changeset_cache = json.dumps(val)
1389 1389 except Exception:
1390 1390 log.error(traceback.format_exc())
1391 1391
1392 1392 @hybrid_property
1393 1393 def repo_name(self):
1394 1394 return self._repo_name
1395 1395
1396 1396 @repo_name.setter
1397 1397 def repo_name(self, value):
1398 1398 self._repo_name = value
1399 1399 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1400 1400
1401 1401 @classmethod
1402 1402 def normalize_repo_name(cls, repo_name):
1403 1403 """
1404 1404 Normalizes os specific repo_name to the format internally stored inside
1405 1405 database using URL_SEP
1406 1406
1407 1407 :param cls:
1408 1408 :param repo_name:
1409 1409 """
1410 1410 return cls.NAME_SEP.join(repo_name.split(os.sep))
1411 1411
1412 1412 @classmethod
1413 1413 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1414 1414 session = Session()
1415 1415 q = session.query(cls).filter(cls.repo_name == repo_name)
1416 1416
1417 1417 if cache:
1418 1418 if identity_cache:
1419 1419 val = cls.identity_cache(session, 'repo_name', repo_name)
1420 1420 if val:
1421 1421 return val
1422 1422 else:
1423 1423 q = q.options(
1424 1424 FromCache("sql_cache_short",
1425 1425 "get_repo_by_name_%s" % _hash_key(repo_name)))
1426 1426
1427 1427 return q.scalar()
1428 1428
1429 1429 @classmethod
1430 1430 def get_by_full_path(cls, repo_full_path):
1431 1431 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1432 1432 repo_name = cls.normalize_repo_name(repo_name)
1433 1433 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1434 1434
1435 1435 @classmethod
1436 1436 def get_repo_forks(cls, repo_id):
1437 1437 return cls.query().filter(Repository.fork_id == repo_id)
1438 1438
1439 1439 @classmethod
1440 1440 def base_path(cls):
1441 1441 """
1442 1442 Returns base path when all repos are stored
1443 1443
1444 1444 :param cls:
1445 1445 """
1446 1446 q = Session().query(RhodeCodeUi)\
1447 1447 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1448 1448 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1449 1449 return q.one().ui_value
1450 1450
1451 1451 @classmethod
1452 1452 def is_valid(cls, repo_name):
1453 1453 """
1454 1454 returns True if given repo name is a valid filesystem repository
1455 1455
1456 1456 :param cls:
1457 1457 :param repo_name:
1458 1458 """
1459 1459 from rhodecode.lib.utils import is_valid_repo
1460 1460
1461 1461 return is_valid_repo(repo_name, cls.base_path())
1462 1462
1463 1463 @classmethod
1464 1464 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1465 1465 case_insensitive=True):
1466 1466 q = Repository.query()
1467 1467
1468 1468 if not isinstance(user_id, Optional):
1469 1469 q = q.filter(Repository.user_id == user_id)
1470 1470
1471 1471 if not isinstance(group_id, Optional):
1472 1472 q = q.filter(Repository.group_id == group_id)
1473 1473
1474 1474 if case_insensitive:
1475 1475 q = q.order_by(func.lower(Repository.repo_name))
1476 1476 else:
1477 1477 q = q.order_by(Repository.repo_name)
1478 1478 return q.all()
1479 1479
1480 1480 @property
1481 1481 def forks(self):
1482 1482 """
1483 1483 Return forks of this repo
1484 1484 """
1485 1485 return Repository.get_repo_forks(self.repo_id)
1486 1486
1487 1487 @property
1488 1488 def parent(self):
1489 1489 """
1490 1490 Returns fork parent
1491 1491 """
1492 1492 return self.fork
1493 1493
1494 1494 @property
1495 1495 def just_name(self):
1496 1496 return self.repo_name.split(self.NAME_SEP)[-1]
1497 1497
1498 1498 @property
1499 1499 def groups_with_parents(self):
1500 1500 groups = []
1501 1501 if self.group is None:
1502 1502 return groups
1503 1503
1504 1504 cur_gr = self.group
1505 1505 groups.insert(0, cur_gr)
1506 1506 while 1:
1507 1507 gr = getattr(cur_gr, 'parent_group', None)
1508 1508 cur_gr = cur_gr.parent_group
1509 1509 if gr is None:
1510 1510 break
1511 1511 groups.insert(0, gr)
1512 1512
1513 1513 return groups
1514 1514
1515 1515 @property
1516 1516 def groups_and_repo(self):
1517 1517 return self.groups_with_parents, self
1518 1518
1519 1519 @LazyProperty
1520 1520 def repo_path(self):
1521 1521 """
1522 1522 Returns base full path for that repository means where it actually
1523 1523 exists on a filesystem
1524 1524 """
1525 1525 q = Session().query(RhodeCodeUi).filter(
1526 1526 RhodeCodeUi.ui_key == self.NAME_SEP)
1527 1527 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1528 1528 return q.one().ui_value
1529 1529
1530 1530 @property
1531 1531 def repo_full_path(self):
1532 1532 p = [self.repo_path]
1533 1533 # we need to split the name by / since this is how we store the
1534 1534 # names in the database, but that eventually needs to be converted
1535 1535 # into a valid system path
1536 1536 p += self.repo_name.split(self.NAME_SEP)
1537 1537 return os.path.join(*map(safe_unicode, p))
1538 1538
1539 1539 @property
1540 1540 def cache_keys(self):
1541 1541 """
1542 1542 Returns associated cache keys for that repo
1543 1543 """
1544 1544 return CacheKey.query()\
1545 1545 .filter(CacheKey.cache_args == self.repo_name)\
1546 1546 .order_by(CacheKey.cache_key)\
1547 1547 .all()
1548 1548
1549 1549 def get_new_name(self, repo_name):
1550 1550 """
1551 1551 returns new full repository name based on assigned group and new new
1552 1552
1553 1553 :param group_name:
1554 1554 """
1555 1555 path_prefix = self.group.full_path_splitted if self.group else []
1556 1556 return self.NAME_SEP.join(path_prefix + [repo_name])
1557 1557
1558 1558 @property
1559 1559 def _config(self):
1560 1560 """
1561 1561 Returns db based config object.
1562 1562 """
1563 1563 from rhodecode.lib.utils import make_db_config
1564 1564 return make_db_config(clear_session=False, repo=self)
1565 1565
1566 1566 def permissions(self, with_admins=True, with_owner=True):
1567 1567 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1568 1568 q = q.options(joinedload(UserRepoToPerm.repository),
1569 1569 joinedload(UserRepoToPerm.user),
1570 1570 joinedload(UserRepoToPerm.permission),)
1571 1571
1572 1572 # get owners and admins and permissions. We do a trick of re-writing
1573 1573 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1574 1574 # has a global reference and changing one object propagates to all
1575 1575 # others. This means if admin is also an owner admin_row that change
1576 1576 # would propagate to both objects
1577 1577 perm_rows = []
1578 1578 for _usr in q.all():
1579 1579 usr = AttributeDict(_usr.user.get_dict())
1580 1580 usr.permission = _usr.permission.permission_name
1581 1581 perm_rows.append(usr)
1582 1582
1583 1583 # filter the perm rows by 'default' first and then sort them by
1584 1584 # admin,write,read,none permissions sorted again alphabetically in
1585 1585 # each group
1586 1586 perm_rows = sorted(perm_rows, key=display_sort)
1587 1587
1588 1588 _admin_perm = 'repository.admin'
1589 1589 owner_row = []
1590 1590 if with_owner:
1591 1591 usr = AttributeDict(self.user.get_dict())
1592 1592 usr.owner_row = True
1593 1593 usr.permission = _admin_perm
1594 1594 owner_row.append(usr)
1595 1595
1596 1596 super_admin_rows = []
1597 1597 if with_admins:
1598 1598 for usr in User.get_all_super_admins():
1599 1599 # if this admin is also owner, don't double the record
1600 1600 if usr.user_id == owner_row[0].user_id:
1601 1601 owner_row[0].admin_row = True
1602 1602 else:
1603 1603 usr = AttributeDict(usr.get_dict())
1604 1604 usr.admin_row = True
1605 1605 usr.permission = _admin_perm
1606 1606 super_admin_rows.append(usr)
1607 1607
1608 1608 return super_admin_rows + owner_row + perm_rows
1609 1609
1610 1610 def permission_user_groups(self):
1611 1611 q = UserGroupRepoToPerm.query().filter(
1612 1612 UserGroupRepoToPerm.repository == self)
1613 1613 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1614 1614 joinedload(UserGroupRepoToPerm.users_group),
1615 1615 joinedload(UserGroupRepoToPerm.permission),)
1616 1616
1617 1617 perm_rows = []
1618 1618 for _user_group in q.all():
1619 1619 usr = AttributeDict(_user_group.users_group.get_dict())
1620 1620 usr.permission = _user_group.permission.permission_name
1621 1621 perm_rows.append(usr)
1622 1622
1623 1623 return perm_rows
1624 1624
1625 1625 def get_api_data(self, include_secrets=False):
1626 1626 """
1627 1627 Common function for generating repo api data
1628 1628
1629 1629 :param include_secrets: See :meth:`User.get_api_data`.
1630 1630
1631 1631 """
1632 1632 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1633 1633 # move this methods on models level.
1634 1634 from rhodecode.model.settings import SettingsModel
1635 1635
1636 1636 repo = self
1637 1637 _user_id, _time, _reason = self.locked
1638 1638
1639 1639 data = {
1640 1640 'repo_id': repo.repo_id,
1641 1641 'repo_name': repo.repo_name,
1642 1642 'repo_type': repo.repo_type,
1643 1643 'clone_uri': repo.clone_uri or '',
1644 1644 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1645 1645 'private': repo.private,
1646 1646 'created_on': repo.created_on,
1647 1647 'description': repo.description,
1648 1648 'landing_rev': repo.landing_rev,
1649 1649 'owner': repo.user.username,
1650 1650 'fork_of': repo.fork.repo_name if repo.fork else None,
1651 1651 'enable_statistics': repo.enable_statistics,
1652 1652 'enable_locking': repo.enable_locking,
1653 1653 'enable_downloads': repo.enable_downloads,
1654 1654 'last_changeset': repo.changeset_cache,
1655 1655 'locked_by': User.get(_user_id).get_api_data(
1656 1656 include_secrets=include_secrets) if _user_id else None,
1657 1657 'locked_date': time_to_datetime(_time) if _time else None,
1658 1658 'lock_reason': _reason if _reason else None,
1659 1659 }
1660 1660
1661 1661 # TODO: mikhail: should be per-repo settings here
1662 1662 rc_config = SettingsModel().get_all_settings()
1663 1663 repository_fields = str2bool(
1664 1664 rc_config.get('rhodecode_repository_fields'))
1665 1665 if repository_fields:
1666 1666 for f in self.extra_fields:
1667 1667 data[f.field_key_prefixed] = f.field_value
1668 1668
1669 1669 return data
1670 1670
1671 1671 @classmethod
1672 1672 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1673 1673 if not lock_time:
1674 1674 lock_time = time.time()
1675 1675 if not lock_reason:
1676 1676 lock_reason = cls.LOCK_AUTOMATIC
1677 1677 repo.locked = [user_id, lock_time, lock_reason]
1678 1678 Session().add(repo)
1679 1679 Session().commit()
1680 1680
1681 1681 @classmethod
1682 1682 def unlock(cls, repo):
1683 1683 repo.locked = None
1684 1684 Session().add(repo)
1685 1685 Session().commit()
1686 1686
1687 1687 @classmethod
1688 1688 def getlock(cls, repo):
1689 1689 return repo.locked
1690 1690
1691 1691 def is_user_lock(self, user_id):
1692 1692 if self.lock[0]:
1693 1693 lock_user_id = safe_int(self.lock[0])
1694 1694 user_id = safe_int(user_id)
1695 1695 # both are ints, and they are equal
1696 1696 return all([lock_user_id, user_id]) and lock_user_id == user_id
1697 1697
1698 1698 return False
1699 1699
1700 1700 def get_locking_state(self, action, user_id, only_when_enabled=True):
1701 1701 """
1702 1702 Checks locking on this repository, if locking is enabled and lock is
1703 1703 present returns a tuple of make_lock, locked, locked_by.
1704 1704 make_lock can have 3 states None (do nothing) True, make lock
1705 1705 False release lock, This value is later propagated to hooks, which
1706 1706 do the locking. Think about this as signals passed to hooks what to do.
1707 1707
1708 1708 """
1709 1709 # TODO: johbo: This is part of the business logic and should be moved
1710 1710 # into the RepositoryModel.
1711 1711
1712 1712 if action not in ('push', 'pull'):
1713 1713 raise ValueError("Invalid action value: %s" % repr(action))
1714 1714
1715 1715 # defines if locked error should be thrown to user
1716 1716 currently_locked = False
1717 1717 # defines if new lock should be made, tri-state
1718 1718 make_lock = None
1719 1719 repo = self
1720 1720 user = User.get(user_id)
1721 1721
1722 1722 lock_info = repo.locked
1723 1723
1724 1724 if repo and (repo.enable_locking or not only_when_enabled):
1725 1725 if action == 'push':
1726 1726 # check if it's already locked !, if it is compare users
1727 1727 locked_by_user_id = lock_info[0]
1728 1728 if user.user_id == locked_by_user_id:
1729 1729 log.debug(
1730 1730 'Got `push` action from user %s, now unlocking', user)
1731 1731 # unlock if we have push from user who locked
1732 1732 make_lock = False
1733 1733 else:
1734 1734 # we're not the same user who locked, ban with
1735 1735 # code defined in settings (default is 423 HTTP Locked) !
1736 1736 log.debug('Repo %s is currently locked by %s', repo, user)
1737 1737 currently_locked = True
1738 1738 elif action == 'pull':
1739 1739 # [0] user [1] date
1740 1740 if lock_info[0] and lock_info[1]:
1741 1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 1742 currently_locked = True
1743 1743 else:
1744 1744 log.debug('Setting lock on repo %s by %s', repo, user)
1745 1745 make_lock = True
1746 1746
1747 1747 else:
1748 1748 log.debug('Repository %s do not have locking enabled', repo)
1749 1749
1750 1750 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1751 1751 make_lock, currently_locked, lock_info)
1752 1752
1753 1753 from rhodecode.lib.auth import HasRepoPermissionAny
1754 1754 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1755 1755 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1756 1756 # if we don't have at least write permission we cannot make a lock
1757 1757 log.debug('lock state reset back to FALSE due to lack '
1758 1758 'of at least read permission')
1759 1759 make_lock = False
1760 1760
1761 1761 return make_lock, currently_locked, lock_info
1762 1762
1763 1763 @property
1764 1764 def last_db_change(self):
1765 1765 return self.updated_on
1766 1766
1767 1767 @property
1768 1768 def clone_uri_hidden(self):
1769 1769 clone_uri = self.clone_uri
1770 1770 if clone_uri:
1771 1771 import urlobject
1772 1772 url_obj = urlobject.URLObject(clone_uri)
1773 1773 if url_obj.password:
1774 1774 clone_uri = url_obj.with_password('*****')
1775 1775 return clone_uri
1776 1776
1777 1777 def clone_url(self, **override):
1778 1778 qualified_home_url = url('home', qualified=True)
1779 1779
1780 1780 uri_tmpl = None
1781 1781 if 'with_id' in override:
1782 1782 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1783 1783 del override['with_id']
1784 1784
1785 1785 if 'uri_tmpl' in override:
1786 1786 uri_tmpl = override['uri_tmpl']
1787 1787 del override['uri_tmpl']
1788 1788
1789 1789 # we didn't override our tmpl from **overrides
1790 1790 if not uri_tmpl:
1791 1791 uri_tmpl = self.DEFAULT_CLONE_URI
1792 1792 try:
1793 1793 from pylons import tmpl_context as c
1794 1794 uri_tmpl = c.clone_uri_tmpl
1795 1795 except Exception:
1796 1796 # in any case if we call this outside of request context,
1797 1797 # ie, not having tmpl_context set up
1798 1798 pass
1799 1799
1800 1800 return get_clone_url(uri_tmpl=uri_tmpl,
1801 1801 qualifed_home_url=qualified_home_url,
1802 1802 repo_name=self.repo_name,
1803 1803 repo_id=self.repo_id, **override)
1804 1804
1805 1805 def set_state(self, state):
1806 1806 self.repo_state = state
1807 1807 Session().add(self)
1808 1808 #==========================================================================
1809 1809 # SCM PROPERTIES
1810 1810 #==========================================================================
1811 1811
1812 1812 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1813 1813 return get_commit_safe(
1814 1814 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1815 1815
1816 1816 def get_changeset(self, rev=None, pre_load=None):
1817 1817 warnings.warn("Use get_commit", DeprecationWarning)
1818 1818 commit_id = None
1819 1819 commit_idx = None
1820 1820 if isinstance(rev, basestring):
1821 1821 commit_id = rev
1822 1822 else:
1823 1823 commit_idx = rev
1824 1824 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1825 1825 pre_load=pre_load)
1826 1826
1827 1827 def get_landing_commit(self):
1828 1828 """
1829 1829 Returns landing commit, or if that doesn't exist returns the tip
1830 1830 """
1831 1831 _rev_type, _rev = self.landing_rev
1832 1832 commit = self.get_commit(_rev)
1833 1833 if isinstance(commit, EmptyCommit):
1834 1834 return self.get_commit()
1835 1835 return commit
1836 1836
1837 1837 def update_commit_cache(self, cs_cache=None, config=None):
1838 1838 """
1839 1839 Update cache of last changeset for repository, keys should be::
1840 1840
1841 1841 short_id
1842 1842 raw_id
1843 1843 revision
1844 1844 parents
1845 1845 message
1846 1846 date
1847 1847 author
1848 1848
1849 1849 :param cs_cache:
1850 1850 """
1851 1851 from rhodecode.lib.vcs.backends.base import BaseChangeset
1852 1852 if cs_cache is None:
1853 1853 # use no-cache version here
1854 1854 scm_repo = self.scm_instance(cache=False, config=config)
1855 1855 if scm_repo:
1856 1856 cs_cache = scm_repo.get_commit(
1857 1857 pre_load=["author", "date", "message", "parents"])
1858 1858 else:
1859 1859 cs_cache = EmptyCommit()
1860 1860
1861 1861 if isinstance(cs_cache, BaseChangeset):
1862 1862 cs_cache = cs_cache.__json__()
1863 1863
1864 1864 def is_outdated(new_cs_cache):
1865 1865 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1866 1866 new_cs_cache['revision'] != self.changeset_cache['revision']):
1867 1867 return True
1868 1868 return False
1869 1869
1870 1870 # check if we have maybe already latest cached revision
1871 1871 if is_outdated(cs_cache) or not self.changeset_cache:
1872 1872 _default = datetime.datetime.fromtimestamp(0)
1873 1873 last_change = cs_cache.get('date') or _default
1874 1874 log.debug('updated repo %s with new cs cache %s',
1875 1875 self.repo_name, cs_cache)
1876 1876 self.updated_on = last_change
1877 1877 self.changeset_cache = cs_cache
1878 1878 Session().add(self)
1879 1879 Session().commit()
1880 1880 else:
1881 1881 log.debug('Skipping update_commit_cache for repo:`%s` '
1882 1882 'commit already with latest changes', self.repo_name)
1883 1883
1884 1884 @property
1885 1885 def tip(self):
1886 1886 return self.get_commit('tip')
1887 1887
1888 1888 @property
1889 1889 def author(self):
1890 1890 return self.tip.author
1891 1891
1892 1892 @property
1893 1893 def last_change(self):
1894 1894 return self.scm_instance().last_change
1895 1895
1896 1896 def get_comments(self, revisions=None):
1897 1897 """
1898 1898 Returns comments for this repository grouped by revisions
1899 1899
1900 1900 :param revisions: filter query by revisions only
1901 1901 """
1902 1902 cmts = ChangesetComment.query()\
1903 1903 .filter(ChangesetComment.repo == self)
1904 1904 if revisions:
1905 1905 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1906 1906 grouped = collections.defaultdict(list)
1907 1907 for cmt in cmts.all():
1908 1908 grouped[cmt.revision].append(cmt)
1909 1909 return grouped
1910 1910
1911 1911 def statuses(self, revisions=None):
1912 1912 """
1913 1913 Returns statuses for this repository
1914 1914
1915 1915 :param revisions: list of revisions to get statuses for
1916 1916 """
1917 1917 statuses = ChangesetStatus.query()\
1918 1918 .filter(ChangesetStatus.repo == self)\
1919 1919 .filter(ChangesetStatus.version == 0)
1920 1920
1921 1921 if revisions:
1922 1922 # Try doing the filtering in chunks to avoid hitting limits
1923 1923 size = 500
1924 1924 status_results = []
1925 1925 for chunk in xrange(0, len(revisions), size):
1926 1926 status_results += statuses.filter(
1927 1927 ChangesetStatus.revision.in_(
1928 1928 revisions[chunk: chunk+size])
1929 1929 ).all()
1930 1930 else:
1931 1931 status_results = statuses.all()
1932 1932
1933 1933 grouped = {}
1934 1934
1935 1935 # maybe we have open new pullrequest without a status?
1936 1936 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1937 1937 status_lbl = ChangesetStatus.get_status_lbl(stat)
1938 1938 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1939 1939 for rev in pr.revisions:
1940 1940 pr_id = pr.pull_request_id
1941 1941 pr_repo = pr.target_repo.repo_name
1942 1942 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1943 1943
1944 1944 for stat in status_results:
1945 1945 pr_id = pr_repo = None
1946 1946 if stat.pull_request:
1947 1947 pr_id = stat.pull_request.pull_request_id
1948 1948 pr_repo = stat.pull_request.target_repo.repo_name
1949 1949 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1950 1950 pr_id, pr_repo]
1951 1951 return grouped
1952 1952
1953 1953 # ==========================================================================
1954 1954 # SCM CACHE INSTANCE
1955 1955 # ==========================================================================
1956 1956
1957 1957 def scm_instance(self, **kwargs):
1958 1958 import rhodecode
1959 1959
1960 1960 # Passing a config will not hit the cache currently only used
1961 1961 # for repo2dbmapper
1962 1962 config = kwargs.pop('config', None)
1963 1963 cache = kwargs.pop('cache', None)
1964 1964 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1965 1965 # if cache is NOT defined use default global, else we have a full
1966 1966 # control over cache behaviour
1967 1967 if cache is None and full_cache and not config:
1968 1968 return self._get_instance_cached()
1969 1969 return self._get_instance(cache=bool(cache), config=config)
1970 1970
1971 1971 def _get_instance_cached(self):
1972 1972 @cache_region('long_term')
1973 1973 def _get_repo(cache_key):
1974 1974 return self._get_instance()
1975 1975
1976 1976 invalidator_context = CacheKey.repo_context_cache(
1977 1977 _get_repo, self.repo_name, None, thread_scoped=True)
1978 1978
1979 1979 with invalidator_context as context:
1980 1980 context.invalidate()
1981 1981 repo = context.compute()
1982 1982
1983 1983 return repo
1984 1984
1985 1985 def _get_instance(self, cache=True, config=None):
1986 1986 config = config or self._config
1987 1987 custom_wire = {
1988 1988 'cache': cache # controls the vcs.remote cache
1989 1989 }
1990 1990
1991 1991 repo = get_vcs_instance(
1992 1992 repo_path=safe_str(self.repo_full_path),
1993 1993 config=config,
1994 1994 with_wire=custom_wire,
1995 1995 create=False)
1996 1996
1997 1997 return repo
1998 1998
1999 1999 def __json__(self):
2000 2000 return {'landing_rev': self.landing_rev}
2001 2001
2002 2002 def get_dict(self):
2003 2003
2004 2004 # Since we transformed `repo_name` to a hybrid property, we need to
2005 2005 # keep compatibility with the code which uses `repo_name` field.
2006 2006
2007 2007 result = super(Repository, self).get_dict()
2008 2008 result['repo_name'] = result.pop('_repo_name', None)
2009 2009 return result
2010 2010
2011 2011
2012 2012 class RepoGroup(Base, BaseModel):
2013 2013 __tablename__ = 'groups'
2014 2014 __table_args__ = (
2015 2015 UniqueConstraint('group_name', 'group_parent_id'),
2016 2016 CheckConstraint('group_id != group_parent_id'),
2017 2017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2018 2018 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2019 2019 )
2020 2020 __mapper_args__ = {'order_by': 'group_name'}
2021 2021
2022 2022 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2023 2023
2024 2024 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2025 2025 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2026 2026 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2027 2027 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2028 2028 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2029 2029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2030 2030 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2031 2031
2032 2032 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2033 2033 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2034 2034 parent_group = relationship('RepoGroup', remote_side=group_id)
2035 2035 user = relationship('User')
2036 2036 integrations = relationship('Integration',
2037 2037 cascade="all, delete, delete-orphan")
2038 2038
2039 2039 def __init__(self, group_name='', parent_group=None):
2040 2040 self.group_name = group_name
2041 2041 self.parent_group = parent_group
2042 2042
2043 2043 def __unicode__(self):
2044 2044 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2045 2045 self.group_name)
2046 2046
2047 2047 @classmethod
2048 2048 def _generate_choice(cls, repo_group):
2049 2049 from webhelpers.html import literal as _literal
2050 2050 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2051 2051 return repo_group.group_id, _name(repo_group.full_path_splitted)
2052 2052
2053 2053 @classmethod
2054 2054 def groups_choices(cls, groups=None, show_empty_group=True):
2055 2055 if not groups:
2056 2056 groups = cls.query().all()
2057 2057
2058 2058 repo_groups = []
2059 2059 if show_empty_group:
2060 2060 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2061 2061
2062 2062 repo_groups.extend([cls._generate_choice(x) for x in groups])
2063 2063
2064 2064 repo_groups = sorted(
2065 2065 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2066 2066 return repo_groups
2067 2067
2068 2068 @classmethod
2069 2069 def url_sep(cls):
2070 2070 return URL_SEP
2071 2071
2072 2072 @classmethod
2073 2073 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2074 2074 if case_insensitive:
2075 2075 gr = cls.query().filter(func.lower(cls.group_name)
2076 2076 == func.lower(group_name))
2077 2077 else:
2078 2078 gr = cls.query().filter(cls.group_name == group_name)
2079 2079 if cache:
2080 2080 gr = gr.options(FromCache(
2081 2081 "sql_cache_short",
2082 2082 "get_group_%s" % _hash_key(group_name)))
2083 2083 return gr.scalar()
2084 2084
2085 2085 @classmethod
2086 2086 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2087 2087 case_insensitive=True):
2088 2088 q = RepoGroup.query()
2089 2089
2090 2090 if not isinstance(user_id, Optional):
2091 2091 q = q.filter(RepoGroup.user_id == user_id)
2092 2092
2093 2093 if not isinstance(group_id, Optional):
2094 2094 q = q.filter(RepoGroup.group_parent_id == group_id)
2095 2095
2096 2096 if case_insensitive:
2097 2097 q = q.order_by(func.lower(RepoGroup.group_name))
2098 2098 else:
2099 2099 q = q.order_by(RepoGroup.group_name)
2100 2100 return q.all()
2101 2101
2102 2102 @property
2103 2103 def parents(self):
2104 2104 parents_recursion_limit = 10
2105 2105 groups = []
2106 2106 if self.parent_group is None:
2107 2107 return groups
2108 2108 cur_gr = self.parent_group
2109 2109 groups.insert(0, cur_gr)
2110 2110 cnt = 0
2111 2111 while 1:
2112 2112 cnt += 1
2113 2113 gr = getattr(cur_gr, 'parent_group', None)
2114 2114 cur_gr = cur_gr.parent_group
2115 2115 if gr is None:
2116 2116 break
2117 2117 if cnt == parents_recursion_limit:
2118 2118 # this will prevent accidental infinit loops
2119 2119 log.error(('more than %s parents found for group %s, stopping '
2120 2120 'recursive parent fetching' % (parents_recursion_limit, self)))
2121 2121 break
2122 2122
2123 2123 groups.insert(0, gr)
2124 2124 return groups
2125 2125
2126 2126 @property
2127 2127 def children(self):
2128 2128 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2129 2129
2130 2130 @property
2131 2131 def name(self):
2132 2132 return self.group_name.split(RepoGroup.url_sep())[-1]
2133 2133
2134 2134 @property
2135 2135 def full_path(self):
2136 2136 return self.group_name
2137 2137
2138 2138 @property
2139 2139 def full_path_splitted(self):
2140 2140 return self.group_name.split(RepoGroup.url_sep())
2141 2141
2142 2142 @property
2143 2143 def repositories(self):
2144 2144 return Repository.query()\
2145 2145 .filter(Repository.group == self)\
2146 2146 .order_by(Repository.repo_name)
2147 2147
2148 2148 @property
2149 2149 def repositories_recursive_count(self):
2150 2150 cnt = self.repositories.count()
2151 2151
2152 2152 def children_count(group):
2153 2153 cnt = 0
2154 2154 for child in group.children:
2155 2155 cnt += child.repositories.count()
2156 2156 cnt += children_count(child)
2157 2157 return cnt
2158 2158
2159 2159 return cnt + children_count(self)
2160 2160
2161 2161 def _recursive_objects(self, include_repos=True):
2162 2162 all_ = []
2163 2163
2164 2164 def _get_members(root_gr):
2165 2165 if include_repos:
2166 2166 for r in root_gr.repositories:
2167 2167 all_.append(r)
2168 2168 childs = root_gr.children.all()
2169 2169 if childs:
2170 2170 for gr in childs:
2171 2171 all_.append(gr)
2172 2172 _get_members(gr)
2173 2173
2174 2174 _get_members(self)
2175 2175 return [self] + all_
2176 2176
2177 2177 def recursive_groups_and_repos(self):
2178 2178 """
2179 2179 Recursive return all groups, with repositories in those groups
2180 2180 """
2181 2181 return self._recursive_objects()
2182 2182
2183 2183 def recursive_groups(self):
2184 2184 """
2185 2185 Returns all children groups for this group including children of children
2186 2186 """
2187 2187 return self._recursive_objects(include_repos=False)
2188 2188
2189 2189 def get_new_name(self, group_name):
2190 2190 """
2191 2191 returns new full group name based on parent and new name
2192 2192
2193 2193 :param group_name:
2194 2194 """
2195 2195 path_prefix = (self.parent_group.full_path_splitted if
2196 2196 self.parent_group else [])
2197 2197 return RepoGroup.url_sep().join(path_prefix + [group_name])
2198 2198
2199 2199 def permissions(self, with_admins=True, with_owner=True):
2200 2200 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2201 2201 q = q.options(joinedload(UserRepoGroupToPerm.group),
2202 2202 joinedload(UserRepoGroupToPerm.user),
2203 2203 joinedload(UserRepoGroupToPerm.permission),)
2204 2204
2205 2205 # get owners and admins and permissions. We do a trick of re-writing
2206 2206 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2207 2207 # has a global reference and changing one object propagates to all
2208 2208 # others. This means if admin is also an owner admin_row that change
2209 2209 # would propagate to both objects
2210 2210 perm_rows = []
2211 2211 for _usr in q.all():
2212 2212 usr = AttributeDict(_usr.user.get_dict())
2213 2213 usr.permission = _usr.permission.permission_name
2214 2214 perm_rows.append(usr)
2215 2215
2216 2216 # filter the perm rows by 'default' first and then sort them by
2217 2217 # admin,write,read,none permissions sorted again alphabetically in
2218 2218 # each group
2219 2219 perm_rows = sorted(perm_rows, key=display_sort)
2220 2220
2221 2221 _admin_perm = 'group.admin'
2222 2222 owner_row = []
2223 2223 if with_owner:
2224 2224 usr = AttributeDict(self.user.get_dict())
2225 2225 usr.owner_row = True
2226 2226 usr.permission = _admin_perm
2227 2227 owner_row.append(usr)
2228 2228
2229 2229 super_admin_rows = []
2230 2230 if with_admins:
2231 2231 for usr in User.get_all_super_admins():
2232 2232 # if this admin is also owner, don't double the record
2233 2233 if usr.user_id == owner_row[0].user_id:
2234 2234 owner_row[0].admin_row = True
2235 2235 else:
2236 2236 usr = AttributeDict(usr.get_dict())
2237 2237 usr.admin_row = True
2238 2238 usr.permission = _admin_perm
2239 2239 super_admin_rows.append(usr)
2240 2240
2241 2241 return super_admin_rows + owner_row + perm_rows
2242 2242
2243 2243 def permission_user_groups(self):
2244 2244 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2245 2245 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2246 2246 joinedload(UserGroupRepoGroupToPerm.users_group),
2247 2247 joinedload(UserGroupRepoGroupToPerm.permission),)
2248 2248
2249 2249 perm_rows = []
2250 2250 for _user_group in q.all():
2251 2251 usr = AttributeDict(_user_group.users_group.get_dict())
2252 2252 usr.permission = _user_group.permission.permission_name
2253 2253 perm_rows.append(usr)
2254 2254
2255 2255 return perm_rows
2256 2256
2257 2257 def get_api_data(self):
2258 2258 """
2259 2259 Common function for generating api data
2260 2260
2261 2261 """
2262 2262 group = self
2263 2263 data = {
2264 2264 'group_id': group.group_id,
2265 2265 'group_name': group.group_name,
2266 2266 'group_description': group.group_description,
2267 2267 'parent_group': group.parent_group.group_name if group.parent_group else None,
2268 2268 'repositories': [x.repo_name for x in group.repositories],
2269 2269 'owner': group.user.username,
2270 2270 }
2271 2271 return data
2272 2272
2273 2273
2274 2274 class Permission(Base, BaseModel):
2275 2275 __tablename__ = 'permissions'
2276 2276 __table_args__ = (
2277 2277 Index('p_perm_name_idx', 'permission_name'),
2278 2278 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2279 2279 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2280 2280 )
2281 2281 PERMS = [
2282 2282 ('hg.admin', _('RhodeCode Super Administrator')),
2283 2283
2284 2284 ('repository.none', _('Repository no access')),
2285 2285 ('repository.read', _('Repository read access')),
2286 2286 ('repository.write', _('Repository write access')),
2287 2287 ('repository.admin', _('Repository admin access')),
2288 2288
2289 2289 ('group.none', _('Repository group no access')),
2290 2290 ('group.read', _('Repository group read access')),
2291 2291 ('group.write', _('Repository group write access')),
2292 2292 ('group.admin', _('Repository group admin access')),
2293 2293
2294 2294 ('usergroup.none', _('User group no access')),
2295 2295 ('usergroup.read', _('User group read access')),
2296 2296 ('usergroup.write', _('User group write access')),
2297 2297 ('usergroup.admin', _('User group admin access')),
2298 2298
2299 2299 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2300 2300 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2301 2301
2302 2302 ('hg.usergroup.create.false', _('User Group creation disabled')),
2303 2303 ('hg.usergroup.create.true', _('User Group creation enabled')),
2304 2304
2305 2305 ('hg.create.none', _('Repository creation disabled')),
2306 2306 ('hg.create.repository', _('Repository creation enabled')),
2307 2307 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2308 2308 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2309 2309
2310 2310 ('hg.fork.none', _('Repository forking disabled')),
2311 2311 ('hg.fork.repository', _('Repository forking enabled')),
2312 2312
2313 2313 ('hg.register.none', _('Registration disabled')),
2314 2314 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2315 2315 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2316 2316
2317 2317 ('hg.password_reset.enabled', _('Password reset enabled')),
2318 2318 ('hg.password_reset.hidden', _('Password reset hidden')),
2319 2319 ('hg.password_reset.disabled', _('Password reset disabled')),
2320 2320
2321 2321 ('hg.extern_activate.manual', _('Manual activation of external account')),
2322 2322 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2323 2323
2324 2324 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2325 2325 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2326 2326 ]
2327 2327
2328 2328 # definition of system default permissions for DEFAULT user
2329 2329 DEFAULT_USER_PERMISSIONS = [
2330 2330 'repository.read',
2331 2331 'group.read',
2332 2332 'usergroup.read',
2333 2333 'hg.create.repository',
2334 2334 'hg.repogroup.create.false',
2335 2335 'hg.usergroup.create.false',
2336 2336 'hg.create.write_on_repogroup.true',
2337 2337 'hg.fork.repository',
2338 2338 'hg.register.manual_activate',
2339 2339 'hg.password_reset.enabled',
2340 2340 'hg.extern_activate.auto',
2341 2341 'hg.inherit_default_perms.true',
2342 2342 ]
2343 2343
2344 2344 # defines which permissions are more important higher the more important
2345 2345 # Weight defines which permissions are more important.
2346 2346 # The higher number the more important.
2347 2347 PERM_WEIGHTS = {
2348 2348 'repository.none': 0,
2349 2349 'repository.read': 1,
2350 2350 'repository.write': 3,
2351 2351 'repository.admin': 4,
2352 2352
2353 2353 'group.none': 0,
2354 2354 'group.read': 1,
2355 2355 'group.write': 3,
2356 2356 'group.admin': 4,
2357 2357
2358 2358 'usergroup.none': 0,
2359 2359 'usergroup.read': 1,
2360 2360 'usergroup.write': 3,
2361 2361 'usergroup.admin': 4,
2362 2362
2363 2363 'hg.repogroup.create.false': 0,
2364 2364 'hg.repogroup.create.true': 1,
2365 2365
2366 2366 'hg.usergroup.create.false': 0,
2367 2367 'hg.usergroup.create.true': 1,
2368 2368
2369 2369 'hg.fork.none': 0,
2370 2370 'hg.fork.repository': 1,
2371 2371 'hg.create.none': 0,
2372 2372 'hg.create.repository': 1
2373 2373 }
2374 2374
2375 2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378 2378
2379 2379 def __unicode__(self):
2380 2380 return u"<%s('%s:%s')>" % (
2381 2381 self.__class__.__name__, self.permission_id, self.permission_name
2382 2382 )
2383 2383
2384 2384 @classmethod
2385 2385 def get_by_key(cls, key):
2386 2386 return cls.query().filter(cls.permission_name == key).scalar()
2387 2387
2388 2388 @classmethod
2389 2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 2393 .filter(UserRepoToPerm.user_id == user_id)
2394 2394 if repo_id:
2395 2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 2396 return q.all()
2397 2397
2398 2398 @classmethod
2399 2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 2401 .join(
2402 2402 Permission,
2403 2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 2404 .join(
2405 2405 Repository,
2406 2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 2407 .join(
2408 2408 UserGroup,
2409 2409 UserGroupRepoToPerm.users_group_id ==
2410 2410 UserGroup.users_group_id)\
2411 2411 .join(
2412 2412 UserGroupMember,
2413 2413 UserGroupRepoToPerm.users_group_id ==
2414 2414 UserGroupMember.users_group_id)\
2415 2415 .filter(
2416 2416 UserGroupMember.user_id == user_id,
2417 2417 UserGroup.users_group_active == true())
2418 2418 if repo_id:
2419 2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 2420 return q.all()
2421 2421
2422 2422 @classmethod
2423 2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 2428 if repo_group_id:
2429 2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 2430 return q.all()
2431 2431
2432 2432 @classmethod
2433 2433 def get_default_group_perms_from_user_group(
2434 2434 cls, user_id, repo_group_id=None):
2435 2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 2436 .join(
2437 2437 Permission,
2438 2438 UserGroupRepoGroupToPerm.permission_id ==
2439 2439 Permission.permission_id)\
2440 2440 .join(
2441 2441 RepoGroup,
2442 2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 2443 .join(
2444 2444 UserGroup,
2445 2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 2446 UserGroup.users_group_id)\
2447 2447 .join(
2448 2448 UserGroupMember,
2449 2449 UserGroupRepoGroupToPerm.users_group_id ==
2450 2450 UserGroupMember.users_group_id)\
2451 2451 .filter(
2452 2452 UserGroupMember.user_id == user_id,
2453 2453 UserGroup.users_group_active == true())
2454 2454 if repo_group_id:
2455 2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 2456 return q.all()
2457 2457
2458 2458 @classmethod
2459 2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 2463 .filter(UserUserGroupToPerm.user_id == user_id)
2464 2464 if user_group_id:
2465 2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 2466 return q.all()
2467 2467
2468 2468 @classmethod
2469 2469 def get_default_user_group_perms_from_user_group(
2470 2470 cls, user_id, user_group_id=None):
2471 2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 2473 .join(
2474 2474 Permission,
2475 2475 UserGroupUserGroupToPerm.permission_id ==
2476 2476 Permission.permission_id)\
2477 2477 .join(
2478 2478 TargetUserGroup,
2479 2479 UserGroupUserGroupToPerm.target_user_group_id ==
2480 2480 TargetUserGroup.users_group_id)\
2481 2481 .join(
2482 2482 UserGroup,
2483 2483 UserGroupUserGroupToPerm.user_group_id ==
2484 2484 UserGroup.users_group_id)\
2485 2485 .join(
2486 2486 UserGroupMember,
2487 2487 UserGroupUserGroupToPerm.user_group_id ==
2488 2488 UserGroupMember.users_group_id)\
2489 2489 .filter(
2490 2490 UserGroupMember.user_id == user_id,
2491 2491 UserGroup.users_group_active == true())
2492 2492 if user_group_id:
2493 2493 q = q.filter(
2494 2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495 2495
2496 2496 return q.all()
2497 2497
2498 2498
2499 2499 class UserRepoToPerm(Base, BaseModel):
2500 2500 __tablename__ = 'repo_to_perm'
2501 2501 __table_args__ = (
2502 2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 2505 )
2506 2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510 2510
2511 2511 user = relationship('User')
2512 2512 repository = relationship('Repository')
2513 2513 permission = relationship('Permission')
2514 2514
2515 2515 @classmethod
2516 2516 def create(cls, user, repository, permission):
2517 2517 n = cls()
2518 2518 n.user = user
2519 2519 n.repository = repository
2520 2520 n.permission = permission
2521 2521 Session().add(n)
2522 2522 return n
2523 2523
2524 2524 def __unicode__(self):
2525 2525 return u'<%s => %s >' % (self.user, self.repository)
2526 2526
2527 2527
2528 2528 class UserUserGroupToPerm(Base, BaseModel):
2529 2529 __tablename__ = 'user_user_group_to_perm'
2530 2530 __table_args__ = (
2531 2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 2534 )
2535 2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2536 2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539 2539
2540 2540 user = relationship('User')
2541 2541 user_group = relationship('UserGroup')
2542 2542 permission = relationship('Permission')
2543 2543
2544 2544 @classmethod
2545 2545 def create(cls, user, user_group, permission):
2546 2546 n = cls()
2547 2547 n.user = user
2548 2548 n.user_group = user_group
2549 2549 n.permission = permission
2550 2550 Session().add(n)
2551 2551 return n
2552 2552
2553 2553 def __unicode__(self):
2554 2554 return u'<%s => %s >' % (self.user, self.user_group)
2555 2555
2556 2556
2557 2557 class UserToPerm(Base, BaseModel):
2558 2558 __tablename__ = 'user_to_perm'
2559 2559 __table_args__ = (
2560 2560 UniqueConstraint('user_id', 'permission_id'),
2561 2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 2563 )
2564 2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567 2567
2568 2568 user = relationship('User')
2569 2569 permission = relationship('Permission', lazy='joined')
2570 2570
2571 2571 def __unicode__(self):
2572 2572 return u'<%s => %s >' % (self.user, self.permission)
2573 2573
2574 2574
2575 2575 class UserGroupRepoToPerm(Base, BaseModel):
2576 2576 __tablename__ = 'users_group_repo_to_perm'
2577 2577 __table_args__ = (
2578 2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 2581 )
2582 2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586 2586
2587 2587 users_group = relationship('UserGroup')
2588 2588 permission = relationship('Permission')
2589 2589 repository = relationship('Repository')
2590 2590
2591 2591 @classmethod
2592 2592 def create(cls, users_group, repository, permission):
2593 2593 n = cls()
2594 2594 n.users_group = users_group
2595 2595 n.repository = repository
2596 2596 n.permission = permission
2597 2597 Session().add(n)
2598 2598 return n
2599 2599
2600 2600 def __unicode__(self):
2601 2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602 2602
2603 2603
2604 2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 2605 __tablename__ = 'user_group_user_group_to_perm'
2606 2606 __table_args__ = (
2607 2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 2608 CheckConstraint('target_user_group_id != user_group_id'),
2609 2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 2611 )
2612 2612 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)
2613 2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616 2616
2617 2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 2619 permission = relationship('Permission')
2620 2620
2621 2621 @classmethod
2622 2622 def create(cls, target_user_group, user_group, permission):
2623 2623 n = cls()
2624 2624 n.target_user_group = target_user_group
2625 2625 n.user_group = user_group
2626 2626 n.permission = permission
2627 2627 Session().add(n)
2628 2628 return n
2629 2629
2630 2630 def __unicode__(self):
2631 2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632 2632
2633 2633
2634 2634 class UserGroupToPerm(Base, BaseModel):
2635 2635 __tablename__ = 'users_group_to_perm'
2636 2636 __table_args__ = (
2637 2637 UniqueConstraint('users_group_id', 'permission_id',),
2638 2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 2640 )
2641 2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644 2644
2645 2645 users_group = relationship('UserGroup')
2646 2646 permission = relationship('Permission')
2647 2647
2648 2648
2649 2649 class UserRepoGroupToPerm(Base, BaseModel):
2650 2650 __tablename__ = 'user_repo_group_to_perm'
2651 2651 __table_args__ = (
2652 2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 2655 )
2656 2656
2657 2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661 2661
2662 2662 user = relationship('User')
2663 2663 group = relationship('RepoGroup')
2664 2664 permission = relationship('Permission')
2665 2665
2666 2666 @classmethod
2667 2667 def create(cls, user, repository_group, permission):
2668 2668 n = cls()
2669 2669 n.user = user
2670 2670 n.group = repository_group
2671 2671 n.permission = permission
2672 2672 Session().add(n)
2673 2673 return n
2674 2674
2675 2675
2676 2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 2677 __tablename__ = 'users_group_repo_group_to_perm'
2678 2678 __table_args__ = (
2679 2679 UniqueConstraint('users_group_id', 'group_id'),
2680 2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 2682 )
2683 2683
2684 2684 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)
2685 2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688 2688
2689 2689 users_group = relationship('UserGroup')
2690 2690 permission = relationship('Permission')
2691 2691 group = relationship('RepoGroup')
2692 2692
2693 2693 @classmethod
2694 2694 def create(cls, user_group, repository_group, permission):
2695 2695 n = cls()
2696 2696 n.users_group = user_group
2697 2697 n.group = repository_group
2698 2698 n.permission = permission
2699 2699 Session().add(n)
2700 2700 return n
2701 2701
2702 2702 def __unicode__(self):
2703 2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704 2704
2705 2705
2706 2706 class Statistics(Base, BaseModel):
2707 2707 __tablename__ = 'statistics'
2708 2708 __table_args__ = (
2709 2709 UniqueConstraint('repository_id'),
2710 2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 2712 )
2713 2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719 2719
2720 2720 repository = relationship('Repository', single_parent=True)
2721 2721
2722 2722
2723 2723 class UserFollowing(Base, BaseModel):
2724 2724 __tablename__ = 'user_followings'
2725 2725 __table_args__ = (
2726 2726 UniqueConstraint('user_id', 'follows_repository_id'),
2727 2727 UniqueConstraint('user_id', 'follows_user_id'),
2728 2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 2730 )
2731 2731
2732 2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737 2737
2738 2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739 2739
2740 2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742 2742
2743 2743 @classmethod
2744 2744 def get_repo_followers(cls, repo_id):
2745 2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2746 2746
2747 2747
2748 2748 class CacheKey(Base, BaseModel):
2749 2749 __tablename__ = 'cache_invalidation'
2750 2750 __table_args__ = (
2751 2751 UniqueConstraint('cache_key'),
2752 2752 Index('key_idx', 'cache_key'),
2753 2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 2755 )
2756 2756 CACHE_TYPE_ATOM = 'ATOM'
2757 2757 CACHE_TYPE_RSS = 'RSS'
2758 2758 CACHE_TYPE_README = 'README'
2759 2759
2760 2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764 2764
2765 2765 def __init__(self, cache_key, cache_args=''):
2766 2766 self.cache_key = cache_key
2767 2767 self.cache_args = cache_args
2768 2768 self.cache_active = False
2769 2769
2770 2770 def __unicode__(self):
2771 2771 return u"<%s('%s:%s[%s]')>" % (
2772 2772 self.__class__.__name__,
2773 2773 self.cache_id, self.cache_key, self.cache_active)
2774 2774
2775 2775 def _cache_key_partition(self):
2776 2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 2777 return prefix, repo_name, suffix
2778 2778
2779 2779 def get_prefix(self):
2780 2780 """
2781 2781 Try to extract prefix from existing cache key. The key could consist
2782 2782 of prefix, repo_name, suffix
2783 2783 """
2784 2784 # this returns prefix, repo_name, suffix
2785 2785 return self._cache_key_partition()[0]
2786 2786
2787 2787 def get_suffix(self):
2788 2788 """
2789 2789 get suffix that might have been used in _get_cache_key to
2790 2790 generate self.cache_key. Only used for informational purposes
2791 2791 in repo_edit.html.
2792 2792 """
2793 2793 # prefix, repo_name, suffix
2794 2794 return self._cache_key_partition()[2]
2795 2795
2796 2796 @classmethod
2797 2797 def delete_all_cache(cls):
2798 2798 """
2799 2799 Delete all cache keys from database.
2800 2800 Should only be run when all instances are down and all entries
2801 2801 thus stale.
2802 2802 """
2803 2803 cls.query().delete()
2804 2804 Session().commit()
2805 2805
2806 2806 @classmethod
2807 2807 def get_cache_key(cls, repo_name, cache_type):
2808 2808 """
2809 2809
2810 2810 Generate a cache key for this process of RhodeCode instance.
2811 2811 Prefix most likely will be process id or maybe explicitly set
2812 2812 instance_id from .ini file.
2813 2813 """
2814 2814 import rhodecode
2815 2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816 2816
2817 2817 repo_as_unicode = safe_unicode(repo_name)
2818 2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 2819 if cache_type else repo_as_unicode
2820 2820
2821 2821 return u'{}{}'.format(prefix, key)
2822 2822
2823 2823 @classmethod
2824 2824 def set_invalidate(cls, repo_name, delete=False):
2825 2825 """
2826 2826 Mark all caches of a repo as invalid in the database.
2827 2827 """
2828 2828
2829 2829 try:
2830 2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 2831 if delete:
2832 2832 log.debug('cache objects deleted for repo %s',
2833 2833 safe_str(repo_name))
2834 2834 qry.delete()
2835 2835 else:
2836 2836 log.debug('cache objects marked as invalid for repo %s',
2837 2837 safe_str(repo_name))
2838 2838 qry.update({"cache_active": False})
2839 2839
2840 2840 Session().commit()
2841 2841 except Exception:
2842 2842 log.exception(
2843 2843 'Cache key invalidation failed for repository %s',
2844 2844 safe_str(repo_name))
2845 2845 Session().rollback()
2846 2846
2847 2847 @classmethod
2848 2848 def get_active_cache(cls, cache_key):
2849 2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 2850 if inv_obj:
2851 2851 return inv_obj
2852 2852 return None
2853 2853
2854 2854 @classmethod
2855 2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 2856 thread_scoped=False):
2857 2857 """
2858 2858 @cache_region('long_term')
2859 2859 def _heavy_calculation(cache_key):
2860 2860 return 'result'
2861 2861
2862 2862 cache_context = CacheKey.repo_context_cache(
2863 2863 _heavy_calculation, repo_name, cache_type)
2864 2864
2865 2865 with cache_context as context:
2866 2866 context.invalidate()
2867 2867 computed = context.compute()
2868 2868
2869 2869 assert computed == 'result'
2870 2870 """
2871 2871 from rhodecode.lib import caches
2872 2872 return caches.InvalidationContext(
2873 2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874 2874
2875 2875
2876 2876 class ChangesetComment(Base, BaseModel):
2877 2877 __tablename__ = 'changeset_comments'
2878 2878 __table_args__ = (
2879 2879 Index('cc_revision_idx', 'revision'),
2880 2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 2882 )
2883 2883
2884 2884 COMMENT_OUTDATED = u'comment_outdated'
2885 2885
2886 2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 2888 revision = Column('revision', String(40), nullable=True)
2889 2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 2891 line_no = Column('line_no', Unicode(10), nullable=True)
2892 2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 2898 renderer = Column('renderer', Unicode(64), nullable=True)
2899 2899 display_state = Column('display_state', Unicode(128), nullable=True)
2900 2900
2901 2901 author = relationship('User', lazy='joined')
2902 2902 repo = relationship('Repository')
2903 2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 2904 pull_request = relationship('PullRequest', lazy='joined')
2905 2905 pull_request_version = relationship('PullRequestVersion')
2906 2906
2907 2907 @classmethod
2908 2908 def get_users(cls, revision=None, pull_request_id=None):
2909 2909 """
2910 2910 Returns user associated with this ChangesetComment. ie those
2911 2911 who actually commented
2912 2912
2913 2913 :param cls:
2914 2914 :param revision:
2915 2915 """
2916 2916 q = Session().query(User)\
2917 2917 .join(ChangesetComment.author)
2918 2918 if revision:
2919 2919 q = q.filter(cls.revision == revision)
2920 2920 elif pull_request_id:
2921 2921 q = q.filter(cls.pull_request_id == pull_request_id)
2922 2922 return q.all()
2923 2923
2924 2924 def render(self, mentions=False):
2925 2925 from rhodecode.lib import helpers as h
2926 2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927 2927
2928 2928 def __repr__(self):
2929 2929 if self.comment_id:
2930 2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 2931 else:
2932 2932 return '<DB:ChangesetComment at %#x>' % id(self)
2933 2933
2934 2934
2935 2935 class ChangesetStatus(Base, BaseModel):
2936 2936 __tablename__ = 'changeset_statuses'
2937 2937 __table_args__ = (
2938 2938 Index('cs_revision_idx', 'revision'),
2939 2939 Index('cs_version_idx', 'version'),
2940 2940 UniqueConstraint('repo_id', 'revision', 'version'),
2941 2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 2943 )
2944 2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 2945 STATUS_APPROVED = 'approved'
2946 2946 STATUS_REJECTED = 'rejected'
2947 2947 STATUS_UNDER_REVIEW = 'under_review'
2948 2948
2949 2949 STATUSES = [
2950 2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 2951 (STATUS_APPROVED, _("Approved")),
2952 2952 (STATUS_REJECTED, _("Rejected")),
2953 2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 2954 ]
2955 2955
2956 2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 2959 revision = Column('revision', String(40), nullable=False)
2960 2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 2963 version = Column('version', Integer(), nullable=False, default=0)
2964 2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965 2965
2966 2966 author = relationship('User', lazy='joined')
2967 2967 repo = relationship('Repository')
2968 2968 comment = relationship('ChangesetComment', lazy='joined')
2969 2969 pull_request = relationship('PullRequest', lazy='joined')
2970 2970
2971 2971 def __unicode__(self):
2972 2972 return u"<%s('%s[%s]:%s')>" % (
2973 2973 self.__class__.__name__,
2974 2974 self.status, self.version, self.author
2975 2975 )
2976 2976
2977 2977 @classmethod
2978 2978 def get_status_lbl(cls, value):
2979 2979 return dict(cls.STATUSES).get(value)
2980 2980
2981 2981 @property
2982 2982 def status_lbl(self):
2983 2983 return ChangesetStatus.get_status_lbl(self.status)
2984 2984
2985 2985
2986 2986 class _PullRequestBase(BaseModel):
2987 2987 """
2988 2988 Common attributes of pull request and version entries.
2989 2989 """
2990 2990
2991 2991 # .status values
2992 2992 STATUS_NEW = u'new'
2993 2993 STATUS_OPEN = u'open'
2994 2994 STATUS_CLOSED = u'closed'
2995 2995
2996 2996 title = Column('title', Unicode(255), nullable=True)
2997 2997 description = Column(
2998 2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 2999 nullable=True)
3000 3000 # new/open/closed status of pull request (not approve/reject/etc)
3001 3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 3002 created_on = Column(
3003 3003 'created_on', DateTime(timezone=False), nullable=False,
3004 3004 default=datetime.datetime.now)
3005 3005 updated_on = Column(
3006 3006 'updated_on', DateTime(timezone=False), nullable=False,
3007 3007 default=datetime.datetime.now)
3008 3008
3009 3009 @declared_attr
3010 3010 def user_id(cls):
3011 3011 return Column(
3012 3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 3013 unique=None)
3014 3014
3015 3015 # 500 revisions max
3016 3016 _revisions = Column(
3017 3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018 3018
3019 3019 @declared_attr
3020 3020 def source_repo_id(cls):
3021 3021 # TODO: dan: rename column to source_repo_id
3022 3022 return Column(
3023 3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 3024 nullable=False)
3025 3025
3026 3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027 3027
3028 3028 @declared_attr
3029 3029 def target_repo_id(cls):
3030 3030 # TODO: dan: rename column to target_repo_id
3031 3031 return Column(
3032 3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 3033 nullable=False)
3034 3034
3035 3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036 3036 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3037 3037
3038 3038 # TODO: dan: rename column to last_merge_source_rev
3039 3039 _last_merge_source_rev = Column(
3040 3040 'last_merge_org_rev', String(40), nullable=True)
3041 3041 # TODO: dan: rename column to last_merge_target_rev
3042 3042 _last_merge_target_rev = Column(
3043 3043 'last_merge_other_rev', String(40), nullable=True)
3044 3044 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3045 3045 merge_rev = Column('merge_rev', String(40), nullable=True)
3046 3046
3047 3047 @hybrid_property
3048 3048 def revisions(self):
3049 3049 return self._revisions.split(':') if self._revisions else []
3050 3050
3051 3051 @revisions.setter
3052 3052 def revisions(self, val):
3053 3053 self._revisions = ':'.join(val)
3054 3054
3055 3055 @declared_attr
3056 3056 def author(cls):
3057 3057 return relationship('User', lazy='joined')
3058 3058
3059 3059 @declared_attr
3060 3060 def source_repo(cls):
3061 3061 return relationship(
3062 3062 'Repository',
3063 3063 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3064 3064
3065 3065 @property
3066 3066 def source_ref_parts(self):
3067 3067 return self.unicode_to_reference(self.source_ref)
3068 3068
3069 3069 @declared_attr
3070 3070 def target_repo(cls):
3071 3071 return relationship(
3072 3072 'Repository',
3073 3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074 3074
3075 3075 @property
3076 3076 def target_ref_parts(self):
3077 3077 return self.unicode_to_reference(self.target_ref)
3078 3078
3079 3079 @property
3080 3080 def shadow_merge_ref(self):
3081 3081 return self.unicode_to_reference(self._shadow_merge_ref)
3082 3082
3083 3083 @shadow_merge_ref.setter
3084 3084 def shadow_merge_ref(self, ref):
3085 3085 self._shadow_merge_ref = self.reference_to_unicode(ref)
3086 3086
3087 3087 def unicode_to_reference(self, raw):
3088 3088 """
3089 3089 Convert a unicode (or string) to a reference object.
3090 3090 If unicode evaluates to False it returns None.
3091 3091 """
3092 3092 if raw:
3093 3093 refs = raw.split(':')
3094 3094 return Reference(*refs)
3095 3095 else:
3096 3096 return None
3097 3097
3098 3098 def reference_to_unicode(self, ref):
3099 3099 """
3100 3100 Convert a reference object to unicode.
3101 3101 If reference is None it returns None.
3102 3102 """
3103 3103 if ref:
3104 3104 return u':'.join(ref)
3105 3105 else:
3106 3106 return None
3107 3107
3108 3108
3109 3109 class PullRequest(Base, _PullRequestBase):
3110 3110 __tablename__ = 'pull_requests'
3111 3111 __table_args__ = (
3112 3112 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3113 3113 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3114 3114 )
3115 3115
3116 3116 pull_request_id = Column(
3117 3117 'pull_request_id', Integer(), nullable=False, primary_key=True)
3118 3118
3119 3119 def __repr__(self):
3120 3120 if self.pull_request_id:
3121 3121 return '<DB:PullRequest #%s>' % self.pull_request_id
3122 3122 else:
3123 3123 return '<DB:PullRequest at %#x>' % id(self)
3124 3124
3125 3125 reviewers = relationship('PullRequestReviewers',
3126 3126 cascade="all, delete, delete-orphan")
3127 3127 statuses = relationship('ChangesetStatus')
3128 3128 comments = relationship('ChangesetComment',
3129 3129 cascade="all, delete, delete-orphan")
3130 3130 versions = relationship('PullRequestVersion',
3131 3131 cascade="all, delete, delete-orphan")
3132 3132
3133 3133 def is_closed(self):
3134 3134 return self.status == self.STATUS_CLOSED
3135 3135
3136 3136 def get_api_data(self):
3137 3137 from rhodecode.model.pull_request import PullRequestModel
3138 3138 pull_request = self
3139 3139 merge_status = PullRequestModel().merge_status(pull_request)
3140 3140
3141 3141 pull_request_url = url(
3142 3142 'pullrequest_show', repo_name=self.target_repo.repo_name,
3143 3143 pull_request_id=self.pull_request_id, qualified=True)
3144 3144
3145 3145 merge_data = {
3146 3146 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3147 3147 'reference': (
3148 3148 pull_request.shadow_merge_ref._asdict()
3149 3149 if pull_request.shadow_merge_ref else None),
3150 3150 }
3151 3151
3152 3152 data = {
3153 3153 'pull_request_id': pull_request.pull_request_id,
3154 3154 'url': pull_request_url,
3155 3155 'title': pull_request.title,
3156 3156 'description': pull_request.description,
3157 3157 'status': pull_request.status,
3158 3158 'created_on': pull_request.created_on,
3159 3159 'updated_on': pull_request.updated_on,
3160 3160 'commit_ids': pull_request.revisions,
3161 3161 'review_status': pull_request.calculated_review_status(),
3162 3162 'mergeable': {
3163 3163 'status': merge_status[0],
3164 3164 'message': unicode(merge_status[1]),
3165 3165 },
3166 3166 'source': {
3167 3167 'clone_url': pull_request.source_repo.clone_url(),
3168 3168 'repository': pull_request.source_repo.repo_name,
3169 3169 'reference': {
3170 3170 'name': pull_request.source_ref_parts.name,
3171 3171 'type': pull_request.source_ref_parts.type,
3172 3172 'commit_id': pull_request.source_ref_parts.commit_id,
3173 3173 },
3174 3174 },
3175 3175 'target': {
3176 3176 'clone_url': pull_request.target_repo.clone_url(),
3177 3177 'repository': pull_request.target_repo.repo_name,
3178 3178 'reference': {
3179 3179 'name': pull_request.target_ref_parts.name,
3180 3180 'type': pull_request.target_ref_parts.type,
3181 3181 'commit_id': pull_request.target_ref_parts.commit_id,
3182 3182 },
3183 3183 },
3184 3184 'merge': merge_data,
3185 3185 'author': pull_request.author.get_api_data(include_secrets=False,
3186 3186 details='basic'),
3187 3187 'reviewers': [
3188 3188 {
3189 3189 'user': reviewer.get_api_data(include_secrets=False,
3190 3190 details='basic'),
3191 3191 'reasons': reasons,
3192 3192 'review_status': st[0][1].status if st else 'not_reviewed',
3193 3193 }
3194 3194 for reviewer, reasons, st in pull_request.reviewers_statuses()
3195 3195 ]
3196 3196 }
3197 3197
3198 3198 return data
3199 3199
3200 3200 def __json__(self):
3201 3201 return {
3202 3202 'revisions': self.revisions,
3203 3203 }
3204 3204
3205 3205 def calculated_review_status(self):
3206 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3207 # because it's tricky on how to use ChangesetStatusModel from there
3208 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3209 3206 from rhodecode.model.changeset_status import ChangesetStatusModel
3210 3207 return ChangesetStatusModel().calculated_review_status(self)
3211 3208
3212 3209 def reviewers_statuses(self):
3213 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3214 3210 from rhodecode.model.changeset_status import ChangesetStatusModel
3215 3211 return ChangesetStatusModel().reviewers_statuses(self)
3216 3212
3217 3213
3218 3214 class PullRequestVersion(Base, _PullRequestBase):
3219 3215 __tablename__ = 'pull_request_versions'
3220 3216 __table_args__ = (
3221 3217 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3222 3218 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3223 3219 )
3224 3220
3225 3221 pull_request_version_id = Column(
3226 3222 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3227 3223 pull_request_id = Column(
3228 3224 'pull_request_id', Integer(),
3229 3225 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3230 3226 pull_request = relationship('PullRequest')
3231 3227
3232 3228 def __repr__(self):
3233 3229 if self.pull_request_version_id:
3234 3230 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3235 3231 else:
3236 3232 return '<DB:PullRequestVersion at %#x>' % id(self)
3237 3233
3238 3234
3239 3235 class PullRequestReviewers(Base, BaseModel):
3240 3236 __tablename__ = 'pull_request_reviewers'
3241 3237 __table_args__ = (
3242 3238 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3243 3239 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3244 3240 )
3245 3241
3246 3242 def __init__(self, user=None, pull_request=None, reasons=None):
3247 3243 self.user = user
3248 3244 self.pull_request = pull_request
3249 3245 self.reasons = reasons or []
3250 3246
3251 3247 @hybrid_property
3252 3248 def reasons(self):
3253 3249 if not self._reasons:
3254 3250 return []
3255 3251 return self._reasons
3256 3252
3257 3253 @reasons.setter
3258 3254 def reasons(self, val):
3259 3255 val = val or []
3260 3256 if any(not isinstance(x, basestring) for x in val):
3261 3257 raise Exception('invalid reasons type, must be list of strings')
3262 3258 self._reasons = val
3263 3259
3264 3260 pull_requests_reviewers_id = Column(
3265 3261 'pull_requests_reviewers_id', Integer(), nullable=False,
3266 3262 primary_key=True)
3267 3263 pull_request_id = Column(
3268 3264 "pull_request_id", Integer(),
3269 3265 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3270 3266 user_id = Column(
3271 3267 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3272 3268 _reasons = Column(
3273 3269 'reason', MutationList.as_mutable(
3274 3270 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3275 3271
3276 3272 user = relationship('User')
3277 3273 pull_request = relationship('PullRequest')
3278 3274
3279 3275
3280 3276 class Notification(Base, BaseModel):
3281 3277 __tablename__ = 'notifications'
3282 3278 __table_args__ = (
3283 3279 Index('notification_type_idx', 'type'),
3284 3280 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3285 3281 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3286 3282 )
3287 3283
3288 3284 TYPE_CHANGESET_COMMENT = u'cs_comment'
3289 3285 TYPE_MESSAGE = u'message'
3290 3286 TYPE_MENTION = u'mention'
3291 3287 TYPE_REGISTRATION = u'registration'
3292 3288 TYPE_PULL_REQUEST = u'pull_request'
3293 3289 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3294 3290
3295 3291 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3296 3292 subject = Column('subject', Unicode(512), nullable=True)
3297 3293 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3298 3294 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3299 3295 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3300 3296 type_ = Column('type', Unicode(255))
3301 3297
3302 3298 created_by_user = relationship('User')
3303 3299 notifications_to_users = relationship('UserNotification', lazy='joined',
3304 3300 cascade="all, delete, delete-orphan")
3305 3301
3306 3302 @property
3307 3303 def recipients(self):
3308 3304 return [x.user for x in UserNotification.query()\
3309 3305 .filter(UserNotification.notification == self)\
3310 3306 .order_by(UserNotification.user_id.asc()).all()]
3311 3307
3312 3308 @classmethod
3313 3309 def create(cls, created_by, subject, body, recipients, type_=None):
3314 3310 if type_ is None:
3315 3311 type_ = Notification.TYPE_MESSAGE
3316 3312
3317 3313 notification = cls()
3318 3314 notification.created_by_user = created_by
3319 3315 notification.subject = subject
3320 3316 notification.body = body
3321 3317 notification.type_ = type_
3322 3318 notification.created_on = datetime.datetime.now()
3323 3319
3324 3320 for u in recipients:
3325 3321 assoc = UserNotification()
3326 3322 assoc.notification = notification
3327 3323
3328 3324 # if created_by is inside recipients mark his notification
3329 3325 # as read
3330 3326 if u.user_id == created_by.user_id:
3331 3327 assoc.read = True
3332 3328
3333 3329 u.notifications.append(assoc)
3334 3330 Session().add(notification)
3335 3331
3336 3332 return notification
3337 3333
3338 3334 @property
3339 3335 def description(self):
3340 3336 from rhodecode.model.notification import NotificationModel
3341 3337 return NotificationModel().make_description(self)
3342 3338
3343 3339
3344 3340 class UserNotification(Base, BaseModel):
3345 3341 __tablename__ = 'user_to_notification'
3346 3342 __table_args__ = (
3347 3343 UniqueConstraint('user_id', 'notification_id'),
3348 3344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3349 3345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3350 3346 )
3351 3347 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3352 3348 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3353 3349 read = Column('read', Boolean, default=False)
3354 3350 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3355 3351
3356 3352 user = relationship('User', lazy="joined")
3357 3353 notification = relationship('Notification', lazy="joined",
3358 3354 order_by=lambda: Notification.created_on.desc(),)
3359 3355
3360 3356 def mark_as_read(self):
3361 3357 self.read = True
3362 3358 Session().add(self)
3363 3359
3364 3360
3365 3361 class Gist(Base, BaseModel):
3366 3362 __tablename__ = 'gists'
3367 3363 __table_args__ = (
3368 3364 Index('g_gist_access_id_idx', 'gist_access_id'),
3369 3365 Index('g_created_on_idx', 'created_on'),
3370 3366 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3371 3367 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3372 3368 )
3373 3369 GIST_PUBLIC = u'public'
3374 3370 GIST_PRIVATE = u'private'
3375 3371 DEFAULT_FILENAME = u'gistfile1.txt'
3376 3372
3377 3373 ACL_LEVEL_PUBLIC = u'acl_public'
3378 3374 ACL_LEVEL_PRIVATE = u'acl_private'
3379 3375
3380 3376 gist_id = Column('gist_id', Integer(), primary_key=True)
3381 3377 gist_access_id = Column('gist_access_id', Unicode(250))
3382 3378 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3383 3379 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3384 3380 gist_expires = Column('gist_expires', Float(53), nullable=False)
3385 3381 gist_type = Column('gist_type', Unicode(128), nullable=False)
3386 3382 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3387 3383 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3388 3384 acl_level = Column('acl_level', Unicode(128), nullable=True)
3389 3385
3390 3386 owner = relationship('User')
3391 3387
3392 3388 def __repr__(self):
3393 3389 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3394 3390
3395 3391 @classmethod
3396 3392 def get_or_404(cls, id_):
3397 3393 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3398 3394 if not res:
3399 3395 raise HTTPNotFound
3400 3396 return res
3401 3397
3402 3398 @classmethod
3403 3399 def get_by_access_id(cls, gist_access_id):
3404 3400 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3405 3401
3406 3402 def gist_url(self):
3407 3403 import rhodecode
3408 3404 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3409 3405 if alias_url:
3410 3406 return alias_url.replace('{gistid}', self.gist_access_id)
3411 3407
3412 3408 return url('gist', gist_id=self.gist_access_id, qualified=True)
3413 3409
3414 3410 @classmethod
3415 3411 def base_path(cls):
3416 3412 """
3417 3413 Returns base path when all gists are stored
3418 3414
3419 3415 :param cls:
3420 3416 """
3421 3417 from rhodecode.model.gist import GIST_STORE_LOC
3422 3418 q = Session().query(RhodeCodeUi)\
3423 3419 .filter(RhodeCodeUi.ui_key == URL_SEP)
3424 3420 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3425 3421 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3426 3422
3427 3423 def get_api_data(self):
3428 3424 """
3429 3425 Common function for generating gist related data for API
3430 3426 """
3431 3427 gist = self
3432 3428 data = {
3433 3429 'gist_id': gist.gist_id,
3434 3430 'type': gist.gist_type,
3435 3431 'access_id': gist.gist_access_id,
3436 3432 'description': gist.gist_description,
3437 3433 'url': gist.gist_url(),
3438 3434 'expires': gist.gist_expires,
3439 3435 'created_on': gist.created_on,
3440 3436 'modified_at': gist.modified_at,
3441 3437 'content': None,
3442 3438 'acl_level': gist.acl_level,
3443 3439 }
3444 3440 return data
3445 3441
3446 3442 def __json__(self):
3447 3443 data = dict(
3448 3444 )
3449 3445 data.update(self.get_api_data())
3450 3446 return data
3451 3447 # SCM functions
3452 3448
3453 3449 def scm_instance(self, **kwargs):
3454 3450 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3455 3451 return get_vcs_instance(
3456 3452 repo_path=safe_str(full_repo_path), create=False)
3457 3453
3458 3454
3459 3455 class DbMigrateVersion(Base, BaseModel):
3460 3456 __tablename__ = 'db_migrate_version'
3461 3457 __table_args__ = (
3462 3458 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3463 3459 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3464 3460 )
3465 3461 repository_id = Column('repository_id', String(250), primary_key=True)
3466 3462 repository_path = Column('repository_path', Text)
3467 3463 version = Column('version', Integer)
3468 3464
3469 3465
3470 3466 class ExternalIdentity(Base, BaseModel):
3471 3467 __tablename__ = 'external_identities'
3472 3468 __table_args__ = (
3473 3469 Index('local_user_id_idx', 'local_user_id'),
3474 3470 Index('external_id_idx', 'external_id'),
3475 3471 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3476 3472 'mysql_charset': 'utf8'})
3477 3473
3478 3474 external_id = Column('external_id', Unicode(255), default=u'',
3479 3475 primary_key=True)
3480 3476 external_username = Column('external_username', Unicode(1024), default=u'')
3481 3477 local_user_id = Column('local_user_id', Integer(),
3482 3478 ForeignKey('users.user_id'), primary_key=True)
3483 3479 provider_name = Column('provider_name', Unicode(255), default=u'',
3484 3480 primary_key=True)
3485 3481 access_token = Column('access_token', String(1024), default=u'')
3486 3482 alt_token = Column('alt_token', String(1024), default=u'')
3487 3483 token_secret = Column('token_secret', String(1024), default=u'')
3488 3484
3489 3485 @classmethod
3490 3486 def by_external_id_and_provider(cls, external_id, provider_name,
3491 3487 local_user_id=None):
3492 3488 """
3493 3489 Returns ExternalIdentity instance based on search params
3494 3490
3495 3491 :param external_id:
3496 3492 :param provider_name:
3497 3493 :return: ExternalIdentity
3498 3494 """
3499 3495 query = cls.query()
3500 3496 query = query.filter(cls.external_id == external_id)
3501 3497 query = query.filter(cls.provider_name == provider_name)
3502 3498 if local_user_id:
3503 3499 query = query.filter(cls.local_user_id == local_user_id)
3504 3500 return query.first()
3505 3501
3506 3502 @classmethod
3507 3503 def user_by_external_id_and_provider(cls, external_id, provider_name):
3508 3504 """
3509 3505 Returns User instance based on search params
3510 3506
3511 3507 :param external_id:
3512 3508 :param provider_name:
3513 3509 :return: User
3514 3510 """
3515 3511 query = User.query()
3516 3512 query = query.filter(cls.external_id == external_id)
3517 3513 query = query.filter(cls.provider_name == provider_name)
3518 3514 query = query.filter(User.user_id == cls.local_user_id)
3519 3515 return query.first()
3520 3516
3521 3517 @classmethod
3522 3518 def by_local_user_id(cls, local_user_id):
3523 3519 """
3524 3520 Returns all tokens for user
3525 3521
3526 3522 :param local_user_id:
3527 3523 :return: ExternalIdentity
3528 3524 """
3529 3525 query = cls.query()
3530 3526 query = query.filter(cls.local_user_id == local_user_id)
3531 3527 return query
3532 3528
3533 3529
3534 3530 class Integration(Base, BaseModel):
3535 3531 __tablename__ = 'integrations'
3536 3532 __table_args__ = (
3537 3533 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3538 3534 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3539 3535 )
3540 3536
3541 3537 integration_id = Column('integration_id', Integer(), primary_key=True)
3542 3538 integration_type = Column('integration_type', String(255))
3543 3539 enabled = Column('enabled', Boolean(), nullable=False)
3544 3540 name = Column('name', String(255), nullable=False)
3545 3541 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3546 3542 default=False)
3547 3543
3548 3544 settings = Column(
3549 3545 'settings_json', MutationObj.as_mutable(
3550 3546 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3551 3547 repo_id = Column(
3552 3548 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3553 3549 nullable=True, unique=None, default=None)
3554 3550 repo = relationship('Repository', lazy='joined')
3555 3551
3556 3552 repo_group_id = Column(
3557 3553 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3558 3554 nullable=True, unique=None, default=None)
3559 3555 repo_group = relationship('RepoGroup', lazy='joined')
3560 3556
3561 3557 @property
3562 3558 def scope(self):
3563 3559 if self.repo:
3564 3560 return repr(self.repo)
3565 3561 if self.repo_group:
3566 3562 if self.child_repos_only:
3567 3563 return repr(self.repo_group) + ' (child repos only)'
3568 3564 else:
3569 3565 return repr(self.repo_group) + ' (recursive)'
3570 3566 if self.child_repos_only:
3571 3567 return 'root_repos'
3572 3568 return 'global'
3573 3569
3574 3570 def __repr__(self):
3575 3571 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3576 3572
3577 3573
3578 3574 class RepoReviewRuleUser(Base, BaseModel):
3579 3575 __tablename__ = 'repo_review_rules_users'
3580 3576 __table_args__ = (
3581 3577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3582 3578 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3583 3579 )
3584 3580 repo_review_rule_user_id = Column(
3585 3581 'repo_review_rule_user_id', Integer(), primary_key=True)
3586 3582 repo_review_rule_id = Column("repo_review_rule_id",
3587 3583 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3588 3584 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3589 3585 nullable=False)
3590 3586 user = relationship('User')
3591 3587
3592 3588
3593 3589 class RepoReviewRuleUserGroup(Base, BaseModel):
3594 3590 __tablename__ = 'repo_review_rules_users_groups'
3595 3591 __table_args__ = (
3596 3592 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3597 3593 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3598 3594 )
3599 3595 repo_review_rule_users_group_id = Column(
3600 3596 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3601 3597 repo_review_rule_id = Column("repo_review_rule_id",
3602 3598 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3603 3599 users_group_id = Column("users_group_id", Integer(),
3604 3600 ForeignKey('users_groups.users_group_id'), nullable=False)
3605 3601 users_group = relationship('UserGroup')
3606 3602
3607 3603
3608 3604 class RepoReviewRule(Base, BaseModel):
3609 3605 __tablename__ = 'repo_review_rules'
3610 3606 __table_args__ = (
3611 3607 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3612 3608 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3613 3609 )
3614 3610
3615 3611 repo_review_rule_id = Column(
3616 3612 'repo_review_rule_id', Integer(), primary_key=True)
3617 3613 repo_id = Column(
3618 3614 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3619 3615 repo = relationship('Repository', backref='review_rules')
3620 3616
3621 3617 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3622 3618 default=u'*') # glob
3623 3619 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3624 3620 default=u'*') # glob
3625 3621
3626 3622 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3627 3623 nullable=False, default=False)
3628 3624 rule_users = relationship('RepoReviewRuleUser')
3629 3625 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3630 3626
3631 3627 @hybrid_property
3632 3628 def branch_pattern(self):
3633 3629 return self._branch_pattern or '*'
3634 3630
3635 3631 def _validate_glob(self, value):
3636 3632 re.compile('^' + glob2re(value) + '$')
3637 3633
3638 3634 @branch_pattern.setter
3639 3635 def branch_pattern(self, value):
3640 3636 self._validate_glob(value)
3641 3637 self._branch_pattern = value or '*'
3642 3638
3643 3639 @hybrid_property
3644 3640 def file_pattern(self):
3645 3641 return self._file_pattern or '*'
3646 3642
3647 3643 @file_pattern.setter
3648 3644 def file_pattern(self, value):
3649 3645 self._validate_glob(value)
3650 3646 self._file_pattern = value or '*'
3651 3647
3652 3648 def matches(self, branch, files_changed):
3653 3649 """
3654 3650 Check if this review rule matches a branch/files in a pull request
3655 3651
3656 3652 :param branch: branch name for the commit
3657 3653 :param files_changed: list of file paths changed in the pull request
3658 3654 """
3659 3655
3660 3656 branch = branch or ''
3661 3657 files_changed = files_changed or []
3662 3658
3663 3659 branch_matches = True
3664 3660 if branch:
3665 3661 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3666 3662 branch_matches = bool(branch_regex.search(branch))
3667 3663
3668 3664 files_matches = True
3669 3665 if self.file_pattern != '*':
3670 3666 files_matches = False
3671 3667 file_regex = re.compile(glob2re(self.file_pattern))
3672 3668 for filename in files_changed:
3673 3669 if file_regex.search(filename):
3674 3670 files_matches = True
3675 3671 break
3676 3672
3677 3673 return branch_matches and files_matches
3678 3674
3679 3675 @property
3680 3676 def review_users(self):
3681 3677 """ Returns the users which this rule applies to """
3682 3678
3683 3679 users = set()
3684 3680 users |= set([
3685 3681 rule_user.user for rule_user in self.rule_users
3686 3682 if rule_user.user.active])
3687 3683 users |= set(
3688 3684 member.user
3689 3685 for rule_user_group in self.rule_user_groups
3690 3686 for member in rule_user_group.users_group.members
3691 3687 if member.user.active
3692 3688 )
3693 3689 return users
3694 3690
3695 3691 def __repr__(self):
3696 3692 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3697 3693 self.repo_review_rule_id, self.repo)
@@ -1,1250 +1,1309 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26 from collections import namedtuple
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31
32 32 from pylons.i18n.translation import _
33 33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 35
35 36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 37 from rhodecode.lib.compat import OrderedDict
37 38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 39 from rhodecode.lib.markup_renderer import (
39 40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 41 from rhodecode.lib.utils import action_logger
41 42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 43 from rhodecode.lib.vcs.backends.base import (
43 44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 46 from rhodecode.lib.vcs.exceptions import (
46 47 CommitDoesNotExistError, EmptyRepositoryError)
47 48 from rhodecode.model import BaseModel
48 49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 50 from rhodecode.model.comment import ChangesetCommentsModel
50 51 from rhodecode.model.db import (
51 52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 53 PullRequestVersion, ChangesetComment)
53 54 from rhodecode.model.meta import Session
54 55 from rhodecode.model.notification import NotificationModel, \
55 56 EmailNotificationModel
56 57 from rhodecode.model.scm import ScmModel
57 58 from rhodecode.model.settings import VcsSettingsModel
58 59
59 60
60 61 log = logging.getLogger(__name__)
61 62
62 63
63 64 # Data structure to hold the response data when updating commits during a pull
64 65 # request update.
65 66 UpdateResponse = namedtuple(
66 67 'UpdateResponse', 'executed, reason, new, old, changes')
67 68
68 69
69 70 class PullRequestModel(BaseModel):
70 71
71 72 cls = PullRequest
72 73
73 74 DIFF_CONTEXT = 3
74 75
75 76 MERGE_STATUS_MESSAGES = {
76 77 MergeFailureReason.NONE: lazy_ugettext(
77 78 'This pull request can be automatically merged.'),
78 79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 80 'This pull request cannot be merged because of an unhandled'
80 81 ' exception.'),
81 82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 83 'This pull request cannot be merged because of conflicts.'),
83 84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 85 'This pull request could not be merged because push to target'
85 86 ' failed.'),
86 87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 88 'This pull request cannot be merged because the target is not a'
88 89 ' head.'),
89 90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 91 'This pull request cannot be merged because the source contains'
91 92 ' more branches than the target.'),
92 93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 94 'This pull request cannot be merged because the target has'
94 95 ' multiple heads.'),
95 96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 97 'This pull request cannot be merged because the target repository'
97 98 ' is locked.'),
98 99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 100 'This pull request cannot be merged because the target or the '
100 101 'source reference is missing.'),
101 102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 103 'This pull request cannot be merged because the target '
103 104 'reference is missing.'),
104 105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 106 'This pull request cannot be merged because the source '
106 107 'reference is missing.'),
107 108 }
108 109
109 110 UPDATE_STATUS_MESSAGES = {
110 111 UpdateFailureReason.NONE: lazy_ugettext(
111 112 'Pull request update successful.'),
112 113 UpdateFailureReason.UNKNOWN: lazy_ugettext(
113 114 'Pull request update failed because of an unknown error.'),
114 115 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
115 116 'No update needed because the source reference is already '
116 117 'up to date.'),
117 118 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
118 119 'Pull request cannot be updated because the reference type is '
119 120 'not supported for an update.'),
120 121 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
121 122 'This pull request cannot be updated because the target '
122 123 'reference is missing.'),
123 124 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
124 125 'This pull request cannot be updated because the source '
125 126 'reference is missing.'),
126 127 }
127 128
128 129 def __get_pull_request(self, pull_request):
129 130 return self._get_instance(PullRequest, pull_request)
130 131
131 132 def _check_perms(self, perms, pull_request, user, api=False):
132 133 if not api:
133 134 return h.HasRepoPermissionAny(*perms)(
134 135 user=user, repo_name=pull_request.target_repo.repo_name)
135 136 else:
136 137 return h.HasRepoPermissionAnyApi(*perms)(
137 138 user=user, repo_name=pull_request.target_repo.repo_name)
138 139
139 140 def check_user_read(self, pull_request, user, api=False):
140 141 _perms = ('repository.admin', 'repository.write', 'repository.read',)
141 142 return self._check_perms(_perms, pull_request, user, api)
142 143
143 144 def check_user_merge(self, pull_request, user, api=False):
144 145 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
145 146 return self._check_perms(_perms, pull_request, user, api)
146 147
147 148 def check_user_update(self, pull_request, user, api=False):
148 149 owner = user.user_id == pull_request.user_id
149 150 return self.check_user_merge(pull_request, user, api) or owner
150 151
151 152 def check_user_change_status(self, pull_request, user, api=False):
152 153 reviewer = user.user_id in [x.user_id for x in
153 154 pull_request.reviewers]
154 155 return self.check_user_update(pull_request, user, api) or reviewer
155 156
156 157 def get(self, pull_request):
157 158 return self.__get_pull_request(pull_request)
158 159
159 160 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
160 161 opened_by=None, order_by=None,
161 162 order_dir='desc'):
162 repo = self._get_repo(repo_name)
163 repo = None
164 if repo_name:
165 repo = self._get_repo(repo_name)
166
163 167 q = PullRequest.query()
168
164 169 # source or target
165 if source:
170 if repo and source:
166 171 q = q.filter(PullRequest.source_repo == repo)
167 else:
172 elif repo:
168 173 q = q.filter(PullRequest.target_repo == repo)
169 174
170 175 # closed,opened
171 176 if statuses:
172 177 q = q.filter(PullRequest.status.in_(statuses))
173 178
174 179 # opened by filter
175 180 if opened_by:
176 181 q = q.filter(PullRequest.user_id.in_(opened_by))
177 182
178 183 if order_by:
179 184 order_map = {
180 185 'name_raw': PullRequest.pull_request_id,
181 186 'title': PullRequest.title,
182 'updated_on_raw': PullRequest.updated_on
187 'updated_on_raw': PullRequest.updated_on,
188 'target_repo': PullRequest.target_repo_id
183 189 }
184 190 if order_dir == 'asc':
185 191 q = q.order_by(order_map[order_by].asc())
186 192 else:
187 193 q = q.order_by(order_map[order_by].desc())
188 194
189 195 return q
190 196
191 197 def count_all(self, repo_name, source=False, statuses=None,
192 198 opened_by=None):
193 199 """
194 200 Count the number of pull requests for a specific repository.
195 201
196 202 :param repo_name: target or source repo
197 203 :param source: boolean flag to specify if repo_name refers to source
198 204 :param statuses: list of pull request statuses
199 205 :param opened_by: author user of the pull request
200 206 :returns: int number of pull requests
201 207 """
202 208 q = self._prepare_get_all_query(
203 209 repo_name, source=source, statuses=statuses, opened_by=opened_by)
204 210
205 211 return q.count()
206 212
207 213 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
208 214 offset=0, length=None, order_by=None, order_dir='desc'):
209 215 """
210 216 Get all pull requests for a specific repository.
211 217
212 218 :param repo_name: target or source repo
213 219 :param source: boolean flag to specify if repo_name refers to source
214 220 :param statuses: list of pull request statuses
215 221 :param opened_by: author user of the pull request
216 222 :param offset: pagination offset
217 223 :param length: length of returned list
218 224 :param order_by: order of the returned list
219 225 :param order_dir: 'asc' or 'desc' ordering direction
220 226 :returns: list of pull requests
221 227 """
222 228 q = self._prepare_get_all_query(
223 229 repo_name, source=source, statuses=statuses, opened_by=opened_by,
224 230 order_by=order_by, order_dir=order_dir)
225 231
226 232 if length:
227 233 pull_requests = q.limit(length).offset(offset).all()
228 234 else:
229 235 pull_requests = q.all()
230 236
231 237 return pull_requests
232 238
233 239 def count_awaiting_review(self, repo_name, source=False, statuses=None,
234 240 opened_by=None):
235 241 """
236 242 Count the number of pull requests for a specific repository that are
237 243 awaiting review.
238 244
239 245 :param repo_name: target or source repo
240 246 :param source: boolean flag to specify if repo_name refers to source
241 247 :param statuses: list of pull request statuses
242 248 :param opened_by: author user of the pull request
243 249 :returns: int number of pull requests
244 250 """
245 251 pull_requests = self.get_awaiting_review(
246 252 repo_name, source=source, statuses=statuses, opened_by=opened_by)
247 253
248 254 return len(pull_requests)
249 255
250 256 def get_awaiting_review(self, repo_name, source=False, statuses=None,
251 257 opened_by=None, offset=0, length=None,
252 258 order_by=None, order_dir='desc'):
253 259 """
254 260 Get all pull requests for a specific repository that are awaiting
255 261 review.
256 262
257 263 :param repo_name: target or source repo
258 264 :param source: boolean flag to specify if repo_name refers to source
259 265 :param statuses: list of pull request statuses
260 266 :param opened_by: author user of the pull request
261 267 :param offset: pagination offset
262 268 :param length: length of returned list
263 269 :param order_by: order of the returned list
264 270 :param order_dir: 'asc' or 'desc' ordering direction
265 271 :returns: list of pull requests
266 272 """
267 273 pull_requests = self.get_all(
268 274 repo_name, source=source, statuses=statuses, opened_by=opened_by,
269 275 order_by=order_by, order_dir=order_dir)
270 276
271 277 _filtered_pull_requests = []
272 278 for pr in pull_requests:
273 279 status = pr.calculated_review_status()
274 280 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
275 281 ChangesetStatus.STATUS_UNDER_REVIEW]:
276 282 _filtered_pull_requests.append(pr)
277 283 if length:
278 284 return _filtered_pull_requests[offset:offset+length]
279 285 else:
280 286 return _filtered_pull_requests
281 287
282 288 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
283 289 opened_by=None, user_id=None):
284 290 """
285 291 Count the number of pull requests for a specific repository that are
286 292 awaiting review from a specific user.
287 293
288 294 :param repo_name: target or source repo
289 295 :param source: boolean flag to specify if repo_name refers to source
290 296 :param statuses: list of pull request statuses
291 297 :param opened_by: author user of the pull request
292 298 :param user_id: reviewer user of the pull request
293 299 :returns: int number of pull requests
294 300 """
295 301 pull_requests = self.get_awaiting_my_review(
296 302 repo_name, source=source, statuses=statuses, opened_by=opened_by,
297 303 user_id=user_id)
298 304
299 305 return len(pull_requests)
300 306
301 307 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 308 opened_by=None, user_id=None, offset=0,
303 309 length=None, order_by=None, order_dir='desc'):
304 310 """
305 311 Get all pull requests for a specific repository that are awaiting
306 312 review from a specific user.
307 313
308 314 :param repo_name: target or source repo
309 315 :param source: boolean flag to specify if repo_name refers to source
310 316 :param statuses: list of pull request statuses
311 317 :param opened_by: author user of the pull request
312 318 :param user_id: reviewer user of the pull request
313 319 :param offset: pagination offset
314 320 :param length: length of returned list
315 321 :param order_by: order of the returned list
316 322 :param order_dir: 'asc' or 'desc' ordering direction
317 323 :returns: list of pull requests
318 324 """
319 325 pull_requests = self.get_all(
320 326 repo_name, source=source, statuses=statuses, opened_by=opened_by,
321 327 order_by=order_by, order_dir=order_dir)
322 328
323 329 _my = PullRequestModel().get_not_reviewed(user_id)
324 330 my_participation = []
325 331 for pr in pull_requests:
326 332 if pr in _my:
327 333 my_participation.append(pr)
328 334 _filtered_pull_requests = my_participation
329 335 if length:
330 336 return _filtered_pull_requests[offset:offset+length]
331 337 else:
332 338 return _filtered_pull_requests
333 339
334 340 def get_not_reviewed(self, user_id):
335 341 return [
336 342 x.pull_request for x in PullRequestReviewers.query().filter(
337 343 PullRequestReviewers.user_id == user_id).all()
338 344 ]
339 345
346 def _prepare_participating_query(self, user_id=None, statuses=None,
347 order_by=None, order_dir='desc'):
348 q = PullRequest.query()
349 if user_id:
350 reviewers_subquery = Session().query(
351 PullRequestReviewers.pull_request_id).filter(
352 PullRequestReviewers.user_id == user_id).subquery()
353 user_filter= or_(
354 PullRequest.user_id == user_id,
355 PullRequest.pull_request_id.in_(reviewers_subquery)
356 )
357 q = PullRequest.query().filter(user_filter)
358
359 # closed,opened
360 if statuses:
361 q = q.filter(PullRequest.status.in_(statuses))
362
363 if order_by:
364 order_map = {
365 'name_raw': PullRequest.pull_request_id,
366 'title': PullRequest.title,
367 'updated_on_raw': PullRequest.updated_on,
368 'target_repo': PullRequest.target_repo_id
369 }
370 if order_dir == 'asc':
371 q = q.order_by(order_map[order_by].asc())
372 else:
373 q = q.order_by(order_map[order_by].desc())
374
375 return q
376
377 def count_im_participating_in(self, user_id=None, statuses=None):
378 q = self._prepare_participating_query(user_id, statuses=statuses)
379 return q.count()
380
381 def get_im_participating_in(
382 self, user_id=None, statuses=None, offset=0,
383 length=None, order_by=None, order_dir='desc'):
384 """
385 Get all Pull requests that i'm participating in, or i have opened
386 """
387
388 q = self._prepare_participating_query(
389 user_id, statuses=statuses, order_by=order_by,
390 order_dir=order_dir)
391
392 if length:
393 pull_requests = q.limit(length).offset(offset).all()
394 else:
395 pull_requests = q.all()
396
397 return pull_requests
398
340 399 def get_versions(self, pull_request):
341 400 """
342 401 returns version of pull request sorted by ID descending
343 402 """
344 403 return PullRequestVersion.query()\
345 404 .filter(PullRequestVersion.pull_request == pull_request)\
346 405 .order_by(PullRequestVersion.pull_request_version_id.asc())\
347 406 .all()
348 407
349 408 def create(self, created_by, source_repo, source_ref, target_repo,
350 409 target_ref, revisions, reviewers, title, description=None):
351 410 created_by_user = self._get_user(created_by)
352 411 source_repo = self._get_repo(source_repo)
353 412 target_repo = self._get_repo(target_repo)
354 413
355 414 pull_request = PullRequest()
356 415 pull_request.source_repo = source_repo
357 416 pull_request.source_ref = source_ref
358 417 pull_request.target_repo = target_repo
359 418 pull_request.target_ref = target_ref
360 419 pull_request.revisions = revisions
361 420 pull_request.title = title
362 421 pull_request.description = description
363 422 pull_request.author = created_by_user
364 423
365 424 Session().add(pull_request)
366 425 Session().flush()
367 426
368 427 reviewer_ids = set()
369 428 # members / reviewers
370 429 for reviewer_object in reviewers:
371 430 if isinstance(reviewer_object, tuple):
372 431 user_id, reasons = reviewer_object
373 432 else:
374 433 user_id, reasons = reviewer_object, []
375 434
376 435 user = self._get_user(user_id)
377 436 reviewer_ids.add(user.user_id)
378 437
379 438 reviewer = PullRequestReviewers(user, pull_request, reasons)
380 439 Session().add(reviewer)
381 440
382 441 # Set approval status to "Under Review" for all commits which are
383 442 # part of this pull request.
384 443 ChangesetStatusModel().set_status(
385 444 repo=target_repo,
386 445 status=ChangesetStatus.STATUS_UNDER_REVIEW,
387 446 user=created_by_user,
388 447 pull_request=pull_request
389 448 )
390 449
391 450 self.notify_reviewers(pull_request, reviewer_ids)
392 451 self._trigger_pull_request_hook(
393 452 pull_request, created_by_user, 'create')
394 453
395 454 return pull_request
396 455
397 456 def _trigger_pull_request_hook(self, pull_request, user, action):
398 457 pull_request = self.__get_pull_request(pull_request)
399 458 target_scm = pull_request.target_repo.scm_instance()
400 459 if action == 'create':
401 460 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
402 461 elif action == 'merge':
403 462 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
404 463 elif action == 'close':
405 464 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
406 465 elif action == 'review_status_change':
407 466 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
408 467 elif action == 'update':
409 468 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
410 469 else:
411 470 return
412 471
413 472 trigger_hook(
414 473 username=user.username,
415 474 repo_name=pull_request.target_repo.repo_name,
416 475 repo_alias=target_scm.alias,
417 476 pull_request=pull_request)
418 477
419 478 def _get_commit_ids(self, pull_request):
420 479 """
421 480 Return the commit ids of the merged pull request.
422 481
423 482 This method is not dealing correctly yet with the lack of autoupdates
424 483 nor with the implicit target updates.
425 484 For example: if a commit in the source repo is already in the target it
426 485 will be reported anyways.
427 486 """
428 487 merge_rev = pull_request.merge_rev
429 488 if merge_rev is None:
430 489 raise ValueError('This pull request was not merged yet')
431 490
432 491 commit_ids = list(pull_request.revisions)
433 492 if merge_rev not in commit_ids:
434 493 commit_ids.append(merge_rev)
435 494
436 495 return commit_ids
437 496
438 497 def merge(self, pull_request, user, extras):
439 498 log.debug("Merging pull request %s", pull_request.pull_request_id)
440 499 merge_state = self._merge_pull_request(pull_request, user, extras)
441 500 if merge_state.executed:
442 501 log.debug(
443 502 "Merge was successful, updating the pull request comments.")
444 503 self._comment_and_close_pr(pull_request, user, merge_state)
445 504 self._log_action('user_merged_pull_request', user, pull_request)
446 505 else:
447 506 log.warn("Merge failed, not updating the pull request.")
448 507 return merge_state
449 508
450 509 def _merge_pull_request(self, pull_request, user, extras):
451 510 target_vcs = pull_request.target_repo.scm_instance()
452 511 source_vcs = pull_request.source_repo.scm_instance()
453 512 target_ref = self._refresh_reference(
454 513 pull_request.target_ref_parts, target_vcs)
455 514
456 515 message = _(
457 516 'Merge pull request #%(pr_id)s from '
458 517 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
459 518 'pr_id': pull_request.pull_request_id,
460 519 'source_repo': source_vcs.name,
461 520 'source_ref_name': pull_request.source_ref_parts.name,
462 521 'pr_title': pull_request.title
463 522 }
464 523
465 524 workspace_id = self._workspace_id(pull_request)
466 525 use_rebase = self._use_rebase_for_merging(pull_request)
467 526
468 527 callback_daemon, extras = prepare_callback_daemon(
469 528 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
470 529 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
471 530
472 531 with callback_daemon:
473 532 # TODO: johbo: Implement a clean way to run a config_override
474 533 # for a single call.
475 534 target_vcs.config.set(
476 535 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
477 536 merge_state = target_vcs.merge(
478 537 target_ref, source_vcs, pull_request.source_ref_parts,
479 538 workspace_id, user_name=user.username,
480 539 user_email=user.email, message=message, use_rebase=use_rebase)
481 540 return merge_state
482 541
483 542 def _comment_and_close_pr(self, pull_request, user, merge_state):
484 543 pull_request.merge_rev = merge_state.merge_ref.commit_id
485 544 pull_request.updated_on = datetime.datetime.now()
486 545
487 546 ChangesetCommentsModel().create(
488 547 text=unicode(_('Pull request merged and closed')),
489 548 repo=pull_request.target_repo.repo_id,
490 549 user=user.user_id,
491 550 pull_request=pull_request.pull_request_id,
492 551 f_path=None,
493 552 line_no=None,
494 553 closing_pr=True
495 554 )
496 555
497 556 Session().add(pull_request)
498 557 Session().flush()
499 558 # TODO: paris: replace invalidation with less radical solution
500 559 ScmModel().mark_for_invalidation(
501 560 pull_request.target_repo.repo_name)
502 561 self._trigger_pull_request_hook(pull_request, user, 'merge')
503 562
504 563 def has_valid_update_type(self, pull_request):
505 564 source_ref_type = pull_request.source_ref_parts.type
506 565 return source_ref_type in ['book', 'branch', 'tag']
507 566
508 567 def update_commits(self, pull_request):
509 568 """
510 569 Get the updated list of commits for the pull request
511 570 and return the new pull request version and the list
512 571 of commits processed by this update action
513 572 """
514 573 pull_request = self.__get_pull_request(pull_request)
515 574 source_ref_type = pull_request.source_ref_parts.type
516 575 source_ref_name = pull_request.source_ref_parts.name
517 576 source_ref_id = pull_request.source_ref_parts.commit_id
518 577
519 578 if not self.has_valid_update_type(pull_request):
520 579 log.debug(
521 580 "Skipping update of pull request %s due to ref type: %s",
522 581 pull_request, source_ref_type)
523 582 return UpdateResponse(
524 583 executed=False,
525 584 reason=UpdateFailureReason.WRONG_REF_TPYE,
526 585 old=pull_request, new=None, changes=None)
527 586
528 587 source_repo = pull_request.source_repo.scm_instance()
529 588 try:
530 589 source_commit = source_repo.get_commit(commit_id=source_ref_name)
531 590 except CommitDoesNotExistError:
532 591 return UpdateResponse(
533 592 executed=False,
534 593 reason=UpdateFailureReason.MISSING_SOURCE_REF,
535 594 old=pull_request, new=None, changes=None)
536 595
537 596 if source_ref_id == source_commit.raw_id:
538 597 log.debug("Nothing changed in pull request %s", pull_request)
539 598 return UpdateResponse(
540 599 executed=False,
541 600 reason=UpdateFailureReason.NO_CHANGE,
542 601 old=pull_request, new=None, changes=None)
543 602
544 603 # Finally there is a need for an update
545 604 pull_request_version = self._create_version_from_snapshot(pull_request)
546 605 self._link_comments_to_version(pull_request_version)
547 606
548 607 target_ref_type = pull_request.target_ref_parts.type
549 608 target_ref_name = pull_request.target_ref_parts.name
550 609 target_ref_id = pull_request.target_ref_parts.commit_id
551 610 target_repo = pull_request.target_repo.scm_instance()
552 611
553 612 try:
554 613 if target_ref_type in ('tag', 'branch', 'book'):
555 614 target_commit = target_repo.get_commit(target_ref_name)
556 615 else:
557 616 target_commit = target_repo.get_commit(target_ref_id)
558 617 except CommitDoesNotExistError:
559 618 return UpdateResponse(
560 619 executed=False,
561 620 reason=UpdateFailureReason.MISSING_TARGET_REF,
562 621 old=pull_request, new=None, changes=None)
563 622
564 623 # re-compute commit ids
565 624 old_commit_ids = set(pull_request.revisions)
566 625 pre_load = ["author", "branch", "date", "message"]
567 626 commit_ranges = target_repo.compare(
568 627 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
569 628 pre_load=pre_load)
570 629
571 630 ancestor = target_repo.get_common_ancestor(
572 631 target_commit.raw_id, source_commit.raw_id, source_repo)
573 632
574 633 pull_request.source_ref = '%s:%s:%s' % (
575 634 source_ref_type, source_ref_name, source_commit.raw_id)
576 635 pull_request.target_ref = '%s:%s:%s' % (
577 636 target_ref_type, target_ref_name, ancestor)
578 637 pull_request.revisions = [
579 638 commit.raw_id for commit in reversed(commit_ranges)]
580 639 pull_request.updated_on = datetime.datetime.now()
581 640 Session().add(pull_request)
582 641 new_commit_ids = set(pull_request.revisions)
583 642
584 643 changes = self._calculate_commit_id_changes(
585 644 old_commit_ids, new_commit_ids)
586 645
587 646 old_diff_data, new_diff_data = self._generate_update_diffs(
588 647 pull_request, pull_request_version)
589 648
590 649 ChangesetCommentsModel().outdate_comments(
591 650 pull_request, old_diff_data=old_diff_data,
592 651 new_diff_data=new_diff_data)
593 652
594 653 file_changes = self._calculate_file_changes(
595 654 old_diff_data, new_diff_data)
596 655
597 656 # Add an automatic comment to the pull request
598 657 update_comment = ChangesetCommentsModel().create(
599 658 text=self._render_update_message(changes, file_changes),
600 659 repo=pull_request.target_repo,
601 660 user=pull_request.author,
602 661 pull_request=pull_request,
603 662 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
604 663
605 664 # Update status to "Under Review" for added commits
606 665 for commit_id in changes.added:
607 666 ChangesetStatusModel().set_status(
608 667 repo=pull_request.source_repo,
609 668 status=ChangesetStatus.STATUS_UNDER_REVIEW,
610 669 comment=update_comment,
611 670 user=pull_request.author,
612 671 pull_request=pull_request,
613 672 revision=commit_id)
614 673
615 674 log.debug(
616 675 'Updated pull request %s, added_ids: %s, common_ids: %s, '
617 676 'removed_ids: %s', pull_request.pull_request_id,
618 677 changes.added, changes.common, changes.removed)
619 678 log.debug('Updated pull request with the following file changes: %s',
620 679 file_changes)
621 680
622 681 log.info(
623 682 "Updated pull request %s from commit %s to commit %s, "
624 683 "stored new version %s of this pull request.",
625 684 pull_request.pull_request_id, source_ref_id,
626 685 pull_request.source_ref_parts.commit_id,
627 686 pull_request_version.pull_request_version_id)
628 687 Session().commit()
629 688 self._trigger_pull_request_hook(pull_request, pull_request.author,
630 689 'update')
631 690
632 691 return UpdateResponse(
633 692 executed=True, reason=UpdateFailureReason.NONE,
634 693 old=pull_request, new=pull_request_version, changes=changes)
635 694
636 695 def _create_version_from_snapshot(self, pull_request):
637 696 version = PullRequestVersion()
638 697 version.title = pull_request.title
639 698 version.description = pull_request.description
640 699 version.status = pull_request.status
641 700 version.created_on = pull_request.created_on
642 701 version.updated_on = pull_request.updated_on
643 702 version.user_id = pull_request.user_id
644 703 version.source_repo = pull_request.source_repo
645 704 version.source_ref = pull_request.source_ref
646 705 version.target_repo = pull_request.target_repo
647 706 version.target_ref = pull_request.target_ref
648 707
649 708 version._last_merge_source_rev = pull_request._last_merge_source_rev
650 709 version._last_merge_target_rev = pull_request._last_merge_target_rev
651 710 version._last_merge_status = pull_request._last_merge_status
652 711 version.shadow_merge_ref = pull_request.shadow_merge_ref
653 712 version.merge_rev = pull_request.merge_rev
654 713
655 714 version.revisions = pull_request.revisions
656 715 version.pull_request = pull_request
657 716 Session().add(version)
658 717 Session().flush()
659 718
660 719 return version
661 720
662 721 def _generate_update_diffs(self, pull_request, pull_request_version):
663 722 diff_context = (
664 723 self.DIFF_CONTEXT +
665 724 ChangesetCommentsModel.needed_extra_diff_context())
666 725 old_diff = self._get_diff_from_pr_or_version(
667 726 pull_request_version, context=diff_context)
668 727 new_diff = self._get_diff_from_pr_or_version(
669 728 pull_request, context=diff_context)
670 729
671 730 old_diff_data = diffs.DiffProcessor(old_diff)
672 731 old_diff_data.prepare()
673 732 new_diff_data = diffs.DiffProcessor(new_diff)
674 733 new_diff_data.prepare()
675 734
676 735 return old_diff_data, new_diff_data
677 736
678 737 def _link_comments_to_version(self, pull_request_version):
679 738 """
680 739 Link all unlinked comments of this pull request to the given version.
681 740
682 741 :param pull_request_version: The `PullRequestVersion` to which
683 742 the comments shall be linked.
684 743
685 744 """
686 745 pull_request = pull_request_version.pull_request
687 746 comments = ChangesetComment.query().filter(
688 747 # TODO: johbo: Should we query for the repo at all here?
689 748 # Pending decision on how comments of PRs are to be related
690 749 # to either the source repo, the target repo or no repo at all.
691 750 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
692 751 ChangesetComment.pull_request == pull_request,
693 752 ChangesetComment.pull_request_version == None)
694 753
695 754 # TODO: johbo: Find out why this breaks if it is done in a bulk
696 755 # operation.
697 756 for comment in comments:
698 757 comment.pull_request_version_id = (
699 758 pull_request_version.pull_request_version_id)
700 759 Session().add(comment)
701 760
702 761 def _calculate_commit_id_changes(self, old_ids, new_ids):
703 762 added = new_ids.difference(old_ids)
704 763 common = old_ids.intersection(new_ids)
705 764 removed = old_ids.difference(new_ids)
706 765 return ChangeTuple(added, common, removed)
707 766
708 767 def _calculate_file_changes(self, old_diff_data, new_diff_data):
709 768
710 769 old_files = OrderedDict()
711 770 for diff_data in old_diff_data.parsed_diff:
712 771 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
713 772
714 773 added_files = []
715 774 modified_files = []
716 775 removed_files = []
717 776 for diff_data in new_diff_data.parsed_diff:
718 777 new_filename = diff_data['filename']
719 778 new_hash = md5_safe(diff_data['raw_diff'])
720 779
721 780 old_hash = old_files.get(new_filename)
722 781 if not old_hash:
723 782 # file is not present in old diff, means it's added
724 783 added_files.append(new_filename)
725 784 else:
726 785 if new_hash != old_hash:
727 786 modified_files.append(new_filename)
728 787 # now remove a file from old, since we have seen it already
729 788 del old_files[new_filename]
730 789
731 790 # removed files is when there are present in old, but not in NEW,
732 791 # since we remove old files that are present in new diff, left-overs
733 792 # if any should be the removed files
734 793 removed_files.extend(old_files.keys())
735 794
736 795 return FileChangeTuple(added_files, modified_files, removed_files)
737 796
738 797 def _render_update_message(self, changes, file_changes):
739 798 """
740 799 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
741 800 so it's always looking the same disregarding on which default
742 801 renderer system is using.
743 802
744 803 :param changes: changes named tuple
745 804 :param file_changes: file changes named tuple
746 805
747 806 """
748 807 new_status = ChangesetStatus.get_status_lbl(
749 808 ChangesetStatus.STATUS_UNDER_REVIEW)
750 809
751 810 changed_files = (
752 811 file_changes.added + file_changes.modified + file_changes.removed)
753 812
754 813 params = {
755 814 'under_review_label': new_status,
756 815 'added_commits': changes.added,
757 816 'removed_commits': changes.removed,
758 817 'changed_files': changed_files,
759 818 'added_files': file_changes.added,
760 819 'modified_files': file_changes.modified,
761 820 'removed_files': file_changes.removed,
762 821 }
763 822 renderer = RstTemplateRenderer()
764 823 return renderer.render('pull_request_update.mako', **params)
765 824
766 825 def edit(self, pull_request, title, description):
767 826 pull_request = self.__get_pull_request(pull_request)
768 827 if pull_request.is_closed():
769 828 raise ValueError('This pull request is closed')
770 829 if title:
771 830 pull_request.title = title
772 831 pull_request.description = description
773 832 pull_request.updated_on = datetime.datetime.now()
774 833 Session().add(pull_request)
775 834
776 835 def update_reviewers(self, pull_request, reviewer_data):
777 836 """
778 837 Update the reviewers in the pull request
779 838
780 839 :param pull_request: the pr to update
781 840 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
782 841 """
783 842
784 843 reviewers_reasons = {}
785 844 for user_id, reasons in reviewer_data:
786 845 if isinstance(user_id, (int, basestring)):
787 846 user_id = self._get_user(user_id).user_id
788 847 reviewers_reasons[user_id] = reasons
789 848
790 849 reviewers_ids = set(reviewers_reasons.keys())
791 850 pull_request = self.__get_pull_request(pull_request)
792 851 current_reviewers = PullRequestReviewers.query()\
793 852 .filter(PullRequestReviewers.pull_request ==
794 853 pull_request).all()
795 854 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
796 855
797 856 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
798 857 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
799 858
800 859 log.debug("Adding %s reviewers", ids_to_add)
801 860 log.debug("Removing %s reviewers", ids_to_remove)
802 861 changed = False
803 862 for uid in ids_to_add:
804 863 changed = True
805 864 _usr = self._get_user(uid)
806 865 reasons = reviewers_reasons[uid]
807 866 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
808 867 Session().add(reviewer)
809 868
810 869 self.notify_reviewers(pull_request, ids_to_add)
811 870
812 871 for uid in ids_to_remove:
813 872 changed = True
814 873 reviewer = PullRequestReviewers.query()\
815 874 .filter(PullRequestReviewers.user_id == uid,
816 875 PullRequestReviewers.pull_request == pull_request)\
817 876 .scalar()
818 877 if reviewer:
819 878 Session().delete(reviewer)
820 879 if changed:
821 880 pull_request.updated_on = datetime.datetime.now()
822 881 Session().add(pull_request)
823 882
824 883 return ids_to_add, ids_to_remove
825 884
826 885 def get_url(self, pull_request):
827 886 return h.url('pullrequest_show',
828 887 repo_name=safe_str(pull_request.target_repo.repo_name),
829 888 pull_request_id=pull_request.pull_request_id,
830 889 qualified=True)
831 890
832 891 def get_shadow_clone_url(self, pull_request):
833 892 """
834 893 Returns qualified url pointing to the shadow repository. If this pull
835 894 request is closed there is no shadow repository and ``None`` will be
836 895 returned.
837 896 """
838 897 if pull_request.is_closed():
839 898 return None
840 899 else:
841 900 pr_url = urllib.unquote(self.get_url(pull_request))
842 901 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
843 902
844 903 def notify_reviewers(self, pull_request, reviewers_ids):
845 904 # notification to reviewers
846 905 if not reviewers_ids:
847 906 return
848 907
849 908 pull_request_obj = pull_request
850 909 # get the current participants of this pull request
851 910 recipients = reviewers_ids
852 911 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
853 912
854 913 pr_source_repo = pull_request_obj.source_repo
855 914 pr_target_repo = pull_request_obj.target_repo
856 915
857 916 pr_url = h.url(
858 917 'pullrequest_show',
859 918 repo_name=pr_target_repo.repo_name,
860 919 pull_request_id=pull_request_obj.pull_request_id,
861 920 qualified=True,)
862 921
863 922 # set some variables for email notification
864 923 pr_target_repo_url = h.url(
865 924 'summary_home',
866 925 repo_name=pr_target_repo.repo_name,
867 926 qualified=True)
868 927
869 928 pr_source_repo_url = h.url(
870 929 'summary_home',
871 930 repo_name=pr_source_repo.repo_name,
872 931 qualified=True)
873 932
874 933 # pull request specifics
875 934 pull_request_commits = [
876 935 (x.raw_id, x.message)
877 936 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
878 937
879 938 kwargs = {
880 939 'user': pull_request.author,
881 940 'pull_request': pull_request_obj,
882 941 'pull_request_commits': pull_request_commits,
883 942
884 943 'pull_request_target_repo': pr_target_repo,
885 944 'pull_request_target_repo_url': pr_target_repo_url,
886 945
887 946 'pull_request_source_repo': pr_source_repo,
888 947 'pull_request_source_repo_url': pr_source_repo_url,
889 948
890 949 'pull_request_url': pr_url,
891 950 }
892 951
893 952 # pre-generate the subject for notification itself
894 953 (subject,
895 954 _h, _e, # we don't care about those
896 955 body_plaintext) = EmailNotificationModel().render_email(
897 956 notification_type, **kwargs)
898 957
899 958 # create notification objects, and emails
900 959 NotificationModel().create(
901 960 created_by=pull_request.author,
902 961 notification_subject=subject,
903 962 notification_body=body_plaintext,
904 963 notification_type=notification_type,
905 964 recipients=recipients,
906 965 email_kwargs=kwargs,
907 966 )
908 967
909 968 def delete(self, pull_request):
910 969 pull_request = self.__get_pull_request(pull_request)
911 970 self._cleanup_merge_workspace(pull_request)
912 971 Session().delete(pull_request)
913 972
914 973 def close_pull_request(self, pull_request, user):
915 974 pull_request = self.__get_pull_request(pull_request)
916 975 self._cleanup_merge_workspace(pull_request)
917 976 pull_request.status = PullRequest.STATUS_CLOSED
918 977 pull_request.updated_on = datetime.datetime.now()
919 978 Session().add(pull_request)
920 979 self._trigger_pull_request_hook(
921 980 pull_request, pull_request.author, 'close')
922 981 self._log_action('user_closed_pull_request', user, pull_request)
923 982
924 983 def close_pull_request_with_comment(self, pull_request, user, repo,
925 984 message=None):
926 985 status = ChangesetStatus.STATUS_REJECTED
927 986
928 987 if not message:
929 988 message = (
930 989 _('Status change %(transition_icon)s %(status)s') % {
931 990 'transition_icon': '>',
932 991 'status': ChangesetStatus.get_status_lbl(status)})
933 992
934 993 internal_message = _('Closing with') + ' ' + message
935 994
936 995 comm = ChangesetCommentsModel().create(
937 996 text=internal_message,
938 997 repo=repo.repo_id,
939 998 user=user.user_id,
940 999 pull_request=pull_request.pull_request_id,
941 1000 f_path=None,
942 1001 line_no=None,
943 1002 status_change=ChangesetStatus.get_status_lbl(status),
944 1003 status_change_type=status,
945 1004 closing_pr=True
946 1005 )
947 1006
948 1007 ChangesetStatusModel().set_status(
949 1008 repo.repo_id,
950 1009 status,
951 1010 user.user_id,
952 1011 comm,
953 1012 pull_request=pull_request.pull_request_id
954 1013 )
955 1014 Session().flush()
956 1015
957 1016 PullRequestModel().close_pull_request(
958 1017 pull_request.pull_request_id, user)
959 1018
960 1019 def merge_status(self, pull_request):
961 1020 if not self._is_merge_enabled(pull_request):
962 1021 return False, _('Server-side pull request merging is disabled.')
963 1022 if pull_request.is_closed():
964 1023 return False, _('This pull request is closed.')
965 1024 merge_possible, msg = self._check_repo_requirements(
966 1025 target=pull_request.target_repo, source=pull_request.source_repo)
967 1026 if not merge_possible:
968 1027 return merge_possible, msg
969 1028
970 1029 try:
971 1030 resp = self._try_merge(pull_request)
972 1031 log.debug("Merge response: %s", resp)
973 1032 status = resp.possible, self.merge_status_message(
974 1033 resp.failure_reason)
975 1034 except NotImplementedError:
976 1035 status = False, _('Pull request merging is not supported.')
977 1036
978 1037 return status
979 1038
980 1039 def _check_repo_requirements(self, target, source):
981 1040 """
982 1041 Check if `target` and `source` have compatible requirements.
983 1042
984 1043 Currently this is just checking for largefiles.
985 1044 """
986 1045 target_has_largefiles = self._has_largefiles(target)
987 1046 source_has_largefiles = self._has_largefiles(source)
988 1047 merge_possible = True
989 1048 message = u''
990 1049
991 1050 if target_has_largefiles != source_has_largefiles:
992 1051 merge_possible = False
993 1052 if source_has_largefiles:
994 1053 message = _(
995 1054 'Target repository large files support is disabled.')
996 1055 else:
997 1056 message = _(
998 1057 'Source repository large files support is disabled.')
999 1058
1000 1059 return merge_possible, message
1001 1060
1002 1061 def _has_largefiles(self, repo):
1003 1062 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1004 1063 'extensions', 'largefiles')
1005 1064 return largefiles_ui and largefiles_ui[0].active
1006 1065
1007 1066 def _try_merge(self, pull_request):
1008 1067 """
1009 1068 Try to merge the pull request and return the merge status.
1010 1069 """
1011 1070 log.debug(
1012 1071 "Trying out if the pull request %s can be merged.",
1013 1072 pull_request.pull_request_id)
1014 1073 target_vcs = pull_request.target_repo.scm_instance()
1015 1074
1016 1075 # Refresh the target reference.
1017 1076 try:
1018 1077 target_ref = self._refresh_reference(
1019 1078 pull_request.target_ref_parts, target_vcs)
1020 1079 except CommitDoesNotExistError:
1021 1080 merge_state = MergeResponse(
1022 1081 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1023 1082 return merge_state
1024 1083
1025 1084 target_locked = pull_request.target_repo.locked
1026 1085 if target_locked and target_locked[0]:
1027 1086 log.debug("The target repository is locked.")
1028 1087 merge_state = MergeResponse(
1029 1088 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1030 1089 elif self._needs_merge_state_refresh(pull_request, target_ref):
1031 1090 log.debug("Refreshing the merge status of the repository.")
1032 1091 merge_state = self._refresh_merge_state(
1033 1092 pull_request, target_vcs, target_ref)
1034 1093 else:
1035 1094 possible = pull_request.\
1036 1095 _last_merge_status == MergeFailureReason.NONE
1037 1096 merge_state = MergeResponse(
1038 1097 possible, False, None, pull_request._last_merge_status)
1039 1098
1040 1099 return merge_state
1041 1100
1042 1101 def _refresh_reference(self, reference, vcs_repository):
1043 1102 if reference.type in ('branch', 'book'):
1044 1103 name_or_id = reference.name
1045 1104 else:
1046 1105 name_or_id = reference.commit_id
1047 1106 refreshed_commit = vcs_repository.get_commit(name_or_id)
1048 1107 refreshed_reference = Reference(
1049 1108 reference.type, reference.name, refreshed_commit.raw_id)
1050 1109 return refreshed_reference
1051 1110
1052 1111 def _needs_merge_state_refresh(self, pull_request, target_reference):
1053 1112 return not(
1054 1113 pull_request.revisions and
1055 1114 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1056 1115 target_reference.commit_id == pull_request._last_merge_target_rev)
1057 1116
1058 1117 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1059 1118 workspace_id = self._workspace_id(pull_request)
1060 1119 source_vcs = pull_request.source_repo.scm_instance()
1061 1120 use_rebase = self._use_rebase_for_merging(pull_request)
1062 1121 merge_state = target_vcs.merge(
1063 1122 target_reference, source_vcs, pull_request.source_ref_parts,
1064 1123 workspace_id, dry_run=True, use_rebase=use_rebase)
1065 1124
1066 1125 # Do not store the response if there was an unknown error.
1067 1126 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1068 1127 pull_request._last_merge_source_rev = \
1069 1128 pull_request.source_ref_parts.commit_id
1070 1129 pull_request._last_merge_target_rev = target_reference.commit_id
1071 1130 pull_request._last_merge_status = merge_state.failure_reason
1072 1131 pull_request.shadow_merge_ref = merge_state.merge_ref
1073 1132 Session().add(pull_request)
1074 1133 Session().commit()
1075 1134
1076 1135 return merge_state
1077 1136
1078 1137 def _workspace_id(self, pull_request):
1079 1138 workspace_id = 'pr-%s' % pull_request.pull_request_id
1080 1139 return workspace_id
1081 1140
1082 1141 def merge_status_message(self, status_code):
1083 1142 """
1084 1143 Return a human friendly error message for the given merge status code.
1085 1144 """
1086 1145 return self.MERGE_STATUS_MESSAGES[status_code]
1087 1146
1088 1147 def generate_repo_data(self, repo, commit_id=None, branch=None,
1089 1148 bookmark=None):
1090 1149 all_refs, selected_ref = \
1091 1150 self._get_repo_pullrequest_sources(
1092 1151 repo.scm_instance(), commit_id=commit_id,
1093 1152 branch=branch, bookmark=bookmark)
1094 1153
1095 1154 refs_select2 = []
1096 1155 for element in all_refs:
1097 1156 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1098 1157 refs_select2.append({'text': element[1], 'children': children})
1099 1158
1100 1159 return {
1101 1160 'user': {
1102 1161 'user_id': repo.user.user_id,
1103 1162 'username': repo.user.username,
1104 1163 'firstname': repo.user.firstname,
1105 1164 'lastname': repo.user.lastname,
1106 1165 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1107 1166 },
1108 1167 'description': h.chop_at_smart(repo.description, '\n'),
1109 1168 'refs': {
1110 1169 'all_refs': all_refs,
1111 1170 'selected_ref': selected_ref,
1112 1171 'select2_refs': refs_select2
1113 1172 }
1114 1173 }
1115 1174
1116 1175 def generate_pullrequest_title(self, source, source_ref, target):
1117 1176 return u'{source}#{at_ref} to {target}'.format(
1118 1177 source=source,
1119 1178 at_ref=source_ref,
1120 1179 target=target,
1121 1180 )
1122 1181
1123 1182 def _cleanup_merge_workspace(self, pull_request):
1124 1183 # Merging related cleanup
1125 1184 target_scm = pull_request.target_repo.scm_instance()
1126 1185 workspace_id = 'pr-%s' % pull_request.pull_request_id
1127 1186
1128 1187 try:
1129 1188 target_scm.cleanup_merge_workspace(workspace_id)
1130 1189 except NotImplementedError:
1131 1190 pass
1132 1191
1133 1192 def _get_repo_pullrequest_sources(
1134 1193 self, repo, commit_id=None, branch=None, bookmark=None):
1135 1194 """
1136 1195 Return a structure with repo's interesting commits, suitable for
1137 1196 the selectors in pullrequest controller
1138 1197
1139 1198 :param commit_id: a commit that must be in the list somehow
1140 1199 and selected by default
1141 1200 :param branch: a branch that must be in the list and selected
1142 1201 by default - even if closed
1143 1202 :param bookmark: a bookmark that must be in the list and selected
1144 1203 """
1145 1204
1146 1205 commit_id = safe_str(commit_id) if commit_id else None
1147 1206 branch = safe_str(branch) if branch else None
1148 1207 bookmark = safe_str(bookmark) if bookmark else None
1149 1208
1150 1209 selected = None
1151 1210
1152 1211 # order matters: first source that has commit_id in it will be selected
1153 1212 sources = []
1154 1213 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1155 1214 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1156 1215
1157 1216 if commit_id:
1158 1217 ref_commit = (h.short_id(commit_id), commit_id)
1159 1218 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1160 1219
1161 1220 sources.append(
1162 1221 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1163 1222 )
1164 1223
1165 1224 groups = []
1166 1225 for group_key, ref_list, group_name, match in sources:
1167 1226 group_refs = []
1168 1227 for ref_name, ref_id in ref_list:
1169 1228 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1170 1229 group_refs.append((ref_key, ref_name))
1171 1230
1172 1231 if not selected:
1173 1232 if set([commit_id, match]) & set([ref_id, ref_name]):
1174 1233 selected = ref_key
1175 1234
1176 1235 if group_refs:
1177 1236 groups.append((group_refs, group_name))
1178 1237
1179 1238 if not selected:
1180 1239 ref = commit_id or branch or bookmark
1181 1240 if ref:
1182 1241 raise CommitDoesNotExistError(
1183 1242 'No commit refs could be found matching: %s' % ref)
1184 1243 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1185 1244 selected = 'branch:%s:%s' % (
1186 1245 repo.DEFAULT_BRANCH_NAME,
1187 1246 repo.branches[repo.DEFAULT_BRANCH_NAME]
1188 1247 )
1189 1248 elif repo.commit_ids:
1190 1249 rev = repo.commit_ids[0]
1191 1250 selected = 'rev:%s:%s' % (rev, rev)
1192 1251 else:
1193 1252 raise EmptyRepositoryError()
1194 1253 return groups, selected
1195 1254
1196 1255 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1197 1256 pull_request = self.__get_pull_request(pull_request)
1198 1257 return self._get_diff_from_pr_or_version(pull_request, context=context)
1199 1258
1200 1259 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1201 1260 source_repo = pr_or_version.source_repo
1202 1261
1203 1262 # we swap org/other ref since we run a simple diff on one repo
1204 1263 target_ref_id = pr_or_version.target_ref_parts.commit_id
1205 1264 source_ref_id = pr_or_version.source_ref_parts.commit_id
1206 1265 target_commit = source_repo.get_commit(
1207 1266 commit_id=safe_str(target_ref_id))
1208 1267 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1209 1268 vcs_repo = source_repo.scm_instance()
1210 1269
1211 1270 # TODO: johbo: In the context of an update, we cannot reach
1212 1271 # the old commit anymore with our normal mechanisms. It needs
1213 1272 # some sort of special support in the vcs layer to avoid this
1214 1273 # workaround.
1215 1274 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1216 1275 vcs_repo.alias == 'git'):
1217 1276 source_commit.raw_id = safe_str(source_ref_id)
1218 1277
1219 1278 log.debug('calculating diff between '
1220 1279 'source_ref:%s and target_ref:%s for repo `%s`',
1221 1280 target_ref_id, source_ref_id,
1222 1281 safe_unicode(vcs_repo.path))
1223 1282
1224 1283 vcs_diff = vcs_repo.get_diff(
1225 1284 commit1=target_commit, commit2=source_commit, context=context)
1226 1285 return vcs_diff
1227 1286
1228 1287 def _is_merge_enabled(self, pull_request):
1229 1288 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1230 1289 settings = settings_model.get_general_settings()
1231 1290 return settings.get('rhodecode_pr_merge_enabled', False)
1232 1291
1233 1292 def _use_rebase_for_merging(self, pull_request):
1234 1293 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1235 1294 settings = settings_model.get_general_settings()
1236 1295 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1237 1296
1238 1297 def _log_action(self, action, user, pull_request):
1239 1298 action_logger(
1240 1299 user,
1241 1300 '{action}:{pr_id}'.format(
1242 1301 action=action, pr_id=pull_request.pull_request_id),
1243 1302 pull_request.target_repo)
1244 1303
1245 1304
1246 1305 ChangeTuple = namedtuple('ChangeTuple',
1247 1306 ['added', 'common', 'removed'])
1248 1307
1249 1308 FileChangeTuple = namedtuple('FileChangeTuple',
1250 1309 ['added', 'modified', 'removed'])
@@ -1,155 +1,78 b''
1 1 <%namespace name="base" file="/base/base.html"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-body">
5 5 %if c.show_closed:
6 6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
7 7 %else:
8 8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
9 9 %endif
10 10 </div>
11 11 </div>
12 12
13 13 <div class="panel panel-default">
14 <div class="panel-heading">
15 <h3 class="panel-title">${_('Pull Requests You Opened')}: ${len(c.my_pull_requests)}</h3>
16 </div>
17 <div class="panel-body">
18 <div class="pullrequestlist">
19 %if c.my_pull_requests:
20 <table class="rctable">
21 <thead>
22 <th class="td-status"></th>
23 <th>${_('Target Repo')}</th>
24 <th>${_('Author')}</th>
25 <th></th>
26 <th>${_('Title')}</th>
27 <th class="td-time">${_('Last Update')}</th>
28 <th></th>
29 </thead>
30 %for pull_request in c.my_pull_requests:
31 <tr class="${'closed' if pull_request.is_closed() else ''} prwrapper">
32 <td class="td-status">
33 <div class="${'flag_status %s' % pull_request.calculated_review_status()} pull-left"></div>
34 </td>
35 <td class="truncate-wrap td-componentname">
36 <div class="truncate">
37 ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))}
38 </div>
39 </td>
40 <td class="user">
41 ${base.gravatar_with_user(pull_request.author.email, 16)}
42 </td>
43 <td class="td-message expand_commit" data-pr-id="m${pull_request.pull_request_id}" title="${_('Expand commit message')}">
44 <div class="show_more_col">
45 <i class="show_more"></i>&nbsp;
46 </div>
47 </td>
48 <td class="mid td-description">
49 <div class="log-container truncate-wrap">
50 <div class="message truncate" id="c-m${pull_request.pull_request_id}"><a href="${h.url('pullrequest_show',repo_name=pull_request.target_repo.repo_name,pull_request_id=pull_request.pull_request_id)}">#${pull_request.pull_request_id}: ${pull_request.title}</a>\
51 %if pull_request.is_closed():
52 &nbsp;(${_('Closed')})\
53 %endif
54 <br/>${pull_request.description}</div>
55 </div>
56 </td>
57 <td class="td-time">
58 ${h.age_component(pull_request.updated_on)}
59 </td>
60 <td class="td-action repolist_actions">
61 ${h.secure_form(url('pullrequest_delete', repo_name=pull_request.target_repo.repo_name, pull_request_id=pull_request.pull_request_id),method='delete')}
62 ${h.submit('remove_%s' % pull_request.pull_request_id, _('Delete'),
63 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
64 ${h.end_form()}
65 </td>
66 </tr>
67 %endfor
68 </table>
69 %else:
70 <h2><span class="empty_data">${_('You currently have no open pull requests.')}</span></h2>
71 %endif
14 <div class="panel-heading">
15 <h3 class="panel-title">${_('Pull Requests You Participate In')}: ${c.records_total_participate}</h3>
72 16 </div>
73 </div>
74 </div>
75
76 <div class="panel panel-default">
77 <div class="panel-heading">
78 <h3 class="panel-title">${_('Pull Requests You Participate In')}: ${len(c.participate_in_pull_requests)}</h3>
79 </div>
80
81 <div class="panel-body">
82 <div class="pullrequestlist">
83 %if c.participate_in_pull_requests:
84 <table class="rctable">
85 <thead>
86 <th class="td-status"></th>
87 <th>${_('Target Repo')}</th>
88 <th>${_('Author')}</th>
89 <th></th>
90 <th>${_('Title')}</th>
91 <th class="td-time">${_('Last Update')}</th>
92 </thead>
93 %for pull_request in c.participate_in_pull_requests:
94 <tr class="${'closed' if pull_request.is_closed() else ''} prwrapper">
95 <td class="td-status">
96 <div class="${'flag_status %s' % pull_request.calculated_review_status()} pull-left"></div>
97 </td>
98 <td class="truncate-wrap td-componentname">
99 <div class="truncate">
100 ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))}
101 </div>
102 </td>
103 <td class="user">
104 ${base.gravatar_with_user(pull_request.author.email, 16)}
105 </td>
106 <td class="td-message expand_commit" data-pr-id="p${pull_request.pull_request_id}" title="${_('Expand commit message')}">
107 <div class="show_more_col">
108 <i class="show_more"></i>&nbsp;
109 </div>
110 </td>
111 <td class="mid td-description">
112 <div class="log-container truncate-wrap">
113 <div class="message truncate" id="c-p${pull_request.pull_request_id}"><a href="${h.url('pullrequest_show',repo_name=pull_request.target_repo.repo_name,pull_request_id=pull_request.pull_request_id)}">#${pull_request.pull_request_id}: ${pull_request.title}</a>\
114 %if pull_request.is_closed():
115 &nbsp;(${_('Closed')})\
116 %endif
117 <br/>${pull_request.description}</div>
118 </div>
119 </td>
120 <td class="td-time">
121 ${h.age_component(pull_request.updated_on)}
122 </td>
123 </tr>
124 %endfor
125 </table>
126 %else:
127 <h2 class="empty_data">${_('There are currently no open pull requests requiring your participation.')}</h2>
128 %endif
17 <div class="panel-body">
18 <table id="pull_request_list_table_participate" class="display"></table>
129 19 </div>
130 </div>
131 20 </div>
132 21
133 22 <script>
134 23 $('#show_closed').on('click', function(e){
135 24 if($(this).is(":checked")){
136 25 window.location = "${h.url('my_account_pullrequests', pr_show_closed=1)}";
137 26 }
138 27 else{
139 28 window.location = "${h.url('my_account_pullrequests')}";
140 29 }
141 30 });
142 $('.expand_commit').on('click',function(e){
143 var target_expand = $(this);
144 var cid = target_expand.data('prId');
31 $(document).ready(function() {
32
33 var columnsDefs = [
34 { data: {"_": "status",
35 "sort": "status"}, title: "", className: "td-status", orderable: false},
36 { data: {"_": "target_repo",
37 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
38 { data: {"_": "name",
39 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
40 { data: {"_": "author",
41 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
42 { data: {"_": "title",
43 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
44 { data: {"_": "comments",
45 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
46 { data: {"_": "updated_on",
47 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
48 ];
145 49
146 if (target_expand.hasClass('open')){
147 $('#c-'+cid).css({'height': '2.75em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
148 target_expand.removeClass('open');
149 }
150 else {
151 $('#c-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible'});
152 target_expand.addClass('open');
153 }
50 // participating object list
51 $('#pull_request_list_table_participate').DataTable({
52 data: ${c.data_participate|n},
53 processing: true,
54 serverSide: true,
55 deferLoading: ${c.records_total_participate},
56 ajax: "",
57 dom: 'tp',
58 pageLength: ${c.visual.dashboard_items},
59 order: [[ 2, "desc" ]],
60 columns: columnsDefs,
61 language: {
62 paginate: DEFAULT_GRID_PAGINATION,
63 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
64 },
65 "drawCallback": function( settings, json ) {
66 timeagoActivate();
67 },
68 "createdRow": function ( row, data, index ) {
69 if (data['closed']) {
70 $(row).addClass('closed');
71 }
72 if (data['owned']) {
73 $(row).addClass('owned');
74 }
75 }
76 });
154 77 });
155 78 </script>
@@ -1,299 +1,309 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.html"/>
4 4 <%namespace name="base" file="/base/base.html"/>
5 5
6 6 ## REPOSITORY RENDERERS
7 7 <%def name="quick_menu(repo_name)">
8 8 <i class="pointer icon-more"></i>
9 9 <div class="menu_items_container hidden">
10 10 <ul class="menu_items">
11 11 <li>
12 12 <a title="${_('Summary')}" href="${h.url('summary_home',repo_name=repo_name)}">
13 13 <span>${_('Summary')}</span>
14 14 </a>
15 15 </li>
16 16 <li>
17 17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
18 18 <span>${_('Changelog')}</span>
19 19 </a>
20 20 </li>
21 21 <li>
22 22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
23 23 <span>${_('Files')}</span>
24 24 </a>
25 25 </li>
26 26 <li>
27 27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 28 <span>${_('Fork')}</span>
29 29 </a>
30 30 </li>
31 31 </ul>
32 32 </div>
33 33 </%def>
34 34
35 35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 36 <%
37 37 def get_name(name,short_name=short_name):
38 38 if short_name:
39 39 return name.split('/')[-1]
40 40 else:
41 41 return name
42 42 %>
43 43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 44 ##NAME
45 45 <a href="${h.url('edit_repo' if admin else 'summary_home',repo_name=name)}">
46 46
47 47 ##TYPE OF REPO
48 48 %if h.is_hg(rtype):
49 49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 50 %elif h.is_git(rtype):
51 51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 52 %elif h.is_svn(rtype):
53 53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 54 %endif
55 55
56 56 ##PRIVATE/PUBLIC
57 57 %if private and c.visual.show_private_icon:
58 58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 59 %elif not private and c.visual.show_public_icon:
60 60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 61 %else:
62 62 <span></span>
63 63 %endif
64 64 ${get_name(name)}
65 65 </a>
66 66 %if fork_of:
67 67 <a href="${h.url('summary_home',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 68 %endif
69 69 %if rstate == 'repo_state_pending':
70 70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 71 %endif
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="last_change(last_change)">
76 76 ${h.age_component(last_change)}
77 77 </%def>
78 78
79 79 <%def name="revision(name,rev,tip,author,last_msg)">
80 80 <div>
81 81 %if rev >= 0:
82 82 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
83 83 %else:
84 84 ${_('No commits yet')}
85 85 %endif
86 86 </div>
87 87 </%def>
88 88
89 89 <%def name="rss(name)">
90 90 %if c.rhodecode_user.username != h.DEFAULT_USER:
91 91 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
92 92 %else:
93 93 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
94 94 %endif
95 95 </%def>
96 96
97 97 <%def name="atom(name)">
98 98 %if c.rhodecode_user.username != h.DEFAULT_USER:
99 99 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
100 100 %else:
101 101 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
102 102 %endif
103 103 </%def>
104 104
105 105 <%def name="user_gravatar(email, size=16)">
106 106 <div class="rc-user tooltip" title="${h.author_string(email)}">
107 107 ${base.gravatar(email, 16)}
108 108 </div>
109 109 </%def>
110 110
111 111 <%def name="repo_actions(repo_name, super_user=True)">
112 112 <div>
113 113 <div class="grid_edit">
114 114 <a href="${h.url('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
115 115 <i class="icon-pencil"></i>Edit</a>
116 116 </div>
117 117 <div class="grid_delete">
118 118 ${h.secure_form(h.url('repo', repo_name=repo_name),method='delete')}
119 119 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
120 120 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
121 121 ${h.end_form()}
122 122 </div>
123 123 </div>
124 124 </%def>
125 125
126 126 <%def name="repo_state(repo_state)">
127 127 <div>
128 128 %if repo_state == 'repo_state_pending':
129 129 <div class="tag tag4">${_('Creating')}</div>
130 130 %elif repo_state == 'repo_state_created':
131 131 <div class="tag tag1">${_('Created')}</div>
132 132 %else:
133 133 <div class="tag alert2" title="${repo_state}">invalid</div>
134 134 %endif
135 135 </div>
136 136 </%def>
137 137
138 138
139 139 ## REPO GROUP RENDERERS
140 140 <%def name="quick_repo_group_menu(repo_group_name)">
141 141 <i class="pointer icon-more"></i>
142 142 <div class="menu_items_container hidden">
143 143 <ul class="menu_items">
144 144 <li>
145 145 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
146 146 <span class="icon">
147 147 <i class="icon-file-text"></i>
148 148 </span>
149 149 <span>${_('Summary')}</span>
150 150 </a>
151 151 </li>
152 152
153 153 </ul>
154 154 </div>
155 155 </%def>
156 156
157 157 <%def name="repo_group_name(repo_group_name, children_groups=None)">
158 158 <div>
159 159 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
160 160 <i class="icon-folder-close" title="${_('Repository group')}"></i>
161 161 %if children_groups:
162 162 ${h.literal(' &raquo; '.join(children_groups))}
163 163 %else:
164 164 ${repo_group_name}
165 165 %endif
166 166 </a>
167 167 </div>
168 168 </%def>
169 169
170 170 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
171 171 <div class="grid_edit">
172 172 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
173 173 </div>
174 174 <div class="grid_delete">
175 175 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
176 176 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
177 177 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
178 178 ${h.end_form()}
179 179 </div>
180 180 </%def>
181 181
182 182
183 183 <%def name="user_actions(user_id, username)">
184 184 <div class="grid_edit">
185 185 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
186 186 <i class="icon-pencil"></i>Edit</a>
187 187 </div>
188 188 <div class="grid_delete">
189 189 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
190 190 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
191 191 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
192 192 ${h.end_form()}
193 193 </div>
194 194 </%def>
195 195
196 196 <%def name="user_group_actions(user_group_id, user_group_name)">
197 197 <div class="grid_edit">
198 198 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
199 199 </div>
200 200 <div class="grid_delete">
201 201 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
202 202 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
203 203 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
204 204 ${h.end_form()}
205 205 </div>
206 206 </%def>
207 207
208 208
209 209 <%def name="user_name(user_id, username)">
210 210 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
211 211 </%def>
212 212
213 213 <%def name="user_profile(username)">
214 214 ${base.gravatar_with_user(username, 16)}
215 215 </%def>
216 216
217 217 <%def name="user_group_name(user_group_id, user_group_name)">
218 218 <div>
219 219 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
220 220 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
221 221 </div>
222 222 </%def>
223 223
224 224
225 225 ## GISTS
226 226
227 227 <%def name="gist_gravatar(full_contact)">
228 228 <div class="gist_gravatar">
229 229 ${base.gravatar(full_contact, 30)}
230 230 </div>
231 231 </%def>
232 232
233 233 <%def name="gist_access_id(gist_access_id, full_contact)">
234 234 <div>
235 235 <b>
236 236 <a href="${h.url('gist',gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
237 237 </b>
238 238 </div>
239 239 </%def>
240 240
241 241 <%def name="gist_author(full_contact, created_on, expires)">
242 242 ${base.gravatar_with_user(full_contact, 16)}
243 243 </%def>
244 244
245 245
246 246 <%def name="gist_created(created_on)">
247 247 <div class="created">
248 248 ${h.age_component(created_on, time_is_local=True)}
249 249 </div>
250 250 </%def>
251 251
252 252 <%def name="gist_expires(expires)">
253 253 <div class="created">
254 254 %if expires == -1:
255 255 ${_('never')}
256 256 %else:
257 257 ${h.age_component(h.time_to_utcdatetime(expires))}
258 258 %endif
259 259 </div>
260 260 </%def>
261 261
262 262 <%def name="gist_type(gist_type)">
263 263 %if gist_type != 'public':
264 264 <div class="tag">${_('Private')}</div>
265 265 %endif
266 266 </%def>
267 267
268 268 <%def name="gist_description(gist_description)">
269 269 ${gist_description}
270 270 </%def>
271 271
272 272
273 273 ## PULL REQUESTS GRID RENDERERS
274
275 <%def name="pullrequest_target_repo(repo_name)">
276 <div class="truncate">
277 ${h.link_to(repo_name,h.url('summary_home',repo_name=repo_name))}
278 </div>
279 </%def>
274 280 <%def name="pullrequest_status(status)">
275 281 <div class="${'flag_status %s' % status} pull-left"></div>
276 282 </%def>
277 283
278 284 <%def name="pullrequest_title(title, description)">
279 285 ${title} <br/>
280 286 ${h.shorter(description, 40)}
281 287 </%def>
282 288
283 289 <%def name="pullrequest_comments(comments_nr)">
284 290 <i class="icon-comment icon-comment-colored"></i> ${comments_nr}
285 291 </%def>
286 292
287 <%def name="pullrequest_name(pull_request_id, target_repo_name)">
293 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
288 294 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
289 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
295 % if short:
296 #${pull_request_id}
297 % else:
298 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
299 % endif
290 300 </a>
291 301 </%def>
292 302
293 303 <%def name="pullrequest_updated_on(updated_on)">
294 304 ${h.age_component(h.time_to_utcdatetime(updated_on))}
295 305 </%def>
296 306
297 307 <%def name="pullrequest_author(full_contact)">
298 308 ${base.gravatar_with_user(full_contact, 16)}
299 309 </%def>
@@ -1,397 +1,396 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 pytest
22 22
23 23 from rhodecode.lib import helpers as h
24 24 from rhodecode.lib.auth import check_password
25 25 from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests import (
28 28 TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
29 29 assert_session_flash)
30 30 from rhodecode.tests.fixture import Fixture
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33 fixture = Fixture()
34 34
35 35
36 36 class TestMyAccountController(TestController):
37 37 test_user_1 = 'testme'
38 38 test_user_1_password = '0jd83nHNS/d23n'
39 39 destroy_users = set()
40 40
41 41 @classmethod
42 42 def teardown_class(cls):
43 43 fixture.destroy_users(cls.destroy_users)
44 44
45 45 def test_my_account(self):
46 46 self.log_user()
47 47 response = self.app.get(url('my_account'))
48 48
49 49 response.mustcontain('test_admin')
50 50 response.mustcontain('href="/_admin/my_account/edit"')
51 51
52 52 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
53 53 response = self.app.get(url('my_account'))
54 54 assert_response = AssertResponse(response)
55 55 element = assert_response.get_element('.logout #csrf_token')
56 56 assert element.value == csrf_token
57 57
58 58 def test_my_account_edit(self):
59 59 self.log_user()
60 60 response = self.app.get(url('my_account_edit'))
61 61
62 62 response.mustcontain('value="test_admin')
63 63
64 64 def test_my_account_my_repos(self):
65 65 self.log_user()
66 66 response = self.app.get(url('my_account_repos'))
67 67 repos = Repository.query().filter(
68 68 Repository.user == User.get_by_username(
69 69 TEST_USER_ADMIN_LOGIN)).all()
70 70 for repo in repos:
71 71 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
72 72
73 73 def test_my_account_my_watched(self):
74 74 self.log_user()
75 75 response = self.app.get(url('my_account_watched'))
76 76
77 77 repos = UserFollowing.query().filter(
78 78 UserFollowing.user == User.get_by_username(
79 79 TEST_USER_ADMIN_LOGIN)).all()
80 80 for repo in repos:
81 81 response.mustcontain(
82 82 '"name_raw": "%s"' % repo.follows_repository.repo_name)
83 83
84 84 @pytest.mark.backends("git", "hg")
85 85 def test_my_account_my_pullrequests(self, pr_util):
86 86 self.log_user()
87 87 response = self.app.get(url('my_account_pullrequests'))
88 response.mustcontain('You currently have no open pull requests.')
88 response.mustcontain('There are currently no open pull '
89 'requests requiring your participation.')
89 90
90 91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 92 response = self.app.get(url('my_account_pullrequests'))
92 response.mustcontain('There are currently no open pull requests '
93 'requiring your participation')
94
95 response.mustcontain('#%s: TestMyAccountPR' % pr.pull_request_id)
93 response.mustcontain('"name_raw": %s' % pr.pull_request_id)
94 response.mustcontain('TestMyAccountPR')
96 95
97 96 def test_my_account_my_emails(self):
98 97 self.log_user()
99 98 response = self.app.get(url('my_account_emails'))
100 99 response.mustcontain('No additional emails specified')
101 100
102 101 def test_my_account_my_emails_add_existing_email(self):
103 102 self.log_user()
104 103 response = self.app.get(url('my_account_emails'))
105 104 response.mustcontain('No additional emails specified')
106 105 response = self.app.post(url('my_account_emails'),
107 106 {'new_email': TEST_USER_REGULAR_EMAIL,
108 107 'csrf_token': self.csrf_token})
109 108 assert_session_flash(response, 'This e-mail address is already taken')
110 109
111 110 def test_my_account_my_emails_add_mising_email_in_form(self):
112 111 self.log_user()
113 112 response = self.app.get(url('my_account_emails'))
114 113 response.mustcontain('No additional emails specified')
115 114 response = self.app.post(url('my_account_emails'),
116 115 {'csrf_token': self.csrf_token})
117 116 assert_session_flash(response, 'Please enter an email address')
118 117
119 118 def test_my_account_my_emails_add_remove(self):
120 119 self.log_user()
121 120 response = self.app.get(url('my_account_emails'))
122 121 response.mustcontain('No additional emails specified')
123 122
124 123 response = self.app.post(url('my_account_emails'),
125 124 {'new_email': 'foo@barz.com',
126 125 'csrf_token': self.csrf_token})
127 126
128 127 response = self.app.get(url('my_account_emails'))
129 128
130 129 from rhodecode.model.db import UserEmailMap
131 130 email_id = UserEmailMap.query().filter(
132 131 UserEmailMap.user == User.get_by_username(
133 132 TEST_USER_ADMIN_LOGIN)).filter(
134 133 UserEmailMap.email == 'foo@barz.com').one().email_id
135 134
136 135 response.mustcontain('foo@barz.com')
137 136 response.mustcontain('<input id="del_email_id" name="del_email_id" '
138 137 'type="hidden" value="%s" />' % email_id)
139 138
140 139 response = self.app.post(
141 140 url('my_account_emails'), {
142 141 'del_email_id': email_id, '_method': 'delete',
143 142 'csrf_token': self.csrf_token})
144 143 assert_session_flash(response, 'Removed email address from user account')
145 144 response = self.app.get(url('my_account_emails'))
146 145 response.mustcontain('No additional emails specified')
147 146
148 147 @pytest.mark.parametrize(
149 148 "name, attrs", [
150 149 ('firstname', {'firstname': 'new_username'}),
151 150 ('lastname', {'lastname': 'new_username'}),
152 151 ('admin', {'admin': True}),
153 152 ('admin', {'admin': False}),
154 153 ('extern_type', {'extern_type': 'ldap'}),
155 154 ('extern_type', {'extern_type': None}),
156 155 # ('extern_name', {'extern_name': 'test'}),
157 156 # ('extern_name', {'extern_name': None}),
158 157 ('active', {'active': False}),
159 158 ('active', {'active': True}),
160 159 ('email', {'email': 'some@email.com'}),
161 160 ])
162 161 def test_my_account_update(self, name, attrs):
163 162 usr = fixture.create_user(self.test_user_1,
164 163 password=self.test_user_1_password,
165 164 email='testme@rhodecode.org',
166 165 extern_type='rhodecode',
167 166 extern_name=self.test_user_1,
168 167 skip_if_exists=True)
169 168 self.destroy_users.add(self.test_user_1)
170 169
171 170 params = usr.get_api_data() # current user data
172 171 user_id = usr.user_id
173 172 self.log_user(
174 173 username=self.test_user_1, password=self.test_user_1_password)
175 174
176 175 params.update({'password_confirmation': ''})
177 176 params.update({'new_password': ''})
178 177 params.update({'extern_type': 'rhodecode'})
179 178 params.update({'extern_name': self.test_user_1})
180 179 params.update({'csrf_token': self.csrf_token})
181 180
182 181 params.update(attrs)
183 182 # my account page cannot set language param yet, only for admins
184 183 del params['language']
185 184 response = self.app.post(url('my_account'), params)
186 185
187 186 assert_session_flash(
188 187 response, 'Your account was updated successfully')
189 188
190 189 del params['csrf_token']
191 190
192 191 updated_user = User.get_by_username(self.test_user_1)
193 192 updated_params = updated_user.get_api_data()
194 193 updated_params.update({'password_confirmation': ''})
195 194 updated_params.update({'new_password': ''})
196 195
197 196 params['last_login'] = updated_params['last_login']
198 197 # my account page cannot set language param yet, only for admins
199 198 # but we get this info from API anyway
200 199 params['language'] = updated_params['language']
201 200
202 201 if name == 'email':
203 202 params['emails'] = [attrs['email']]
204 203 if name == 'extern_type':
205 204 # cannot update this via form, expected value is original one
206 205 params['extern_type'] = "rhodecode"
207 206 if name == 'extern_name':
208 207 # cannot update this via form, expected value is original one
209 208 params['extern_name'] = str(user_id)
210 209 if name == 'active':
211 210 # my account cannot deactivate account
212 211 params['active'] = True
213 212 if name == 'admin':
214 213 # my account cannot make you an admin !
215 214 params['admin'] = False
216 215
217 216 assert params == updated_params
218 217
219 218 def test_my_account_update_err_email_exists(self):
220 219 self.log_user()
221 220
222 221 new_email = 'test_regular@mail.com' # already exisitn email
223 222 response = self.app.post(url('my_account'),
224 223 params={
225 224 'username': 'test_admin',
226 225 'new_password': 'test12',
227 226 'password_confirmation': 'test122',
228 227 'firstname': 'NewName',
229 228 'lastname': 'NewLastname',
230 229 'email': new_email,
231 230 'csrf_token': self.csrf_token,
232 231 })
233 232
234 233 response.mustcontain('This e-mail address is already taken')
235 234
236 235 def test_my_account_update_err(self):
237 236 self.log_user('test_regular2', 'test12')
238 237
239 238 new_email = 'newmail.pl'
240 239 response = self.app.post(url('my_account'),
241 240 params={
242 241 'username': 'test_admin',
243 242 'new_password': 'test12',
244 243 'password_confirmation': 'test122',
245 244 'firstname': 'NewName',
246 245 'lastname': 'NewLastname',
247 246 'email': new_email,
248 247 'csrf_token': self.csrf_token,
249 248 })
250 249
251 250 response.mustcontain('An email address must contain a single @')
252 251 from rhodecode.model import validators
253 252 msg = validators.ValidUsername(
254 253 edit=False, old_data={})._messages['username_exists']
255 254 msg = h.html_escape(msg % {'username': 'test_admin'})
256 255 response.mustcontain(u"%s" % msg)
257 256
258 257 def test_my_account_auth_tokens(self):
259 258 usr = self.log_user('test_regular2', 'test12')
260 259 user = User.get(usr['user_id'])
261 260 response = self.app.get(url('my_account_auth_tokens'))
262 261 response.mustcontain(user.api_key)
263 262 response.mustcontain('expires: never')
264 263
265 264 @pytest.mark.parametrize("desc, lifetime", [
266 265 ('forever', -1),
267 266 ('5mins', 60*5),
268 267 ('30days', 60*60*24*30),
269 268 ])
270 269 def test_my_account_add_auth_tokens(self, desc, lifetime):
271 270 usr = self.log_user('test_regular2', 'test12')
272 271 user = User.get(usr['user_id'])
273 272 response = self.app.post(url('my_account_auth_tokens'),
274 273 {'description': desc, 'lifetime': lifetime,
275 274 'csrf_token': self.csrf_token})
276 275 assert_session_flash(response, 'Auth token successfully created')
277 276 try:
278 277 response = response.follow()
279 278 user = User.get(usr['user_id'])
280 279 for auth_token in user.auth_tokens:
281 280 response.mustcontain(auth_token)
282 281 finally:
283 282 for auth_token in UserApiKeys.query().all():
284 283 Session().delete(auth_token)
285 284 Session().commit()
286 285
287 286 def test_my_account_remove_auth_token(self):
288 287 # TODO: without this cleanup it fails when run with the whole
289 288 # test suite, so there must be some interference with other tests.
290 289 UserApiKeys.query().delete()
291 290
292 291 usr = self.log_user('test_regular2', 'test12')
293 292 User.get(usr['user_id'])
294 293 response = self.app.post(url('my_account_auth_tokens'),
295 294 {'description': 'desc', 'lifetime': -1,
296 295 'csrf_token': self.csrf_token})
297 296 assert_session_flash(response, 'Auth token successfully created')
298 297 response = response.follow()
299 298
300 299 # now delete our key
301 300 keys = UserApiKeys.query().all()
302 301 assert 1 == len(keys)
303 302
304 303 response = self.app.post(
305 304 url('my_account_auth_tokens'),
306 305 {'_method': 'delete', 'del_auth_token': keys[0].api_key,
307 306 'csrf_token': self.csrf_token})
308 307 assert_session_flash(response, 'Auth token successfully deleted')
309 308 keys = UserApiKeys.query().all()
310 309 assert 0 == len(keys)
311 310
312 311 def test_my_account_reset_main_auth_token(self):
313 312 usr = self.log_user('test_regular2', 'test12')
314 313 user = User.get(usr['user_id'])
315 314 api_key = user.api_key
316 315 response = self.app.get(url('my_account_auth_tokens'))
317 316 response.mustcontain(api_key)
318 317 response.mustcontain('expires: never')
319 318
320 319 response = self.app.post(
321 320 url('my_account_auth_tokens'),
322 321 {'_method': 'delete', 'del_auth_token_builtin': api_key,
323 322 'csrf_token': self.csrf_token})
324 323 assert_session_flash(response, 'Auth token successfully reset')
325 324 response = response.follow()
326 325 response.mustcontain(no=[api_key])
327 326
328 327 def test_valid_change_password(self, user_util):
329 328 new_password = 'my_new_valid_password'
330 329 user = user_util.create_user(password=self.test_user_1_password)
331 330 session = self.log_user(user.username, self.test_user_1_password)
332 331 form_data = [
333 332 ('current_password', self.test_user_1_password),
334 333 ('__start__', 'new_password:mapping'),
335 334 ('new_password', new_password),
336 335 ('new_password-confirm', new_password),
337 336 ('__end__', 'new_password:mapping'),
338 337 ('csrf_token', self.csrf_token),
339 338 ]
340 339 response = self.app.post(url('my_account_password'), form_data).follow()
341 340 assert 'Successfully updated password' in response
342 341
343 342 # check_password depends on user being in session
344 343 Session().add(user)
345 344 try:
346 345 assert check_password(new_password, user.password)
347 346 finally:
348 347 Session().expunge(user)
349 348
350 349 @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [
351 350 ('', 'abcdef123', 'abcdef123'),
352 351 ('wrong_pw', 'abcdef123', 'abcdef123'),
353 352 (test_user_1_password, test_user_1_password, test_user_1_password),
354 353 (test_user_1_password, '', ''),
355 354 (test_user_1_password, 'abcdef123', ''),
356 355 (test_user_1_password, '', 'abcdef123'),
357 356 (test_user_1_password, 'not_the', 'same_pw'),
358 357 (test_user_1_password, 'short', 'short'),
359 358 ])
360 359 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
361 360 user_util):
362 361 user = user_util.create_user(password=self.test_user_1_password)
363 362 session = self.log_user(user.username, self.test_user_1_password)
364 363 old_password_hash = session['password']
365 364 form_data = [
366 365 ('current_password', current_pw),
367 366 ('__start__', 'new_password:mapping'),
368 367 ('new_password', new_pw),
369 368 ('new_password-confirm', confirm_pw),
370 369 ('__end__', 'new_password:mapping'),
371 370 ('csrf_token', self.csrf_token),
372 371 ]
373 372 response = self.app.post(url('my_account_password'), form_data)
374 373 assert 'Error occurred' in response
375 374
376 375 def test_password_is_updated_in_session_on_password_change(self, user_util):
377 376 old_password = 'abcdef123'
378 377 new_password = 'abcdef124'
379 378
380 379 user = user_util.create_user(password=old_password)
381 380 session = self.log_user(user.username, old_password)
382 381 old_password_hash = session['password']
383 382
384 383 form_data = [
385 384 ('current_password', old_password),
386 385 ('__start__', 'new_password:mapping'),
387 386 ('new_password', new_password),
388 387 ('new_password-confirm', new_password),
389 388 ('__end__', 'new_password:mapping'),
390 389 ('csrf_token', self.csrf_token),
391 390 ]
392 391 self.app.post(url('my_account_password'), form_data)
393 392
394 393 response = self.app.get(url('home'))
395 394 new_password_hash = response.session['rhodecode_user']['password']
396 395
397 396 assert old_password_hash != new_password_hash
General Comments 0
You need to be logged in to leave comments. Login now