##// 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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2016 RhodeCode GmbH
3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 my account controller for RhodeCode admin
23 my account controller for RhodeCode admin
24 """
24 """
25
25
26 import logging
26 import logging
27
27
28 import formencode
28 import formencode
29 from formencode import htmlfill
29 from formencode import htmlfill
30 from pylons import request, tmpl_context as c, url, session
30 from pylons import request, tmpl_context as c, url, session
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from sqlalchemy.orm import joinedload
33 from sqlalchemy.orm import joinedload
34
34
35 from rhodecode import forms
35 from rhodecode import forms
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib import auth
37 from rhodecode.lib import auth
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
40 from rhodecode.lib.base import BaseController, render
40 from rhodecode.lib.base import BaseController, render
41 from rhodecode.lib.utils import jsonify
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 from rhodecode.lib.ext_json import json
43 from rhodecode.lib.ext_json import json
44
44
45 from rhodecode.model.validation_schema.schemas import user_schema
45 from rhodecode.model.validation_schema.schemas import user_schema
46 from rhodecode.model.db import (
46 from rhodecode.model.db import (
47 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
47 Repository, PullRequest, UserEmailMap, User, UserFollowing)
48 UserFollowing)
49 from rhodecode.model.forms import UserForm
48 from rhodecode.model.forms import UserForm
50 from rhodecode.model.scm import RepoList
49 from rhodecode.model.scm import RepoList
51 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
52 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
53 from rhodecode.model.auth_token import AuthTokenModel
52 from rhodecode.model.auth_token import AuthTokenModel
54 from rhodecode.model.meta import Session
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 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
57
58
58
59
59 class MyAccountController(BaseController):
60 class MyAccountController(BaseController):
60 """REST Controller styled on the Atom Publishing Protocol"""
61 """REST Controller styled on the Atom Publishing Protocol"""
61 # To properly map this controller, ensure your config/routing.py
62 # To properly map this controller, ensure your config/routing.py
62 # file has a resource setup:
63 # file has a resource setup:
63 # map.resource('setting', 'settings', controller='admin/settings',
64 # map.resource('setting', 'settings', controller='admin/settings',
64 # path_prefix='/admin', name_prefix='admin_')
65 # path_prefix='/admin', name_prefix='admin_')
65
66
66 @LoginRequired()
67 @LoginRequired()
67 @NotAnonymous()
68 @NotAnonymous()
68 def __before__(self):
69 def __before__(self):
69 super(MyAccountController, self).__before__()
70 super(MyAccountController, self).__before__()
70
71
71 def __load_data(self):
72 def __load_data(self):
72 c.user = User.get(c.rhodecode_user.user_id)
73 c.user = User.get(c.rhodecode_user.user_id)
73 if c.user.username == User.DEFAULT_USER:
74 if c.user.username == User.DEFAULT_USER:
74 h.flash(_("You can't edit this user since it's"
75 h.flash(_("You can't edit this user since it's"
75 " crucial for entire application"), category='warning')
76 " crucial for entire application"), category='warning')
76 return redirect(url('users'))
77 return redirect(url('users'))
77
78
78 def _load_my_repos_data(self, watched=False):
79 def _load_my_repos_data(self, watched=False):
79 if watched:
80 if watched:
80 admin = False
81 admin = False
81 follows_repos = Session().query(UserFollowing)\
82 follows_repos = Session().query(UserFollowing)\
82 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
83 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
83 .options(joinedload(UserFollowing.follows_repository))\
84 .options(joinedload(UserFollowing.follows_repository))\
84 .all()
85 .all()
85 repo_list = [x.follows_repository for x in follows_repos]
86 repo_list = [x.follows_repository for x in follows_repos]
86 else:
87 else:
87 admin = True
88 admin = True
88 repo_list = Repository.get_all_repos(
89 repo_list = Repository.get_all_repos(
89 user_id=c.rhodecode_user.user_id)
90 user_id=c.rhodecode_user.user_id)
90 repo_list = RepoList(repo_list, perm_set=[
91 repo_list = RepoList(repo_list, perm_set=[
91 'repository.read', 'repository.write', 'repository.admin'])
92 'repository.read', 'repository.write', 'repository.admin'])
92
93
93 repos_data = RepoModel().get_repos_as_dict(
94 repos_data = RepoModel().get_repos_as_dict(
94 repo_list=repo_list, admin=admin)
95 repo_list=repo_list, admin=admin)
95 # json used to render the grid
96 # json used to render the grid
96 return json.dumps(repos_data)
97 return json.dumps(repos_data)
97
98
98 @auth.CSRFRequired()
99 @auth.CSRFRequired()
99 def my_account_update(self):
100 def my_account_update(self):
100 """
101 """
101 POST /_admin/my_account Updates info of my account
102 POST /_admin/my_account Updates info of my account
102 """
103 """
103 # url('my_account')
104 # url('my_account')
104 c.active = 'profile_edit'
105 c.active = 'profile_edit'
105 self.__load_data()
106 self.__load_data()
106 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
107 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
107 ip_addr=self.ip_addr)
108 ip_addr=self.ip_addr)
108 c.extern_type = c.user.extern_type
109 c.extern_type = c.user.extern_type
109 c.extern_name = c.user.extern_name
110 c.extern_name = c.user.extern_name
110
111
111 defaults = c.user.get_dict()
112 defaults = c.user.get_dict()
112 update = False
113 update = False
113 _form = UserForm(edit=True,
114 _form = UserForm(edit=True,
114 old_data={'user_id': c.rhodecode_user.user_id,
115 old_data={'user_id': c.rhodecode_user.user_id,
115 'email': c.rhodecode_user.email})()
116 'email': c.rhodecode_user.email})()
116 form_result = {}
117 form_result = {}
117 try:
118 try:
118 post_data = dict(request.POST)
119 post_data = dict(request.POST)
119 post_data['new_password'] = ''
120 post_data['new_password'] = ''
120 post_data['password_confirmation'] = ''
121 post_data['password_confirmation'] = ''
121 form_result = _form.to_python(post_data)
122 form_result = _form.to_python(post_data)
122 # skip updating those attrs for my account
123 # skip updating those attrs for my account
123 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
124 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
124 'new_password', 'password_confirmation']
125 'new_password', 'password_confirmation']
125 # TODO: plugin should define if username can be updated
126 # TODO: plugin should define if username can be updated
126 if c.extern_type != "rhodecode":
127 if c.extern_type != "rhodecode":
127 # forbid updating username for external accounts
128 # forbid updating username for external accounts
128 skip_attrs.append('username')
129 skip_attrs.append('username')
129
130
130 UserModel().update_user(
131 UserModel().update_user(
131 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
132 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
132 h.flash(_('Your account was updated successfully'),
133 h.flash(_('Your account was updated successfully'),
133 category='success')
134 category='success')
134 Session().commit()
135 Session().commit()
135 update = True
136 update = True
136
137
137 except formencode.Invalid as errors:
138 except formencode.Invalid as errors:
138 return htmlfill.render(
139 return htmlfill.render(
139 render('admin/my_account/my_account.html'),
140 render('admin/my_account/my_account.html'),
140 defaults=errors.value,
141 defaults=errors.value,
141 errors=errors.error_dict or {},
142 errors=errors.error_dict or {},
142 prefix_error=False,
143 prefix_error=False,
143 encoding="UTF-8",
144 encoding="UTF-8",
144 force_defaults=False)
145 force_defaults=False)
145 except Exception:
146 except Exception:
146 log.exception("Exception updating user")
147 log.exception("Exception updating user")
147 h.flash(_('Error occurred during update of user %s')
148 h.flash(_('Error occurred during update of user %s')
148 % form_result.get('username'), category='error')
149 % form_result.get('username'), category='error')
149
150
150 if update:
151 if update:
151 return redirect('my_account')
152 return redirect('my_account')
152
153
153 return htmlfill.render(
154 return htmlfill.render(
154 render('admin/my_account/my_account.html'),
155 render('admin/my_account/my_account.html'),
155 defaults=defaults,
156 defaults=defaults,
156 encoding="UTF-8",
157 encoding="UTF-8",
157 force_defaults=False
158 force_defaults=False
158 )
159 )
159
160
160 def my_account(self):
161 def my_account(self):
161 """
162 """
162 GET /_admin/my_account Displays info about my account
163 GET /_admin/my_account Displays info about my account
163 """
164 """
164 # url('my_account')
165 # url('my_account')
165 c.active = 'profile'
166 c.active = 'profile'
166 self.__load_data()
167 self.__load_data()
167
168
168 defaults = c.user.get_dict()
169 defaults = c.user.get_dict()
169 return htmlfill.render(
170 return htmlfill.render(
170 render('admin/my_account/my_account.html'),
171 render('admin/my_account/my_account.html'),
171 defaults=defaults, encoding="UTF-8", force_defaults=False)
172 defaults=defaults, encoding="UTF-8", force_defaults=False)
172
173
173 def my_account_edit(self):
174 def my_account_edit(self):
174 """
175 """
175 GET /_admin/my_account/edit Displays edit form of my account
176 GET /_admin/my_account/edit Displays edit form of my account
176 """
177 """
177 c.active = 'profile_edit'
178 c.active = 'profile_edit'
178 self.__load_data()
179 self.__load_data()
179 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
180 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
180 ip_addr=self.ip_addr)
181 ip_addr=self.ip_addr)
181 c.extern_type = c.user.extern_type
182 c.extern_type = c.user.extern_type
182 c.extern_name = c.user.extern_name
183 c.extern_name = c.user.extern_name
183
184
184 defaults = c.user.get_dict()
185 defaults = c.user.get_dict()
185 return htmlfill.render(
186 return htmlfill.render(
186 render('admin/my_account/my_account.html'),
187 render('admin/my_account/my_account.html'),
187 defaults=defaults,
188 defaults=defaults,
188 encoding="UTF-8",
189 encoding="UTF-8",
189 force_defaults=False
190 force_defaults=False
190 )
191 )
191
192
192 @auth.CSRFRequired(except_methods=['GET'])
193 @auth.CSRFRequired(except_methods=['GET'])
193 def my_account_password(self):
194 def my_account_password(self):
194 c.active = 'password'
195 c.active = 'password'
195 self.__load_data()
196 self.__load_data()
196
197
197 schema = user_schema.ChangePasswordSchema().bind(
198 schema = user_schema.ChangePasswordSchema().bind(
198 username=c.rhodecode_user.username)
199 username=c.rhodecode_user.username)
199
200
200 form = forms.Form(schema,
201 form = forms.Form(schema,
201 buttons=(forms.buttons.save, forms.buttons.reset))
202 buttons=(forms.buttons.save, forms.buttons.reset))
202
203
203 if request.method == 'POST':
204 if request.method == 'POST':
204 controls = request.POST.items()
205 controls = request.POST.items()
205 try:
206 try:
206 valid_data = form.validate(controls)
207 valid_data = form.validate(controls)
207 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
208 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
208 instance = c.rhodecode_user.get_instance()
209 instance = c.rhodecode_user.get_instance()
209 instance.update_userdata(force_password_change=False)
210 instance.update_userdata(force_password_change=False)
210 Session().commit()
211 Session().commit()
211 except forms.ValidationFailure as e:
212 except forms.ValidationFailure as e:
212 request.session.flash(
213 request.session.flash(
213 _('Error occurred during update of user password'),
214 _('Error occurred during update of user password'),
214 queue='error')
215 queue='error')
215 form = e
216 form = e
216 except Exception:
217 except Exception:
217 log.exception("Exception updating password")
218 log.exception("Exception updating password")
218 request.session.flash(
219 request.session.flash(
219 _('Error occurred during update of user password'),
220 _('Error occurred during update of user password'),
220 queue='error')
221 queue='error')
221 else:
222 else:
222 session.setdefault('rhodecode_user', {}).update(
223 session.setdefault('rhodecode_user', {}).update(
223 {'password': md5(instance.password)})
224 {'password': md5(instance.password)})
224 session.save()
225 session.save()
225 request.session.flash(
226 request.session.flash(
226 _("Successfully updated password"), queue='success')
227 _("Successfully updated password"), queue='success')
227 return redirect(url('my_account_password'))
228 return redirect(url('my_account_password'))
228
229
229 c.form = form
230 c.form = form
230 return render('admin/my_account/my_account.html')
231 return render('admin/my_account/my_account.html')
231
232
232 def my_account_repos(self):
233 def my_account_repos(self):
233 c.active = 'repos'
234 c.active = 'repos'
234 self.__load_data()
235 self.__load_data()
235
236
236 # json used to render the grid
237 # json used to render the grid
237 c.data = self._load_my_repos_data()
238 c.data = self._load_my_repos_data()
238 return render('admin/my_account/my_account.html')
239 return render('admin/my_account/my_account.html')
239
240
240 def my_account_watched(self):
241 def my_account_watched(self):
241 c.active = 'watched'
242 c.active = 'watched'
242 self.__load_data()
243 self.__load_data()
243
244
244 # json used to render the grid
245 # json used to render the grid
245 c.data = self._load_my_repos_data(watched=True)
246 c.data = self._load_my_repos_data(watched=True)
246 return render('admin/my_account/my_account.html')
247 return render('admin/my_account/my_account.html')
247
248
248 def my_account_perms(self):
249 def my_account_perms(self):
249 c.active = 'perms'
250 c.active = 'perms'
250 self.__load_data()
251 self.__load_data()
251 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
252 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
252 ip_addr=self.ip_addr)
253 ip_addr=self.ip_addr)
253
254
254 return render('admin/my_account/my_account.html')
255 return render('admin/my_account/my_account.html')
255
256
256 def my_account_emails(self):
257 def my_account_emails(self):
257 c.active = 'emails'
258 c.active = 'emails'
258 self.__load_data()
259 self.__load_data()
259
260
260 c.user_email_map = UserEmailMap.query()\
261 c.user_email_map = UserEmailMap.query()\
261 .filter(UserEmailMap.user == c.user).all()
262 .filter(UserEmailMap.user == c.user).all()
262 return render('admin/my_account/my_account.html')
263 return render('admin/my_account/my_account.html')
263
264
264 @auth.CSRFRequired()
265 @auth.CSRFRequired()
265 def my_account_emails_add(self):
266 def my_account_emails_add(self):
266 email = request.POST.get('new_email')
267 email = request.POST.get('new_email')
267
268
268 try:
269 try:
269 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
270 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
270 Session().commit()
271 Session().commit()
271 h.flash(_("Added new email address `%s` for user account") % email,
272 h.flash(_("Added new email address `%s` for user account") % email,
272 category='success')
273 category='success')
273 except formencode.Invalid as error:
274 except formencode.Invalid as error:
274 msg = error.error_dict['email']
275 msg = error.error_dict['email']
275 h.flash(msg, category='error')
276 h.flash(msg, category='error')
276 except Exception:
277 except Exception:
277 log.exception("Exception in my_account_emails")
278 log.exception("Exception in my_account_emails")
278 h.flash(_('An error occurred during email saving'),
279 h.flash(_('An error occurred during email saving'),
279 category='error')
280 category='error')
280 return redirect(url('my_account_emails'))
281 return redirect(url('my_account_emails'))
281
282
282 @auth.CSRFRequired()
283 @auth.CSRFRequired()
283 def my_account_emails_delete(self):
284 def my_account_emails_delete(self):
284 email_id = request.POST.get('del_email_id')
285 email_id = request.POST.get('del_email_id')
285 user_model = UserModel()
286 user_model = UserModel()
286 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
287 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
287 Session().commit()
288 Session().commit()
288 h.flash(_("Removed email address from user account"),
289 h.flash(_("Removed email address from user account"),
289 category='success')
290 category='success')
290 return redirect(url('my_account_emails'))
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 def my_account_pullrequests(self):
357 def my_account_pullrequests(self):
293 c.active = 'pullrequests'
358 c.active = 'pullrequests'
294 self.__load_data()
359 self.__load_data()
295 c.show_closed = request.GET.get('pr_show_closed')
360 c.show_closed = str2bool(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
302
361
303 c.my_pull_requests = _filter(
362 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
304 PullRequest.query().filter(
363 if c.show_closed:
305 PullRequest.user_id == c.rhodecode_user.user_id).all())
364 statuses += [PullRequest.STATUS_CLOSED]
306 my_prs = [
365 data = self._get_pull_requests_list(statuses)
307 x.pull_request for x in PullRequestReviewers.query().filter(
366 if not request.is_xhr:
308 PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
367 c.data_participate = json.dumps(data['data'])
309 c.participate_in_pull_requests = _filter(my_prs)
368 c.records_total_participate = data['recordsTotal']
310 return render('admin/my_account/my_account.html')
369 return render('admin/my_account/my_account.html')
370 else:
371 return json.dumps(data)
311
372
312 def my_account_auth_tokens(self):
373 def my_account_auth_tokens(self):
313 c.active = 'auth_tokens'
374 c.active = 'auth_tokens'
314 self.__load_data()
375 self.__load_data()
315 show_expired = True
376 show_expired = True
316 c.lifetime_values = [
377 c.lifetime_values = [
317 (str(-1), _('forever')),
378 (str(-1), _('forever')),
318 (str(5), _('5 minutes')),
379 (str(5), _('5 minutes')),
319 (str(60), _('1 hour')),
380 (str(60), _('1 hour')),
320 (str(60 * 24), _('1 day')),
381 (str(60 * 24), _('1 day')),
321 (str(60 * 24 * 30), _('1 month')),
382 (str(60 * 24 * 30), _('1 month')),
322 ]
383 ]
323 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
384 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
324 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
385 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
325 for x in AuthTokenModel.cls.ROLES]
386 for x in AuthTokenModel.cls.ROLES]
326 c.role_options = [(c.role_values, _("Role"))]
387 c.role_options = [(c.role_values, _("Role"))]
327 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
388 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
328 c.rhodecode_user.user_id, show_expired=show_expired)
389 c.rhodecode_user.user_id, show_expired=show_expired)
329 return render('admin/my_account/my_account.html')
390 return render('admin/my_account/my_account.html')
330
391
331 @auth.CSRFRequired()
392 @auth.CSRFRequired()
332 def my_account_auth_tokens_add(self):
393 def my_account_auth_tokens_add(self):
333 lifetime = safe_int(request.POST.get('lifetime'), -1)
394 lifetime = safe_int(request.POST.get('lifetime'), -1)
334 description = request.POST.get('description')
395 description = request.POST.get('description')
335 role = request.POST.get('role')
396 role = request.POST.get('role')
336 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
397 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
337 role)
398 role)
338 Session().commit()
399 Session().commit()
339 h.flash(_("Auth token successfully created"), category='success')
400 h.flash(_("Auth token successfully created"), category='success')
340 return redirect(url('my_account_auth_tokens'))
401 return redirect(url('my_account_auth_tokens'))
341
402
342 @auth.CSRFRequired()
403 @auth.CSRFRequired()
343 def my_account_auth_tokens_delete(self):
404 def my_account_auth_tokens_delete(self):
344 auth_token = request.POST.get('del_auth_token')
405 auth_token = request.POST.get('del_auth_token')
345 user_id = c.rhodecode_user.user_id
406 user_id = c.rhodecode_user.user_id
346 if request.POST.get('del_auth_token_builtin'):
407 if request.POST.get('del_auth_token_builtin'):
347 user = User.get(user_id)
408 user = User.get(user_id)
348 if user:
409 if user:
349 user.api_key = generate_auth_token(user.username)
410 user.api_key = generate_auth_token(user.username)
350 Session().add(user)
411 Session().add(user)
351 Session().commit()
412 Session().commit()
352 h.flash(_("Auth token successfully reset"), category='success')
413 h.flash(_("Auth token successfully reset"), category='success')
353 elif auth_token:
414 elif auth_token:
354 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
415 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
355 Session().commit()
416 Session().commit()
356 h.flash(_("Auth token successfully deleted"), category='success')
417 h.flash(_("Auth token successfully deleted"), category='success')
357
418
358 return redirect(url('my_account_auth_tokens'))
419 return redirect(url('my_account_auth_tokens'))
359
420
360 def my_notifications(self):
421 def my_notifications(self):
361 c.active = 'notifications'
422 c.active = 'notifications'
362 return render('admin/my_account/my_account.html')
423 return render('admin/my_account/my_account.html')
363
424
364 @auth.CSRFRequired()
425 @auth.CSRFRequired()
365 @jsonify
426 @jsonify
366 def my_notifications_toggle_visibility(self):
427 def my_notifications_toggle_visibility(self):
367 user = c.rhodecode_user.get_instance()
428 user = c.rhodecode_user.get_instance()
368 new_status = not user.user_data.get('notification_status', True)
429 new_status = not user.user_data.get('notification_status', True)
369 user.update_userdata(notification_status=new_status)
430 user.update_userdata(notification_status=new_status)
370 Session().commit()
431 Session().commit()
371 return user.user_data['notification_status']
432 return user.user_data['notification_status']
@@ -1,3697 +1,3693 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from webob.exc import HTTPNotFound
45 from webob.exc import HTTPNotFound
46 from zope.cachedescriptors.property import Lazy as LazyProperty
46 from zope.cachedescriptors.property import Lazy as LazyProperty
47
47
48 from pylons import url
48 from pylons import url
49 from pylons.i18n.translation import lazy_ugettext as _
49 from pylons.i18n.translation import lazy_ugettext as _
50
50
51 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs import get_vcs_instance
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 from rhodecode.lib.utils2 import (
53 from rhodecode.lib.utils2 import (
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re)
56 glob2re)
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.ext_json import json
59 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.caching_query import FromCache
60 from rhodecode.lib.encrypt import AESCipher
60 from rhodecode.lib.encrypt import AESCipher
61
61
62 from rhodecode.model.meta import Base, Session
62 from rhodecode.model.meta import Base, Session
63
63
64 URL_SEP = '/'
64 URL_SEP = '/'
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67 # =============================================================================
67 # =============================================================================
68 # BASE CLASSES
68 # BASE CLASSES
69 # =============================================================================
69 # =============================================================================
70
70
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 # beaker.session.secret if first is not set.
72 # beaker.session.secret if first is not set.
73 # and initialized at environment.py
73 # and initialized at environment.py
74 ENCRYPTION_KEY = None
74 ENCRYPTION_KEY = None
75
75
76 # used to sort permissions by types, '#' used here is not allowed to be in
76 # used to sort permissions by types, '#' used here is not allowed to be in
77 # usernames, and it's very early in sorted string.printable table.
77 # usernames, and it's very early in sorted string.printable table.
78 PERMISSION_TYPE_SORT = {
78 PERMISSION_TYPE_SORT = {
79 'admin': '####',
79 'admin': '####',
80 'write': '###',
80 'write': '###',
81 'read': '##',
81 'read': '##',
82 'none': '#',
82 'none': '#',
83 }
83 }
84
84
85
85
86 def display_sort(obj):
86 def display_sort(obj):
87 """
87 """
88 Sort function used to sort permissions in .permissions() function of
88 Sort function used to sort permissions in .permissions() function of
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 of all other resources
90 of all other resources
91 """
91 """
92
92
93 if obj.username == User.DEFAULT_USER:
93 if obj.username == User.DEFAULT_USER:
94 return '#####'
94 return '#####'
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 return prefix + obj.username
96 return prefix + obj.username
97
97
98
98
99 def _hash_key(k):
99 def _hash_key(k):
100 return md5_safe(k)
100 return md5_safe(k)
101
101
102
102
103 class EncryptedTextValue(TypeDecorator):
103 class EncryptedTextValue(TypeDecorator):
104 """
104 """
105 Special column for encrypted long text data, use like::
105 Special column for encrypted long text data, use like::
106
106
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108
108
109 This column is intelligent so if value is in unencrypted form it return
109 This column is intelligent so if value is in unencrypted form it return
110 unencrypted form, but on save it always encrypts
110 unencrypted form, but on save it always encrypts
111 """
111 """
112 impl = Text
112 impl = Text
113
113
114 def process_bind_param(self, value, dialect):
114 def process_bind_param(self, value, dialect):
115 if not value:
115 if not value:
116 return value
116 return value
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 # protect against double encrypting if someone manually starts
118 # protect against double encrypting if someone manually starts
119 # doing
119 # doing
120 raise ValueError('value needs to be in unencrypted format, ie. '
120 raise ValueError('value needs to be in unencrypted format, ie. '
121 'not starting with enc$aes')
121 'not starting with enc$aes')
122 return 'enc$aes_hmac$%s' % AESCipher(
122 return 'enc$aes_hmac$%s' % AESCipher(
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124
124
125 def process_result_value(self, value, dialect):
125 def process_result_value(self, value, dialect):
126 import rhodecode
126 import rhodecode
127
127
128 if not value:
128 if not value:
129 return value
129 return value
130
130
131 parts = value.split('$', 3)
131 parts = value.split('$', 3)
132 if not len(parts) == 3:
132 if not len(parts) == 3:
133 # probably not encrypted values
133 # probably not encrypted values
134 return value
134 return value
135 else:
135 else:
136 if parts[0] != 'enc':
136 if parts[0] != 'enc':
137 # parts ok but without our header ?
137 # parts ok but without our header ?
138 return value
138 return value
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 'rhodecode.encrypted_values.strict') or True)
140 'rhodecode.encrypted_values.strict') or True)
141 # at that stage we know it's our encryption
141 # at that stage we know it's our encryption
142 if parts[1] == 'aes':
142 if parts[1] == 'aes':
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 elif parts[1] == 'aes_hmac':
144 elif parts[1] == 'aes_hmac':
145 decrypted_data = AESCipher(
145 decrypted_data = AESCipher(
146 ENCRYPTION_KEY, hmac=True,
146 ENCRYPTION_KEY, hmac=True,
147 strict_verification=enc_strict_mode).decrypt(parts[2])
147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 else:
148 else:
149 raise ValueError(
149 raise ValueError(
150 'Encryption type part is wrong, must be `aes` '
150 'Encryption type part is wrong, must be `aes` '
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 return decrypted_data
152 return decrypted_data
153
153
154
154
155 class BaseModel(object):
155 class BaseModel(object):
156 """
156 """
157 Base Model for all classes
157 Base Model for all classes
158 """
158 """
159
159
160 @classmethod
160 @classmethod
161 def _get_keys(cls):
161 def _get_keys(cls):
162 """return column names for this model """
162 """return column names for this model """
163 return class_mapper(cls).c.keys()
163 return class_mapper(cls).c.keys()
164
164
165 def get_dict(self):
165 def get_dict(self):
166 """
166 """
167 return dict with keys and values corresponding
167 return dict with keys and values corresponding
168 to this model data """
168 to this model data """
169
169
170 d = {}
170 d = {}
171 for k in self._get_keys():
171 for k in self._get_keys():
172 d[k] = getattr(self, k)
172 d[k] = getattr(self, k)
173
173
174 # also use __json__() if present to get additional fields
174 # also use __json__() if present to get additional fields
175 _json_attr = getattr(self, '__json__', None)
175 _json_attr = getattr(self, '__json__', None)
176 if _json_attr:
176 if _json_attr:
177 # update with attributes from __json__
177 # update with attributes from __json__
178 if callable(_json_attr):
178 if callable(_json_attr):
179 _json_attr = _json_attr()
179 _json_attr = _json_attr()
180 for k, val in _json_attr.iteritems():
180 for k, val in _json_attr.iteritems():
181 d[k] = val
181 d[k] = val
182 return d
182 return d
183
183
184 def get_appstruct(self):
184 def get_appstruct(self):
185 """return list with keys and values tuples corresponding
185 """return list with keys and values tuples corresponding
186 to this model data """
186 to this model data """
187
187
188 l = []
188 l = []
189 for k in self._get_keys():
189 for k in self._get_keys():
190 l.append((k, getattr(self, k),))
190 l.append((k, getattr(self, k),))
191 return l
191 return l
192
192
193 def populate_obj(self, populate_dict):
193 def populate_obj(self, populate_dict):
194 """populate model with data from given populate_dict"""
194 """populate model with data from given populate_dict"""
195
195
196 for k in self._get_keys():
196 for k in self._get_keys():
197 if k in populate_dict:
197 if k in populate_dict:
198 setattr(self, k, populate_dict[k])
198 setattr(self, k, populate_dict[k])
199
199
200 @classmethod
200 @classmethod
201 def query(cls):
201 def query(cls):
202 return Session().query(cls)
202 return Session().query(cls)
203
203
204 @classmethod
204 @classmethod
205 def get(cls, id_):
205 def get(cls, id_):
206 if id_:
206 if id_:
207 return cls.query().get(id_)
207 return cls.query().get(id_)
208
208
209 @classmethod
209 @classmethod
210 def get_or_404(cls, id_):
210 def get_or_404(cls, id_):
211 try:
211 try:
212 id_ = int(id_)
212 id_ = int(id_)
213 except (TypeError, ValueError):
213 except (TypeError, ValueError):
214 raise HTTPNotFound
214 raise HTTPNotFound
215
215
216 res = cls.query().get(id_)
216 res = cls.query().get(id_)
217 if not res:
217 if not res:
218 raise HTTPNotFound
218 raise HTTPNotFound
219 return res
219 return res
220
220
221 @classmethod
221 @classmethod
222 def getAll(cls):
222 def getAll(cls):
223 # deprecated and left for backward compatibility
223 # deprecated and left for backward compatibility
224 return cls.get_all()
224 return cls.get_all()
225
225
226 @classmethod
226 @classmethod
227 def get_all(cls):
227 def get_all(cls):
228 return cls.query().all()
228 return cls.query().all()
229
229
230 @classmethod
230 @classmethod
231 def delete(cls, id_):
231 def delete(cls, id_):
232 obj = cls.query().get(id_)
232 obj = cls.query().get(id_)
233 Session().delete(obj)
233 Session().delete(obj)
234
234
235 @classmethod
235 @classmethod
236 def identity_cache(cls, session, attr_name, value):
236 def identity_cache(cls, session, attr_name, value):
237 exist_in_session = []
237 exist_in_session = []
238 for (item_cls, pkey), instance in session.identity_map.items():
238 for (item_cls, pkey), instance in session.identity_map.items():
239 if cls == item_cls and getattr(instance, attr_name) == value:
239 if cls == item_cls and getattr(instance, attr_name) == value:
240 exist_in_session.append(instance)
240 exist_in_session.append(instance)
241 if exist_in_session:
241 if exist_in_session:
242 if len(exist_in_session) == 1:
242 if len(exist_in_session) == 1:
243 return exist_in_session[0]
243 return exist_in_session[0]
244 log.exception(
244 log.exception(
245 'multiple objects with attr %s and '
245 'multiple objects with attr %s and '
246 'value %s found with same name: %r',
246 'value %s found with same name: %r',
247 attr_name, value, exist_in_session)
247 attr_name, value, exist_in_session)
248
248
249 def __repr__(self):
249 def __repr__(self):
250 if hasattr(self, '__unicode__'):
250 if hasattr(self, '__unicode__'):
251 # python repr needs to return str
251 # python repr needs to return str
252 try:
252 try:
253 return safe_str(self.__unicode__())
253 return safe_str(self.__unicode__())
254 except UnicodeDecodeError:
254 except UnicodeDecodeError:
255 pass
255 pass
256 return '<DB:%s>' % (self.__class__.__name__)
256 return '<DB:%s>' % (self.__class__.__name__)
257
257
258
258
259 class RhodeCodeSetting(Base, BaseModel):
259 class RhodeCodeSetting(Base, BaseModel):
260 __tablename__ = 'rhodecode_settings'
260 __tablename__ = 'rhodecode_settings'
261 __table_args__ = (
261 __table_args__ = (
262 UniqueConstraint('app_settings_name'),
262 UniqueConstraint('app_settings_name'),
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 )
265 )
266
266
267 SETTINGS_TYPES = {
267 SETTINGS_TYPES = {
268 'str': safe_str,
268 'str': safe_str,
269 'int': safe_int,
269 'int': safe_int,
270 'unicode': safe_unicode,
270 'unicode': safe_unicode,
271 'bool': str2bool,
271 'bool': str2bool,
272 'list': functools.partial(aslist, sep=',')
272 'list': functools.partial(aslist, sep=',')
273 }
273 }
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 GLOBAL_CONF_KEY = 'app_settings'
275 GLOBAL_CONF_KEY = 'app_settings'
276
276
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281
281
282 def __init__(self, key='', val='', type='unicode'):
282 def __init__(self, key='', val='', type='unicode'):
283 self.app_settings_name = key
283 self.app_settings_name = key
284 self.app_settings_type = type
284 self.app_settings_type = type
285 self.app_settings_value = val
285 self.app_settings_value = val
286
286
287 @validates('_app_settings_value')
287 @validates('_app_settings_value')
288 def validate_settings_value(self, key, val):
288 def validate_settings_value(self, key, val):
289 assert type(val) == unicode
289 assert type(val) == unicode
290 return val
290 return val
291
291
292 @hybrid_property
292 @hybrid_property
293 def app_settings_value(self):
293 def app_settings_value(self):
294 v = self._app_settings_value
294 v = self._app_settings_value
295 _type = self.app_settings_type
295 _type = self.app_settings_type
296 if _type:
296 if _type:
297 _type = self.app_settings_type.split('.')[0]
297 _type = self.app_settings_type.split('.')[0]
298 # decode the encrypted value
298 # decode the encrypted value
299 if 'encrypted' in self.app_settings_type:
299 if 'encrypted' in self.app_settings_type:
300 cipher = EncryptedTextValue()
300 cipher = EncryptedTextValue()
301 v = safe_unicode(cipher.process_result_value(v, None))
301 v = safe_unicode(cipher.process_result_value(v, None))
302
302
303 converter = self.SETTINGS_TYPES.get(_type) or \
303 converter = self.SETTINGS_TYPES.get(_type) or \
304 self.SETTINGS_TYPES['unicode']
304 self.SETTINGS_TYPES['unicode']
305 return converter(v)
305 return converter(v)
306
306
307 @app_settings_value.setter
307 @app_settings_value.setter
308 def app_settings_value(self, val):
308 def app_settings_value(self, val):
309 """
309 """
310 Setter that will always make sure we use unicode in app_settings_value
310 Setter that will always make sure we use unicode in app_settings_value
311
311
312 :param val:
312 :param val:
313 """
313 """
314 val = safe_unicode(val)
314 val = safe_unicode(val)
315 # encode the encrypted value
315 # encode the encrypted value
316 if 'encrypted' in self.app_settings_type:
316 if 'encrypted' in self.app_settings_type:
317 cipher = EncryptedTextValue()
317 cipher = EncryptedTextValue()
318 val = safe_unicode(cipher.process_bind_param(val, None))
318 val = safe_unicode(cipher.process_bind_param(val, None))
319 self._app_settings_value = val
319 self._app_settings_value = val
320
320
321 @hybrid_property
321 @hybrid_property
322 def app_settings_type(self):
322 def app_settings_type(self):
323 return self._app_settings_type
323 return self._app_settings_type
324
324
325 @app_settings_type.setter
325 @app_settings_type.setter
326 def app_settings_type(self, val):
326 def app_settings_type(self, val):
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 raise Exception('type must be one of %s got %s'
328 raise Exception('type must be one of %s got %s'
329 % (self.SETTINGS_TYPES.keys(), val))
329 % (self.SETTINGS_TYPES.keys(), val))
330 self._app_settings_type = val
330 self._app_settings_type = val
331
331
332 def __unicode__(self):
332 def __unicode__(self):
333 return u"<%s('%s:%s[%s]')>" % (
333 return u"<%s('%s:%s[%s]')>" % (
334 self.__class__.__name__,
334 self.__class__.__name__,
335 self.app_settings_name, self.app_settings_value,
335 self.app_settings_name, self.app_settings_value,
336 self.app_settings_type
336 self.app_settings_type
337 )
337 )
338
338
339
339
340 class RhodeCodeUi(Base, BaseModel):
340 class RhodeCodeUi(Base, BaseModel):
341 __tablename__ = 'rhodecode_ui'
341 __tablename__ = 'rhodecode_ui'
342 __table_args__ = (
342 __table_args__ = (
343 UniqueConstraint('ui_key'),
343 UniqueConstraint('ui_key'),
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 )
346 )
347
347
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 # HG
349 # HG
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 HOOK_PULL = 'outgoing.pull_logger'
351 HOOK_PULL = 'outgoing.pull_logger'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 HOOK_PUSH = 'changegroup.push_logger'
353 HOOK_PUSH = 'changegroup.push_logger'
354
354
355 # TODO: johbo: Unify way how hooks are configured for git and hg,
355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 # git part is currently hardcoded.
356 # git part is currently hardcoded.
357
357
358 # SVN PATTERNS
358 # SVN PATTERNS
359 SVN_BRANCH_ID = 'vcs_svn_branch'
359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 SVN_TAG_ID = 'vcs_svn_tag'
360 SVN_TAG_ID = 'vcs_svn_tag'
361
361
362 ui_id = Column(
362 ui_id = Column(
363 "ui_id", Integer(), nullable=False, unique=True, default=None,
363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 primary_key=True)
364 primary_key=True)
365 ui_section = Column(
365 ui_section = Column(
366 "ui_section", String(255), nullable=True, unique=None, default=None)
366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 ui_key = Column(
367 ui_key = Column(
368 "ui_key", String(255), nullable=True, unique=None, default=None)
368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 ui_value = Column(
369 ui_value = Column(
370 "ui_value", String(255), nullable=True, unique=None, default=None)
370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 ui_active = Column(
371 ui_active = Column(
372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373
373
374 def __repr__(self):
374 def __repr__(self):
375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 self.ui_key, self.ui_value)
376 self.ui_key, self.ui_value)
377
377
378
378
379 class RepoRhodeCodeSetting(Base, BaseModel):
379 class RepoRhodeCodeSetting(Base, BaseModel):
380 __tablename__ = 'repo_rhodecode_settings'
380 __tablename__ = 'repo_rhodecode_settings'
381 __table_args__ = (
381 __table_args__ = (
382 UniqueConstraint(
382 UniqueConstraint(
383 'app_settings_name', 'repository_id',
383 'app_settings_name', 'repository_id',
384 name='uq_repo_rhodecode_setting_name_repo_id'),
384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 )
387 )
388
388
389 repository_id = Column(
389 repository_id = Column(
390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 nullable=False)
391 nullable=False)
392 app_settings_id = Column(
392 app_settings_id = Column(
393 "app_settings_id", Integer(), nullable=False, unique=True,
393 "app_settings_id", Integer(), nullable=False, unique=True,
394 default=None, primary_key=True)
394 default=None, primary_key=True)
395 app_settings_name = Column(
395 app_settings_name = Column(
396 "app_settings_name", String(255), nullable=True, unique=None,
396 "app_settings_name", String(255), nullable=True, unique=None,
397 default=None)
397 default=None)
398 _app_settings_value = Column(
398 _app_settings_value = Column(
399 "app_settings_value", String(4096), nullable=True, unique=None,
399 "app_settings_value", String(4096), nullable=True, unique=None,
400 default=None)
400 default=None)
401 _app_settings_type = Column(
401 _app_settings_type = Column(
402 "app_settings_type", String(255), nullable=True, unique=None,
402 "app_settings_type", String(255), nullable=True, unique=None,
403 default=None)
403 default=None)
404
404
405 repository = relationship('Repository')
405 repository = relationship('Repository')
406
406
407 def __init__(self, repository_id, key='', val='', type='unicode'):
407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 self.repository_id = repository_id
408 self.repository_id = repository_id
409 self.app_settings_name = key
409 self.app_settings_name = key
410 self.app_settings_type = type
410 self.app_settings_type = type
411 self.app_settings_value = val
411 self.app_settings_value = val
412
412
413 @validates('_app_settings_value')
413 @validates('_app_settings_value')
414 def validate_settings_value(self, key, val):
414 def validate_settings_value(self, key, val):
415 assert type(val) == unicode
415 assert type(val) == unicode
416 return val
416 return val
417
417
418 @hybrid_property
418 @hybrid_property
419 def app_settings_value(self):
419 def app_settings_value(self):
420 v = self._app_settings_value
420 v = self._app_settings_value
421 type_ = self.app_settings_type
421 type_ = self.app_settings_type
422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 return converter(v)
424 return converter(v)
425
425
426 @app_settings_value.setter
426 @app_settings_value.setter
427 def app_settings_value(self, val):
427 def app_settings_value(self, val):
428 """
428 """
429 Setter that will always make sure we use unicode in app_settings_value
429 Setter that will always make sure we use unicode in app_settings_value
430
430
431 :param val:
431 :param val:
432 """
432 """
433 self._app_settings_value = safe_unicode(val)
433 self._app_settings_value = safe_unicode(val)
434
434
435 @hybrid_property
435 @hybrid_property
436 def app_settings_type(self):
436 def app_settings_type(self):
437 return self._app_settings_type
437 return self._app_settings_type
438
438
439 @app_settings_type.setter
439 @app_settings_type.setter
440 def app_settings_type(self, val):
440 def app_settings_type(self, val):
441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 if val not in SETTINGS_TYPES:
442 if val not in SETTINGS_TYPES:
443 raise Exception('type must be one of %s got %s'
443 raise Exception('type must be one of %s got %s'
444 % (SETTINGS_TYPES.keys(), val))
444 % (SETTINGS_TYPES.keys(), val))
445 self._app_settings_type = val
445 self._app_settings_type = val
446
446
447 def __unicode__(self):
447 def __unicode__(self):
448 return u"<%s('%s:%s:%s[%s]')>" % (
448 return u"<%s('%s:%s:%s[%s]')>" % (
449 self.__class__.__name__, self.repository.repo_name,
449 self.__class__.__name__, self.repository.repo_name,
450 self.app_settings_name, self.app_settings_value,
450 self.app_settings_name, self.app_settings_value,
451 self.app_settings_type
451 self.app_settings_type
452 )
452 )
453
453
454
454
455 class RepoRhodeCodeUi(Base, BaseModel):
455 class RepoRhodeCodeUi(Base, BaseModel):
456 __tablename__ = 'repo_rhodecode_ui'
456 __tablename__ = 'repo_rhodecode_ui'
457 __table_args__ = (
457 __table_args__ = (
458 UniqueConstraint(
458 UniqueConstraint(
459 'repository_id', 'ui_section', 'ui_key',
459 'repository_id', 'ui_section', 'ui_key',
460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 )
463 )
464
464
465 repository_id = Column(
465 repository_id = Column(
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 nullable=False)
467 nullable=False)
468 ui_id = Column(
468 ui_id = Column(
469 "ui_id", Integer(), nullable=False, unique=True, default=None,
469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 primary_key=True)
470 primary_key=True)
471 ui_section = Column(
471 ui_section = Column(
472 "ui_section", String(255), nullable=True, unique=None, default=None)
472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 ui_key = Column(
473 ui_key = Column(
474 "ui_key", String(255), nullable=True, unique=None, default=None)
474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 ui_value = Column(
475 ui_value = Column(
476 "ui_value", String(255), nullable=True, unique=None, default=None)
476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 ui_active = Column(
477 ui_active = Column(
478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479
479
480 repository = relationship('Repository')
480 repository = relationship('Repository')
481
481
482 def __repr__(self):
482 def __repr__(self):
483 return '<%s[%s:%s]%s=>%s]>' % (
483 return '<%s[%s:%s]%s=>%s]>' % (
484 self.__class__.__name__, self.repository.repo_name,
484 self.__class__.__name__, self.repository.repo_name,
485 self.ui_section, self.ui_key, self.ui_value)
485 self.ui_section, self.ui_key, self.ui_value)
486
486
487
487
488 class User(Base, BaseModel):
488 class User(Base, BaseModel):
489 __tablename__ = 'users'
489 __tablename__ = 'users'
490 __table_args__ = (
490 __table_args__ = (
491 UniqueConstraint('username'), UniqueConstraint('email'),
491 UniqueConstraint('username'), UniqueConstraint('email'),
492 Index('u_username_idx', 'username'),
492 Index('u_username_idx', 'username'),
493 Index('u_email_idx', 'email'),
493 Index('u_email_idx', 'email'),
494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 )
496 )
497 DEFAULT_USER = 'default'
497 DEFAULT_USER = 'default'
498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500
500
501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 username = Column("username", String(255), nullable=True, unique=None, default=None)
502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 password = Column("password", String(255), nullable=True, unique=None, default=None)
503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516
516
517 user_log = relationship('UserLog')
517 user_log = relationship('UserLog')
518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519
519
520 repositories = relationship('Repository')
520 repositories = relationship('Repository')
521 repository_groups = relationship('RepoGroup')
521 repository_groups = relationship('RepoGroup')
522 user_groups = relationship('UserGroup')
522 user_groups = relationship('UserGroup')
523
523
524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526
526
527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530
530
531 group_member = relationship('UserGroupMember', cascade='all')
531 group_member = relationship('UserGroupMember', cascade='all')
532
532
533 notifications = relationship('UserNotification', cascade='all')
533 notifications = relationship('UserNotification', cascade='all')
534 # notifications assigned to this user
534 # notifications assigned to this user
535 user_created_notifications = relationship('Notification', cascade='all')
535 user_created_notifications = relationship('Notification', cascade='all')
536 # comments created by this user
536 # comments created by this user
537 user_comments = relationship('ChangesetComment', cascade='all')
537 user_comments = relationship('ChangesetComment', cascade='all')
538 # user profile extra info
538 # user profile extra info
539 user_emails = relationship('UserEmailMap', cascade='all')
539 user_emails = relationship('UserEmailMap', cascade='all')
540 user_ip_map = relationship('UserIpMap', cascade='all')
540 user_ip_map = relationship('UserIpMap', cascade='all')
541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 # gists
542 # gists
543 user_gists = relationship('Gist', cascade='all')
543 user_gists = relationship('Gist', cascade='all')
544 # user pull requests
544 # user pull requests
545 user_pull_requests = relationship('PullRequest', cascade='all')
545 user_pull_requests = relationship('PullRequest', cascade='all')
546 # external identities
546 # external identities
547 extenal_identities = relationship(
547 extenal_identities = relationship(
548 'ExternalIdentity',
548 'ExternalIdentity',
549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 cascade='all')
550 cascade='all')
551
551
552 def __unicode__(self):
552 def __unicode__(self):
553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 self.user_id, self.username)
554 self.user_id, self.username)
555
555
556 @hybrid_property
556 @hybrid_property
557 def email(self):
557 def email(self):
558 return self._email
558 return self._email
559
559
560 @email.setter
560 @email.setter
561 def email(self, val):
561 def email(self, val):
562 self._email = val.lower() if val else None
562 self._email = val.lower() if val else None
563
563
564 @property
564 @property
565 def firstname(self):
565 def firstname(self):
566 # alias for future
566 # alias for future
567 return self.name
567 return self.name
568
568
569 @property
569 @property
570 def emails(self):
570 def emails(self):
571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 return [self.email] + [x.email for x in other]
572 return [self.email] + [x.email for x in other]
573
573
574 @property
574 @property
575 def auth_tokens(self):
575 def auth_tokens(self):
576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577
577
578 @property
578 @property
579 def extra_auth_tokens(self):
579 def extra_auth_tokens(self):
580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581
581
582 @property
582 @property
583 def feed_token(self):
583 def feed_token(self):
584 feed_tokens = UserApiKeys.query()\
584 feed_tokens = UserApiKeys.query()\
585 .filter(UserApiKeys.user == self)\
585 .filter(UserApiKeys.user == self)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 .all()
587 .all()
588 if feed_tokens:
588 if feed_tokens:
589 return feed_tokens[0].api_key
589 return feed_tokens[0].api_key
590 else:
590 else:
591 # use the main token so we don't end up with nothing...
591 # use the main token so we don't end up with nothing...
592 return self.api_key
592 return self.api_key
593
593
594 @classmethod
594 @classmethod
595 def extra_valid_auth_tokens(cls, user, role=None):
595 def extra_valid_auth_tokens(cls, user, role=None):
596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 .filter(or_(UserApiKeys.expires == -1,
597 .filter(or_(UserApiKeys.expires == -1,
598 UserApiKeys.expires >= time.time()))
598 UserApiKeys.expires >= time.time()))
599 if role:
599 if role:
600 tokens = tokens.filter(or_(UserApiKeys.role == role,
600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 return tokens.all()
602 return tokens.all()
603
603
604 @property
604 @property
605 def ip_addresses(self):
605 def ip_addresses(self):
606 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
606 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
607 return [x.ip_addr for x in ret]
607 return [x.ip_addr for x in ret]
608
608
609 @property
609 @property
610 def username_and_name(self):
610 def username_and_name(self):
611 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
611 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
612
612
613 @property
613 @property
614 def username_or_name_or_email(self):
614 def username_or_name_or_email(self):
615 full_name = self.full_name if self.full_name is not ' ' else None
615 full_name = self.full_name if self.full_name is not ' ' else None
616 return self.username or full_name or self.email
616 return self.username or full_name or self.email
617
617
618 @property
618 @property
619 def full_name(self):
619 def full_name(self):
620 return '%s %s' % (self.firstname, self.lastname)
620 return '%s %s' % (self.firstname, self.lastname)
621
621
622 @property
622 @property
623 def full_name_or_username(self):
623 def full_name_or_username(self):
624 return ('%s %s' % (self.firstname, self.lastname)
624 return ('%s %s' % (self.firstname, self.lastname)
625 if (self.firstname and self.lastname) else self.username)
625 if (self.firstname and self.lastname) else self.username)
626
626
627 @property
627 @property
628 def full_contact(self):
628 def full_contact(self):
629 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
629 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
630
630
631 @property
631 @property
632 def short_contact(self):
632 def short_contact(self):
633 return '%s %s' % (self.firstname, self.lastname)
633 return '%s %s' % (self.firstname, self.lastname)
634
634
635 @property
635 @property
636 def is_admin(self):
636 def is_admin(self):
637 return self.admin
637 return self.admin
638
638
639 @property
639 @property
640 def AuthUser(self):
640 def AuthUser(self):
641 """
641 """
642 Returns instance of AuthUser for this user
642 Returns instance of AuthUser for this user
643 """
643 """
644 from rhodecode.lib.auth import AuthUser
644 from rhodecode.lib.auth import AuthUser
645 return AuthUser(user_id=self.user_id, api_key=self.api_key,
645 return AuthUser(user_id=self.user_id, api_key=self.api_key,
646 username=self.username)
646 username=self.username)
647
647
648 @hybrid_property
648 @hybrid_property
649 def user_data(self):
649 def user_data(self):
650 if not self._user_data:
650 if not self._user_data:
651 return {}
651 return {}
652
652
653 try:
653 try:
654 return json.loads(self._user_data)
654 return json.loads(self._user_data)
655 except TypeError:
655 except TypeError:
656 return {}
656 return {}
657
657
658 @user_data.setter
658 @user_data.setter
659 def user_data(self, val):
659 def user_data(self, val):
660 if not isinstance(val, dict):
660 if not isinstance(val, dict):
661 raise Exception('user_data must be dict, got %s' % type(val))
661 raise Exception('user_data must be dict, got %s' % type(val))
662 try:
662 try:
663 self._user_data = json.dumps(val)
663 self._user_data = json.dumps(val)
664 except Exception:
664 except Exception:
665 log.error(traceback.format_exc())
665 log.error(traceback.format_exc())
666
666
667 @classmethod
667 @classmethod
668 def get_by_username(cls, username, case_insensitive=False,
668 def get_by_username(cls, username, case_insensitive=False,
669 cache=False, identity_cache=False):
669 cache=False, identity_cache=False):
670 session = Session()
670 session = Session()
671
671
672 if case_insensitive:
672 if case_insensitive:
673 q = cls.query().filter(
673 q = cls.query().filter(
674 func.lower(cls.username) == func.lower(username))
674 func.lower(cls.username) == func.lower(username))
675 else:
675 else:
676 q = cls.query().filter(cls.username == username)
676 q = cls.query().filter(cls.username == username)
677
677
678 if cache:
678 if cache:
679 if identity_cache:
679 if identity_cache:
680 val = cls.identity_cache(session, 'username', username)
680 val = cls.identity_cache(session, 'username', username)
681 if val:
681 if val:
682 return val
682 return val
683 else:
683 else:
684 q = q.options(
684 q = q.options(
685 FromCache("sql_cache_short",
685 FromCache("sql_cache_short",
686 "get_user_by_name_%s" % _hash_key(username)))
686 "get_user_by_name_%s" % _hash_key(username)))
687
687
688 return q.scalar()
688 return q.scalar()
689
689
690 @classmethod
690 @classmethod
691 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
691 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
692 q = cls.query().filter(cls.api_key == auth_token)
692 q = cls.query().filter(cls.api_key == auth_token)
693
693
694 if cache:
694 if cache:
695 q = q.options(FromCache("sql_cache_short",
695 q = q.options(FromCache("sql_cache_short",
696 "get_auth_token_%s" % auth_token))
696 "get_auth_token_%s" % auth_token))
697 res = q.scalar()
697 res = q.scalar()
698
698
699 if fallback and not res:
699 if fallback and not res:
700 #fallback to additional keys
700 #fallback to additional keys
701 _res = UserApiKeys.query()\
701 _res = UserApiKeys.query()\
702 .filter(UserApiKeys.api_key == auth_token)\
702 .filter(UserApiKeys.api_key == auth_token)\
703 .filter(or_(UserApiKeys.expires == -1,
703 .filter(or_(UserApiKeys.expires == -1,
704 UserApiKeys.expires >= time.time()))\
704 UserApiKeys.expires >= time.time()))\
705 .first()
705 .first()
706 if _res:
706 if _res:
707 res = _res.user
707 res = _res.user
708 return res
708 return res
709
709
710 @classmethod
710 @classmethod
711 def get_by_email(cls, email, case_insensitive=False, cache=False):
711 def get_by_email(cls, email, case_insensitive=False, cache=False):
712
712
713 if case_insensitive:
713 if case_insensitive:
714 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
714 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
715
715
716 else:
716 else:
717 q = cls.query().filter(cls.email == email)
717 q = cls.query().filter(cls.email == email)
718
718
719 if cache:
719 if cache:
720 q = q.options(FromCache("sql_cache_short",
720 q = q.options(FromCache("sql_cache_short",
721 "get_email_key_%s" % _hash_key(email)))
721 "get_email_key_%s" % _hash_key(email)))
722
722
723 ret = q.scalar()
723 ret = q.scalar()
724 if ret is None:
724 if ret is None:
725 q = UserEmailMap.query()
725 q = UserEmailMap.query()
726 # try fetching in alternate email map
726 # try fetching in alternate email map
727 if case_insensitive:
727 if case_insensitive:
728 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
728 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
729 else:
729 else:
730 q = q.filter(UserEmailMap.email == email)
730 q = q.filter(UserEmailMap.email == email)
731 q = q.options(joinedload(UserEmailMap.user))
731 q = q.options(joinedload(UserEmailMap.user))
732 if cache:
732 if cache:
733 q = q.options(FromCache("sql_cache_short",
733 q = q.options(FromCache("sql_cache_short",
734 "get_email_map_key_%s" % email))
734 "get_email_map_key_%s" % email))
735 ret = getattr(q.scalar(), 'user', None)
735 ret = getattr(q.scalar(), 'user', None)
736
736
737 return ret
737 return ret
738
738
739 @classmethod
739 @classmethod
740 def get_from_cs_author(cls, author):
740 def get_from_cs_author(cls, author):
741 """
741 """
742 Tries to get User objects out of commit author string
742 Tries to get User objects out of commit author string
743
743
744 :param author:
744 :param author:
745 """
745 """
746 from rhodecode.lib.helpers import email, author_name
746 from rhodecode.lib.helpers import email, author_name
747 # Valid email in the attribute passed, see if they're in the system
747 # Valid email in the attribute passed, see if they're in the system
748 _email = email(author)
748 _email = email(author)
749 if _email:
749 if _email:
750 user = cls.get_by_email(_email, case_insensitive=True)
750 user = cls.get_by_email(_email, case_insensitive=True)
751 if user:
751 if user:
752 return user
752 return user
753 # Maybe we can match by username?
753 # Maybe we can match by username?
754 _author = author_name(author)
754 _author = author_name(author)
755 user = cls.get_by_username(_author, case_insensitive=True)
755 user = cls.get_by_username(_author, case_insensitive=True)
756 if user:
756 if user:
757 return user
757 return user
758
758
759 def update_userdata(self, **kwargs):
759 def update_userdata(self, **kwargs):
760 usr = self
760 usr = self
761 old = usr.user_data
761 old = usr.user_data
762 old.update(**kwargs)
762 old.update(**kwargs)
763 usr.user_data = old
763 usr.user_data = old
764 Session().add(usr)
764 Session().add(usr)
765 log.debug('updated userdata with ', kwargs)
765 log.debug('updated userdata with ', kwargs)
766
766
767 def update_lastlogin(self):
767 def update_lastlogin(self):
768 """Update user lastlogin"""
768 """Update user lastlogin"""
769 self.last_login = datetime.datetime.now()
769 self.last_login = datetime.datetime.now()
770 Session().add(self)
770 Session().add(self)
771 log.debug('updated user %s lastlogin', self.username)
771 log.debug('updated user %s lastlogin', self.username)
772
772
773 def update_lastactivity(self):
773 def update_lastactivity(self):
774 """Update user lastactivity"""
774 """Update user lastactivity"""
775 usr = self
775 usr = self
776 old = usr.user_data
776 old = usr.user_data
777 old.update({'last_activity': time.time()})
777 old.update({'last_activity': time.time()})
778 usr.user_data = old
778 usr.user_data = old
779 Session().add(usr)
779 Session().add(usr)
780 log.debug('updated user %s lastactivity', usr.username)
780 log.debug('updated user %s lastactivity', usr.username)
781
781
782 def update_password(self, new_password, change_api_key=False):
782 def update_password(self, new_password, change_api_key=False):
783 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
783 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
784
784
785 self.password = get_crypt_password(new_password)
785 self.password = get_crypt_password(new_password)
786 if change_api_key:
786 if change_api_key:
787 self.api_key = generate_auth_token(self.username)
787 self.api_key = generate_auth_token(self.username)
788 Session().add(self)
788 Session().add(self)
789
789
790 @classmethod
790 @classmethod
791 def get_first_super_admin(cls):
791 def get_first_super_admin(cls):
792 user = User.query().filter(User.admin == true()).first()
792 user = User.query().filter(User.admin == true()).first()
793 if user is None:
793 if user is None:
794 raise Exception('FATAL: Missing administrative account!')
794 raise Exception('FATAL: Missing administrative account!')
795 return user
795 return user
796
796
797 @classmethod
797 @classmethod
798 def get_all_super_admins(cls):
798 def get_all_super_admins(cls):
799 """
799 """
800 Returns all admin accounts sorted by username
800 Returns all admin accounts sorted by username
801 """
801 """
802 return User.query().filter(User.admin == true())\
802 return User.query().filter(User.admin == true())\
803 .order_by(User.username.asc()).all()
803 .order_by(User.username.asc()).all()
804
804
805 @classmethod
805 @classmethod
806 def get_default_user(cls, cache=False):
806 def get_default_user(cls, cache=False):
807 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
807 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
808 if user is None:
808 if user is None:
809 raise Exception('FATAL: Missing default account!')
809 raise Exception('FATAL: Missing default account!')
810 return user
810 return user
811
811
812 def _get_default_perms(self, user, suffix=''):
812 def _get_default_perms(self, user, suffix=''):
813 from rhodecode.model.permission import PermissionModel
813 from rhodecode.model.permission import PermissionModel
814 return PermissionModel().get_default_perms(user.user_perms, suffix)
814 return PermissionModel().get_default_perms(user.user_perms, suffix)
815
815
816 def get_default_perms(self, suffix=''):
816 def get_default_perms(self, suffix=''):
817 return self._get_default_perms(self, suffix)
817 return self._get_default_perms(self, suffix)
818
818
819 def get_api_data(self, include_secrets=False, details='full'):
819 def get_api_data(self, include_secrets=False, details='full'):
820 """
820 """
821 Common function for generating user related data for API
821 Common function for generating user related data for API
822
822
823 :param include_secrets: By default secrets in the API data will be replaced
823 :param include_secrets: By default secrets in the API data will be replaced
824 by a placeholder value to prevent exposing this data by accident. In case
824 by a placeholder value to prevent exposing this data by accident. In case
825 this data shall be exposed, set this flag to ``True``.
825 this data shall be exposed, set this flag to ``True``.
826
826
827 :param details: details can be 'basic|full' basic gives only a subset of
827 :param details: details can be 'basic|full' basic gives only a subset of
828 the available user information that includes user_id, name and emails.
828 the available user information that includes user_id, name and emails.
829 """
829 """
830 user = self
830 user = self
831 user_data = self.user_data
831 user_data = self.user_data
832 data = {
832 data = {
833 'user_id': user.user_id,
833 'user_id': user.user_id,
834 'username': user.username,
834 'username': user.username,
835 'firstname': user.name,
835 'firstname': user.name,
836 'lastname': user.lastname,
836 'lastname': user.lastname,
837 'email': user.email,
837 'email': user.email,
838 'emails': user.emails,
838 'emails': user.emails,
839 }
839 }
840 if details == 'basic':
840 if details == 'basic':
841 return data
841 return data
842
842
843 api_key_length = 40
843 api_key_length = 40
844 api_key_replacement = '*' * api_key_length
844 api_key_replacement = '*' * api_key_length
845
845
846 extras = {
846 extras = {
847 'api_key': api_key_replacement,
847 'api_key': api_key_replacement,
848 'api_keys': [api_key_replacement],
848 'api_keys': [api_key_replacement],
849 'active': user.active,
849 'active': user.active,
850 'admin': user.admin,
850 'admin': user.admin,
851 'extern_type': user.extern_type,
851 'extern_type': user.extern_type,
852 'extern_name': user.extern_name,
852 'extern_name': user.extern_name,
853 'last_login': user.last_login,
853 'last_login': user.last_login,
854 'ip_addresses': user.ip_addresses,
854 'ip_addresses': user.ip_addresses,
855 'language': user_data.get('language')
855 'language': user_data.get('language')
856 }
856 }
857 data.update(extras)
857 data.update(extras)
858
858
859 if include_secrets:
859 if include_secrets:
860 data['api_key'] = user.api_key
860 data['api_key'] = user.api_key
861 data['api_keys'] = user.auth_tokens
861 data['api_keys'] = user.auth_tokens
862 return data
862 return data
863
863
864 def __json__(self):
864 def __json__(self):
865 data = {
865 data = {
866 'full_name': self.full_name,
866 'full_name': self.full_name,
867 'full_name_or_username': self.full_name_or_username,
867 'full_name_or_username': self.full_name_or_username,
868 'short_contact': self.short_contact,
868 'short_contact': self.short_contact,
869 'full_contact': self.full_contact,
869 'full_contact': self.full_contact,
870 }
870 }
871 data.update(self.get_api_data())
871 data.update(self.get_api_data())
872 return data
872 return data
873
873
874
874
875 class UserApiKeys(Base, BaseModel):
875 class UserApiKeys(Base, BaseModel):
876 __tablename__ = 'user_api_keys'
876 __tablename__ = 'user_api_keys'
877 __table_args__ = (
877 __table_args__ = (
878 Index('uak_api_key_idx', 'api_key'),
878 Index('uak_api_key_idx', 'api_key'),
879 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
879 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
880 UniqueConstraint('api_key'),
880 UniqueConstraint('api_key'),
881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
883 )
883 )
884 __mapper_args__ = {}
884 __mapper_args__ = {}
885
885
886 # ApiKey role
886 # ApiKey role
887 ROLE_ALL = 'token_role_all'
887 ROLE_ALL = 'token_role_all'
888 ROLE_HTTP = 'token_role_http'
888 ROLE_HTTP = 'token_role_http'
889 ROLE_VCS = 'token_role_vcs'
889 ROLE_VCS = 'token_role_vcs'
890 ROLE_API = 'token_role_api'
890 ROLE_API = 'token_role_api'
891 ROLE_FEED = 'token_role_feed'
891 ROLE_FEED = 'token_role_feed'
892 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
892 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
893
893
894 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
894 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
895 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
895 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
896 api_key = Column("api_key", String(255), nullable=False, unique=True)
896 api_key = Column("api_key", String(255), nullable=False, unique=True)
897 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
897 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
898 expires = Column('expires', Float(53), nullable=False)
898 expires = Column('expires', Float(53), nullable=False)
899 role = Column('role', String(255), nullable=True)
899 role = Column('role', String(255), nullable=True)
900 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
900 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
901
901
902 user = relationship('User', lazy='joined')
902 user = relationship('User', lazy='joined')
903
903
904 @classmethod
904 @classmethod
905 def _get_role_name(cls, role):
905 def _get_role_name(cls, role):
906 return {
906 return {
907 cls.ROLE_ALL: _('all'),
907 cls.ROLE_ALL: _('all'),
908 cls.ROLE_HTTP: _('http/web interface'),
908 cls.ROLE_HTTP: _('http/web interface'),
909 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
909 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
910 cls.ROLE_API: _('api calls'),
910 cls.ROLE_API: _('api calls'),
911 cls.ROLE_FEED: _('feed access'),
911 cls.ROLE_FEED: _('feed access'),
912 }.get(role, role)
912 }.get(role, role)
913
913
914 @property
914 @property
915 def expired(self):
915 def expired(self):
916 if self.expires == -1:
916 if self.expires == -1:
917 return False
917 return False
918 return time.time() > self.expires
918 return time.time() > self.expires
919
919
920 @property
920 @property
921 def role_humanized(self):
921 def role_humanized(self):
922 return self._get_role_name(self.role)
922 return self._get_role_name(self.role)
923
923
924
924
925 class UserEmailMap(Base, BaseModel):
925 class UserEmailMap(Base, BaseModel):
926 __tablename__ = 'user_email_map'
926 __tablename__ = 'user_email_map'
927 __table_args__ = (
927 __table_args__ = (
928 Index('uem_email_idx', 'email'),
928 Index('uem_email_idx', 'email'),
929 UniqueConstraint('email'),
929 UniqueConstraint('email'),
930 {'extend_existing': True, 'mysql_engine': 'InnoDB',
930 {'extend_existing': True, 'mysql_engine': 'InnoDB',
931 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
931 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
932 )
932 )
933 __mapper_args__ = {}
933 __mapper_args__ = {}
934
934
935 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
935 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
936 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
936 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
937 _email = Column("email", String(255), nullable=True, unique=False, default=None)
937 _email = Column("email", String(255), nullable=True, unique=False, default=None)
938 user = relationship('User', lazy='joined')
938 user = relationship('User', lazy='joined')
939
939
940 @validates('_email')
940 @validates('_email')
941 def validate_email(self, key, email):
941 def validate_email(self, key, email):
942 # check if this email is not main one
942 # check if this email is not main one
943 main_email = Session().query(User).filter(User.email == email).scalar()
943 main_email = Session().query(User).filter(User.email == email).scalar()
944 if main_email is not None:
944 if main_email is not None:
945 raise AttributeError('email %s is present is user table' % email)
945 raise AttributeError('email %s is present is user table' % email)
946 return email
946 return email
947
947
948 @hybrid_property
948 @hybrid_property
949 def email(self):
949 def email(self):
950 return self._email
950 return self._email
951
951
952 @email.setter
952 @email.setter
953 def email(self, val):
953 def email(self, val):
954 self._email = val.lower() if val else None
954 self._email = val.lower() if val else None
955
955
956
956
957 class UserIpMap(Base, BaseModel):
957 class UserIpMap(Base, BaseModel):
958 __tablename__ = 'user_ip_map'
958 __tablename__ = 'user_ip_map'
959 __table_args__ = (
959 __table_args__ = (
960 UniqueConstraint('user_id', 'ip_addr'),
960 UniqueConstraint('user_id', 'ip_addr'),
961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
961 {'extend_existing': True, 'mysql_engine': 'InnoDB',
962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
962 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
963 )
963 )
964 __mapper_args__ = {}
964 __mapper_args__ = {}
965
965
966 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
966 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
967 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
967 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
968 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
968 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
969 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
969 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
970 description = Column("description", String(10000), nullable=True, unique=None, default=None)
970 description = Column("description", String(10000), nullable=True, unique=None, default=None)
971 user = relationship('User', lazy='joined')
971 user = relationship('User', lazy='joined')
972
972
973 @classmethod
973 @classmethod
974 def _get_ip_range(cls, ip_addr):
974 def _get_ip_range(cls, ip_addr):
975 net = ipaddress.ip_network(ip_addr, strict=False)
975 net = ipaddress.ip_network(ip_addr, strict=False)
976 return [str(net.network_address), str(net.broadcast_address)]
976 return [str(net.network_address), str(net.broadcast_address)]
977
977
978 def __json__(self):
978 def __json__(self):
979 return {
979 return {
980 'ip_addr': self.ip_addr,
980 'ip_addr': self.ip_addr,
981 'ip_range': self._get_ip_range(self.ip_addr),
981 'ip_range': self._get_ip_range(self.ip_addr),
982 }
982 }
983
983
984 def __unicode__(self):
984 def __unicode__(self):
985 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
985 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
986 self.user_id, self.ip_addr)
986 self.user_id, self.ip_addr)
987
987
988 class UserLog(Base, BaseModel):
988 class UserLog(Base, BaseModel):
989 __tablename__ = 'user_logs'
989 __tablename__ = 'user_logs'
990 __table_args__ = (
990 __table_args__ = (
991 {'extend_existing': True, 'mysql_engine': 'InnoDB',
991 {'extend_existing': True, 'mysql_engine': 'InnoDB',
992 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
992 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
993 )
993 )
994 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
994 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
995 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
995 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
996 username = Column("username", String(255), nullable=True, unique=None, default=None)
996 username = Column("username", String(255), nullable=True, unique=None, default=None)
997 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
997 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
998 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
998 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
999 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
999 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1000 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1000 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1001 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1001 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1002
1002
1003 def __unicode__(self):
1003 def __unicode__(self):
1004 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1004 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1005 self.repository_name,
1005 self.repository_name,
1006 self.action)
1006 self.action)
1007
1007
1008 @property
1008 @property
1009 def action_as_day(self):
1009 def action_as_day(self):
1010 return datetime.date(*self.action_date.timetuple()[:3])
1010 return datetime.date(*self.action_date.timetuple()[:3])
1011
1011
1012 user = relationship('User')
1012 user = relationship('User')
1013 repository = relationship('Repository', cascade='')
1013 repository = relationship('Repository', cascade='')
1014
1014
1015
1015
1016 class UserGroup(Base, BaseModel):
1016 class UserGroup(Base, BaseModel):
1017 __tablename__ = 'users_groups'
1017 __tablename__ = 'users_groups'
1018 __table_args__ = (
1018 __table_args__ = (
1019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1019 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1020 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1020 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1021 )
1021 )
1022
1022
1023 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1023 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1024 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1024 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1025 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1025 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1026 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1026 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1027 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1027 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1028 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1028 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1029 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1029 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1030 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1030 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1031
1031
1032 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1032 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1033 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1033 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1034 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1034 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1035 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1035 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1036 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1036 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1037 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1037 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1038
1038
1039 user = relationship('User')
1039 user = relationship('User')
1040
1040
1041 @hybrid_property
1041 @hybrid_property
1042 def group_data(self):
1042 def group_data(self):
1043 if not self._group_data:
1043 if not self._group_data:
1044 return {}
1044 return {}
1045
1045
1046 try:
1046 try:
1047 return json.loads(self._group_data)
1047 return json.loads(self._group_data)
1048 except TypeError:
1048 except TypeError:
1049 return {}
1049 return {}
1050
1050
1051 @group_data.setter
1051 @group_data.setter
1052 def group_data(self, val):
1052 def group_data(self, val):
1053 try:
1053 try:
1054 self._group_data = json.dumps(val)
1054 self._group_data = json.dumps(val)
1055 except Exception:
1055 except Exception:
1056 log.error(traceback.format_exc())
1056 log.error(traceback.format_exc())
1057
1057
1058 def __unicode__(self):
1058 def __unicode__(self):
1059 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1059 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1060 self.users_group_id,
1060 self.users_group_id,
1061 self.users_group_name)
1061 self.users_group_name)
1062
1062
1063 @classmethod
1063 @classmethod
1064 def get_by_group_name(cls, group_name, cache=False,
1064 def get_by_group_name(cls, group_name, cache=False,
1065 case_insensitive=False):
1065 case_insensitive=False):
1066 if case_insensitive:
1066 if case_insensitive:
1067 q = cls.query().filter(func.lower(cls.users_group_name) ==
1067 q = cls.query().filter(func.lower(cls.users_group_name) ==
1068 func.lower(group_name))
1068 func.lower(group_name))
1069
1069
1070 else:
1070 else:
1071 q = cls.query().filter(cls.users_group_name == group_name)
1071 q = cls.query().filter(cls.users_group_name == group_name)
1072 if cache:
1072 if cache:
1073 q = q.options(FromCache(
1073 q = q.options(FromCache(
1074 "sql_cache_short",
1074 "sql_cache_short",
1075 "get_group_%s" % _hash_key(group_name)))
1075 "get_group_%s" % _hash_key(group_name)))
1076 return q.scalar()
1076 return q.scalar()
1077
1077
1078 @classmethod
1078 @classmethod
1079 def get(cls, user_group_id, cache=False):
1079 def get(cls, user_group_id, cache=False):
1080 user_group = cls.query()
1080 user_group = cls.query()
1081 if cache:
1081 if cache:
1082 user_group = user_group.options(FromCache("sql_cache_short",
1082 user_group = user_group.options(FromCache("sql_cache_short",
1083 "get_users_group_%s" % user_group_id))
1083 "get_users_group_%s" % user_group_id))
1084 return user_group.get(user_group_id)
1084 return user_group.get(user_group_id)
1085
1085
1086 def permissions(self, with_admins=True, with_owner=True):
1086 def permissions(self, with_admins=True, with_owner=True):
1087 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1087 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1088 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1088 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1089 joinedload(UserUserGroupToPerm.user),
1089 joinedload(UserUserGroupToPerm.user),
1090 joinedload(UserUserGroupToPerm.permission),)
1090 joinedload(UserUserGroupToPerm.permission),)
1091
1091
1092 # get owners and admins and permissions. We do a trick of re-writing
1092 # get owners and admins and permissions. We do a trick of re-writing
1093 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1093 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1094 # has a global reference and changing one object propagates to all
1094 # has a global reference and changing one object propagates to all
1095 # others. This means if admin is also an owner admin_row that change
1095 # others. This means if admin is also an owner admin_row that change
1096 # would propagate to both objects
1096 # would propagate to both objects
1097 perm_rows = []
1097 perm_rows = []
1098 for _usr in q.all():
1098 for _usr in q.all():
1099 usr = AttributeDict(_usr.user.get_dict())
1099 usr = AttributeDict(_usr.user.get_dict())
1100 usr.permission = _usr.permission.permission_name
1100 usr.permission = _usr.permission.permission_name
1101 perm_rows.append(usr)
1101 perm_rows.append(usr)
1102
1102
1103 # filter the perm rows by 'default' first and then sort them by
1103 # filter the perm rows by 'default' first and then sort them by
1104 # admin,write,read,none permissions sorted again alphabetically in
1104 # admin,write,read,none permissions sorted again alphabetically in
1105 # each group
1105 # each group
1106 perm_rows = sorted(perm_rows, key=display_sort)
1106 perm_rows = sorted(perm_rows, key=display_sort)
1107
1107
1108 _admin_perm = 'usergroup.admin'
1108 _admin_perm = 'usergroup.admin'
1109 owner_row = []
1109 owner_row = []
1110 if with_owner:
1110 if with_owner:
1111 usr = AttributeDict(self.user.get_dict())
1111 usr = AttributeDict(self.user.get_dict())
1112 usr.owner_row = True
1112 usr.owner_row = True
1113 usr.permission = _admin_perm
1113 usr.permission = _admin_perm
1114 owner_row.append(usr)
1114 owner_row.append(usr)
1115
1115
1116 super_admin_rows = []
1116 super_admin_rows = []
1117 if with_admins:
1117 if with_admins:
1118 for usr in User.get_all_super_admins():
1118 for usr in User.get_all_super_admins():
1119 # if this admin is also owner, don't double the record
1119 # if this admin is also owner, don't double the record
1120 if usr.user_id == owner_row[0].user_id:
1120 if usr.user_id == owner_row[0].user_id:
1121 owner_row[0].admin_row = True
1121 owner_row[0].admin_row = True
1122 else:
1122 else:
1123 usr = AttributeDict(usr.get_dict())
1123 usr = AttributeDict(usr.get_dict())
1124 usr.admin_row = True
1124 usr.admin_row = True
1125 usr.permission = _admin_perm
1125 usr.permission = _admin_perm
1126 super_admin_rows.append(usr)
1126 super_admin_rows.append(usr)
1127
1127
1128 return super_admin_rows + owner_row + perm_rows
1128 return super_admin_rows + owner_row + perm_rows
1129
1129
1130 def permission_user_groups(self):
1130 def permission_user_groups(self):
1131 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1131 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1132 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1132 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1133 joinedload(UserGroupUserGroupToPerm.target_user_group),
1133 joinedload(UserGroupUserGroupToPerm.target_user_group),
1134 joinedload(UserGroupUserGroupToPerm.permission),)
1134 joinedload(UserGroupUserGroupToPerm.permission),)
1135
1135
1136 perm_rows = []
1136 perm_rows = []
1137 for _user_group in q.all():
1137 for _user_group in q.all():
1138 usr = AttributeDict(_user_group.user_group.get_dict())
1138 usr = AttributeDict(_user_group.user_group.get_dict())
1139 usr.permission = _user_group.permission.permission_name
1139 usr.permission = _user_group.permission.permission_name
1140 perm_rows.append(usr)
1140 perm_rows.append(usr)
1141
1141
1142 return perm_rows
1142 return perm_rows
1143
1143
1144 def _get_default_perms(self, user_group, suffix=''):
1144 def _get_default_perms(self, user_group, suffix=''):
1145 from rhodecode.model.permission import PermissionModel
1145 from rhodecode.model.permission import PermissionModel
1146 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1146 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1147
1147
1148 def get_default_perms(self, suffix=''):
1148 def get_default_perms(self, suffix=''):
1149 return self._get_default_perms(self, suffix)
1149 return self._get_default_perms(self, suffix)
1150
1150
1151 def get_api_data(self, with_group_members=True, include_secrets=False):
1151 def get_api_data(self, with_group_members=True, include_secrets=False):
1152 """
1152 """
1153 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1153 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1154 basically forwarded.
1154 basically forwarded.
1155
1155
1156 """
1156 """
1157 user_group = self
1157 user_group = self
1158
1158
1159 data = {
1159 data = {
1160 'users_group_id': user_group.users_group_id,
1160 'users_group_id': user_group.users_group_id,
1161 'group_name': user_group.users_group_name,
1161 'group_name': user_group.users_group_name,
1162 'group_description': user_group.user_group_description,
1162 'group_description': user_group.user_group_description,
1163 'active': user_group.users_group_active,
1163 'active': user_group.users_group_active,
1164 'owner': user_group.user.username,
1164 'owner': user_group.user.username,
1165 }
1165 }
1166 if with_group_members:
1166 if with_group_members:
1167 users = []
1167 users = []
1168 for user in user_group.members:
1168 for user in user_group.members:
1169 user = user.user
1169 user = user.user
1170 users.append(user.get_api_data(include_secrets=include_secrets))
1170 users.append(user.get_api_data(include_secrets=include_secrets))
1171 data['users'] = users
1171 data['users'] = users
1172
1172
1173 return data
1173 return data
1174
1174
1175
1175
1176 class UserGroupMember(Base, BaseModel):
1176 class UserGroupMember(Base, BaseModel):
1177 __tablename__ = 'users_groups_members'
1177 __tablename__ = 'users_groups_members'
1178 __table_args__ = (
1178 __table_args__ = (
1179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1181 )
1181 )
1182
1182
1183 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1183 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1184 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1184 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1185 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1186
1186
1187 user = relationship('User', lazy='joined')
1187 user = relationship('User', lazy='joined')
1188 users_group = relationship('UserGroup')
1188 users_group = relationship('UserGroup')
1189
1189
1190 def __init__(self, gr_id='', u_id=''):
1190 def __init__(self, gr_id='', u_id=''):
1191 self.users_group_id = gr_id
1191 self.users_group_id = gr_id
1192 self.user_id = u_id
1192 self.user_id = u_id
1193
1193
1194
1194
1195 class RepositoryField(Base, BaseModel):
1195 class RepositoryField(Base, BaseModel):
1196 __tablename__ = 'repositories_fields'
1196 __tablename__ = 'repositories_fields'
1197 __table_args__ = (
1197 __table_args__ = (
1198 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1198 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1199 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1199 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1200 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1200 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1201 )
1201 )
1202 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1202 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1203
1203
1204 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1204 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1205 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1205 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1206 field_key = Column("field_key", String(250))
1206 field_key = Column("field_key", String(250))
1207 field_label = Column("field_label", String(1024), nullable=False)
1207 field_label = Column("field_label", String(1024), nullable=False)
1208 field_value = Column("field_value", String(10000), nullable=False)
1208 field_value = Column("field_value", String(10000), nullable=False)
1209 field_desc = Column("field_desc", String(1024), nullable=False)
1209 field_desc = Column("field_desc", String(1024), nullable=False)
1210 field_type = Column("field_type", String(255), nullable=False, unique=None)
1210 field_type = Column("field_type", String(255), nullable=False, unique=None)
1211 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1211 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1212
1212
1213 repository = relationship('Repository')
1213 repository = relationship('Repository')
1214
1214
1215 @property
1215 @property
1216 def field_key_prefixed(self):
1216 def field_key_prefixed(self):
1217 return 'ex_%s' % self.field_key
1217 return 'ex_%s' % self.field_key
1218
1218
1219 @classmethod
1219 @classmethod
1220 def un_prefix_key(cls, key):
1220 def un_prefix_key(cls, key):
1221 if key.startswith(cls.PREFIX):
1221 if key.startswith(cls.PREFIX):
1222 return key[len(cls.PREFIX):]
1222 return key[len(cls.PREFIX):]
1223 return key
1223 return key
1224
1224
1225 @classmethod
1225 @classmethod
1226 def get_by_key_name(cls, key, repo):
1226 def get_by_key_name(cls, key, repo):
1227 row = cls.query()\
1227 row = cls.query()\
1228 .filter(cls.repository == repo)\
1228 .filter(cls.repository == repo)\
1229 .filter(cls.field_key == key).scalar()
1229 .filter(cls.field_key == key).scalar()
1230 return row
1230 return row
1231
1231
1232
1232
1233 class Repository(Base, BaseModel):
1233 class Repository(Base, BaseModel):
1234 __tablename__ = 'repositories'
1234 __tablename__ = 'repositories'
1235 __table_args__ = (
1235 __table_args__ = (
1236 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1236 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1237 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1237 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1238 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1238 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1239 )
1239 )
1240 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1240 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1241 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1241 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1242
1242
1243 STATE_CREATED = 'repo_state_created'
1243 STATE_CREATED = 'repo_state_created'
1244 STATE_PENDING = 'repo_state_pending'
1244 STATE_PENDING = 'repo_state_pending'
1245 STATE_ERROR = 'repo_state_error'
1245 STATE_ERROR = 'repo_state_error'
1246
1246
1247 LOCK_AUTOMATIC = 'lock_auto'
1247 LOCK_AUTOMATIC = 'lock_auto'
1248 LOCK_API = 'lock_api'
1248 LOCK_API = 'lock_api'
1249 LOCK_WEB = 'lock_web'
1249 LOCK_WEB = 'lock_web'
1250 LOCK_PULL = 'lock_pull'
1250 LOCK_PULL = 'lock_pull'
1251
1251
1252 NAME_SEP = URL_SEP
1252 NAME_SEP = URL_SEP
1253
1253
1254 repo_id = Column(
1254 repo_id = Column(
1255 "repo_id", Integer(), nullable=False, unique=True, default=None,
1255 "repo_id", Integer(), nullable=False, unique=True, default=None,
1256 primary_key=True)
1256 primary_key=True)
1257 _repo_name = Column(
1257 _repo_name = Column(
1258 "repo_name", Text(), nullable=False, default=None)
1258 "repo_name", Text(), nullable=False, default=None)
1259 _repo_name_hash = Column(
1259 _repo_name_hash = Column(
1260 "repo_name_hash", String(255), nullable=False, unique=True)
1260 "repo_name_hash", String(255), nullable=False, unique=True)
1261 repo_state = Column("repo_state", String(255), nullable=True)
1261 repo_state = Column("repo_state", String(255), nullable=True)
1262
1262
1263 clone_uri = Column(
1263 clone_uri = Column(
1264 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1264 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1265 default=None)
1265 default=None)
1266 repo_type = Column(
1266 repo_type = Column(
1267 "repo_type", String(255), nullable=False, unique=False, default=None)
1267 "repo_type", String(255), nullable=False, unique=False, default=None)
1268 user_id = Column(
1268 user_id = Column(
1269 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1269 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1270 unique=False, default=None)
1270 unique=False, default=None)
1271 private = Column(
1271 private = Column(
1272 "private", Boolean(), nullable=True, unique=None, default=None)
1272 "private", Boolean(), nullable=True, unique=None, default=None)
1273 enable_statistics = Column(
1273 enable_statistics = Column(
1274 "statistics", Boolean(), nullable=True, unique=None, default=True)
1274 "statistics", Boolean(), nullable=True, unique=None, default=True)
1275 enable_downloads = Column(
1275 enable_downloads = Column(
1276 "downloads", Boolean(), nullable=True, unique=None, default=True)
1276 "downloads", Boolean(), nullable=True, unique=None, default=True)
1277 description = Column(
1277 description = Column(
1278 "description", String(10000), nullable=True, unique=None, default=None)
1278 "description", String(10000), nullable=True, unique=None, default=None)
1279 created_on = Column(
1279 created_on = Column(
1280 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1280 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1281 default=datetime.datetime.now)
1281 default=datetime.datetime.now)
1282 updated_on = Column(
1282 updated_on = Column(
1283 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1283 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1284 default=datetime.datetime.now)
1284 default=datetime.datetime.now)
1285 _landing_revision = Column(
1285 _landing_revision = Column(
1286 "landing_revision", String(255), nullable=False, unique=False,
1286 "landing_revision", String(255), nullable=False, unique=False,
1287 default=None)
1287 default=None)
1288 enable_locking = Column(
1288 enable_locking = Column(
1289 "enable_locking", Boolean(), nullable=False, unique=None,
1289 "enable_locking", Boolean(), nullable=False, unique=None,
1290 default=False)
1290 default=False)
1291 _locked = Column(
1291 _locked = Column(
1292 "locked", String(255), nullable=True, unique=False, default=None)
1292 "locked", String(255), nullable=True, unique=False, default=None)
1293 _changeset_cache = Column(
1293 _changeset_cache = Column(
1294 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1294 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1295
1295
1296 fork_id = Column(
1296 fork_id = Column(
1297 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1297 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1298 nullable=True, unique=False, default=None)
1298 nullable=True, unique=False, default=None)
1299 group_id = Column(
1299 group_id = Column(
1300 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1300 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1301 unique=False, default=None)
1301 unique=False, default=None)
1302
1302
1303 user = relationship('User', lazy='joined')
1303 user = relationship('User', lazy='joined')
1304 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1304 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1305 group = relationship('RepoGroup', lazy='joined')
1305 group = relationship('RepoGroup', lazy='joined')
1306 repo_to_perm = relationship(
1306 repo_to_perm = relationship(
1307 'UserRepoToPerm', cascade='all',
1307 'UserRepoToPerm', cascade='all',
1308 order_by='UserRepoToPerm.repo_to_perm_id')
1308 order_by='UserRepoToPerm.repo_to_perm_id')
1309 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1309 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1310 stats = relationship('Statistics', cascade='all', uselist=False)
1310 stats = relationship('Statistics', cascade='all', uselist=False)
1311
1311
1312 followers = relationship(
1312 followers = relationship(
1313 'UserFollowing',
1313 'UserFollowing',
1314 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1314 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1315 cascade='all')
1315 cascade='all')
1316 extra_fields = relationship(
1316 extra_fields = relationship(
1317 'RepositoryField', cascade="all, delete, delete-orphan")
1317 'RepositoryField', cascade="all, delete, delete-orphan")
1318 logs = relationship('UserLog')
1318 logs = relationship('UserLog')
1319 comments = relationship(
1319 comments = relationship(
1320 'ChangesetComment', cascade="all, delete, delete-orphan")
1320 'ChangesetComment', cascade="all, delete, delete-orphan")
1321 pull_requests_source = relationship(
1321 pull_requests_source = relationship(
1322 'PullRequest',
1322 'PullRequest',
1323 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1323 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1324 cascade="all, delete, delete-orphan")
1324 cascade="all, delete, delete-orphan")
1325 pull_requests_target = relationship(
1325 pull_requests_target = relationship(
1326 'PullRequest',
1326 'PullRequest',
1327 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1327 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1328 cascade="all, delete, delete-orphan")
1328 cascade="all, delete, delete-orphan")
1329 ui = relationship('RepoRhodeCodeUi', cascade="all")
1329 ui = relationship('RepoRhodeCodeUi', cascade="all")
1330 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1330 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1331 integrations = relationship('Integration',
1331 integrations = relationship('Integration',
1332 cascade="all, delete, delete-orphan")
1332 cascade="all, delete, delete-orphan")
1333
1333
1334 def __unicode__(self):
1334 def __unicode__(self):
1335 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1335 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1336 safe_unicode(self.repo_name))
1336 safe_unicode(self.repo_name))
1337
1337
1338 @hybrid_property
1338 @hybrid_property
1339 def landing_rev(self):
1339 def landing_rev(self):
1340 # always should return [rev_type, rev]
1340 # always should return [rev_type, rev]
1341 if self._landing_revision:
1341 if self._landing_revision:
1342 _rev_info = self._landing_revision.split(':')
1342 _rev_info = self._landing_revision.split(':')
1343 if len(_rev_info) < 2:
1343 if len(_rev_info) < 2:
1344 _rev_info.insert(0, 'rev')
1344 _rev_info.insert(0, 'rev')
1345 return [_rev_info[0], _rev_info[1]]
1345 return [_rev_info[0], _rev_info[1]]
1346 return [None, None]
1346 return [None, None]
1347
1347
1348 @landing_rev.setter
1348 @landing_rev.setter
1349 def landing_rev(self, val):
1349 def landing_rev(self, val):
1350 if ':' not in val:
1350 if ':' not in val:
1351 raise ValueError('value must be delimited with `:` and consist '
1351 raise ValueError('value must be delimited with `:` and consist '
1352 'of <rev_type>:<rev>, got %s instead' % val)
1352 'of <rev_type>:<rev>, got %s instead' % val)
1353 self._landing_revision = val
1353 self._landing_revision = val
1354
1354
1355 @hybrid_property
1355 @hybrid_property
1356 def locked(self):
1356 def locked(self):
1357 if self._locked:
1357 if self._locked:
1358 user_id, timelocked, reason = self._locked.split(':')
1358 user_id, timelocked, reason = self._locked.split(':')
1359 lock_values = int(user_id), timelocked, reason
1359 lock_values = int(user_id), timelocked, reason
1360 else:
1360 else:
1361 lock_values = [None, None, None]
1361 lock_values = [None, None, None]
1362 return lock_values
1362 return lock_values
1363
1363
1364 @locked.setter
1364 @locked.setter
1365 def locked(self, val):
1365 def locked(self, val):
1366 if val and isinstance(val, (list, tuple)):
1366 if val and isinstance(val, (list, tuple)):
1367 self._locked = ':'.join(map(str, val))
1367 self._locked = ':'.join(map(str, val))
1368 else:
1368 else:
1369 self._locked = None
1369 self._locked = None
1370
1370
1371 @hybrid_property
1371 @hybrid_property
1372 def changeset_cache(self):
1372 def changeset_cache(self):
1373 from rhodecode.lib.vcs.backends.base import EmptyCommit
1373 from rhodecode.lib.vcs.backends.base import EmptyCommit
1374 dummy = EmptyCommit().__json__()
1374 dummy = EmptyCommit().__json__()
1375 if not self._changeset_cache:
1375 if not self._changeset_cache:
1376 return dummy
1376 return dummy
1377 try:
1377 try:
1378 return json.loads(self._changeset_cache)
1378 return json.loads(self._changeset_cache)
1379 except TypeError:
1379 except TypeError:
1380 return dummy
1380 return dummy
1381 except Exception:
1381 except Exception:
1382 log.error(traceback.format_exc())
1382 log.error(traceback.format_exc())
1383 return dummy
1383 return dummy
1384
1384
1385 @changeset_cache.setter
1385 @changeset_cache.setter
1386 def changeset_cache(self, val):
1386 def changeset_cache(self, val):
1387 try:
1387 try:
1388 self._changeset_cache = json.dumps(val)
1388 self._changeset_cache = json.dumps(val)
1389 except Exception:
1389 except Exception:
1390 log.error(traceback.format_exc())
1390 log.error(traceback.format_exc())
1391
1391
1392 @hybrid_property
1392 @hybrid_property
1393 def repo_name(self):
1393 def repo_name(self):
1394 return self._repo_name
1394 return self._repo_name
1395
1395
1396 @repo_name.setter
1396 @repo_name.setter
1397 def repo_name(self, value):
1397 def repo_name(self, value):
1398 self._repo_name = value
1398 self._repo_name = value
1399 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1399 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1400
1400
1401 @classmethod
1401 @classmethod
1402 def normalize_repo_name(cls, repo_name):
1402 def normalize_repo_name(cls, repo_name):
1403 """
1403 """
1404 Normalizes os specific repo_name to the format internally stored inside
1404 Normalizes os specific repo_name to the format internally stored inside
1405 database using URL_SEP
1405 database using URL_SEP
1406
1406
1407 :param cls:
1407 :param cls:
1408 :param repo_name:
1408 :param repo_name:
1409 """
1409 """
1410 return cls.NAME_SEP.join(repo_name.split(os.sep))
1410 return cls.NAME_SEP.join(repo_name.split(os.sep))
1411
1411
1412 @classmethod
1412 @classmethod
1413 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1413 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1414 session = Session()
1414 session = Session()
1415 q = session.query(cls).filter(cls.repo_name == repo_name)
1415 q = session.query(cls).filter(cls.repo_name == repo_name)
1416
1416
1417 if cache:
1417 if cache:
1418 if identity_cache:
1418 if identity_cache:
1419 val = cls.identity_cache(session, 'repo_name', repo_name)
1419 val = cls.identity_cache(session, 'repo_name', repo_name)
1420 if val:
1420 if val:
1421 return val
1421 return val
1422 else:
1422 else:
1423 q = q.options(
1423 q = q.options(
1424 FromCache("sql_cache_short",
1424 FromCache("sql_cache_short",
1425 "get_repo_by_name_%s" % _hash_key(repo_name)))
1425 "get_repo_by_name_%s" % _hash_key(repo_name)))
1426
1426
1427 return q.scalar()
1427 return q.scalar()
1428
1428
1429 @classmethod
1429 @classmethod
1430 def get_by_full_path(cls, repo_full_path):
1430 def get_by_full_path(cls, repo_full_path):
1431 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1431 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1432 repo_name = cls.normalize_repo_name(repo_name)
1432 repo_name = cls.normalize_repo_name(repo_name)
1433 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1433 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1434
1434
1435 @classmethod
1435 @classmethod
1436 def get_repo_forks(cls, repo_id):
1436 def get_repo_forks(cls, repo_id):
1437 return cls.query().filter(Repository.fork_id == repo_id)
1437 return cls.query().filter(Repository.fork_id == repo_id)
1438
1438
1439 @classmethod
1439 @classmethod
1440 def base_path(cls):
1440 def base_path(cls):
1441 """
1441 """
1442 Returns base path when all repos are stored
1442 Returns base path when all repos are stored
1443
1443
1444 :param cls:
1444 :param cls:
1445 """
1445 """
1446 q = Session().query(RhodeCodeUi)\
1446 q = Session().query(RhodeCodeUi)\
1447 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1447 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1448 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1448 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1449 return q.one().ui_value
1449 return q.one().ui_value
1450
1450
1451 @classmethod
1451 @classmethod
1452 def is_valid(cls, repo_name):
1452 def is_valid(cls, repo_name):
1453 """
1453 """
1454 returns True if given repo name is a valid filesystem repository
1454 returns True if given repo name is a valid filesystem repository
1455
1455
1456 :param cls:
1456 :param cls:
1457 :param repo_name:
1457 :param repo_name:
1458 """
1458 """
1459 from rhodecode.lib.utils import is_valid_repo
1459 from rhodecode.lib.utils import is_valid_repo
1460
1460
1461 return is_valid_repo(repo_name, cls.base_path())
1461 return is_valid_repo(repo_name, cls.base_path())
1462
1462
1463 @classmethod
1463 @classmethod
1464 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1464 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1465 case_insensitive=True):
1465 case_insensitive=True):
1466 q = Repository.query()
1466 q = Repository.query()
1467
1467
1468 if not isinstance(user_id, Optional):
1468 if not isinstance(user_id, Optional):
1469 q = q.filter(Repository.user_id == user_id)
1469 q = q.filter(Repository.user_id == user_id)
1470
1470
1471 if not isinstance(group_id, Optional):
1471 if not isinstance(group_id, Optional):
1472 q = q.filter(Repository.group_id == group_id)
1472 q = q.filter(Repository.group_id == group_id)
1473
1473
1474 if case_insensitive:
1474 if case_insensitive:
1475 q = q.order_by(func.lower(Repository.repo_name))
1475 q = q.order_by(func.lower(Repository.repo_name))
1476 else:
1476 else:
1477 q = q.order_by(Repository.repo_name)
1477 q = q.order_by(Repository.repo_name)
1478 return q.all()
1478 return q.all()
1479
1479
1480 @property
1480 @property
1481 def forks(self):
1481 def forks(self):
1482 """
1482 """
1483 Return forks of this repo
1483 Return forks of this repo
1484 """
1484 """
1485 return Repository.get_repo_forks(self.repo_id)
1485 return Repository.get_repo_forks(self.repo_id)
1486
1486
1487 @property
1487 @property
1488 def parent(self):
1488 def parent(self):
1489 """
1489 """
1490 Returns fork parent
1490 Returns fork parent
1491 """
1491 """
1492 return self.fork
1492 return self.fork
1493
1493
1494 @property
1494 @property
1495 def just_name(self):
1495 def just_name(self):
1496 return self.repo_name.split(self.NAME_SEP)[-1]
1496 return self.repo_name.split(self.NAME_SEP)[-1]
1497
1497
1498 @property
1498 @property
1499 def groups_with_parents(self):
1499 def groups_with_parents(self):
1500 groups = []
1500 groups = []
1501 if self.group is None:
1501 if self.group is None:
1502 return groups
1502 return groups
1503
1503
1504 cur_gr = self.group
1504 cur_gr = self.group
1505 groups.insert(0, cur_gr)
1505 groups.insert(0, cur_gr)
1506 while 1:
1506 while 1:
1507 gr = getattr(cur_gr, 'parent_group', None)
1507 gr = getattr(cur_gr, 'parent_group', None)
1508 cur_gr = cur_gr.parent_group
1508 cur_gr = cur_gr.parent_group
1509 if gr is None:
1509 if gr is None:
1510 break
1510 break
1511 groups.insert(0, gr)
1511 groups.insert(0, gr)
1512
1512
1513 return groups
1513 return groups
1514
1514
1515 @property
1515 @property
1516 def groups_and_repo(self):
1516 def groups_and_repo(self):
1517 return self.groups_with_parents, self
1517 return self.groups_with_parents, self
1518
1518
1519 @LazyProperty
1519 @LazyProperty
1520 def repo_path(self):
1520 def repo_path(self):
1521 """
1521 """
1522 Returns base full path for that repository means where it actually
1522 Returns base full path for that repository means where it actually
1523 exists on a filesystem
1523 exists on a filesystem
1524 """
1524 """
1525 q = Session().query(RhodeCodeUi).filter(
1525 q = Session().query(RhodeCodeUi).filter(
1526 RhodeCodeUi.ui_key == self.NAME_SEP)
1526 RhodeCodeUi.ui_key == self.NAME_SEP)
1527 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1527 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1528 return q.one().ui_value
1528 return q.one().ui_value
1529
1529
1530 @property
1530 @property
1531 def repo_full_path(self):
1531 def repo_full_path(self):
1532 p = [self.repo_path]
1532 p = [self.repo_path]
1533 # we need to split the name by / since this is how we store the
1533 # we need to split the name by / since this is how we store the
1534 # names in the database, but that eventually needs to be converted
1534 # names in the database, but that eventually needs to be converted
1535 # into a valid system path
1535 # into a valid system path
1536 p += self.repo_name.split(self.NAME_SEP)
1536 p += self.repo_name.split(self.NAME_SEP)
1537 return os.path.join(*map(safe_unicode, p))
1537 return os.path.join(*map(safe_unicode, p))
1538
1538
1539 @property
1539 @property
1540 def cache_keys(self):
1540 def cache_keys(self):
1541 """
1541 """
1542 Returns associated cache keys for that repo
1542 Returns associated cache keys for that repo
1543 """
1543 """
1544 return CacheKey.query()\
1544 return CacheKey.query()\
1545 .filter(CacheKey.cache_args == self.repo_name)\
1545 .filter(CacheKey.cache_args == self.repo_name)\
1546 .order_by(CacheKey.cache_key)\
1546 .order_by(CacheKey.cache_key)\
1547 .all()
1547 .all()
1548
1548
1549 def get_new_name(self, repo_name):
1549 def get_new_name(self, repo_name):
1550 """
1550 """
1551 returns new full repository name based on assigned group and new new
1551 returns new full repository name based on assigned group and new new
1552
1552
1553 :param group_name:
1553 :param group_name:
1554 """
1554 """
1555 path_prefix = self.group.full_path_splitted if self.group else []
1555 path_prefix = self.group.full_path_splitted if self.group else []
1556 return self.NAME_SEP.join(path_prefix + [repo_name])
1556 return self.NAME_SEP.join(path_prefix + [repo_name])
1557
1557
1558 @property
1558 @property
1559 def _config(self):
1559 def _config(self):
1560 """
1560 """
1561 Returns db based config object.
1561 Returns db based config object.
1562 """
1562 """
1563 from rhodecode.lib.utils import make_db_config
1563 from rhodecode.lib.utils import make_db_config
1564 return make_db_config(clear_session=False, repo=self)
1564 return make_db_config(clear_session=False, repo=self)
1565
1565
1566 def permissions(self, with_admins=True, with_owner=True):
1566 def permissions(self, with_admins=True, with_owner=True):
1567 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1567 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1568 q = q.options(joinedload(UserRepoToPerm.repository),
1568 q = q.options(joinedload(UserRepoToPerm.repository),
1569 joinedload(UserRepoToPerm.user),
1569 joinedload(UserRepoToPerm.user),
1570 joinedload(UserRepoToPerm.permission),)
1570 joinedload(UserRepoToPerm.permission),)
1571
1571
1572 # get owners and admins and permissions. We do a trick of re-writing
1572 # get owners and admins and permissions. We do a trick of re-writing
1573 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1573 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1574 # has a global reference and changing one object propagates to all
1574 # has a global reference and changing one object propagates to all
1575 # others. This means if admin is also an owner admin_row that change
1575 # others. This means if admin is also an owner admin_row that change
1576 # would propagate to both objects
1576 # would propagate to both objects
1577 perm_rows = []
1577 perm_rows = []
1578 for _usr in q.all():
1578 for _usr in q.all():
1579 usr = AttributeDict(_usr.user.get_dict())
1579 usr = AttributeDict(_usr.user.get_dict())
1580 usr.permission = _usr.permission.permission_name
1580 usr.permission = _usr.permission.permission_name
1581 perm_rows.append(usr)
1581 perm_rows.append(usr)
1582
1582
1583 # filter the perm rows by 'default' first and then sort them by
1583 # filter the perm rows by 'default' first and then sort them by
1584 # admin,write,read,none permissions sorted again alphabetically in
1584 # admin,write,read,none permissions sorted again alphabetically in
1585 # each group
1585 # each group
1586 perm_rows = sorted(perm_rows, key=display_sort)
1586 perm_rows = sorted(perm_rows, key=display_sort)
1587
1587
1588 _admin_perm = 'repository.admin'
1588 _admin_perm = 'repository.admin'
1589 owner_row = []
1589 owner_row = []
1590 if with_owner:
1590 if with_owner:
1591 usr = AttributeDict(self.user.get_dict())
1591 usr = AttributeDict(self.user.get_dict())
1592 usr.owner_row = True
1592 usr.owner_row = True
1593 usr.permission = _admin_perm
1593 usr.permission = _admin_perm
1594 owner_row.append(usr)
1594 owner_row.append(usr)
1595
1595
1596 super_admin_rows = []
1596 super_admin_rows = []
1597 if with_admins:
1597 if with_admins:
1598 for usr in User.get_all_super_admins():
1598 for usr in User.get_all_super_admins():
1599 # if this admin is also owner, don't double the record
1599 # if this admin is also owner, don't double the record
1600 if usr.user_id == owner_row[0].user_id:
1600 if usr.user_id == owner_row[0].user_id:
1601 owner_row[0].admin_row = True
1601 owner_row[0].admin_row = True
1602 else:
1602 else:
1603 usr = AttributeDict(usr.get_dict())
1603 usr = AttributeDict(usr.get_dict())
1604 usr.admin_row = True
1604 usr.admin_row = True
1605 usr.permission = _admin_perm
1605 usr.permission = _admin_perm
1606 super_admin_rows.append(usr)
1606 super_admin_rows.append(usr)
1607
1607
1608 return super_admin_rows + owner_row + perm_rows
1608 return super_admin_rows + owner_row + perm_rows
1609
1609
1610 def permission_user_groups(self):
1610 def permission_user_groups(self):
1611 q = UserGroupRepoToPerm.query().filter(
1611 q = UserGroupRepoToPerm.query().filter(
1612 UserGroupRepoToPerm.repository == self)
1612 UserGroupRepoToPerm.repository == self)
1613 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1613 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1614 joinedload(UserGroupRepoToPerm.users_group),
1614 joinedload(UserGroupRepoToPerm.users_group),
1615 joinedload(UserGroupRepoToPerm.permission),)
1615 joinedload(UserGroupRepoToPerm.permission),)
1616
1616
1617 perm_rows = []
1617 perm_rows = []
1618 for _user_group in q.all():
1618 for _user_group in q.all():
1619 usr = AttributeDict(_user_group.users_group.get_dict())
1619 usr = AttributeDict(_user_group.users_group.get_dict())
1620 usr.permission = _user_group.permission.permission_name
1620 usr.permission = _user_group.permission.permission_name
1621 perm_rows.append(usr)
1621 perm_rows.append(usr)
1622
1622
1623 return perm_rows
1623 return perm_rows
1624
1624
1625 def get_api_data(self, include_secrets=False):
1625 def get_api_data(self, include_secrets=False):
1626 """
1626 """
1627 Common function for generating repo api data
1627 Common function for generating repo api data
1628
1628
1629 :param include_secrets: See :meth:`User.get_api_data`.
1629 :param include_secrets: See :meth:`User.get_api_data`.
1630
1630
1631 """
1631 """
1632 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1632 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1633 # move this methods on models level.
1633 # move this methods on models level.
1634 from rhodecode.model.settings import SettingsModel
1634 from rhodecode.model.settings import SettingsModel
1635
1635
1636 repo = self
1636 repo = self
1637 _user_id, _time, _reason = self.locked
1637 _user_id, _time, _reason = self.locked
1638
1638
1639 data = {
1639 data = {
1640 'repo_id': repo.repo_id,
1640 'repo_id': repo.repo_id,
1641 'repo_name': repo.repo_name,
1641 'repo_name': repo.repo_name,
1642 'repo_type': repo.repo_type,
1642 'repo_type': repo.repo_type,
1643 'clone_uri': repo.clone_uri or '',
1643 'clone_uri': repo.clone_uri or '',
1644 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1644 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1645 'private': repo.private,
1645 'private': repo.private,
1646 'created_on': repo.created_on,
1646 'created_on': repo.created_on,
1647 'description': repo.description,
1647 'description': repo.description,
1648 'landing_rev': repo.landing_rev,
1648 'landing_rev': repo.landing_rev,
1649 'owner': repo.user.username,
1649 'owner': repo.user.username,
1650 'fork_of': repo.fork.repo_name if repo.fork else None,
1650 'fork_of': repo.fork.repo_name if repo.fork else None,
1651 'enable_statistics': repo.enable_statistics,
1651 'enable_statistics': repo.enable_statistics,
1652 'enable_locking': repo.enable_locking,
1652 'enable_locking': repo.enable_locking,
1653 'enable_downloads': repo.enable_downloads,
1653 'enable_downloads': repo.enable_downloads,
1654 'last_changeset': repo.changeset_cache,
1654 'last_changeset': repo.changeset_cache,
1655 'locked_by': User.get(_user_id).get_api_data(
1655 'locked_by': User.get(_user_id).get_api_data(
1656 include_secrets=include_secrets) if _user_id else None,
1656 include_secrets=include_secrets) if _user_id else None,
1657 'locked_date': time_to_datetime(_time) if _time else None,
1657 'locked_date': time_to_datetime(_time) if _time else None,
1658 'lock_reason': _reason if _reason else None,
1658 'lock_reason': _reason if _reason else None,
1659 }
1659 }
1660
1660
1661 # TODO: mikhail: should be per-repo settings here
1661 # TODO: mikhail: should be per-repo settings here
1662 rc_config = SettingsModel().get_all_settings()
1662 rc_config = SettingsModel().get_all_settings()
1663 repository_fields = str2bool(
1663 repository_fields = str2bool(
1664 rc_config.get('rhodecode_repository_fields'))
1664 rc_config.get('rhodecode_repository_fields'))
1665 if repository_fields:
1665 if repository_fields:
1666 for f in self.extra_fields:
1666 for f in self.extra_fields:
1667 data[f.field_key_prefixed] = f.field_value
1667 data[f.field_key_prefixed] = f.field_value
1668
1668
1669 return data
1669 return data
1670
1670
1671 @classmethod
1671 @classmethod
1672 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1672 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1673 if not lock_time:
1673 if not lock_time:
1674 lock_time = time.time()
1674 lock_time = time.time()
1675 if not lock_reason:
1675 if not lock_reason:
1676 lock_reason = cls.LOCK_AUTOMATIC
1676 lock_reason = cls.LOCK_AUTOMATIC
1677 repo.locked = [user_id, lock_time, lock_reason]
1677 repo.locked = [user_id, lock_time, lock_reason]
1678 Session().add(repo)
1678 Session().add(repo)
1679 Session().commit()
1679 Session().commit()
1680
1680
1681 @classmethod
1681 @classmethod
1682 def unlock(cls, repo):
1682 def unlock(cls, repo):
1683 repo.locked = None
1683 repo.locked = None
1684 Session().add(repo)
1684 Session().add(repo)
1685 Session().commit()
1685 Session().commit()
1686
1686
1687 @classmethod
1687 @classmethod
1688 def getlock(cls, repo):
1688 def getlock(cls, repo):
1689 return repo.locked
1689 return repo.locked
1690
1690
1691 def is_user_lock(self, user_id):
1691 def is_user_lock(self, user_id):
1692 if self.lock[0]:
1692 if self.lock[0]:
1693 lock_user_id = safe_int(self.lock[0])
1693 lock_user_id = safe_int(self.lock[0])
1694 user_id = safe_int(user_id)
1694 user_id = safe_int(user_id)
1695 # both are ints, and they are equal
1695 # both are ints, and they are equal
1696 return all([lock_user_id, user_id]) and lock_user_id == user_id
1696 return all([lock_user_id, user_id]) and lock_user_id == user_id
1697
1697
1698 return False
1698 return False
1699
1699
1700 def get_locking_state(self, action, user_id, only_when_enabled=True):
1700 def get_locking_state(self, action, user_id, only_when_enabled=True):
1701 """
1701 """
1702 Checks locking on this repository, if locking is enabled and lock is
1702 Checks locking on this repository, if locking is enabled and lock is
1703 present returns a tuple of make_lock, locked, locked_by.
1703 present returns a tuple of make_lock, locked, locked_by.
1704 make_lock can have 3 states None (do nothing) True, make lock
1704 make_lock can have 3 states None (do nothing) True, make lock
1705 False release lock, This value is later propagated to hooks, which
1705 False release lock, This value is later propagated to hooks, which
1706 do the locking. Think about this as signals passed to hooks what to do.
1706 do the locking. Think about this as signals passed to hooks what to do.
1707
1707
1708 """
1708 """
1709 # TODO: johbo: This is part of the business logic and should be moved
1709 # TODO: johbo: This is part of the business logic and should be moved
1710 # into the RepositoryModel.
1710 # into the RepositoryModel.
1711
1711
1712 if action not in ('push', 'pull'):
1712 if action not in ('push', 'pull'):
1713 raise ValueError("Invalid action value: %s" % repr(action))
1713 raise ValueError("Invalid action value: %s" % repr(action))
1714
1714
1715 # defines if locked error should be thrown to user
1715 # defines if locked error should be thrown to user
1716 currently_locked = False
1716 currently_locked = False
1717 # defines if new lock should be made, tri-state
1717 # defines if new lock should be made, tri-state
1718 make_lock = None
1718 make_lock = None
1719 repo = self
1719 repo = self
1720 user = User.get(user_id)
1720 user = User.get(user_id)
1721
1721
1722 lock_info = repo.locked
1722 lock_info = repo.locked
1723
1723
1724 if repo and (repo.enable_locking or not only_when_enabled):
1724 if repo and (repo.enable_locking or not only_when_enabled):
1725 if action == 'push':
1725 if action == 'push':
1726 # check if it's already locked !, if it is compare users
1726 # check if it's already locked !, if it is compare users
1727 locked_by_user_id = lock_info[0]
1727 locked_by_user_id = lock_info[0]
1728 if user.user_id == locked_by_user_id:
1728 if user.user_id == locked_by_user_id:
1729 log.debug(
1729 log.debug(
1730 'Got `push` action from user %s, now unlocking', user)
1730 'Got `push` action from user %s, now unlocking', user)
1731 # unlock if we have push from user who locked
1731 # unlock if we have push from user who locked
1732 make_lock = False
1732 make_lock = False
1733 else:
1733 else:
1734 # we're not the same user who locked, ban with
1734 # we're not the same user who locked, ban with
1735 # code defined in settings (default is 423 HTTP Locked) !
1735 # code defined in settings (default is 423 HTTP Locked) !
1736 log.debug('Repo %s is currently locked by %s', repo, user)
1736 log.debug('Repo %s is currently locked by %s', repo, user)
1737 currently_locked = True
1737 currently_locked = True
1738 elif action == 'pull':
1738 elif action == 'pull':
1739 # [0] user [1] date
1739 # [0] user [1] date
1740 if lock_info[0] and lock_info[1]:
1740 if lock_info[0] and lock_info[1]:
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 currently_locked = True
1742 currently_locked = True
1743 else:
1743 else:
1744 log.debug('Setting lock on repo %s by %s', repo, user)
1744 log.debug('Setting lock on repo %s by %s', repo, user)
1745 make_lock = True
1745 make_lock = True
1746
1746
1747 else:
1747 else:
1748 log.debug('Repository %s do not have locking enabled', repo)
1748 log.debug('Repository %s do not have locking enabled', repo)
1749
1749
1750 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1750 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1751 make_lock, currently_locked, lock_info)
1751 make_lock, currently_locked, lock_info)
1752
1752
1753 from rhodecode.lib.auth import HasRepoPermissionAny
1753 from rhodecode.lib.auth import HasRepoPermissionAny
1754 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1754 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1755 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1755 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1756 # if we don't have at least write permission we cannot make a lock
1756 # if we don't have at least write permission we cannot make a lock
1757 log.debug('lock state reset back to FALSE due to lack '
1757 log.debug('lock state reset back to FALSE due to lack '
1758 'of at least read permission')
1758 'of at least read permission')
1759 make_lock = False
1759 make_lock = False
1760
1760
1761 return make_lock, currently_locked, lock_info
1761 return make_lock, currently_locked, lock_info
1762
1762
1763 @property
1763 @property
1764 def last_db_change(self):
1764 def last_db_change(self):
1765 return self.updated_on
1765 return self.updated_on
1766
1766
1767 @property
1767 @property
1768 def clone_uri_hidden(self):
1768 def clone_uri_hidden(self):
1769 clone_uri = self.clone_uri
1769 clone_uri = self.clone_uri
1770 if clone_uri:
1770 if clone_uri:
1771 import urlobject
1771 import urlobject
1772 url_obj = urlobject.URLObject(clone_uri)
1772 url_obj = urlobject.URLObject(clone_uri)
1773 if url_obj.password:
1773 if url_obj.password:
1774 clone_uri = url_obj.with_password('*****')
1774 clone_uri = url_obj.with_password('*****')
1775 return clone_uri
1775 return clone_uri
1776
1776
1777 def clone_url(self, **override):
1777 def clone_url(self, **override):
1778 qualified_home_url = url('home', qualified=True)
1778 qualified_home_url = url('home', qualified=True)
1779
1779
1780 uri_tmpl = None
1780 uri_tmpl = None
1781 if 'with_id' in override:
1781 if 'with_id' in override:
1782 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1782 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1783 del override['with_id']
1783 del override['with_id']
1784
1784
1785 if 'uri_tmpl' in override:
1785 if 'uri_tmpl' in override:
1786 uri_tmpl = override['uri_tmpl']
1786 uri_tmpl = override['uri_tmpl']
1787 del override['uri_tmpl']
1787 del override['uri_tmpl']
1788
1788
1789 # we didn't override our tmpl from **overrides
1789 # we didn't override our tmpl from **overrides
1790 if not uri_tmpl:
1790 if not uri_tmpl:
1791 uri_tmpl = self.DEFAULT_CLONE_URI
1791 uri_tmpl = self.DEFAULT_CLONE_URI
1792 try:
1792 try:
1793 from pylons import tmpl_context as c
1793 from pylons import tmpl_context as c
1794 uri_tmpl = c.clone_uri_tmpl
1794 uri_tmpl = c.clone_uri_tmpl
1795 except Exception:
1795 except Exception:
1796 # in any case if we call this outside of request context,
1796 # in any case if we call this outside of request context,
1797 # ie, not having tmpl_context set up
1797 # ie, not having tmpl_context set up
1798 pass
1798 pass
1799
1799
1800 return get_clone_url(uri_tmpl=uri_tmpl,
1800 return get_clone_url(uri_tmpl=uri_tmpl,
1801 qualifed_home_url=qualified_home_url,
1801 qualifed_home_url=qualified_home_url,
1802 repo_name=self.repo_name,
1802 repo_name=self.repo_name,
1803 repo_id=self.repo_id, **override)
1803 repo_id=self.repo_id, **override)
1804
1804
1805 def set_state(self, state):
1805 def set_state(self, state):
1806 self.repo_state = state
1806 self.repo_state = state
1807 Session().add(self)
1807 Session().add(self)
1808 #==========================================================================
1808 #==========================================================================
1809 # SCM PROPERTIES
1809 # SCM PROPERTIES
1810 #==========================================================================
1810 #==========================================================================
1811
1811
1812 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1812 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1813 return get_commit_safe(
1813 return get_commit_safe(
1814 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1814 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1815
1815
1816 def get_changeset(self, rev=None, pre_load=None):
1816 def get_changeset(self, rev=None, pre_load=None):
1817 warnings.warn("Use get_commit", DeprecationWarning)
1817 warnings.warn("Use get_commit", DeprecationWarning)
1818 commit_id = None
1818 commit_id = None
1819 commit_idx = None
1819 commit_idx = None
1820 if isinstance(rev, basestring):
1820 if isinstance(rev, basestring):
1821 commit_id = rev
1821 commit_id = rev
1822 else:
1822 else:
1823 commit_idx = rev
1823 commit_idx = rev
1824 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1824 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1825 pre_load=pre_load)
1825 pre_load=pre_load)
1826
1826
1827 def get_landing_commit(self):
1827 def get_landing_commit(self):
1828 """
1828 """
1829 Returns landing commit, or if that doesn't exist returns the tip
1829 Returns landing commit, or if that doesn't exist returns the tip
1830 """
1830 """
1831 _rev_type, _rev = self.landing_rev
1831 _rev_type, _rev = self.landing_rev
1832 commit = self.get_commit(_rev)
1832 commit = self.get_commit(_rev)
1833 if isinstance(commit, EmptyCommit):
1833 if isinstance(commit, EmptyCommit):
1834 return self.get_commit()
1834 return self.get_commit()
1835 return commit
1835 return commit
1836
1836
1837 def update_commit_cache(self, cs_cache=None, config=None):
1837 def update_commit_cache(self, cs_cache=None, config=None):
1838 """
1838 """
1839 Update cache of last changeset for repository, keys should be::
1839 Update cache of last changeset for repository, keys should be::
1840
1840
1841 short_id
1841 short_id
1842 raw_id
1842 raw_id
1843 revision
1843 revision
1844 parents
1844 parents
1845 message
1845 message
1846 date
1846 date
1847 author
1847 author
1848
1848
1849 :param cs_cache:
1849 :param cs_cache:
1850 """
1850 """
1851 from rhodecode.lib.vcs.backends.base import BaseChangeset
1851 from rhodecode.lib.vcs.backends.base import BaseChangeset
1852 if cs_cache is None:
1852 if cs_cache is None:
1853 # use no-cache version here
1853 # use no-cache version here
1854 scm_repo = self.scm_instance(cache=False, config=config)
1854 scm_repo = self.scm_instance(cache=False, config=config)
1855 if scm_repo:
1855 if scm_repo:
1856 cs_cache = scm_repo.get_commit(
1856 cs_cache = scm_repo.get_commit(
1857 pre_load=["author", "date", "message", "parents"])
1857 pre_load=["author", "date", "message", "parents"])
1858 else:
1858 else:
1859 cs_cache = EmptyCommit()
1859 cs_cache = EmptyCommit()
1860
1860
1861 if isinstance(cs_cache, BaseChangeset):
1861 if isinstance(cs_cache, BaseChangeset):
1862 cs_cache = cs_cache.__json__()
1862 cs_cache = cs_cache.__json__()
1863
1863
1864 def is_outdated(new_cs_cache):
1864 def is_outdated(new_cs_cache):
1865 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1865 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1866 new_cs_cache['revision'] != self.changeset_cache['revision']):
1866 new_cs_cache['revision'] != self.changeset_cache['revision']):
1867 return True
1867 return True
1868 return False
1868 return False
1869
1869
1870 # check if we have maybe already latest cached revision
1870 # check if we have maybe already latest cached revision
1871 if is_outdated(cs_cache) or not self.changeset_cache:
1871 if is_outdated(cs_cache) or not self.changeset_cache:
1872 _default = datetime.datetime.fromtimestamp(0)
1872 _default = datetime.datetime.fromtimestamp(0)
1873 last_change = cs_cache.get('date') or _default
1873 last_change = cs_cache.get('date') or _default
1874 log.debug('updated repo %s with new cs cache %s',
1874 log.debug('updated repo %s with new cs cache %s',
1875 self.repo_name, cs_cache)
1875 self.repo_name, cs_cache)
1876 self.updated_on = last_change
1876 self.updated_on = last_change
1877 self.changeset_cache = cs_cache
1877 self.changeset_cache = cs_cache
1878 Session().add(self)
1878 Session().add(self)
1879 Session().commit()
1879 Session().commit()
1880 else:
1880 else:
1881 log.debug('Skipping update_commit_cache for repo:`%s` '
1881 log.debug('Skipping update_commit_cache for repo:`%s` '
1882 'commit already with latest changes', self.repo_name)
1882 'commit already with latest changes', self.repo_name)
1883
1883
1884 @property
1884 @property
1885 def tip(self):
1885 def tip(self):
1886 return self.get_commit('tip')
1886 return self.get_commit('tip')
1887
1887
1888 @property
1888 @property
1889 def author(self):
1889 def author(self):
1890 return self.tip.author
1890 return self.tip.author
1891
1891
1892 @property
1892 @property
1893 def last_change(self):
1893 def last_change(self):
1894 return self.scm_instance().last_change
1894 return self.scm_instance().last_change
1895
1895
1896 def get_comments(self, revisions=None):
1896 def get_comments(self, revisions=None):
1897 """
1897 """
1898 Returns comments for this repository grouped by revisions
1898 Returns comments for this repository grouped by revisions
1899
1899
1900 :param revisions: filter query by revisions only
1900 :param revisions: filter query by revisions only
1901 """
1901 """
1902 cmts = ChangesetComment.query()\
1902 cmts = ChangesetComment.query()\
1903 .filter(ChangesetComment.repo == self)
1903 .filter(ChangesetComment.repo == self)
1904 if revisions:
1904 if revisions:
1905 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1905 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1906 grouped = collections.defaultdict(list)
1906 grouped = collections.defaultdict(list)
1907 for cmt in cmts.all():
1907 for cmt in cmts.all():
1908 grouped[cmt.revision].append(cmt)
1908 grouped[cmt.revision].append(cmt)
1909 return grouped
1909 return grouped
1910
1910
1911 def statuses(self, revisions=None):
1911 def statuses(self, revisions=None):
1912 """
1912 """
1913 Returns statuses for this repository
1913 Returns statuses for this repository
1914
1914
1915 :param revisions: list of revisions to get statuses for
1915 :param revisions: list of revisions to get statuses for
1916 """
1916 """
1917 statuses = ChangesetStatus.query()\
1917 statuses = ChangesetStatus.query()\
1918 .filter(ChangesetStatus.repo == self)\
1918 .filter(ChangesetStatus.repo == self)\
1919 .filter(ChangesetStatus.version == 0)
1919 .filter(ChangesetStatus.version == 0)
1920
1920
1921 if revisions:
1921 if revisions:
1922 # Try doing the filtering in chunks to avoid hitting limits
1922 # Try doing the filtering in chunks to avoid hitting limits
1923 size = 500
1923 size = 500
1924 status_results = []
1924 status_results = []
1925 for chunk in xrange(0, len(revisions), size):
1925 for chunk in xrange(0, len(revisions), size):
1926 status_results += statuses.filter(
1926 status_results += statuses.filter(
1927 ChangesetStatus.revision.in_(
1927 ChangesetStatus.revision.in_(
1928 revisions[chunk: chunk+size])
1928 revisions[chunk: chunk+size])
1929 ).all()
1929 ).all()
1930 else:
1930 else:
1931 status_results = statuses.all()
1931 status_results = statuses.all()
1932
1932
1933 grouped = {}
1933 grouped = {}
1934
1934
1935 # maybe we have open new pullrequest without a status?
1935 # maybe we have open new pullrequest without a status?
1936 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1936 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1937 status_lbl = ChangesetStatus.get_status_lbl(stat)
1937 status_lbl = ChangesetStatus.get_status_lbl(stat)
1938 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1938 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1939 for rev in pr.revisions:
1939 for rev in pr.revisions:
1940 pr_id = pr.pull_request_id
1940 pr_id = pr.pull_request_id
1941 pr_repo = pr.target_repo.repo_name
1941 pr_repo = pr.target_repo.repo_name
1942 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1942 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1943
1943
1944 for stat in status_results:
1944 for stat in status_results:
1945 pr_id = pr_repo = None
1945 pr_id = pr_repo = None
1946 if stat.pull_request:
1946 if stat.pull_request:
1947 pr_id = stat.pull_request.pull_request_id
1947 pr_id = stat.pull_request.pull_request_id
1948 pr_repo = stat.pull_request.target_repo.repo_name
1948 pr_repo = stat.pull_request.target_repo.repo_name
1949 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1949 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1950 pr_id, pr_repo]
1950 pr_id, pr_repo]
1951 return grouped
1951 return grouped
1952
1952
1953 # ==========================================================================
1953 # ==========================================================================
1954 # SCM CACHE INSTANCE
1954 # SCM CACHE INSTANCE
1955 # ==========================================================================
1955 # ==========================================================================
1956
1956
1957 def scm_instance(self, **kwargs):
1957 def scm_instance(self, **kwargs):
1958 import rhodecode
1958 import rhodecode
1959
1959
1960 # Passing a config will not hit the cache currently only used
1960 # Passing a config will not hit the cache currently only used
1961 # for repo2dbmapper
1961 # for repo2dbmapper
1962 config = kwargs.pop('config', None)
1962 config = kwargs.pop('config', None)
1963 cache = kwargs.pop('cache', None)
1963 cache = kwargs.pop('cache', None)
1964 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1964 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1965 # if cache is NOT defined use default global, else we have a full
1965 # if cache is NOT defined use default global, else we have a full
1966 # control over cache behaviour
1966 # control over cache behaviour
1967 if cache is None and full_cache and not config:
1967 if cache is None and full_cache and not config:
1968 return self._get_instance_cached()
1968 return self._get_instance_cached()
1969 return self._get_instance(cache=bool(cache), config=config)
1969 return self._get_instance(cache=bool(cache), config=config)
1970
1970
1971 def _get_instance_cached(self):
1971 def _get_instance_cached(self):
1972 @cache_region('long_term')
1972 @cache_region('long_term')
1973 def _get_repo(cache_key):
1973 def _get_repo(cache_key):
1974 return self._get_instance()
1974 return self._get_instance()
1975
1975
1976 invalidator_context = CacheKey.repo_context_cache(
1976 invalidator_context = CacheKey.repo_context_cache(
1977 _get_repo, self.repo_name, None, thread_scoped=True)
1977 _get_repo, self.repo_name, None, thread_scoped=True)
1978
1978
1979 with invalidator_context as context:
1979 with invalidator_context as context:
1980 context.invalidate()
1980 context.invalidate()
1981 repo = context.compute()
1981 repo = context.compute()
1982
1982
1983 return repo
1983 return repo
1984
1984
1985 def _get_instance(self, cache=True, config=None):
1985 def _get_instance(self, cache=True, config=None):
1986 config = config or self._config
1986 config = config or self._config
1987 custom_wire = {
1987 custom_wire = {
1988 'cache': cache # controls the vcs.remote cache
1988 'cache': cache # controls the vcs.remote cache
1989 }
1989 }
1990
1990
1991 repo = get_vcs_instance(
1991 repo = get_vcs_instance(
1992 repo_path=safe_str(self.repo_full_path),
1992 repo_path=safe_str(self.repo_full_path),
1993 config=config,
1993 config=config,
1994 with_wire=custom_wire,
1994 with_wire=custom_wire,
1995 create=False)
1995 create=False)
1996
1996
1997 return repo
1997 return repo
1998
1998
1999 def __json__(self):
1999 def __json__(self):
2000 return {'landing_rev': self.landing_rev}
2000 return {'landing_rev': self.landing_rev}
2001
2001
2002 def get_dict(self):
2002 def get_dict(self):
2003
2003
2004 # Since we transformed `repo_name` to a hybrid property, we need to
2004 # Since we transformed `repo_name` to a hybrid property, we need to
2005 # keep compatibility with the code which uses `repo_name` field.
2005 # keep compatibility with the code which uses `repo_name` field.
2006
2006
2007 result = super(Repository, self).get_dict()
2007 result = super(Repository, self).get_dict()
2008 result['repo_name'] = result.pop('_repo_name', None)
2008 result['repo_name'] = result.pop('_repo_name', None)
2009 return result
2009 return result
2010
2010
2011
2011
2012 class RepoGroup(Base, BaseModel):
2012 class RepoGroup(Base, BaseModel):
2013 __tablename__ = 'groups'
2013 __tablename__ = 'groups'
2014 __table_args__ = (
2014 __table_args__ = (
2015 UniqueConstraint('group_name', 'group_parent_id'),
2015 UniqueConstraint('group_name', 'group_parent_id'),
2016 CheckConstraint('group_id != group_parent_id'),
2016 CheckConstraint('group_id != group_parent_id'),
2017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2018 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2018 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2019 )
2019 )
2020 __mapper_args__ = {'order_by': 'group_name'}
2020 __mapper_args__ = {'order_by': 'group_name'}
2021
2021
2022 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2022 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2023
2023
2024 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2024 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2025 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2025 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2026 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2026 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2027 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2027 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2028 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2028 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2030 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2030 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2031
2031
2032 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2032 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2033 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2033 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2034 parent_group = relationship('RepoGroup', remote_side=group_id)
2034 parent_group = relationship('RepoGroup', remote_side=group_id)
2035 user = relationship('User')
2035 user = relationship('User')
2036 integrations = relationship('Integration',
2036 integrations = relationship('Integration',
2037 cascade="all, delete, delete-orphan")
2037 cascade="all, delete, delete-orphan")
2038
2038
2039 def __init__(self, group_name='', parent_group=None):
2039 def __init__(self, group_name='', parent_group=None):
2040 self.group_name = group_name
2040 self.group_name = group_name
2041 self.parent_group = parent_group
2041 self.parent_group = parent_group
2042
2042
2043 def __unicode__(self):
2043 def __unicode__(self):
2044 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2044 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2045 self.group_name)
2045 self.group_name)
2046
2046
2047 @classmethod
2047 @classmethod
2048 def _generate_choice(cls, repo_group):
2048 def _generate_choice(cls, repo_group):
2049 from webhelpers.html import literal as _literal
2049 from webhelpers.html import literal as _literal
2050 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2050 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2051 return repo_group.group_id, _name(repo_group.full_path_splitted)
2051 return repo_group.group_id, _name(repo_group.full_path_splitted)
2052
2052
2053 @classmethod
2053 @classmethod
2054 def groups_choices(cls, groups=None, show_empty_group=True):
2054 def groups_choices(cls, groups=None, show_empty_group=True):
2055 if not groups:
2055 if not groups:
2056 groups = cls.query().all()
2056 groups = cls.query().all()
2057
2057
2058 repo_groups = []
2058 repo_groups = []
2059 if show_empty_group:
2059 if show_empty_group:
2060 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2060 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2061
2061
2062 repo_groups.extend([cls._generate_choice(x) for x in groups])
2062 repo_groups.extend([cls._generate_choice(x) for x in groups])
2063
2063
2064 repo_groups = sorted(
2064 repo_groups = sorted(
2065 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2065 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2066 return repo_groups
2066 return repo_groups
2067
2067
2068 @classmethod
2068 @classmethod
2069 def url_sep(cls):
2069 def url_sep(cls):
2070 return URL_SEP
2070 return URL_SEP
2071
2071
2072 @classmethod
2072 @classmethod
2073 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2073 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2074 if case_insensitive:
2074 if case_insensitive:
2075 gr = cls.query().filter(func.lower(cls.group_name)
2075 gr = cls.query().filter(func.lower(cls.group_name)
2076 == func.lower(group_name))
2076 == func.lower(group_name))
2077 else:
2077 else:
2078 gr = cls.query().filter(cls.group_name == group_name)
2078 gr = cls.query().filter(cls.group_name == group_name)
2079 if cache:
2079 if cache:
2080 gr = gr.options(FromCache(
2080 gr = gr.options(FromCache(
2081 "sql_cache_short",
2081 "sql_cache_short",
2082 "get_group_%s" % _hash_key(group_name)))
2082 "get_group_%s" % _hash_key(group_name)))
2083 return gr.scalar()
2083 return gr.scalar()
2084
2084
2085 @classmethod
2085 @classmethod
2086 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2086 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2087 case_insensitive=True):
2087 case_insensitive=True):
2088 q = RepoGroup.query()
2088 q = RepoGroup.query()
2089
2089
2090 if not isinstance(user_id, Optional):
2090 if not isinstance(user_id, Optional):
2091 q = q.filter(RepoGroup.user_id == user_id)
2091 q = q.filter(RepoGroup.user_id == user_id)
2092
2092
2093 if not isinstance(group_id, Optional):
2093 if not isinstance(group_id, Optional):
2094 q = q.filter(RepoGroup.group_parent_id == group_id)
2094 q = q.filter(RepoGroup.group_parent_id == group_id)
2095
2095
2096 if case_insensitive:
2096 if case_insensitive:
2097 q = q.order_by(func.lower(RepoGroup.group_name))
2097 q = q.order_by(func.lower(RepoGroup.group_name))
2098 else:
2098 else:
2099 q = q.order_by(RepoGroup.group_name)
2099 q = q.order_by(RepoGroup.group_name)
2100 return q.all()
2100 return q.all()
2101
2101
2102 @property
2102 @property
2103 def parents(self):
2103 def parents(self):
2104 parents_recursion_limit = 10
2104 parents_recursion_limit = 10
2105 groups = []
2105 groups = []
2106 if self.parent_group is None:
2106 if self.parent_group is None:
2107 return groups
2107 return groups
2108 cur_gr = self.parent_group
2108 cur_gr = self.parent_group
2109 groups.insert(0, cur_gr)
2109 groups.insert(0, cur_gr)
2110 cnt = 0
2110 cnt = 0
2111 while 1:
2111 while 1:
2112 cnt += 1
2112 cnt += 1
2113 gr = getattr(cur_gr, 'parent_group', None)
2113 gr = getattr(cur_gr, 'parent_group', None)
2114 cur_gr = cur_gr.parent_group
2114 cur_gr = cur_gr.parent_group
2115 if gr is None:
2115 if gr is None:
2116 break
2116 break
2117 if cnt == parents_recursion_limit:
2117 if cnt == parents_recursion_limit:
2118 # this will prevent accidental infinit loops
2118 # this will prevent accidental infinit loops
2119 log.error(('more than %s parents found for group %s, stopping '
2119 log.error(('more than %s parents found for group %s, stopping '
2120 'recursive parent fetching' % (parents_recursion_limit, self)))
2120 'recursive parent fetching' % (parents_recursion_limit, self)))
2121 break
2121 break
2122
2122
2123 groups.insert(0, gr)
2123 groups.insert(0, gr)
2124 return groups
2124 return groups
2125
2125
2126 @property
2126 @property
2127 def children(self):
2127 def children(self):
2128 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2128 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2129
2129
2130 @property
2130 @property
2131 def name(self):
2131 def name(self):
2132 return self.group_name.split(RepoGroup.url_sep())[-1]
2132 return self.group_name.split(RepoGroup.url_sep())[-1]
2133
2133
2134 @property
2134 @property
2135 def full_path(self):
2135 def full_path(self):
2136 return self.group_name
2136 return self.group_name
2137
2137
2138 @property
2138 @property
2139 def full_path_splitted(self):
2139 def full_path_splitted(self):
2140 return self.group_name.split(RepoGroup.url_sep())
2140 return self.group_name.split(RepoGroup.url_sep())
2141
2141
2142 @property
2142 @property
2143 def repositories(self):
2143 def repositories(self):
2144 return Repository.query()\
2144 return Repository.query()\
2145 .filter(Repository.group == self)\
2145 .filter(Repository.group == self)\
2146 .order_by(Repository.repo_name)
2146 .order_by(Repository.repo_name)
2147
2147
2148 @property
2148 @property
2149 def repositories_recursive_count(self):
2149 def repositories_recursive_count(self):
2150 cnt = self.repositories.count()
2150 cnt = self.repositories.count()
2151
2151
2152 def children_count(group):
2152 def children_count(group):
2153 cnt = 0
2153 cnt = 0
2154 for child in group.children:
2154 for child in group.children:
2155 cnt += child.repositories.count()
2155 cnt += child.repositories.count()
2156 cnt += children_count(child)
2156 cnt += children_count(child)
2157 return cnt
2157 return cnt
2158
2158
2159 return cnt + children_count(self)
2159 return cnt + children_count(self)
2160
2160
2161 def _recursive_objects(self, include_repos=True):
2161 def _recursive_objects(self, include_repos=True):
2162 all_ = []
2162 all_ = []
2163
2163
2164 def _get_members(root_gr):
2164 def _get_members(root_gr):
2165 if include_repos:
2165 if include_repos:
2166 for r in root_gr.repositories:
2166 for r in root_gr.repositories:
2167 all_.append(r)
2167 all_.append(r)
2168 childs = root_gr.children.all()
2168 childs = root_gr.children.all()
2169 if childs:
2169 if childs:
2170 for gr in childs:
2170 for gr in childs:
2171 all_.append(gr)
2171 all_.append(gr)
2172 _get_members(gr)
2172 _get_members(gr)
2173
2173
2174 _get_members(self)
2174 _get_members(self)
2175 return [self] + all_
2175 return [self] + all_
2176
2176
2177 def recursive_groups_and_repos(self):
2177 def recursive_groups_and_repos(self):
2178 """
2178 """
2179 Recursive return all groups, with repositories in those groups
2179 Recursive return all groups, with repositories in those groups
2180 """
2180 """
2181 return self._recursive_objects()
2181 return self._recursive_objects()
2182
2182
2183 def recursive_groups(self):
2183 def recursive_groups(self):
2184 """
2184 """
2185 Returns all children groups for this group including children of children
2185 Returns all children groups for this group including children of children
2186 """
2186 """
2187 return self._recursive_objects(include_repos=False)
2187 return self._recursive_objects(include_repos=False)
2188
2188
2189 def get_new_name(self, group_name):
2189 def get_new_name(self, group_name):
2190 """
2190 """
2191 returns new full group name based on parent and new name
2191 returns new full group name based on parent and new name
2192
2192
2193 :param group_name:
2193 :param group_name:
2194 """
2194 """
2195 path_prefix = (self.parent_group.full_path_splitted if
2195 path_prefix = (self.parent_group.full_path_splitted if
2196 self.parent_group else [])
2196 self.parent_group else [])
2197 return RepoGroup.url_sep().join(path_prefix + [group_name])
2197 return RepoGroup.url_sep().join(path_prefix + [group_name])
2198
2198
2199 def permissions(self, with_admins=True, with_owner=True):
2199 def permissions(self, with_admins=True, with_owner=True):
2200 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2200 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2201 q = q.options(joinedload(UserRepoGroupToPerm.group),
2201 q = q.options(joinedload(UserRepoGroupToPerm.group),
2202 joinedload(UserRepoGroupToPerm.user),
2202 joinedload(UserRepoGroupToPerm.user),
2203 joinedload(UserRepoGroupToPerm.permission),)
2203 joinedload(UserRepoGroupToPerm.permission),)
2204
2204
2205 # get owners and admins and permissions. We do a trick of re-writing
2205 # get owners and admins and permissions. We do a trick of re-writing
2206 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2206 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2207 # has a global reference and changing one object propagates to all
2207 # has a global reference and changing one object propagates to all
2208 # others. This means if admin is also an owner admin_row that change
2208 # others. This means if admin is also an owner admin_row that change
2209 # would propagate to both objects
2209 # would propagate to both objects
2210 perm_rows = []
2210 perm_rows = []
2211 for _usr in q.all():
2211 for _usr in q.all():
2212 usr = AttributeDict(_usr.user.get_dict())
2212 usr = AttributeDict(_usr.user.get_dict())
2213 usr.permission = _usr.permission.permission_name
2213 usr.permission = _usr.permission.permission_name
2214 perm_rows.append(usr)
2214 perm_rows.append(usr)
2215
2215
2216 # filter the perm rows by 'default' first and then sort them by
2216 # filter the perm rows by 'default' first and then sort them by
2217 # admin,write,read,none permissions sorted again alphabetically in
2217 # admin,write,read,none permissions sorted again alphabetically in
2218 # each group
2218 # each group
2219 perm_rows = sorted(perm_rows, key=display_sort)
2219 perm_rows = sorted(perm_rows, key=display_sort)
2220
2220
2221 _admin_perm = 'group.admin'
2221 _admin_perm = 'group.admin'
2222 owner_row = []
2222 owner_row = []
2223 if with_owner:
2223 if with_owner:
2224 usr = AttributeDict(self.user.get_dict())
2224 usr = AttributeDict(self.user.get_dict())
2225 usr.owner_row = True
2225 usr.owner_row = True
2226 usr.permission = _admin_perm
2226 usr.permission = _admin_perm
2227 owner_row.append(usr)
2227 owner_row.append(usr)
2228
2228
2229 super_admin_rows = []
2229 super_admin_rows = []
2230 if with_admins:
2230 if with_admins:
2231 for usr in User.get_all_super_admins():
2231 for usr in User.get_all_super_admins():
2232 # if this admin is also owner, don't double the record
2232 # if this admin is also owner, don't double the record
2233 if usr.user_id == owner_row[0].user_id:
2233 if usr.user_id == owner_row[0].user_id:
2234 owner_row[0].admin_row = True
2234 owner_row[0].admin_row = True
2235 else:
2235 else:
2236 usr = AttributeDict(usr.get_dict())
2236 usr = AttributeDict(usr.get_dict())
2237 usr.admin_row = True
2237 usr.admin_row = True
2238 usr.permission = _admin_perm
2238 usr.permission = _admin_perm
2239 super_admin_rows.append(usr)
2239 super_admin_rows.append(usr)
2240
2240
2241 return super_admin_rows + owner_row + perm_rows
2241 return super_admin_rows + owner_row + perm_rows
2242
2242
2243 def permission_user_groups(self):
2243 def permission_user_groups(self):
2244 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2244 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2245 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2245 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2246 joinedload(UserGroupRepoGroupToPerm.users_group),
2246 joinedload(UserGroupRepoGroupToPerm.users_group),
2247 joinedload(UserGroupRepoGroupToPerm.permission),)
2247 joinedload(UserGroupRepoGroupToPerm.permission),)
2248
2248
2249 perm_rows = []
2249 perm_rows = []
2250 for _user_group in q.all():
2250 for _user_group in q.all():
2251 usr = AttributeDict(_user_group.users_group.get_dict())
2251 usr = AttributeDict(_user_group.users_group.get_dict())
2252 usr.permission = _user_group.permission.permission_name
2252 usr.permission = _user_group.permission.permission_name
2253 perm_rows.append(usr)
2253 perm_rows.append(usr)
2254
2254
2255 return perm_rows
2255 return perm_rows
2256
2256
2257 def get_api_data(self):
2257 def get_api_data(self):
2258 """
2258 """
2259 Common function for generating api data
2259 Common function for generating api data
2260
2260
2261 """
2261 """
2262 group = self
2262 group = self
2263 data = {
2263 data = {
2264 'group_id': group.group_id,
2264 'group_id': group.group_id,
2265 'group_name': group.group_name,
2265 'group_name': group.group_name,
2266 'group_description': group.group_description,
2266 'group_description': group.group_description,
2267 'parent_group': group.parent_group.group_name if group.parent_group else None,
2267 'parent_group': group.parent_group.group_name if group.parent_group else None,
2268 'repositories': [x.repo_name for x in group.repositories],
2268 'repositories': [x.repo_name for x in group.repositories],
2269 'owner': group.user.username,
2269 'owner': group.user.username,
2270 }
2270 }
2271 return data
2271 return data
2272
2272
2273
2273
2274 class Permission(Base, BaseModel):
2274 class Permission(Base, BaseModel):
2275 __tablename__ = 'permissions'
2275 __tablename__ = 'permissions'
2276 __table_args__ = (
2276 __table_args__ = (
2277 Index('p_perm_name_idx', 'permission_name'),
2277 Index('p_perm_name_idx', 'permission_name'),
2278 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2278 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2279 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2279 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2280 )
2280 )
2281 PERMS = [
2281 PERMS = [
2282 ('hg.admin', _('RhodeCode Super Administrator')),
2282 ('hg.admin', _('RhodeCode Super Administrator')),
2283
2283
2284 ('repository.none', _('Repository no access')),
2284 ('repository.none', _('Repository no access')),
2285 ('repository.read', _('Repository read access')),
2285 ('repository.read', _('Repository read access')),
2286 ('repository.write', _('Repository write access')),
2286 ('repository.write', _('Repository write access')),
2287 ('repository.admin', _('Repository admin access')),
2287 ('repository.admin', _('Repository admin access')),
2288
2288
2289 ('group.none', _('Repository group no access')),
2289 ('group.none', _('Repository group no access')),
2290 ('group.read', _('Repository group read access')),
2290 ('group.read', _('Repository group read access')),
2291 ('group.write', _('Repository group write access')),
2291 ('group.write', _('Repository group write access')),
2292 ('group.admin', _('Repository group admin access')),
2292 ('group.admin', _('Repository group admin access')),
2293
2293
2294 ('usergroup.none', _('User group no access')),
2294 ('usergroup.none', _('User group no access')),
2295 ('usergroup.read', _('User group read access')),
2295 ('usergroup.read', _('User group read access')),
2296 ('usergroup.write', _('User group write access')),
2296 ('usergroup.write', _('User group write access')),
2297 ('usergroup.admin', _('User group admin access')),
2297 ('usergroup.admin', _('User group admin access')),
2298
2298
2299 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2299 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2300 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2300 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2301
2301
2302 ('hg.usergroup.create.false', _('User Group creation disabled')),
2302 ('hg.usergroup.create.false', _('User Group creation disabled')),
2303 ('hg.usergroup.create.true', _('User Group creation enabled')),
2303 ('hg.usergroup.create.true', _('User Group creation enabled')),
2304
2304
2305 ('hg.create.none', _('Repository creation disabled')),
2305 ('hg.create.none', _('Repository creation disabled')),
2306 ('hg.create.repository', _('Repository creation enabled')),
2306 ('hg.create.repository', _('Repository creation enabled')),
2307 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2307 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2308 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2308 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2309
2309
2310 ('hg.fork.none', _('Repository forking disabled')),
2310 ('hg.fork.none', _('Repository forking disabled')),
2311 ('hg.fork.repository', _('Repository forking enabled')),
2311 ('hg.fork.repository', _('Repository forking enabled')),
2312
2312
2313 ('hg.register.none', _('Registration disabled')),
2313 ('hg.register.none', _('Registration disabled')),
2314 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2314 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2315 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2315 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2316
2316
2317 ('hg.password_reset.enabled', _('Password reset enabled')),
2317 ('hg.password_reset.enabled', _('Password reset enabled')),
2318 ('hg.password_reset.hidden', _('Password reset hidden')),
2318 ('hg.password_reset.hidden', _('Password reset hidden')),
2319 ('hg.password_reset.disabled', _('Password reset disabled')),
2319 ('hg.password_reset.disabled', _('Password reset disabled')),
2320
2320
2321 ('hg.extern_activate.manual', _('Manual activation of external account')),
2321 ('hg.extern_activate.manual', _('Manual activation of external account')),
2322 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2322 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2323
2323
2324 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2324 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2325 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2325 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2326 ]
2326 ]
2327
2327
2328 # definition of system default permissions for DEFAULT user
2328 # definition of system default permissions for DEFAULT user
2329 DEFAULT_USER_PERMISSIONS = [
2329 DEFAULT_USER_PERMISSIONS = [
2330 'repository.read',
2330 'repository.read',
2331 'group.read',
2331 'group.read',
2332 'usergroup.read',
2332 'usergroup.read',
2333 'hg.create.repository',
2333 'hg.create.repository',
2334 'hg.repogroup.create.false',
2334 'hg.repogroup.create.false',
2335 'hg.usergroup.create.false',
2335 'hg.usergroup.create.false',
2336 'hg.create.write_on_repogroup.true',
2336 'hg.create.write_on_repogroup.true',
2337 'hg.fork.repository',
2337 'hg.fork.repository',
2338 'hg.register.manual_activate',
2338 'hg.register.manual_activate',
2339 'hg.password_reset.enabled',
2339 'hg.password_reset.enabled',
2340 'hg.extern_activate.auto',
2340 'hg.extern_activate.auto',
2341 'hg.inherit_default_perms.true',
2341 'hg.inherit_default_perms.true',
2342 ]
2342 ]
2343
2343
2344 # defines which permissions are more important higher the more important
2344 # defines which permissions are more important higher the more important
2345 # Weight defines which permissions are more important.
2345 # Weight defines which permissions are more important.
2346 # The higher number the more important.
2346 # The higher number the more important.
2347 PERM_WEIGHTS = {
2347 PERM_WEIGHTS = {
2348 'repository.none': 0,
2348 'repository.none': 0,
2349 'repository.read': 1,
2349 'repository.read': 1,
2350 'repository.write': 3,
2350 'repository.write': 3,
2351 'repository.admin': 4,
2351 'repository.admin': 4,
2352
2352
2353 'group.none': 0,
2353 'group.none': 0,
2354 'group.read': 1,
2354 'group.read': 1,
2355 'group.write': 3,
2355 'group.write': 3,
2356 'group.admin': 4,
2356 'group.admin': 4,
2357
2357
2358 'usergroup.none': 0,
2358 'usergroup.none': 0,
2359 'usergroup.read': 1,
2359 'usergroup.read': 1,
2360 'usergroup.write': 3,
2360 'usergroup.write': 3,
2361 'usergroup.admin': 4,
2361 'usergroup.admin': 4,
2362
2362
2363 'hg.repogroup.create.false': 0,
2363 'hg.repogroup.create.false': 0,
2364 'hg.repogroup.create.true': 1,
2364 'hg.repogroup.create.true': 1,
2365
2365
2366 'hg.usergroup.create.false': 0,
2366 'hg.usergroup.create.false': 0,
2367 'hg.usergroup.create.true': 1,
2367 'hg.usergroup.create.true': 1,
2368
2368
2369 'hg.fork.none': 0,
2369 'hg.fork.none': 0,
2370 'hg.fork.repository': 1,
2370 'hg.fork.repository': 1,
2371 'hg.create.none': 0,
2371 'hg.create.none': 0,
2372 'hg.create.repository': 1
2372 'hg.create.repository': 1
2373 }
2373 }
2374
2374
2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378
2378
2379 def __unicode__(self):
2379 def __unicode__(self):
2380 return u"<%s('%s:%s')>" % (
2380 return u"<%s('%s:%s')>" % (
2381 self.__class__.__name__, self.permission_id, self.permission_name
2381 self.__class__.__name__, self.permission_id, self.permission_name
2382 )
2382 )
2383
2383
2384 @classmethod
2384 @classmethod
2385 def get_by_key(cls, key):
2385 def get_by_key(cls, key):
2386 return cls.query().filter(cls.permission_name == key).scalar()
2386 return cls.query().filter(cls.permission_name == key).scalar()
2387
2387
2388 @classmethod
2388 @classmethod
2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 .filter(UserRepoToPerm.user_id == user_id)
2393 .filter(UserRepoToPerm.user_id == user_id)
2394 if repo_id:
2394 if repo_id:
2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 return q.all()
2396 return q.all()
2397
2397
2398 @classmethod
2398 @classmethod
2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 .join(
2401 .join(
2402 Permission,
2402 Permission,
2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 .join(
2404 .join(
2405 Repository,
2405 Repository,
2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 .join(
2407 .join(
2408 UserGroup,
2408 UserGroup,
2409 UserGroupRepoToPerm.users_group_id ==
2409 UserGroupRepoToPerm.users_group_id ==
2410 UserGroup.users_group_id)\
2410 UserGroup.users_group_id)\
2411 .join(
2411 .join(
2412 UserGroupMember,
2412 UserGroupMember,
2413 UserGroupRepoToPerm.users_group_id ==
2413 UserGroupRepoToPerm.users_group_id ==
2414 UserGroupMember.users_group_id)\
2414 UserGroupMember.users_group_id)\
2415 .filter(
2415 .filter(
2416 UserGroupMember.user_id == user_id,
2416 UserGroupMember.user_id == user_id,
2417 UserGroup.users_group_active == true())
2417 UserGroup.users_group_active == true())
2418 if repo_id:
2418 if repo_id:
2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 return q.all()
2420 return q.all()
2421
2421
2422 @classmethod
2422 @classmethod
2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 if repo_group_id:
2428 if repo_group_id:
2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 return q.all()
2430 return q.all()
2431
2431
2432 @classmethod
2432 @classmethod
2433 def get_default_group_perms_from_user_group(
2433 def get_default_group_perms_from_user_group(
2434 cls, user_id, repo_group_id=None):
2434 cls, user_id, repo_group_id=None):
2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 .join(
2436 .join(
2437 Permission,
2437 Permission,
2438 UserGroupRepoGroupToPerm.permission_id ==
2438 UserGroupRepoGroupToPerm.permission_id ==
2439 Permission.permission_id)\
2439 Permission.permission_id)\
2440 .join(
2440 .join(
2441 RepoGroup,
2441 RepoGroup,
2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 .join(
2443 .join(
2444 UserGroup,
2444 UserGroup,
2445 UserGroupRepoGroupToPerm.users_group_id ==
2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 UserGroup.users_group_id)\
2446 UserGroup.users_group_id)\
2447 .join(
2447 .join(
2448 UserGroupMember,
2448 UserGroupMember,
2449 UserGroupRepoGroupToPerm.users_group_id ==
2449 UserGroupRepoGroupToPerm.users_group_id ==
2450 UserGroupMember.users_group_id)\
2450 UserGroupMember.users_group_id)\
2451 .filter(
2451 .filter(
2452 UserGroupMember.user_id == user_id,
2452 UserGroupMember.user_id == user_id,
2453 UserGroup.users_group_active == true())
2453 UserGroup.users_group_active == true())
2454 if repo_group_id:
2454 if repo_group_id:
2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 return q.all()
2456 return q.all()
2457
2457
2458 @classmethod
2458 @classmethod
2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 .filter(UserUserGroupToPerm.user_id == user_id)
2463 .filter(UserUserGroupToPerm.user_id == user_id)
2464 if user_group_id:
2464 if user_group_id:
2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 return q.all()
2466 return q.all()
2467
2467
2468 @classmethod
2468 @classmethod
2469 def get_default_user_group_perms_from_user_group(
2469 def get_default_user_group_perms_from_user_group(
2470 cls, user_id, user_group_id=None):
2470 cls, user_id, user_group_id=None):
2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 .join(
2473 .join(
2474 Permission,
2474 Permission,
2475 UserGroupUserGroupToPerm.permission_id ==
2475 UserGroupUserGroupToPerm.permission_id ==
2476 Permission.permission_id)\
2476 Permission.permission_id)\
2477 .join(
2477 .join(
2478 TargetUserGroup,
2478 TargetUserGroup,
2479 UserGroupUserGroupToPerm.target_user_group_id ==
2479 UserGroupUserGroupToPerm.target_user_group_id ==
2480 TargetUserGroup.users_group_id)\
2480 TargetUserGroup.users_group_id)\
2481 .join(
2481 .join(
2482 UserGroup,
2482 UserGroup,
2483 UserGroupUserGroupToPerm.user_group_id ==
2483 UserGroupUserGroupToPerm.user_group_id ==
2484 UserGroup.users_group_id)\
2484 UserGroup.users_group_id)\
2485 .join(
2485 .join(
2486 UserGroupMember,
2486 UserGroupMember,
2487 UserGroupUserGroupToPerm.user_group_id ==
2487 UserGroupUserGroupToPerm.user_group_id ==
2488 UserGroupMember.users_group_id)\
2488 UserGroupMember.users_group_id)\
2489 .filter(
2489 .filter(
2490 UserGroupMember.user_id == user_id,
2490 UserGroupMember.user_id == user_id,
2491 UserGroup.users_group_active == true())
2491 UserGroup.users_group_active == true())
2492 if user_group_id:
2492 if user_group_id:
2493 q = q.filter(
2493 q = q.filter(
2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495
2495
2496 return q.all()
2496 return q.all()
2497
2497
2498
2498
2499 class UserRepoToPerm(Base, BaseModel):
2499 class UserRepoToPerm(Base, BaseModel):
2500 __tablename__ = 'repo_to_perm'
2500 __tablename__ = 'repo_to_perm'
2501 __table_args__ = (
2501 __table_args__ = (
2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 )
2505 )
2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510
2510
2511 user = relationship('User')
2511 user = relationship('User')
2512 repository = relationship('Repository')
2512 repository = relationship('Repository')
2513 permission = relationship('Permission')
2513 permission = relationship('Permission')
2514
2514
2515 @classmethod
2515 @classmethod
2516 def create(cls, user, repository, permission):
2516 def create(cls, user, repository, permission):
2517 n = cls()
2517 n = cls()
2518 n.user = user
2518 n.user = user
2519 n.repository = repository
2519 n.repository = repository
2520 n.permission = permission
2520 n.permission = permission
2521 Session().add(n)
2521 Session().add(n)
2522 return n
2522 return n
2523
2523
2524 def __unicode__(self):
2524 def __unicode__(self):
2525 return u'<%s => %s >' % (self.user, self.repository)
2525 return u'<%s => %s >' % (self.user, self.repository)
2526
2526
2527
2527
2528 class UserUserGroupToPerm(Base, BaseModel):
2528 class UserUserGroupToPerm(Base, BaseModel):
2529 __tablename__ = 'user_user_group_to_perm'
2529 __tablename__ = 'user_user_group_to_perm'
2530 __table_args__ = (
2530 __table_args__ = (
2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 )
2534 )
2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
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 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539
2539
2540 user = relationship('User')
2540 user = relationship('User')
2541 user_group = relationship('UserGroup')
2541 user_group = relationship('UserGroup')
2542 permission = relationship('Permission')
2542 permission = relationship('Permission')
2543
2543
2544 @classmethod
2544 @classmethod
2545 def create(cls, user, user_group, permission):
2545 def create(cls, user, user_group, permission):
2546 n = cls()
2546 n = cls()
2547 n.user = user
2547 n.user = user
2548 n.user_group = user_group
2548 n.user_group = user_group
2549 n.permission = permission
2549 n.permission = permission
2550 Session().add(n)
2550 Session().add(n)
2551 return n
2551 return n
2552
2552
2553 def __unicode__(self):
2553 def __unicode__(self):
2554 return u'<%s => %s >' % (self.user, self.user_group)
2554 return u'<%s => %s >' % (self.user, self.user_group)
2555
2555
2556
2556
2557 class UserToPerm(Base, BaseModel):
2557 class UserToPerm(Base, BaseModel):
2558 __tablename__ = 'user_to_perm'
2558 __tablename__ = 'user_to_perm'
2559 __table_args__ = (
2559 __table_args__ = (
2560 UniqueConstraint('user_id', 'permission_id'),
2560 UniqueConstraint('user_id', 'permission_id'),
2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 )
2563 )
2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567
2567
2568 user = relationship('User')
2568 user = relationship('User')
2569 permission = relationship('Permission', lazy='joined')
2569 permission = relationship('Permission', lazy='joined')
2570
2570
2571 def __unicode__(self):
2571 def __unicode__(self):
2572 return u'<%s => %s >' % (self.user, self.permission)
2572 return u'<%s => %s >' % (self.user, self.permission)
2573
2573
2574
2574
2575 class UserGroupRepoToPerm(Base, BaseModel):
2575 class UserGroupRepoToPerm(Base, BaseModel):
2576 __tablename__ = 'users_group_repo_to_perm'
2576 __tablename__ = 'users_group_repo_to_perm'
2577 __table_args__ = (
2577 __table_args__ = (
2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 )
2581 )
2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586
2586
2587 users_group = relationship('UserGroup')
2587 users_group = relationship('UserGroup')
2588 permission = relationship('Permission')
2588 permission = relationship('Permission')
2589 repository = relationship('Repository')
2589 repository = relationship('Repository')
2590
2590
2591 @classmethod
2591 @classmethod
2592 def create(cls, users_group, repository, permission):
2592 def create(cls, users_group, repository, permission):
2593 n = cls()
2593 n = cls()
2594 n.users_group = users_group
2594 n.users_group = users_group
2595 n.repository = repository
2595 n.repository = repository
2596 n.permission = permission
2596 n.permission = permission
2597 Session().add(n)
2597 Session().add(n)
2598 return n
2598 return n
2599
2599
2600 def __unicode__(self):
2600 def __unicode__(self):
2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602
2602
2603
2603
2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 __tablename__ = 'user_group_user_group_to_perm'
2605 __tablename__ = 'user_group_user_group_to_perm'
2606 __table_args__ = (
2606 __table_args__ = (
2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 CheckConstraint('target_user_group_id != user_group_id'),
2608 CheckConstraint('target_user_group_id != user_group_id'),
2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 )
2611 )
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)
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 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616
2616
2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 permission = relationship('Permission')
2619 permission = relationship('Permission')
2620
2620
2621 @classmethod
2621 @classmethod
2622 def create(cls, target_user_group, user_group, permission):
2622 def create(cls, target_user_group, user_group, permission):
2623 n = cls()
2623 n = cls()
2624 n.target_user_group = target_user_group
2624 n.target_user_group = target_user_group
2625 n.user_group = user_group
2625 n.user_group = user_group
2626 n.permission = permission
2626 n.permission = permission
2627 Session().add(n)
2627 Session().add(n)
2628 return n
2628 return n
2629
2629
2630 def __unicode__(self):
2630 def __unicode__(self):
2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632
2632
2633
2633
2634 class UserGroupToPerm(Base, BaseModel):
2634 class UserGroupToPerm(Base, BaseModel):
2635 __tablename__ = 'users_group_to_perm'
2635 __tablename__ = 'users_group_to_perm'
2636 __table_args__ = (
2636 __table_args__ = (
2637 UniqueConstraint('users_group_id', 'permission_id',),
2637 UniqueConstraint('users_group_id', 'permission_id',),
2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 )
2640 )
2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644
2644
2645 users_group = relationship('UserGroup')
2645 users_group = relationship('UserGroup')
2646 permission = relationship('Permission')
2646 permission = relationship('Permission')
2647
2647
2648
2648
2649 class UserRepoGroupToPerm(Base, BaseModel):
2649 class UserRepoGroupToPerm(Base, BaseModel):
2650 __tablename__ = 'user_repo_group_to_perm'
2650 __tablename__ = 'user_repo_group_to_perm'
2651 __table_args__ = (
2651 __table_args__ = (
2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 )
2655 )
2656
2656
2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661
2661
2662 user = relationship('User')
2662 user = relationship('User')
2663 group = relationship('RepoGroup')
2663 group = relationship('RepoGroup')
2664 permission = relationship('Permission')
2664 permission = relationship('Permission')
2665
2665
2666 @classmethod
2666 @classmethod
2667 def create(cls, user, repository_group, permission):
2667 def create(cls, user, repository_group, permission):
2668 n = cls()
2668 n = cls()
2669 n.user = user
2669 n.user = user
2670 n.group = repository_group
2670 n.group = repository_group
2671 n.permission = permission
2671 n.permission = permission
2672 Session().add(n)
2672 Session().add(n)
2673 return n
2673 return n
2674
2674
2675
2675
2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 __tablename__ = 'users_group_repo_group_to_perm'
2677 __tablename__ = 'users_group_repo_group_to_perm'
2678 __table_args__ = (
2678 __table_args__ = (
2679 UniqueConstraint('users_group_id', 'group_id'),
2679 UniqueConstraint('users_group_id', 'group_id'),
2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 )
2682 )
2683
2683
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)
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 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688
2688
2689 users_group = relationship('UserGroup')
2689 users_group = relationship('UserGroup')
2690 permission = relationship('Permission')
2690 permission = relationship('Permission')
2691 group = relationship('RepoGroup')
2691 group = relationship('RepoGroup')
2692
2692
2693 @classmethod
2693 @classmethod
2694 def create(cls, user_group, repository_group, permission):
2694 def create(cls, user_group, repository_group, permission):
2695 n = cls()
2695 n = cls()
2696 n.users_group = user_group
2696 n.users_group = user_group
2697 n.group = repository_group
2697 n.group = repository_group
2698 n.permission = permission
2698 n.permission = permission
2699 Session().add(n)
2699 Session().add(n)
2700 return n
2700 return n
2701
2701
2702 def __unicode__(self):
2702 def __unicode__(self):
2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704
2704
2705
2705
2706 class Statistics(Base, BaseModel):
2706 class Statistics(Base, BaseModel):
2707 __tablename__ = 'statistics'
2707 __tablename__ = 'statistics'
2708 __table_args__ = (
2708 __table_args__ = (
2709 UniqueConstraint('repository_id'),
2709 UniqueConstraint('repository_id'),
2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 )
2712 )
2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719
2719
2720 repository = relationship('Repository', single_parent=True)
2720 repository = relationship('Repository', single_parent=True)
2721
2721
2722
2722
2723 class UserFollowing(Base, BaseModel):
2723 class UserFollowing(Base, BaseModel):
2724 __tablename__ = 'user_followings'
2724 __tablename__ = 'user_followings'
2725 __table_args__ = (
2725 __table_args__ = (
2726 UniqueConstraint('user_id', 'follows_repository_id'),
2726 UniqueConstraint('user_id', 'follows_repository_id'),
2727 UniqueConstraint('user_id', 'follows_user_id'),
2727 UniqueConstraint('user_id', 'follows_user_id'),
2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 )
2730 )
2731
2731
2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737
2737
2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739
2739
2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742
2742
2743 @classmethod
2743 @classmethod
2744 def get_repo_followers(cls, repo_id):
2744 def get_repo_followers(cls, repo_id):
2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2746
2746
2747
2747
2748 class CacheKey(Base, BaseModel):
2748 class CacheKey(Base, BaseModel):
2749 __tablename__ = 'cache_invalidation'
2749 __tablename__ = 'cache_invalidation'
2750 __table_args__ = (
2750 __table_args__ = (
2751 UniqueConstraint('cache_key'),
2751 UniqueConstraint('cache_key'),
2752 Index('key_idx', 'cache_key'),
2752 Index('key_idx', 'cache_key'),
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 )
2755 )
2756 CACHE_TYPE_ATOM = 'ATOM'
2756 CACHE_TYPE_ATOM = 'ATOM'
2757 CACHE_TYPE_RSS = 'RSS'
2757 CACHE_TYPE_RSS = 'RSS'
2758 CACHE_TYPE_README = 'README'
2758 CACHE_TYPE_README = 'README'
2759
2759
2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764
2764
2765 def __init__(self, cache_key, cache_args=''):
2765 def __init__(self, cache_key, cache_args=''):
2766 self.cache_key = cache_key
2766 self.cache_key = cache_key
2767 self.cache_args = cache_args
2767 self.cache_args = cache_args
2768 self.cache_active = False
2768 self.cache_active = False
2769
2769
2770 def __unicode__(self):
2770 def __unicode__(self):
2771 return u"<%s('%s:%s[%s]')>" % (
2771 return u"<%s('%s:%s[%s]')>" % (
2772 self.__class__.__name__,
2772 self.__class__.__name__,
2773 self.cache_id, self.cache_key, self.cache_active)
2773 self.cache_id, self.cache_key, self.cache_active)
2774
2774
2775 def _cache_key_partition(self):
2775 def _cache_key_partition(self):
2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 return prefix, repo_name, suffix
2777 return prefix, repo_name, suffix
2778
2778
2779 def get_prefix(self):
2779 def get_prefix(self):
2780 """
2780 """
2781 Try to extract prefix from existing cache key. The key could consist
2781 Try to extract prefix from existing cache key. The key could consist
2782 of prefix, repo_name, suffix
2782 of prefix, repo_name, suffix
2783 """
2783 """
2784 # this returns prefix, repo_name, suffix
2784 # this returns prefix, repo_name, suffix
2785 return self._cache_key_partition()[0]
2785 return self._cache_key_partition()[0]
2786
2786
2787 def get_suffix(self):
2787 def get_suffix(self):
2788 """
2788 """
2789 get suffix that might have been used in _get_cache_key to
2789 get suffix that might have been used in _get_cache_key to
2790 generate self.cache_key. Only used for informational purposes
2790 generate self.cache_key. Only used for informational purposes
2791 in repo_edit.html.
2791 in repo_edit.html.
2792 """
2792 """
2793 # prefix, repo_name, suffix
2793 # prefix, repo_name, suffix
2794 return self._cache_key_partition()[2]
2794 return self._cache_key_partition()[2]
2795
2795
2796 @classmethod
2796 @classmethod
2797 def delete_all_cache(cls):
2797 def delete_all_cache(cls):
2798 """
2798 """
2799 Delete all cache keys from database.
2799 Delete all cache keys from database.
2800 Should only be run when all instances are down and all entries
2800 Should only be run when all instances are down and all entries
2801 thus stale.
2801 thus stale.
2802 """
2802 """
2803 cls.query().delete()
2803 cls.query().delete()
2804 Session().commit()
2804 Session().commit()
2805
2805
2806 @classmethod
2806 @classmethod
2807 def get_cache_key(cls, repo_name, cache_type):
2807 def get_cache_key(cls, repo_name, cache_type):
2808 """
2808 """
2809
2809
2810 Generate a cache key for this process of RhodeCode instance.
2810 Generate a cache key for this process of RhodeCode instance.
2811 Prefix most likely will be process id or maybe explicitly set
2811 Prefix most likely will be process id or maybe explicitly set
2812 instance_id from .ini file.
2812 instance_id from .ini file.
2813 """
2813 """
2814 import rhodecode
2814 import rhodecode
2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816
2816
2817 repo_as_unicode = safe_unicode(repo_name)
2817 repo_as_unicode = safe_unicode(repo_name)
2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 if cache_type else repo_as_unicode
2819 if cache_type else repo_as_unicode
2820
2820
2821 return u'{}{}'.format(prefix, key)
2821 return u'{}{}'.format(prefix, key)
2822
2822
2823 @classmethod
2823 @classmethod
2824 def set_invalidate(cls, repo_name, delete=False):
2824 def set_invalidate(cls, repo_name, delete=False):
2825 """
2825 """
2826 Mark all caches of a repo as invalid in the database.
2826 Mark all caches of a repo as invalid in the database.
2827 """
2827 """
2828
2828
2829 try:
2829 try:
2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 if delete:
2831 if delete:
2832 log.debug('cache objects deleted for repo %s',
2832 log.debug('cache objects deleted for repo %s',
2833 safe_str(repo_name))
2833 safe_str(repo_name))
2834 qry.delete()
2834 qry.delete()
2835 else:
2835 else:
2836 log.debug('cache objects marked as invalid for repo %s',
2836 log.debug('cache objects marked as invalid for repo %s',
2837 safe_str(repo_name))
2837 safe_str(repo_name))
2838 qry.update({"cache_active": False})
2838 qry.update({"cache_active": False})
2839
2839
2840 Session().commit()
2840 Session().commit()
2841 except Exception:
2841 except Exception:
2842 log.exception(
2842 log.exception(
2843 'Cache key invalidation failed for repository %s',
2843 'Cache key invalidation failed for repository %s',
2844 safe_str(repo_name))
2844 safe_str(repo_name))
2845 Session().rollback()
2845 Session().rollback()
2846
2846
2847 @classmethod
2847 @classmethod
2848 def get_active_cache(cls, cache_key):
2848 def get_active_cache(cls, cache_key):
2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 if inv_obj:
2850 if inv_obj:
2851 return inv_obj
2851 return inv_obj
2852 return None
2852 return None
2853
2853
2854 @classmethod
2854 @classmethod
2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 thread_scoped=False):
2856 thread_scoped=False):
2857 """
2857 """
2858 @cache_region('long_term')
2858 @cache_region('long_term')
2859 def _heavy_calculation(cache_key):
2859 def _heavy_calculation(cache_key):
2860 return 'result'
2860 return 'result'
2861
2861
2862 cache_context = CacheKey.repo_context_cache(
2862 cache_context = CacheKey.repo_context_cache(
2863 _heavy_calculation, repo_name, cache_type)
2863 _heavy_calculation, repo_name, cache_type)
2864
2864
2865 with cache_context as context:
2865 with cache_context as context:
2866 context.invalidate()
2866 context.invalidate()
2867 computed = context.compute()
2867 computed = context.compute()
2868
2868
2869 assert computed == 'result'
2869 assert computed == 'result'
2870 """
2870 """
2871 from rhodecode.lib import caches
2871 from rhodecode.lib import caches
2872 return caches.InvalidationContext(
2872 return caches.InvalidationContext(
2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874
2874
2875
2875
2876 class ChangesetComment(Base, BaseModel):
2876 class ChangesetComment(Base, BaseModel):
2877 __tablename__ = 'changeset_comments'
2877 __tablename__ = 'changeset_comments'
2878 __table_args__ = (
2878 __table_args__ = (
2879 Index('cc_revision_idx', 'revision'),
2879 Index('cc_revision_idx', 'revision'),
2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 )
2882 )
2883
2883
2884 COMMENT_OUTDATED = u'comment_outdated'
2884 COMMENT_OUTDATED = u'comment_outdated'
2885
2885
2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 revision = Column('revision', String(40), nullable=True)
2888 revision = Column('revision', String(40), nullable=True)
2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 line_no = Column('line_no', Unicode(10), nullable=True)
2891 line_no = Column('line_no', Unicode(10), nullable=True)
2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 renderer = Column('renderer', Unicode(64), nullable=True)
2898 renderer = Column('renderer', Unicode(64), nullable=True)
2899 display_state = Column('display_state', Unicode(128), nullable=True)
2899 display_state = Column('display_state', Unicode(128), nullable=True)
2900
2900
2901 author = relationship('User', lazy='joined')
2901 author = relationship('User', lazy='joined')
2902 repo = relationship('Repository')
2902 repo = relationship('Repository')
2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 pull_request = relationship('PullRequest', lazy='joined')
2904 pull_request = relationship('PullRequest', lazy='joined')
2905 pull_request_version = relationship('PullRequestVersion')
2905 pull_request_version = relationship('PullRequestVersion')
2906
2906
2907 @classmethod
2907 @classmethod
2908 def get_users(cls, revision=None, pull_request_id=None):
2908 def get_users(cls, revision=None, pull_request_id=None):
2909 """
2909 """
2910 Returns user associated with this ChangesetComment. ie those
2910 Returns user associated with this ChangesetComment. ie those
2911 who actually commented
2911 who actually commented
2912
2912
2913 :param cls:
2913 :param cls:
2914 :param revision:
2914 :param revision:
2915 """
2915 """
2916 q = Session().query(User)\
2916 q = Session().query(User)\
2917 .join(ChangesetComment.author)
2917 .join(ChangesetComment.author)
2918 if revision:
2918 if revision:
2919 q = q.filter(cls.revision == revision)
2919 q = q.filter(cls.revision == revision)
2920 elif pull_request_id:
2920 elif pull_request_id:
2921 q = q.filter(cls.pull_request_id == pull_request_id)
2921 q = q.filter(cls.pull_request_id == pull_request_id)
2922 return q.all()
2922 return q.all()
2923
2923
2924 def render(self, mentions=False):
2924 def render(self, mentions=False):
2925 from rhodecode.lib import helpers as h
2925 from rhodecode.lib import helpers as h
2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927
2927
2928 def __repr__(self):
2928 def __repr__(self):
2929 if self.comment_id:
2929 if self.comment_id:
2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 else:
2931 else:
2932 return '<DB:ChangesetComment at %#x>' % id(self)
2932 return '<DB:ChangesetComment at %#x>' % id(self)
2933
2933
2934
2934
2935 class ChangesetStatus(Base, BaseModel):
2935 class ChangesetStatus(Base, BaseModel):
2936 __tablename__ = 'changeset_statuses'
2936 __tablename__ = 'changeset_statuses'
2937 __table_args__ = (
2937 __table_args__ = (
2938 Index('cs_revision_idx', 'revision'),
2938 Index('cs_revision_idx', 'revision'),
2939 Index('cs_version_idx', 'version'),
2939 Index('cs_version_idx', 'version'),
2940 UniqueConstraint('repo_id', 'revision', 'version'),
2940 UniqueConstraint('repo_id', 'revision', 'version'),
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 )
2943 )
2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 STATUS_APPROVED = 'approved'
2945 STATUS_APPROVED = 'approved'
2946 STATUS_REJECTED = 'rejected'
2946 STATUS_REJECTED = 'rejected'
2947 STATUS_UNDER_REVIEW = 'under_review'
2947 STATUS_UNDER_REVIEW = 'under_review'
2948
2948
2949 STATUSES = [
2949 STATUSES = [
2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 (STATUS_APPROVED, _("Approved")),
2951 (STATUS_APPROVED, _("Approved")),
2952 (STATUS_REJECTED, _("Rejected")),
2952 (STATUS_REJECTED, _("Rejected")),
2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 ]
2954 ]
2955
2955
2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 revision = Column('revision', String(40), nullable=False)
2959 revision = Column('revision', String(40), nullable=False)
2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 version = Column('version', Integer(), nullable=False, default=0)
2963 version = Column('version', Integer(), nullable=False, default=0)
2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965
2965
2966 author = relationship('User', lazy='joined')
2966 author = relationship('User', lazy='joined')
2967 repo = relationship('Repository')
2967 repo = relationship('Repository')
2968 comment = relationship('ChangesetComment', lazy='joined')
2968 comment = relationship('ChangesetComment', lazy='joined')
2969 pull_request = relationship('PullRequest', lazy='joined')
2969 pull_request = relationship('PullRequest', lazy='joined')
2970
2970
2971 def __unicode__(self):
2971 def __unicode__(self):
2972 return u"<%s('%s[%s]:%s')>" % (
2972 return u"<%s('%s[%s]:%s')>" % (
2973 self.__class__.__name__,
2973 self.__class__.__name__,
2974 self.status, self.version, self.author
2974 self.status, self.version, self.author
2975 )
2975 )
2976
2976
2977 @classmethod
2977 @classmethod
2978 def get_status_lbl(cls, value):
2978 def get_status_lbl(cls, value):
2979 return dict(cls.STATUSES).get(value)
2979 return dict(cls.STATUSES).get(value)
2980
2980
2981 @property
2981 @property
2982 def status_lbl(self):
2982 def status_lbl(self):
2983 return ChangesetStatus.get_status_lbl(self.status)
2983 return ChangesetStatus.get_status_lbl(self.status)
2984
2984
2985
2985
2986 class _PullRequestBase(BaseModel):
2986 class _PullRequestBase(BaseModel):
2987 """
2987 """
2988 Common attributes of pull request and version entries.
2988 Common attributes of pull request and version entries.
2989 """
2989 """
2990
2990
2991 # .status values
2991 # .status values
2992 STATUS_NEW = u'new'
2992 STATUS_NEW = u'new'
2993 STATUS_OPEN = u'open'
2993 STATUS_OPEN = u'open'
2994 STATUS_CLOSED = u'closed'
2994 STATUS_CLOSED = u'closed'
2995
2995
2996 title = Column('title', Unicode(255), nullable=True)
2996 title = Column('title', Unicode(255), nullable=True)
2997 description = Column(
2997 description = Column(
2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 nullable=True)
2999 nullable=True)
3000 # new/open/closed status of pull request (not approve/reject/etc)
3000 # new/open/closed status of pull request (not approve/reject/etc)
3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 created_on = Column(
3002 created_on = Column(
3003 'created_on', DateTime(timezone=False), nullable=False,
3003 'created_on', DateTime(timezone=False), nullable=False,
3004 default=datetime.datetime.now)
3004 default=datetime.datetime.now)
3005 updated_on = Column(
3005 updated_on = Column(
3006 'updated_on', DateTime(timezone=False), nullable=False,
3006 'updated_on', DateTime(timezone=False), nullable=False,
3007 default=datetime.datetime.now)
3007 default=datetime.datetime.now)
3008
3008
3009 @declared_attr
3009 @declared_attr
3010 def user_id(cls):
3010 def user_id(cls):
3011 return Column(
3011 return Column(
3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 unique=None)
3013 unique=None)
3014
3014
3015 # 500 revisions max
3015 # 500 revisions max
3016 _revisions = Column(
3016 _revisions = Column(
3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018
3018
3019 @declared_attr
3019 @declared_attr
3020 def source_repo_id(cls):
3020 def source_repo_id(cls):
3021 # TODO: dan: rename column to source_repo_id
3021 # TODO: dan: rename column to source_repo_id
3022 return Column(
3022 return Column(
3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 nullable=False)
3024 nullable=False)
3025
3025
3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027
3027
3028 @declared_attr
3028 @declared_attr
3029 def target_repo_id(cls):
3029 def target_repo_id(cls):
3030 # TODO: dan: rename column to target_repo_id
3030 # TODO: dan: rename column to target_repo_id
3031 return Column(
3031 return Column(
3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 nullable=False)
3033 nullable=False)
3034
3034
3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3036 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3037
3037
3038 # TODO: dan: rename column to last_merge_source_rev
3038 # TODO: dan: rename column to last_merge_source_rev
3039 _last_merge_source_rev = Column(
3039 _last_merge_source_rev = Column(
3040 'last_merge_org_rev', String(40), nullable=True)
3040 'last_merge_org_rev', String(40), nullable=True)
3041 # TODO: dan: rename column to last_merge_target_rev
3041 # TODO: dan: rename column to last_merge_target_rev
3042 _last_merge_target_rev = Column(
3042 _last_merge_target_rev = Column(
3043 'last_merge_other_rev', String(40), nullable=True)
3043 'last_merge_other_rev', String(40), nullable=True)
3044 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3044 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3045 merge_rev = Column('merge_rev', String(40), nullable=True)
3045 merge_rev = Column('merge_rev', String(40), nullable=True)
3046
3046
3047 @hybrid_property
3047 @hybrid_property
3048 def revisions(self):
3048 def revisions(self):
3049 return self._revisions.split(':') if self._revisions else []
3049 return self._revisions.split(':') if self._revisions else []
3050
3050
3051 @revisions.setter
3051 @revisions.setter
3052 def revisions(self, val):
3052 def revisions(self, val):
3053 self._revisions = ':'.join(val)
3053 self._revisions = ':'.join(val)
3054
3054
3055 @declared_attr
3055 @declared_attr
3056 def author(cls):
3056 def author(cls):
3057 return relationship('User', lazy='joined')
3057 return relationship('User', lazy='joined')
3058
3058
3059 @declared_attr
3059 @declared_attr
3060 def source_repo(cls):
3060 def source_repo(cls):
3061 return relationship(
3061 return relationship(
3062 'Repository',
3062 'Repository',
3063 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3063 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3064
3064
3065 @property
3065 @property
3066 def source_ref_parts(self):
3066 def source_ref_parts(self):
3067 return self.unicode_to_reference(self.source_ref)
3067 return self.unicode_to_reference(self.source_ref)
3068
3068
3069 @declared_attr
3069 @declared_attr
3070 def target_repo(cls):
3070 def target_repo(cls):
3071 return relationship(
3071 return relationship(
3072 'Repository',
3072 'Repository',
3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074
3074
3075 @property
3075 @property
3076 def target_ref_parts(self):
3076 def target_ref_parts(self):
3077 return self.unicode_to_reference(self.target_ref)
3077 return self.unicode_to_reference(self.target_ref)
3078
3078
3079 @property
3079 @property
3080 def shadow_merge_ref(self):
3080 def shadow_merge_ref(self):
3081 return self.unicode_to_reference(self._shadow_merge_ref)
3081 return self.unicode_to_reference(self._shadow_merge_ref)
3082
3082
3083 @shadow_merge_ref.setter
3083 @shadow_merge_ref.setter
3084 def shadow_merge_ref(self, ref):
3084 def shadow_merge_ref(self, ref):
3085 self._shadow_merge_ref = self.reference_to_unicode(ref)
3085 self._shadow_merge_ref = self.reference_to_unicode(ref)
3086
3086
3087 def unicode_to_reference(self, raw):
3087 def unicode_to_reference(self, raw):
3088 """
3088 """
3089 Convert a unicode (or string) to a reference object.
3089 Convert a unicode (or string) to a reference object.
3090 If unicode evaluates to False it returns None.
3090 If unicode evaluates to False it returns None.
3091 """
3091 """
3092 if raw:
3092 if raw:
3093 refs = raw.split(':')
3093 refs = raw.split(':')
3094 return Reference(*refs)
3094 return Reference(*refs)
3095 else:
3095 else:
3096 return None
3096 return None
3097
3097
3098 def reference_to_unicode(self, ref):
3098 def reference_to_unicode(self, ref):
3099 """
3099 """
3100 Convert a reference object to unicode.
3100 Convert a reference object to unicode.
3101 If reference is None it returns None.
3101 If reference is None it returns None.
3102 """
3102 """
3103 if ref:
3103 if ref:
3104 return u':'.join(ref)
3104 return u':'.join(ref)
3105 else:
3105 else:
3106 return None
3106 return None
3107
3107
3108
3108
3109 class PullRequest(Base, _PullRequestBase):
3109 class PullRequest(Base, _PullRequestBase):
3110 __tablename__ = 'pull_requests'
3110 __tablename__ = 'pull_requests'
3111 __table_args__ = (
3111 __table_args__ = (
3112 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3112 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3113 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3113 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3114 )
3114 )
3115
3115
3116 pull_request_id = Column(
3116 pull_request_id = Column(
3117 'pull_request_id', Integer(), nullable=False, primary_key=True)
3117 'pull_request_id', Integer(), nullable=False, primary_key=True)
3118
3118
3119 def __repr__(self):
3119 def __repr__(self):
3120 if self.pull_request_id:
3120 if self.pull_request_id:
3121 return '<DB:PullRequest #%s>' % self.pull_request_id
3121 return '<DB:PullRequest #%s>' % self.pull_request_id
3122 else:
3122 else:
3123 return '<DB:PullRequest at %#x>' % id(self)
3123 return '<DB:PullRequest at %#x>' % id(self)
3124
3124
3125 reviewers = relationship('PullRequestReviewers',
3125 reviewers = relationship('PullRequestReviewers',
3126 cascade="all, delete, delete-orphan")
3126 cascade="all, delete, delete-orphan")
3127 statuses = relationship('ChangesetStatus')
3127 statuses = relationship('ChangesetStatus')
3128 comments = relationship('ChangesetComment',
3128 comments = relationship('ChangesetComment',
3129 cascade="all, delete, delete-orphan")
3129 cascade="all, delete, delete-orphan")
3130 versions = relationship('PullRequestVersion',
3130 versions = relationship('PullRequestVersion',
3131 cascade="all, delete, delete-orphan")
3131 cascade="all, delete, delete-orphan")
3132
3132
3133 def is_closed(self):
3133 def is_closed(self):
3134 return self.status == self.STATUS_CLOSED
3134 return self.status == self.STATUS_CLOSED
3135
3135
3136 def get_api_data(self):
3136 def get_api_data(self):
3137 from rhodecode.model.pull_request import PullRequestModel
3137 from rhodecode.model.pull_request import PullRequestModel
3138 pull_request = self
3138 pull_request = self
3139 merge_status = PullRequestModel().merge_status(pull_request)
3139 merge_status = PullRequestModel().merge_status(pull_request)
3140
3140
3141 pull_request_url = url(
3141 pull_request_url = url(
3142 'pullrequest_show', repo_name=self.target_repo.repo_name,
3142 'pullrequest_show', repo_name=self.target_repo.repo_name,
3143 pull_request_id=self.pull_request_id, qualified=True)
3143 pull_request_id=self.pull_request_id, qualified=True)
3144
3144
3145 merge_data = {
3145 merge_data = {
3146 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3146 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3147 'reference': (
3147 'reference': (
3148 pull_request.shadow_merge_ref._asdict()
3148 pull_request.shadow_merge_ref._asdict()
3149 if pull_request.shadow_merge_ref else None),
3149 if pull_request.shadow_merge_ref else None),
3150 }
3150 }
3151
3151
3152 data = {
3152 data = {
3153 'pull_request_id': pull_request.pull_request_id,
3153 'pull_request_id': pull_request.pull_request_id,
3154 'url': pull_request_url,
3154 'url': pull_request_url,
3155 'title': pull_request.title,
3155 'title': pull_request.title,
3156 'description': pull_request.description,
3156 'description': pull_request.description,
3157 'status': pull_request.status,
3157 'status': pull_request.status,
3158 'created_on': pull_request.created_on,
3158 'created_on': pull_request.created_on,
3159 'updated_on': pull_request.updated_on,
3159 'updated_on': pull_request.updated_on,
3160 'commit_ids': pull_request.revisions,
3160 'commit_ids': pull_request.revisions,
3161 'review_status': pull_request.calculated_review_status(),
3161 'review_status': pull_request.calculated_review_status(),
3162 'mergeable': {
3162 'mergeable': {
3163 'status': merge_status[0],
3163 'status': merge_status[0],
3164 'message': unicode(merge_status[1]),
3164 'message': unicode(merge_status[1]),
3165 },
3165 },
3166 'source': {
3166 'source': {
3167 'clone_url': pull_request.source_repo.clone_url(),
3167 'clone_url': pull_request.source_repo.clone_url(),
3168 'repository': pull_request.source_repo.repo_name,
3168 'repository': pull_request.source_repo.repo_name,
3169 'reference': {
3169 'reference': {
3170 'name': pull_request.source_ref_parts.name,
3170 'name': pull_request.source_ref_parts.name,
3171 'type': pull_request.source_ref_parts.type,
3171 'type': pull_request.source_ref_parts.type,
3172 'commit_id': pull_request.source_ref_parts.commit_id,
3172 'commit_id': pull_request.source_ref_parts.commit_id,
3173 },
3173 },
3174 },
3174 },
3175 'target': {
3175 'target': {
3176 'clone_url': pull_request.target_repo.clone_url(),
3176 'clone_url': pull_request.target_repo.clone_url(),
3177 'repository': pull_request.target_repo.repo_name,
3177 'repository': pull_request.target_repo.repo_name,
3178 'reference': {
3178 'reference': {
3179 'name': pull_request.target_ref_parts.name,
3179 'name': pull_request.target_ref_parts.name,
3180 'type': pull_request.target_ref_parts.type,
3180 'type': pull_request.target_ref_parts.type,
3181 'commit_id': pull_request.target_ref_parts.commit_id,
3181 'commit_id': pull_request.target_ref_parts.commit_id,
3182 },
3182 },
3183 },
3183 },
3184 'merge': merge_data,
3184 'merge': merge_data,
3185 'author': pull_request.author.get_api_data(include_secrets=False,
3185 'author': pull_request.author.get_api_data(include_secrets=False,
3186 details='basic'),
3186 details='basic'),
3187 'reviewers': [
3187 'reviewers': [
3188 {
3188 {
3189 'user': reviewer.get_api_data(include_secrets=False,
3189 'user': reviewer.get_api_data(include_secrets=False,
3190 details='basic'),
3190 details='basic'),
3191 'reasons': reasons,
3191 'reasons': reasons,
3192 'review_status': st[0][1].status if st else 'not_reviewed',
3192 'review_status': st[0][1].status if st else 'not_reviewed',
3193 }
3193 }
3194 for reviewer, reasons, st in pull_request.reviewers_statuses()
3194 for reviewer, reasons, st in pull_request.reviewers_statuses()
3195 ]
3195 ]
3196 }
3196 }
3197
3197
3198 return data
3198 return data
3199
3199
3200 def __json__(self):
3200 def __json__(self):
3201 return {
3201 return {
3202 'revisions': self.revisions,
3202 'revisions': self.revisions,
3203 }
3203 }
3204
3204
3205 def calculated_review_status(self):
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 from rhodecode.model.changeset_status import ChangesetStatusModel
3206 from rhodecode.model.changeset_status import ChangesetStatusModel
3210 return ChangesetStatusModel().calculated_review_status(self)
3207 return ChangesetStatusModel().calculated_review_status(self)
3211
3208
3212 def reviewers_statuses(self):
3209 def reviewers_statuses(self):
3213 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3214 from rhodecode.model.changeset_status import ChangesetStatusModel
3210 from rhodecode.model.changeset_status import ChangesetStatusModel
3215 return ChangesetStatusModel().reviewers_statuses(self)
3211 return ChangesetStatusModel().reviewers_statuses(self)
3216
3212
3217
3213
3218 class PullRequestVersion(Base, _PullRequestBase):
3214 class PullRequestVersion(Base, _PullRequestBase):
3219 __tablename__ = 'pull_request_versions'
3215 __tablename__ = 'pull_request_versions'
3220 __table_args__ = (
3216 __table_args__ = (
3221 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3217 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3222 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3218 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3223 )
3219 )
3224
3220
3225 pull_request_version_id = Column(
3221 pull_request_version_id = Column(
3226 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3222 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3227 pull_request_id = Column(
3223 pull_request_id = Column(
3228 'pull_request_id', Integer(),
3224 'pull_request_id', Integer(),
3229 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3225 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3230 pull_request = relationship('PullRequest')
3226 pull_request = relationship('PullRequest')
3231
3227
3232 def __repr__(self):
3228 def __repr__(self):
3233 if self.pull_request_version_id:
3229 if self.pull_request_version_id:
3234 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3230 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3235 else:
3231 else:
3236 return '<DB:PullRequestVersion at %#x>' % id(self)
3232 return '<DB:PullRequestVersion at %#x>' % id(self)
3237
3233
3238
3234
3239 class PullRequestReviewers(Base, BaseModel):
3235 class PullRequestReviewers(Base, BaseModel):
3240 __tablename__ = 'pull_request_reviewers'
3236 __tablename__ = 'pull_request_reviewers'
3241 __table_args__ = (
3237 __table_args__ = (
3242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3238 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3239 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3244 )
3240 )
3245
3241
3246 def __init__(self, user=None, pull_request=None, reasons=None):
3242 def __init__(self, user=None, pull_request=None, reasons=None):
3247 self.user = user
3243 self.user = user
3248 self.pull_request = pull_request
3244 self.pull_request = pull_request
3249 self.reasons = reasons or []
3245 self.reasons = reasons or []
3250
3246
3251 @hybrid_property
3247 @hybrid_property
3252 def reasons(self):
3248 def reasons(self):
3253 if not self._reasons:
3249 if not self._reasons:
3254 return []
3250 return []
3255 return self._reasons
3251 return self._reasons
3256
3252
3257 @reasons.setter
3253 @reasons.setter
3258 def reasons(self, val):
3254 def reasons(self, val):
3259 val = val or []
3255 val = val or []
3260 if any(not isinstance(x, basestring) for x in val):
3256 if any(not isinstance(x, basestring) for x in val):
3261 raise Exception('invalid reasons type, must be list of strings')
3257 raise Exception('invalid reasons type, must be list of strings')
3262 self._reasons = val
3258 self._reasons = val
3263
3259
3264 pull_requests_reviewers_id = Column(
3260 pull_requests_reviewers_id = Column(
3265 'pull_requests_reviewers_id', Integer(), nullable=False,
3261 'pull_requests_reviewers_id', Integer(), nullable=False,
3266 primary_key=True)
3262 primary_key=True)
3267 pull_request_id = Column(
3263 pull_request_id = Column(
3268 "pull_request_id", Integer(),
3264 "pull_request_id", Integer(),
3269 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3265 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3270 user_id = Column(
3266 user_id = Column(
3271 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3267 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3272 _reasons = Column(
3268 _reasons = Column(
3273 'reason', MutationList.as_mutable(
3269 'reason', MutationList.as_mutable(
3274 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3270 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3275
3271
3276 user = relationship('User')
3272 user = relationship('User')
3277 pull_request = relationship('PullRequest')
3273 pull_request = relationship('PullRequest')
3278
3274
3279
3275
3280 class Notification(Base, BaseModel):
3276 class Notification(Base, BaseModel):
3281 __tablename__ = 'notifications'
3277 __tablename__ = 'notifications'
3282 __table_args__ = (
3278 __table_args__ = (
3283 Index('notification_type_idx', 'type'),
3279 Index('notification_type_idx', 'type'),
3284 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3280 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3285 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3281 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3286 )
3282 )
3287
3283
3288 TYPE_CHANGESET_COMMENT = u'cs_comment'
3284 TYPE_CHANGESET_COMMENT = u'cs_comment'
3289 TYPE_MESSAGE = u'message'
3285 TYPE_MESSAGE = u'message'
3290 TYPE_MENTION = u'mention'
3286 TYPE_MENTION = u'mention'
3291 TYPE_REGISTRATION = u'registration'
3287 TYPE_REGISTRATION = u'registration'
3292 TYPE_PULL_REQUEST = u'pull_request'
3288 TYPE_PULL_REQUEST = u'pull_request'
3293 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3289 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3294
3290
3295 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3291 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3296 subject = Column('subject', Unicode(512), nullable=True)
3292 subject = Column('subject', Unicode(512), nullable=True)
3297 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3293 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3298 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3294 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3299 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3295 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3300 type_ = Column('type', Unicode(255))
3296 type_ = Column('type', Unicode(255))
3301
3297
3302 created_by_user = relationship('User')
3298 created_by_user = relationship('User')
3303 notifications_to_users = relationship('UserNotification', lazy='joined',
3299 notifications_to_users = relationship('UserNotification', lazy='joined',
3304 cascade="all, delete, delete-orphan")
3300 cascade="all, delete, delete-orphan")
3305
3301
3306 @property
3302 @property
3307 def recipients(self):
3303 def recipients(self):
3308 return [x.user for x in UserNotification.query()\
3304 return [x.user for x in UserNotification.query()\
3309 .filter(UserNotification.notification == self)\
3305 .filter(UserNotification.notification == self)\
3310 .order_by(UserNotification.user_id.asc()).all()]
3306 .order_by(UserNotification.user_id.asc()).all()]
3311
3307
3312 @classmethod
3308 @classmethod
3313 def create(cls, created_by, subject, body, recipients, type_=None):
3309 def create(cls, created_by, subject, body, recipients, type_=None):
3314 if type_ is None:
3310 if type_ is None:
3315 type_ = Notification.TYPE_MESSAGE
3311 type_ = Notification.TYPE_MESSAGE
3316
3312
3317 notification = cls()
3313 notification = cls()
3318 notification.created_by_user = created_by
3314 notification.created_by_user = created_by
3319 notification.subject = subject
3315 notification.subject = subject
3320 notification.body = body
3316 notification.body = body
3321 notification.type_ = type_
3317 notification.type_ = type_
3322 notification.created_on = datetime.datetime.now()
3318 notification.created_on = datetime.datetime.now()
3323
3319
3324 for u in recipients:
3320 for u in recipients:
3325 assoc = UserNotification()
3321 assoc = UserNotification()
3326 assoc.notification = notification
3322 assoc.notification = notification
3327
3323
3328 # if created_by is inside recipients mark his notification
3324 # if created_by is inside recipients mark his notification
3329 # as read
3325 # as read
3330 if u.user_id == created_by.user_id:
3326 if u.user_id == created_by.user_id:
3331 assoc.read = True
3327 assoc.read = True
3332
3328
3333 u.notifications.append(assoc)
3329 u.notifications.append(assoc)
3334 Session().add(notification)
3330 Session().add(notification)
3335
3331
3336 return notification
3332 return notification
3337
3333
3338 @property
3334 @property
3339 def description(self):
3335 def description(self):
3340 from rhodecode.model.notification import NotificationModel
3336 from rhodecode.model.notification import NotificationModel
3341 return NotificationModel().make_description(self)
3337 return NotificationModel().make_description(self)
3342
3338
3343
3339
3344 class UserNotification(Base, BaseModel):
3340 class UserNotification(Base, BaseModel):
3345 __tablename__ = 'user_to_notification'
3341 __tablename__ = 'user_to_notification'
3346 __table_args__ = (
3342 __table_args__ = (
3347 UniqueConstraint('user_id', 'notification_id'),
3343 UniqueConstraint('user_id', 'notification_id'),
3348 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3349 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3350 )
3346 )
3351 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3347 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3352 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3348 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3353 read = Column('read', Boolean, default=False)
3349 read = Column('read', Boolean, default=False)
3354 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3350 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3355
3351
3356 user = relationship('User', lazy="joined")
3352 user = relationship('User', lazy="joined")
3357 notification = relationship('Notification', lazy="joined",
3353 notification = relationship('Notification', lazy="joined",
3358 order_by=lambda: Notification.created_on.desc(),)
3354 order_by=lambda: Notification.created_on.desc(),)
3359
3355
3360 def mark_as_read(self):
3356 def mark_as_read(self):
3361 self.read = True
3357 self.read = True
3362 Session().add(self)
3358 Session().add(self)
3363
3359
3364
3360
3365 class Gist(Base, BaseModel):
3361 class Gist(Base, BaseModel):
3366 __tablename__ = 'gists'
3362 __tablename__ = 'gists'
3367 __table_args__ = (
3363 __table_args__ = (
3368 Index('g_gist_access_id_idx', 'gist_access_id'),
3364 Index('g_gist_access_id_idx', 'gist_access_id'),
3369 Index('g_created_on_idx', 'created_on'),
3365 Index('g_created_on_idx', 'created_on'),
3370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3366 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3367 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3372 )
3368 )
3373 GIST_PUBLIC = u'public'
3369 GIST_PUBLIC = u'public'
3374 GIST_PRIVATE = u'private'
3370 GIST_PRIVATE = u'private'
3375 DEFAULT_FILENAME = u'gistfile1.txt'
3371 DEFAULT_FILENAME = u'gistfile1.txt'
3376
3372
3377 ACL_LEVEL_PUBLIC = u'acl_public'
3373 ACL_LEVEL_PUBLIC = u'acl_public'
3378 ACL_LEVEL_PRIVATE = u'acl_private'
3374 ACL_LEVEL_PRIVATE = u'acl_private'
3379
3375
3380 gist_id = Column('gist_id', Integer(), primary_key=True)
3376 gist_id = Column('gist_id', Integer(), primary_key=True)
3381 gist_access_id = Column('gist_access_id', Unicode(250))
3377 gist_access_id = Column('gist_access_id', Unicode(250))
3382 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3378 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3383 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3379 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3384 gist_expires = Column('gist_expires', Float(53), nullable=False)
3380 gist_expires = Column('gist_expires', Float(53), nullable=False)
3385 gist_type = Column('gist_type', Unicode(128), nullable=False)
3381 gist_type = Column('gist_type', Unicode(128), nullable=False)
3386 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3382 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3387 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3383 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3388 acl_level = Column('acl_level', Unicode(128), nullable=True)
3384 acl_level = Column('acl_level', Unicode(128), nullable=True)
3389
3385
3390 owner = relationship('User')
3386 owner = relationship('User')
3391
3387
3392 def __repr__(self):
3388 def __repr__(self):
3393 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3389 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3394
3390
3395 @classmethod
3391 @classmethod
3396 def get_or_404(cls, id_):
3392 def get_or_404(cls, id_):
3397 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3393 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3398 if not res:
3394 if not res:
3399 raise HTTPNotFound
3395 raise HTTPNotFound
3400 return res
3396 return res
3401
3397
3402 @classmethod
3398 @classmethod
3403 def get_by_access_id(cls, gist_access_id):
3399 def get_by_access_id(cls, gist_access_id):
3404 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3400 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3405
3401
3406 def gist_url(self):
3402 def gist_url(self):
3407 import rhodecode
3403 import rhodecode
3408 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3404 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3409 if alias_url:
3405 if alias_url:
3410 return alias_url.replace('{gistid}', self.gist_access_id)
3406 return alias_url.replace('{gistid}', self.gist_access_id)
3411
3407
3412 return url('gist', gist_id=self.gist_access_id, qualified=True)
3408 return url('gist', gist_id=self.gist_access_id, qualified=True)
3413
3409
3414 @classmethod
3410 @classmethod
3415 def base_path(cls):
3411 def base_path(cls):
3416 """
3412 """
3417 Returns base path when all gists are stored
3413 Returns base path when all gists are stored
3418
3414
3419 :param cls:
3415 :param cls:
3420 """
3416 """
3421 from rhodecode.model.gist import GIST_STORE_LOC
3417 from rhodecode.model.gist import GIST_STORE_LOC
3422 q = Session().query(RhodeCodeUi)\
3418 q = Session().query(RhodeCodeUi)\
3423 .filter(RhodeCodeUi.ui_key == URL_SEP)
3419 .filter(RhodeCodeUi.ui_key == URL_SEP)
3424 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3420 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3425 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3421 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3426
3422
3427 def get_api_data(self):
3423 def get_api_data(self):
3428 """
3424 """
3429 Common function for generating gist related data for API
3425 Common function for generating gist related data for API
3430 """
3426 """
3431 gist = self
3427 gist = self
3432 data = {
3428 data = {
3433 'gist_id': gist.gist_id,
3429 'gist_id': gist.gist_id,
3434 'type': gist.gist_type,
3430 'type': gist.gist_type,
3435 'access_id': gist.gist_access_id,
3431 'access_id': gist.gist_access_id,
3436 'description': gist.gist_description,
3432 'description': gist.gist_description,
3437 'url': gist.gist_url(),
3433 'url': gist.gist_url(),
3438 'expires': gist.gist_expires,
3434 'expires': gist.gist_expires,
3439 'created_on': gist.created_on,
3435 'created_on': gist.created_on,
3440 'modified_at': gist.modified_at,
3436 'modified_at': gist.modified_at,
3441 'content': None,
3437 'content': None,
3442 'acl_level': gist.acl_level,
3438 'acl_level': gist.acl_level,
3443 }
3439 }
3444 return data
3440 return data
3445
3441
3446 def __json__(self):
3442 def __json__(self):
3447 data = dict(
3443 data = dict(
3448 )
3444 )
3449 data.update(self.get_api_data())
3445 data.update(self.get_api_data())
3450 return data
3446 return data
3451 # SCM functions
3447 # SCM functions
3452
3448
3453 def scm_instance(self, **kwargs):
3449 def scm_instance(self, **kwargs):
3454 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3450 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3455 return get_vcs_instance(
3451 return get_vcs_instance(
3456 repo_path=safe_str(full_repo_path), create=False)
3452 repo_path=safe_str(full_repo_path), create=False)
3457
3453
3458
3454
3459 class DbMigrateVersion(Base, BaseModel):
3455 class DbMigrateVersion(Base, BaseModel):
3460 __tablename__ = 'db_migrate_version'
3456 __tablename__ = 'db_migrate_version'
3461 __table_args__ = (
3457 __table_args__ = (
3462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3458 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3463 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3459 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3464 )
3460 )
3465 repository_id = Column('repository_id', String(250), primary_key=True)
3461 repository_id = Column('repository_id', String(250), primary_key=True)
3466 repository_path = Column('repository_path', Text)
3462 repository_path = Column('repository_path', Text)
3467 version = Column('version', Integer)
3463 version = Column('version', Integer)
3468
3464
3469
3465
3470 class ExternalIdentity(Base, BaseModel):
3466 class ExternalIdentity(Base, BaseModel):
3471 __tablename__ = 'external_identities'
3467 __tablename__ = 'external_identities'
3472 __table_args__ = (
3468 __table_args__ = (
3473 Index('local_user_id_idx', 'local_user_id'),
3469 Index('local_user_id_idx', 'local_user_id'),
3474 Index('external_id_idx', 'external_id'),
3470 Index('external_id_idx', 'external_id'),
3475 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3471 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3476 'mysql_charset': 'utf8'})
3472 'mysql_charset': 'utf8'})
3477
3473
3478 external_id = Column('external_id', Unicode(255), default=u'',
3474 external_id = Column('external_id', Unicode(255), default=u'',
3479 primary_key=True)
3475 primary_key=True)
3480 external_username = Column('external_username', Unicode(1024), default=u'')
3476 external_username = Column('external_username', Unicode(1024), default=u'')
3481 local_user_id = Column('local_user_id', Integer(),
3477 local_user_id = Column('local_user_id', Integer(),
3482 ForeignKey('users.user_id'), primary_key=True)
3478 ForeignKey('users.user_id'), primary_key=True)
3483 provider_name = Column('provider_name', Unicode(255), default=u'',
3479 provider_name = Column('provider_name', Unicode(255), default=u'',
3484 primary_key=True)
3480 primary_key=True)
3485 access_token = Column('access_token', String(1024), default=u'')
3481 access_token = Column('access_token', String(1024), default=u'')
3486 alt_token = Column('alt_token', String(1024), default=u'')
3482 alt_token = Column('alt_token', String(1024), default=u'')
3487 token_secret = Column('token_secret', String(1024), default=u'')
3483 token_secret = Column('token_secret', String(1024), default=u'')
3488
3484
3489 @classmethod
3485 @classmethod
3490 def by_external_id_and_provider(cls, external_id, provider_name,
3486 def by_external_id_and_provider(cls, external_id, provider_name,
3491 local_user_id=None):
3487 local_user_id=None):
3492 """
3488 """
3493 Returns ExternalIdentity instance based on search params
3489 Returns ExternalIdentity instance based on search params
3494
3490
3495 :param external_id:
3491 :param external_id:
3496 :param provider_name:
3492 :param provider_name:
3497 :return: ExternalIdentity
3493 :return: ExternalIdentity
3498 """
3494 """
3499 query = cls.query()
3495 query = cls.query()
3500 query = query.filter(cls.external_id == external_id)
3496 query = query.filter(cls.external_id == external_id)
3501 query = query.filter(cls.provider_name == provider_name)
3497 query = query.filter(cls.provider_name == provider_name)
3502 if local_user_id:
3498 if local_user_id:
3503 query = query.filter(cls.local_user_id == local_user_id)
3499 query = query.filter(cls.local_user_id == local_user_id)
3504 return query.first()
3500 return query.first()
3505
3501
3506 @classmethod
3502 @classmethod
3507 def user_by_external_id_and_provider(cls, external_id, provider_name):
3503 def user_by_external_id_and_provider(cls, external_id, provider_name):
3508 """
3504 """
3509 Returns User instance based on search params
3505 Returns User instance based on search params
3510
3506
3511 :param external_id:
3507 :param external_id:
3512 :param provider_name:
3508 :param provider_name:
3513 :return: User
3509 :return: User
3514 """
3510 """
3515 query = User.query()
3511 query = User.query()
3516 query = query.filter(cls.external_id == external_id)
3512 query = query.filter(cls.external_id == external_id)
3517 query = query.filter(cls.provider_name == provider_name)
3513 query = query.filter(cls.provider_name == provider_name)
3518 query = query.filter(User.user_id == cls.local_user_id)
3514 query = query.filter(User.user_id == cls.local_user_id)
3519 return query.first()
3515 return query.first()
3520
3516
3521 @classmethod
3517 @classmethod
3522 def by_local_user_id(cls, local_user_id):
3518 def by_local_user_id(cls, local_user_id):
3523 """
3519 """
3524 Returns all tokens for user
3520 Returns all tokens for user
3525
3521
3526 :param local_user_id:
3522 :param local_user_id:
3527 :return: ExternalIdentity
3523 :return: ExternalIdentity
3528 """
3524 """
3529 query = cls.query()
3525 query = cls.query()
3530 query = query.filter(cls.local_user_id == local_user_id)
3526 query = query.filter(cls.local_user_id == local_user_id)
3531 return query
3527 return query
3532
3528
3533
3529
3534 class Integration(Base, BaseModel):
3530 class Integration(Base, BaseModel):
3535 __tablename__ = 'integrations'
3531 __tablename__ = 'integrations'
3536 __table_args__ = (
3532 __table_args__ = (
3537 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3533 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3538 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3534 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3539 )
3535 )
3540
3536
3541 integration_id = Column('integration_id', Integer(), primary_key=True)
3537 integration_id = Column('integration_id', Integer(), primary_key=True)
3542 integration_type = Column('integration_type', String(255))
3538 integration_type = Column('integration_type', String(255))
3543 enabled = Column('enabled', Boolean(), nullable=False)
3539 enabled = Column('enabled', Boolean(), nullable=False)
3544 name = Column('name', String(255), nullable=False)
3540 name = Column('name', String(255), nullable=False)
3545 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3541 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3546 default=False)
3542 default=False)
3547
3543
3548 settings = Column(
3544 settings = Column(
3549 'settings_json', MutationObj.as_mutable(
3545 'settings_json', MutationObj.as_mutable(
3550 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3546 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3551 repo_id = Column(
3547 repo_id = Column(
3552 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3548 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3553 nullable=True, unique=None, default=None)
3549 nullable=True, unique=None, default=None)
3554 repo = relationship('Repository', lazy='joined')
3550 repo = relationship('Repository', lazy='joined')
3555
3551
3556 repo_group_id = Column(
3552 repo_group_id = Column(
3557 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3553 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3558 nullable=True, unique=None, default=None)
3554 nullable=True, unique=None, default=None)
3559 repo_group = relationship('RepoGroup', lazy='joined')
3555 repo_group = relationship('RepoGroup', lazy='joined')
3560
3556
3561 @property
3557 @property
3562 def scope(self):
3558 def scope(self):
3563 if self.repo:
3559 if self.repo:
3564 return repr(self.repo)
3560 return repr(self.repo)
3565 if self.repo_group:
3561 if self.repo_group:
3566 if self.child_repos_only:
3562 if self.child_repos_only:
3567 return repr(self.repo_group) + ' (child repos only)'
3563 return repr(self.repo_group) + ' (child repos only)'
3568 else:
3564 else:
3569 return repr(self.repo_group) + ' (recursive)'
3565 return repr(self.repo_group) + ' (recursive)'
3570 if self.child_repos_only:
3566 if self.child_repos_only:
3571 return 'root_repos'
3567 return 'root_repos'
3572 return 'global'
3568 return 'global'
3573
3569
3574 def __repr__(self):
3570 def __repr__(self):
3575 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3571 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3576
3572
3577
3573
3578 class RepoReviewRuleUser(Base, BaseModel):
3574 class RepoReviewRuleUser(Base, BaseModel):
3579 __tablename__ = 'repo_review_rules_users'
3575 __tablename__ = 'repo_review_rules_users'
3580 __table_args__ = (
3576 __table_args__ = (
3581 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3582 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3578 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3583 )
3579 )
3584 repo_review_rule_user_id = Column(
3580 repo_review_rule_user_id = Column(
3585 'repo_review_rule_user_id', Integer(), primary_key=True)
3581 'repo_review_rule_user_id', Integer(), primary_key=True)
3586 repo_review_rule_id = Column("repo_review_rule_id",
3582 repo_review_rule_id = Column("repo_review_rule_id",
3587 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3583 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3588 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3584 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3589 nullable=False)
3585 nullable=False)
3590 user = relationship('User')
3586 user = relationship('User')
3591
3587
3592
3588
3593 class RepoReviewRuleUserGroup(Base, BaseModel):
3589 class RepoReviewRuleUserGroup(Base, BaseModel):
3594 __tablename__ = 'repo_review_rules_users_groups'
3590 __tablename__ = 'repo_review_rules_users_groups'
3595 __table_args__ = (
3591 __table_args__ = (
3596 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3592 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3597 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3593 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3598 )
3594 )
3599 repo_review_rule_users_group_id = Column(
3595 repo_review_rule_users_group_id = Column(
3600 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3596 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3601 repo_review_rule_id = Column("repo_review_rule_id",
3597 repo_review_rule_id = Column("repo_review_rule_id",
3602 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3598 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3603 users_group_id = Column("users_group_id", Integer(),
3599 users_group_id = Column("users_group_id", Integer(),
3604 ForeignKey('users_groups.users_group_id'), nullable=False)
3600 ForeignKey('users_groups.users_group_id'), nullable=False)
3605 users_group = relationship('UserGroup')
3601 users_group = relationship('UserGroup')
3606
3602
3607
3603
3608 class RepoReviewRule(Base, BaseModel):
3604 class RepoReviewRule(Base, BaseModel):
3609 __tablename__ = 'repo_review_rules'
3605 __tablename__ = 'repo_review_rules'
3610 __table_args__ = (
3606 __table_args__ = (
3611 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3607 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3612 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3608 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3613 )
3609 )
3614
3610
3615 repo_review_rule_id = Column(
3611 repo_review_rule_id = Column(
3616 'repo_review_rule_id', Integer(), primary_key=True)
3612 'repo_review_rule_id', Integer(), primary_key=True)
3617 repo_id = Column(
3613 repo_id = Column(
3618 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3614 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3619 repo = relationship('Repository', backref='review_rules')
3615 repo = relationship('Repository', backref='review_rules')
3620
3616
3621 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3617 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3622 default=u'*') # glob
3618 default=u'*') # glob
3623 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3619 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3624 default=u'*') # glob
3620 default=u'*') # glob
3625
3621
3626 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3622 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3627 nullable=False, default=False)
3623 nullable=False, default=False)
3628 rule_users = relationship('RepoReviewRuleUser')
3624 rule_users = relationship('RepoReviewRuleUser')
3629 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3625 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3630
3626
3631 @hybrid_property
3627 @hybrid_property
3632 def branch_pattern(self):
3628 def branch_pattern(self):
3633 return self._branch_pattern or '*'
3629 return self._branch_pattern or '*'
3634
3630
3635 def _validate_glob(self, value):
3631 def _validate_glob(self, value):
3636 re.compile('^' + glob2re(value) + '$')
3632 re.compile('^' + glob2re(value) + '$')
3637
3633
3638 @branch_pattern.setter
3634 @branch_pattern.setter
3639 def branch_pattern(self, value):
3635 def branch_pattern(self, value):
3640 self._validate_glob(value)
3636 self._validate_glob(value)
3641 self._branch_pattern = value or '*'
3637 self._branch_pattern = value or '*'
3642
3638
3643 @hybrid_property
3639 @hybrid_property
3644 def file_pattern(self):
3640 def file_pattern(self):
3645 return self._file_pattern or '*'
3641 return self._file_pattern or '*'
3646
3642
3647 @file_pattern.setter
3643 @file_pattern.setter
3648 def file_pattern(self, value):
3644 def file_pattern(self, value):
3649 self._validate_glob(value)
3645 self._validate_glob(value)
3650 self._file_pattern = value or '*'
3646 self._file_pattern = value or '*'
3651
3647
3652 def matches(self, branch, files_changed):
3648 def matches(self, branch, files_changed):
3653 """
3649 """
3654 Check if this review rule matches a branch/files in a pull request
3650 Check if this review rule matches a branch/files in a pull request
3655
3651
3656 :param branch: branch name for the commit
3652 :param branch: branch name for the commit
3657 :param files_changed: list of file paths changed in the pull request
3653 :param files_changed: list of file paths changed in the pull request
3658 """
3654 """
3659
3655
3660 branch = branch or ''
3656 branch = branch or ''
3661 files_changed = files_changed or []
3657 files_changed = files_changed or []
3662
3658
3663 branch_matches = True
3659 branch_matches = True
3664 if branch:
3660 if branch:
3665 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3661 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3666 branch_matches = bool(branch_regex.search(branch))
3662 branch_matches = bool(branch_regex.search(branch))
3667
3663
3668 files_matches = True
3664 files_matches = True
3669 if self.file_pattern != '*':
3665 if self.file_pattern != '*':
3670 files_matches = False
3666 files_matches = False
3671 file_regex = re.compile(glob2re(self.file_pattern))
3667 file_regex = re.compile(glob2re(self.file_pattern))
3672 for filename in files_changed:
3668 for filename in files_changed:
3673 if file_regex.search(filename):
3669 if file_regex.search(filename):
3674 files_matches = True
3670 files_matches = True
3675 break
3671 break
3676
3672
3677 return branch_matches and files_matches
3673 return branch_matches and files_matches
3678
3674
3679 @property
3675 @property
3680 def review_users(self):
3676 def review_users(self):
3681 """ Returns the users which this rule applies to """
3677 """ Returns the users which this rule applies to """
3682
3678
3683 users = set()
3679 users = set()
3684 users |= set([
3680 users |= set([
3685 rule_user.user for rule_user in self.rule_users
3681 rule_user.user for rule_user in self.rule_users
3686 if rule_user.user.active])
3682 if rule_user.user.active])
3687 users |= set(
3683 users |= set(
3688 member.user
3684 member.user
3689 for rule_user_group in self.rule_user_groups
3685 for rule_user_group in self.rule_user_groups
3690 for member in rule_user_group.users_group.members
3686 for member in rule_user_group.users_group.members
3691 if member.user.active
3687 if member.user.active
3692 )
3688 )
3693 return users
3689 return users
3694
3690
3695 def __repr__(self):
3691 def __repr__(self):
3696 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3692 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3697 self.repo_review_rule_id, self.repo)
3693 self.repo_review_rule_id, self.repo)
@@ -1,1250 +1,1309 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34
35
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.markup_renderer import (
39 from rhodecode.lib.markup_renderer import (
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
43 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, EmptyRepositoryError)
47 CommitDoesNotExistError, EmptyRepositoryError)
47 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.db import (
51 from rhodecode.model.db import (
51 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequestVersion, ChangesetComment)
53 PullRequestVersion, ChangesetComment)
53 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
54 from rhodecode.model.notification import NotificationModel, \
55 from rhodecode.model.notification import NotificationModel, \
55 EmailNotificationModel
56 EmailNotificationModel
56 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.settings import VcsSettingsModel
58 from rhodecode.model.settings import VcsSettingsModel
58
59
59
60
60 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
61
62
62
63
63 # Data structure to hold the response data when updating commits during a pull
64 # Data structure to hold the response data when updating commits during a pull
64 # request update.
65 # request update.
65 UpdateResponse = namedtuple(
66 UpdateResponse = namedtuple(
66 'UpdateResponse', 'executed, reason, new, old, changes')
67 'UpdateResponse', 'executed, reason, new, old, changes')
67
68
68
69
69 class PullRequestModel(BaseModel):
70 class PullRequestModel(BaseModel):
70
71
71 cls = PullRequest
72 cls = PullRequest
72
73
73 DIFF_CONTEXT = 3
74 DIFF_CONTEXT = 3
74
75
75 MERGE_STATUS_MESSAGES = {
76 MERGE_STATUS_MESSAGES = {
76 MergeFailureReason.NONE: lazy_ugettext(
77 MergeFailureReason.NONE: lazy_ugettext(
77 'This pull request can be automatically merged.'),
78 'This pull request can be automatically merged.'),
78 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 'This pull request cannot be merged because of an unhandled'
80 'This pull request cannot be merged because of an unhandled'
80 ' exception.'),
81 ' exception.'),
81 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 'This pull request cannot be merged because of conflicts.'),
83 'This pull request cannot be merged because of conflicts.'),
83 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 'This pull request could not be merged because push to target'
85 'This pull request could not be merged because push to target'
85 ' failed.'),
86 ' failed.'),
86 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 'This pull request cannot be merged because the target is not a'
88 'This pull request cannot be merged because the target is not a'
88 ' head.'),
89 ' head.'),
89 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 'This pull request cannot be merged because the source contains'
91 'This pull request cannot be merged because the source contains'
91 ' more branches than the target.'),
92 ' more branches than the target.'),
92 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 'This pull request cannot be merged because the target has'
94 'This pull request cannot be merged because the target has'
94 ' multiple heads.'),
95 ' multiple heads.'),
95 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 'This pull request cannot be merged because the target repository'
97 'This pull request cannot be merged because the target repository'
97 ' is locked.'),
98 ' is locked.'),
98 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 'This pull request cannot be merged because the target or the '
100 'This pull request cannot be merged because the target or the '
100 'source reference is missing.'),
101 'source reference is missing.'),
101 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 'This pull request cannot be merged because the target '
103 'This pull request cannot be merged because the target '
103 'reference is missing.'),
104 'reference is missing.'),
104 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 'This pull request cannot be merged because the source '
106 'This pull request cannot be merged because the source '
106 'reference is missing.'),
107 'reference is missing.'),
107 }
108 }
108
109
109 UPDATE_STATUS_MESSAGES = {
110 UPDATE_STATUS_MESSAGES = {
110 UpdateFailureReason.NONE: lazy_ugettext(
111 UpdateFailureReason.NONE: lazy_ugettext(
111 'Pull request update successful.'),
112 'Pull request update successful.'),
112 UpdateFailureReason.UNKNOWN: lazy_ugettext(
113 UpdateFailureReason.UNKNOWN: lazy_ugettext(
113 'Pull request update failed because of an unknown error.'),
114 'Pull request update failed because of an unknown error.'),
114 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
115 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
115 'No update needed because the source reference is already '
116 'No update needed because the source reference is already '
116 'up to date.'),
117 'up to date.'),
117 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
118 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
118 'Pull request cannot be updated because the reference type is '
119 'Pull request cannot be updated because the reference type is '
119 'not supported for an update.'),
120 'not supported for an update.'),
120 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
121 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
121 'This pull request cannot be updated because the target '
122 'This pull request cannot be updated because the target '
122 'reference is missing.'),
123 'reference is missing.'),
123 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
124 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
124 'This pull request cannot be updated because the source '
125 'This pull request cannot be updated because the source '
125 'reference is missing.'),
126 'reference is missing.'),
126 }
127 }
127
128
128 def __get_pull_request(self, pull_request):
129 def __get_pull_request(self, pull_request):
129 return self._get_instance(PullRequest, pull_request)
130 return self._get_instance(PullRequest, pull_request)
130
131
131 def _check_perms(self, perms, pull_request, user, api=False):
132 def _check_perms(self, perms, pull_request, user, api=False):
132 if not api:
133 if not api:
133 return h.HasRepoPermissionAny(*perms)(
134 return h.HasRepoPermissionAny(*perms)(
134 user=user, repo_name=pull_request.target_repo.repo_name)
135 user=user, repo_name=pull_request.target_repo.repo_name)
135 else:
136 else:
136 return h.HasRepoPermissionAnyApi(*perms)(
137 return h.HasRepoPermissionAnyApi(*perms)(
137 user=user, repo_name=pull_request.target_repo.repo_name)
138 user=user, repo_name=pull_request.target_repo.repo_name)
138
139
139 def check_user_read(self, pull_request, user, api=False):
140 def check_user_read(self, pull_request, user, api=False):
140 _perms = ('repository.admin', 'repository.write', 'repository.read',)
141 _perms = ('repository.admin', 'repository.write', 'repository.read',)
141 return self._check_perms(_perms, pull_request, user, api)
142 return self._check_perms(_perms, pull_request, user, api)
142
143
143 def check_user_merge(self, pull_request, user, api=False):
144 def check_user_merge(self, pull_request, user, api=False):
144 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
145 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
145 return self._check_perms(_perms, pull_request, user, api)
146 return self._check_perms(_perms, pull_request, user, api)
146
147
147 def check_user_update(self, pull_request, user, api=False):
148 def check_user_update(self, pull_request, user, api=False):
148 owner = user.user_id == pull_request.user_id
149 owner = user.user_id == pull_request.user_id
149 return self.check_user_merge(pull_request, user, api) or owner
150 return self.check_user_merge(pull_request, user, api) or owner
150
151
151 def check_user_change_status(self, pull_request, user, api=False):
152 def check_user_change_status(self, pull_request, user, api=False):
152 reviewer = user.user_id in [x.user_id for x in
153 reviewer = user.user_id in [x.user_id for x in
153 pull_request.reviewers]
154 pull_request.reviewers]
154 return self.check_user_update(pull_request, user, api) or reviewer
155 return self.check_user_update(pull_request, user, api) or reviewer
155
156
156 def get(self, pull_request):
157 def get(self, pull_request):
157 return self.__get_pull_request(pull_request)
158 return self.__get_pull_request(pull_request)
158
159
159 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
160 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
160 opened_by=None, order_by=None,
161 opened_by=None, order_by=None,
161 order_dir='desc'):
162 order_dir='desc'):
163 repo = None
164 if repo_name:
162 repo = self._get_repo(repo_name)
165 repo = self._get_repo(repo_name)
166
163 q = PullRequest.query()
167 q = PullRequest.query()
168
164 # source or target
169 # source or target
165 if source:
170 if repo and source:
166 q = q.filter(PullRequest.source_repo == repo)
171 q = q.filter(PullRequest.source_repo == repo)
167 else:
172 elif repo:
168 q = q.filter(PullRequest.target_repo == repo)
173 q = q.filter(PullRequest.target_repo == repo)
169
174
170 # closed,opened
175 # closed,opened
171 if statuses:
176 if statuses:
172 q = q.filter(PullRequest.status.in_(statuses))
177 q = q.filter(PullRequest.status.in_(statuses))
173
178
174 # opened by filter
179 # opened by filter
175 if opened_by:
180 if opened_by:
176 q = q.filter(PullRequest.user_id.in_(opened_by))
181 q = q.filter(PullRequest.user_id.in_(opened_by))
177
182
178 if order_by:
183 if order_by:
179 order_map = {
184 order_map = {
180 'name_raw': PullRequest.pull_request_id,
185 'name_raw': PullRequest.pull_request_id,
181 'title': PullRequest.title,
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 if order_dir == 'asc':
190 if order_dir == 'asc':
185 q = q.order_by(order_map[order_by].asc())
191 q = q.order_by(order_map[order_by].asc())
186 else:
192 else:
187 q = q.order_by(order_map[order_by].desc())
193 q = q.order_by(order_map[order_by].desc())
188
194
189 return q
195 return q
190
196
191 def count_all(self, repo_name, source=False, statuses=None,
197 def count_all(self, repo_name, source=False, statuses=None,
192 opened_by=None):
198 opened_by=None):
193 """
199 """
194 Count the number of pull requests for a specific repository.
200 Count the number of pull requests for a specific repository.
195
201
196 :param repo_name: target or source repo
202 :param repo_name: target or source repo
197 :param source: boolean flag to specify if repo_name refers to source
203 :param source: boolean flag to specify if repo_name refers to source
198 :param statuses: list of pull request statuses
204 :param statuses: list of pull request statuses
199 :param opened_by: author user of the pull request
205 :param opened_by: author user of the pull request
200 :returns: int number of pull requests
206 :returns: int number of pull requests
201 """
207 """
202 q = self._prepare_get_all_query(
208 q = self._prepare_get_all_query(
203 repo_name, source=source, statuses=statuses, opened_by=opened_by)
209 repo_name, source=source, statuses=statuses, opened_by=opened_by)
204
210
205 return q.count()
211 return q.count()
206
212
207 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
213 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
208 offset=0, length=None, order_by=None, order_dir='desc'):
214 offset=0, length=None, order_by=None, order_dir='desc'):
209 """
215 """
210 Get all pull requests for a specific repository.
216 Get all pull requests for a specific repository.
211
217
212 :param repo_name: target or source repo
218 :param repo_name: target or source repo
213 :param source: boolean flag to specify if repo_name refers to source
219 :param source: boolean flag to specify if repo_name refers to source
214 :param statuses: list of pull request statuses
220 :param statuses: list of pull request statuses
215 :param opened_by: author user of the pull request
221 :param opened_by: author user of the pull request
216 :param offset: pagination offset
222 :param offset: pagination offset
217 :param length: length of returned list
223 :param length: length of returned list
218 :param order_by: order of the returned list
224 :param order_by: order of the returned list
219 :param order_dir: 'asc' or 'desc' ordering direction
225 :param order_dir: 'asc' or 'desc' ordering direction
220 :returns: list of pull requests
226 :returns: list of pull requests
221 """
227 """
222 q = self._prepare_get_all_query(
228 q = self._prepare_get_all_query(
223 repo_name, source=source, statuses=statuses, opened_by=opened_by,
229 repo_name, source=source, statuses=statuses, opened_by=opened_by,
224 order_by=order_by, order_dir=order_dir)
230 order_by=order_by, order_dir=order_dir)
225
231
226 if length:
232 if length:
227 pull_requests = q.limit(length).offset(offset).all()
233 pull_requests = q.limit(length).offset(offset).all()
228 else:
234 else:
229 pull_requests = q.all()
235 pull_requests = q.all()
230
236
231 return pull_requests
237 return pull_requests
232
238
233 def count_awaiting_review(self, repo_name, source=False, statuses=None,
239 def count_awaiting_review(self, repo_name, source=False, statuses=None,
234 opened_by=None):
240 opened_by=None):
235 """
241 """
236 Count the number of pull requests for a specific repository that are
242 Count the number of pull requests for a specific repository that are
237 awaiting review.
243 awaiting review.
238
244
239 :param repo_name: target or source repo
245 :param repo_name: target or source repo
240 :param source: boolean flag to specify if repo_name refers to source
246 :param source: boolean flag to specify if repo_name refers to source
241 :param statuses: list of pull request statuses
247 :param statuses: list of pull request statuses
242 :param opened_by: author user of the pull request
248 :param opened_by: author user of the pull request
243 :returns: int number of pull requests
249 :returns: int number of pull requests
244 """
250 """
245 pull_requests = self.get_awaiting_review(
251 pull_requests = self.get_awaiting_review(
246 repo_name, source=source, statuses=statuses, opened_by=opened_by)
252 repo_name, source=source, statuses=statuses, opened_by=opened_by)
247
253
248 return len(pull_requests)
254 return len(pull_requests)
249
255
250 def get_awaiting_review(self, repo_name, source=False, statuses=None,
256 def get_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None, offset=0, length=None,
257 opened_by=None, offset=0, length=None,
252 order_by=None, order_dir='desc'):
258 order_by=None, order_dir='desc'):
253 """
259 """
254 Get all pull requests for a specific repository that are awaiting
260 Get all pull requests for a specific repository that are awaiting
255 review.
261 review.
256
262
257 :param repo_name: target or source repo
263 :param repo_name: target or source repo
258 :param source: boolean flag to specify if repo_name refers to source
264 :param source: boolean flag to specify if repo_name refers to source
259 :param statuses: list of pull request statuses
265 :param statuses: list of pull request statuses
260 :param opened_by: author user of the pull request
266 :param opened_by: author user of the pull request
261 :param offset: pagination offset
267 :param offset: pagination offset
262 :param length: length of returned list
268 :param length: length of returned list
263 :param order_by: order of the returned list
269 :param order_by: order of the returned list
264 :param order_dir: 'asc' or 'desc' ordering direction
270 :param order_dir: 'asc' or 'desc' ordering direction
265 :returns: list of pull requests
271 :returns: list of pull requests
266 """
272 """
267 pull_requests = self.get_all(
273 pull_requests = self.get_all(
268 repo_name, source=source, statuses=statuses, opened_by=opened_by,
274 repo_name, source=source, statuses=statuses, opened_by=opened_by,
269 order_by=order_by, order_dir=order_dir)
275 order_by=order_by, order_dir=order_dir)
270
276
271 _filtered_pull_requests = []
277 _filtered_pull_requests = []
272 for pr in pull_requests:
278 for pr in pull_requests:
273 status = pr.calculated_review_status()
279 status = pr.calculated_review_status()
274 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
280 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
275 ChangesetStatus.STATUS_UNDER_REVIEW]:
281 ChangesetStatus.STATUS_UNDER_REVIEW]:
276 _filtered_pull_requests.append(pr)
282 _filtered_pull_requests.append(pr)
277 if length:
283 if length:
278 return _filtered_pull_requests[offset:offset+length]
284 return _filtered_pull_requests[offset:offset+length]
279 else:
285 else:
280 return _filtered_pull_requests
286 return _filtered_pull_requests
281
287
282 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
288 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
283 opened_by=None, user_id=None):
289 opened_by=None, user_id=None):
284 """
290 """
285 Count the number of pull requests for a specific repository that are
291 Count the number of pull requests for a specific repository that are
286 awaiting review from a specific user.
292 awaiting review from a specific user.
287
293
288 :param repo_name: target or source repo
294 :param repo_name: target or source repo
289 :param source: boolean flag to specify if repo_name refers to source
295 :param source: boolean flag to specify if repo_name refers to source
290 :param statuses: list of pull request statuses
296 :param statuses: list of pull request statuses
291 :param opened_by: author user of the pull request
297 :param opened_by: author user of the pull request
292 :param user_id: reviewer user of the pull request
298 :param user_id: reviewer user of the pull request
293 :returns: int number of pull requests
299 :returns: int number of pull requests
294 """
300 """
295 pull_requests = self.get_awaiting_my_review(
301 pull_requests = self.get_awaiting_my_review(
296 repo_name, source=source, statuses=statuses, opened_by=opened_by,
302 repo_name, source=source, statuses=statuses, opened_by=opened_by,
297 user_id=user_id)
303 user_id=user_id)
298
304
299 return len(pull_requests)
305 return len(pull_requests)
300
306
301 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
307 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 opened_by=None, user_id=None, offset=0,
308 opened_by=None, user_id=None, offset=0,
303 length=None, order_by=None, order_dir='desc'):
309 length=None, order_by=None, order_dir='desc'):
304 """
310 """
305 Get all pull requests for a specific repository that are awaiting
311 Get all pull requests for a specific repository that are awaiting
306 review from a specific user.
312 review from a specific user.
307
313
308 :param repo_name: target or source repo
314 :param repo_name: target or source repo
309 :param source: boolean flag to specify if repo_name refers to source
315 :param source: boolean flag to specify if repo_name refers to source
310 :param statuses: list of pull request statuses
316 :param statuses: list of pull request statuses
311 :param opened_by: author user of the pull request
317 :param opened_by: author user of the pull request
312 :param user_id: reviewer user of the pull request
318 :param user_id: reviewer user of the pull request
313 :param offset: pagination offset
319 :param offset: pagination offset
314 :param length: length of returned list
320 :param length: length of returned list
315 :param order_by: order of the returned list
321 :param order_by: order of the returned list
316 :param order_dir: 'asc' or 'desc' ordering direction
322 :param order_dir: 'asc' or 'desc' ordering direction
317 :returns: list of pull requests
323 :returns: list of pull requests
318 """
324 """
319 pull_requests = self.get_all(
325 pull_requests = self.get_all(
320 repo_name, source=source, statuses=statuses, opened_by=opened_by,
326 repo_name, source=source, statuses=statuses, opened_by=opened_by,
321 order_by=order_by, order_dir=order_dir)
327 order_by=order_by, order_dir=order_dir)
322
328
323 _my = PullRequestModel().get_not_reviewed(user_id)
329 _my = PullRequestModel().get_not_reviewed(user_id)
324 my_participation = []
330 my_participation = []
325 for pr in pull_requests:
331 for pr in pull_requests:
326 if pr in _my:
332 if pr in _my:
327 my_participation.append(pr)
333 my_participation.append(pr)
328 _filtered_pull_requests = my_participation
334 _filtered_pull_requests = my_participation
329 if length:
335 if length:
330 return _filtered_pull_requests[offset:offset+length]
336 return _filtered_pull_requests[offset:offset+length]
331 else:
337 else:
332 return _filtered_pull_requests
338 return _filtered_pull_requests
333
339
334 def get_not_reviewed(self, user_id):
340 def get_not_reviewed(self, user_id):
335 return [
341 return [
336 x.pull_request for x in PullRequestReviewers.query().filter(
342 x.pull_request for x in PullRequestReviewers.query().filter(
337 PullRequestReviewers.user_id == user_id).all()
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 def get_versions(self, pull_request):
399 def get_versions(self, pull_request):
341 """
400 """
342 returns version of pull request sorted by ID descending
401 returns version of pull request sorted by ID descending
343 """
402 """
344 return PullRequestVersion.query()\
403 return PullRequestVersion.query()\
345 .filter(PullRequestVersion.pull_request == pull_request)\
404 .filter(PullRequestVersion.pull_request == pull_request)\
346 .order_by(PullRequestVersion.pull_request_version_id.asc())\
405 .order_by(PullRequestVersion.pull_request_version_id.asc())\
347 .all()
406 .all()
348
407
349 def create(self, created_by, source_repo, source_ref, target_repo,
408 def create(self, created_by, source_repo, source_ref, target_repo,
350 target_ref, revisions, reviewers, title, description=None):
409 target_ref, revisions, reviewers, title, description=None):
351 created_by_user = self._get_user(created_by)
410 created_by_user = self._get_user(created_by)
352 source_repo = self._get_repo(source_repo)
411 source_repo = self._get_repo(source_repo)
353 target_repo = self._get_repo(target_repo)
412 target_repo = self._get_repo(target_repo)
354
413
355 pull_request = PullRequest()
414 pull_request = PullRequest()
356 pull_request.source_repo = source_repo
415 pull_request.source_repo = source_repo
357 pull_request.source_ref = source_ref
416 pull_request.source_ref = source_ref
358 pull_request.target_repo = target_repo
417 pull_request.target_repo = target_repo
359 pull_request.target_ref = target_ref
418 pull_request.target_ref = target_ref
360 pull_request.revisions = revisions
419 pull_request.revisions = revisions
361 pull_request.title = title
420 pull_request.title = title
362 pull_request.description = description
421 pull_request.description = description
363 pull_request.author = created_by_user
422 pull_request.author = created_by_user
364
423
365 Session().add(pull_request)
424 Session().add(pull_request)
366 Session().flush()
425 Session().flush()
367
426
368 reviewer_ids = set()
427 reviewer_ids = set()
369 # members / reviewers
428 # members / reviewers
370 for reviewer_object in reviewers:
429 for reviewer_object in reviewers:
371 if isinstance(reviewer_object, tuple):
430 if isinstance(reviewer_object, tuple):
372 user_id, reasons = reviewer_object
431 user_id, reasons = reviewer_object
373 else:
432 else:
374 user_id, reasons = reviewer_object, []
433 user_id, reasons = reviewer_object, []
375
434
376 user = self._get_user(user_id)
435 user = self._get_user(user_id)
377 reviewer_ids.add(user.user_id)
436 reviewer_ids.add(user.user_id)
378
437
379 reviewer = PullRequestReviewers(user, pull_request, reasons)
438 reviewer = PullRequestReviewers(user, pull_request, reasons)
380 Session().add(reviewer)
439 Session().add(reviewer)
381
440
382 # Set approval status to "Under Review" for all commits which are
441 # Set approval status to "Under Review" for all commits which are
383 # part of this pull request.
442 # part of this pull request.
384 ChangesetStatusModel().set_status(
443 ChangesetStatusModel().set_status(
385 repo=target_repo,
444 repo=target_repo,
386 status=ChangesetStatus.STATUS_UNDER_REVIEW,
445 status=ChangesetStatus.STATUS_UNDER_REVIEW,
387 user=created_by_user,
446 user=created_by_user,
388 pull_request=pull_request
447 pull_request=pull_request
389 )
448 )
390
449
391 self.notify_reviewers(pull_request, reviewer_ids)
450 self.notify_reviewers(pull_request, reviewer_ids)
392 self._trigger_pull_request_hook(
451 self._trigger_pull_request_hook(
393 pull_request, created_by_user, 'create')
452 pull_request, created_by_user, 'create')
394
453
395 return pull_request
454 return pull_request
396
455
397 def _trigger_pull_request_hook(self, pull_request, user, action):
456 def _trigger_pull_request_hook(self, pull_request, user, action):
398 pull_request = self.__get_pull_request(pull_request)
457 pull_request = self.__get_pull_request(pull_request)
399 target_scm = pull_request.target_repo.scm_instance()
458 target_scm = pull_request.target_repo.scm_instance()
400 if action == 'create':
459 if action == 'create':
401 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
460 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
402 elif action == 'merge':
461 elif action == 'merge':
403 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
462 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
404 elif action == 'close':
463 elif action == 'close':
405 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
464 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
406 elif action == 'review_status_change':
465 elif action == 'review_status_change':
407 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
466 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
408 elif action == 'update':
467 elif action == 'update':
409 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
468 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
410 else:
469 else:
411 return
470 return
412
471
413 trigger_hook(
472 trigger_hook(
414 username=user.username,
473 username=user.username,
415 repo_name=pull_request.target_repo.repo_name,
474 repo_name=pull_request.target_repo.repo_name,
416 repo_alias=target_scm.alias,
475 repo_alias=target_scm.alias,
417 pull_request=pull_request)
476 pull_request=pull_request)
418
477
419 def _get_commit_ids(self, pull_request):
478 def _get_commit_ids(self, pull_request):
420 """
479 """
421 Return the commit ids of the merged pull request.
480 Return the commit ids of the merged pull request.
422
481
423 This method is not dealing correctly yet with the lack of autoupdates
482 This method is not dealing correctly yet with the lack of autoupdates
424 nor with the implicit target updates.
483 nor with the implicit target updates.
425 For example: if a commit in the source repo is already in the target it
484 For example: if a commit in the source repo is already in the target it
426 will be reported anyways.
485 will be reported anyways.
427 """
486 """
428 merge_rev = pull_request.merge_rev
487 merge_rev = pull_request.merge_rev
429 if merge_rev is None:
488 if merge_rev is None:
430 raise ValueError('This pull request was not merged yet')
489 raise ValueError('This pull request was not merged yet')
431
490
432 commit_ids = list(pull_request.revisions)
491 commit_ids = list(pull_request.revisions)
433 if merge_rev not in commit_ids:
492 if merge_rev not in commit_ids:
434 commit_ids.append(merge_rev)
493 commit_ids.append(merge_rev)
435
494
436 return commit_ids
495 return commit_ids
437
496
438 def merge(self, pull_request, user, extras):
497 def merge(self, pull_request, user, extras):
439 log.debug("Merging pull request %s", pull_request.pull_request_id)
498 log.debug("Merging pull request %s", pull_request.pull_request_id)
440 merge_state = self._merge_pull_request(pull_request, user, extras)
499 merge_state = self._merge_pull_request(pull_request, user, extras)
441 if merge_state.executed:
500 if merge_state.executed:
442 log.debug(
501 log.debug(
443 "Merge was successful, updating the pull request comments.")
502 "Merge was successful, updating the pull request comments.")
444 self._comment_and_close_pr(pull_request, user, merge_state)
503 self._comment_and_close_pr(pull_request, user, merge_state)
445 self._log_action('user_merged_pull_request', user, pull_request)
504 self._log_action('user_merged_pull_request', user, pull_request)
446 else:
505 else:
447 log.warn("Merge failed, not updating the pull request.")
506 log.warn("Merge failed, not updating the pull request.")
448 return merge_state
507 return merge_state
449
508
450 def _merge_pull_request(self, pull_request, user, extras):
509 def _merge_pull_request(self, pull_request, user, extras):
451 target_vcs = pull_request.target_repo.scm_instance()
510 target_vcs = pull_request.target_repo.scm_instance()
452 source_vcs = pull_request.source_repo.scm_instance()
511 source_vcs = pull_request.source_repo.scm_instance()
453 target_ref = self._refresh_reference(
512 target_ref = self._refresh_reference(
454 pull_request.target_ref_parts, target_vcs)
513 pull_request.target_ref_parts, target_vcs)
455
514
456 message = _(
515 message = _(
457 'Merge pull request #%(pr_id)s from '
516 'Merge pull request #%(pr_id)s from '
458 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
517 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
459 'pr_id': pull_request.pull_request_id,
518 'pr_id': pull_request.pull_request_id,
460 'source_repo': source_vcs.name,
519 'source_repo': source_vcs.name,
461 'source_ref_name': pull_request.source_ref_parts.name,
520 'source_ref_name': pull_request.source_ref_parts.name,
462 'pr_title': pull_request.title
521 'pr_title': pull_request.title
463 }
522 }
464
523
465 workspace_id = self._workspace_id(pull_request)
524 workspace_id = self._workspace_id(pull_request)
466 use_rebase = self._use_rebase_for_merging(pull_request)
525 use_rebase = self._use_rebase_for_merging(pull_request)
467
526
468 callback_daemon, extras = prepare_callback_daemon(
527 callback_daemon, extras = prepare_callback_daemon(
469 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
528 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
470 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
529 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
471
530
472 with callback_daemon:
531 with callback_daemon:
473 # TODO: johbo: Implement a clean way to run a config_override
532 # TODO: johbo: Implement a clean way to run a config_override
474 # for a single call.
533 # for a single call.
475 target_vcs.config.set(
534 target_vcs.config.set(
476 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
535 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
477 merge_state = target_vcs.merge(
536 merge_state = target_vcs.merge(
478 target_ref, source_vcs, pull_request.source_ref_parts,
537 target_ref, source_vcs, pull_request.source_ref_parts,
479 workspace_id, user_name=user.username,
538 workspace_id, user_name=user.username,
480 user_email=user.email, message=message, use_rebase=use_rebase)
539 user_email=user.email, message=message, use_rebase=use_rebase)
481 return merge_state
540 return merge_state
482
541
483 def _comment_and_close_pr(self, pull_request, user, merge_state):
542 def _comment_and_close_pr(self, pull_request, user, merge_state):
484 pull_request.merge_rev = merge_state.merge_ref.commit_id
543 pull_request.merge_rev = merge_state.merge_ref.commit_id
485 pull_request.updated_on = datetime.datetime.now()
544 pull_request.updated_on = datetime.datetime.now()
486
545
487 ChangesetCommentsModel().create(
546 ChangesetCommentsModel().create(
488 text=unicode(_('Pull request merged and closed')),
547 text=unicode(_('Pull request merged and closed')),
489 repo=pull_request.target_repo.repo_id,
548 repo=pull_request.target_repo.repo_id,
490 user=user.user_id,
549 user=user.user_id,
491 pull_request=pull_request.pull_request_id,
550 pull_request=pull_request.pull_request_id,
492 f_path=None,
551 f_path=None,
493 line_no=None,
552 line_no=None,
494 closing_pr=True
553 closing_pr=True
495 )
554 )
496
555
497 Session().add(pull_request)
556 Session().add(pull_request)
498 Session().flush()
557 Session().flush()
499 # TODO: paris: replace invalidation with less radical solution
558 # TODO: paris: replace invalidation with less radical solution
500 ScmModel().mark_for_invalidation(
559 ScmModel().mark_for_invalidation(
501 pull_request.target_repo.repo_name)
560 pull_request.target_repo.repo_name)
502 self._trigger_pull_request_hook(pull_request, user, 'merge')
561 self._trigger_pull_request_hook(pull_request, user, 'merge')
503
562
504 def has_valid_update_type(self, pull_request):
563 def has_valid_update_type(self, pull_request):
505 source_ref_type = pull_request.source_ref_parts.type
564 source_ref_type = pull_request.source_ref_parts.type
506 return source_ref_type in ['book', 'branch', 'tag']
565 return source_ref_type in ['book', 'branch', 'tag']
507
566
508 def update_commits(self, pull_request):
567 def update_commits(self, pull_request):
509 """
568 """
510 Get the updated list of commits for the pull request
569 Get the updated list of commits for the pull request
511 and return the new pull request version and the list
570 and return the new pull request version and the list
512 of commits processed by this update action
571 of commits processed by this update action
513 """
572 """
514 pull_request = self.__get_pull_request(pull_request)
573 pull_request = self.__get_pull_request(pull_request)
515 source_ref_type = pull_request.source_ref_parts.type
574 source_ref_type = pull_request.source_ref_parts.type
516 source_ref_name = pull_request.source_ref_parts.name
575 source_ref_name = pull_request.source_ref_parts.name
517 source_ref_id = pull_request.source_ref_parts.commit_id
576 source_ref_id = pull_request.source_ref_parts.commit_id
518
577
519 if not self.has_valid_update_type(pull_request):
578 if not self.has_valid_update_type(pull_request):
520 log.debug(
579 log.debug(
521 "Skipping update of pull request %s due to ref type: %s",
580 "Skipping update of pull request %s due to ref type: %s",
522 pull_request, source_ref_type)
581 pull_request, source_ref_type)
523 return UpdateResponse(
582 return UpdateResponse(
524 executed=False,
583 executed=False,
525 reason=UpdateFailureReason.WRONG_REF_TPYE,
584 reason=UpdateFailureReason.WRONG_REF_TPYE,
526 old=pull_request, new=None, changes=None)
585 old=pull_request, new=None, changes=None)
527
586
528 source_repo = pull_request.source_repo.scm_instance()
587 source_repo = pull_request.source_repo.scm_instance()
529 try:
588 try:
530 source_commit = source_repo.get_commit(commit_id=source_ref_name)
589 source_commit = source_repo.get_commit(commit_id=source_ref_name)
531 except CommitDoesNotExistError:
590 except CommitDoesNotExistError:
532 return UpdateResponse(
591 return UpdateResponse(
533 executed=False,
592 executed=False,
534 reason=UpdateFailureReason.MISSING_SOURCE_REF,
593 reason=UpdateFailureReason.MISSING_SOURCE_REF,
535 old=pull_request, new=None, changes=None)
594 old=pull_request, new=None, changes=None)
536
595
537 if source_ref_id == source_commit.raw_id:
596 if source_ref_id == source_commit.raw_id:
538 log.debug("Nothing changed in pull request %s", pull_request)
597 log.debug("Nothing changed in pull request %s", pull_request)
539 return UpdateResponse(
598 return UpdateResponse(
540 executed=False,
599 executed=False,
541 reason=UpdateFailureReason.NO_CHANGE,
600 reason=UpdateFailureReason.NO_CHANGE,
542 old=pull_request, new=None, changes=None)
601 old=pull_request, new=None, changes=None)
543
602
544 # Finally there is a need for an update
603 # Finally there is a need for an update
545 pull_request_version = self._create_version_from_snapshot(pull_request)
604 pull_request_version = self._create_version_from_snapshot(pull_request)
546 self._link_comments_to_version(pull_request_version)
605 self._link_comments_to_version(pull_request_version)
547
606
548 target_ref_type = pull_request.target_ref_parts.type
607 target_ref_type = pull_request.target_ref_parts.type
549 target_ref_name = pull_request.target_ref_parts.name
608 target_ref_name = pull_request.target_ref_parts.name
550 target_ref_id = pull_request.target_ref_parts.commit_id
609 target_ref_id = pull_request.target_ref_parts.commit_id
551 target_repo = pull_request.target_repo.scm_instance()
610 target_repo = pull_request.target_repo.scm_instance()
552
611
553 try:
612 try:
554 if target_ref_type in ('tag', 'branch', 'book'):
613 if target_ref_type in ('tag', 'branch', 'book'):
555 target_commit = target_repo.get_commit(target_ref_name)
614 target_commit = target_repo.get_commit(target_ref_name)
556 else:
615 else:
557 target_commit = target_repo.get_commit(target_ref_id)
616 target_commit = target_repo.get_commit(target_ref_id)
558 except CommitDoesNotExistError:
617 except CommitDoesNotExistError:
559 return UpdateResponse(
618 return UpdateResponse(
560 executed=False,
619 executed=False,
561 reason=UpdateFailureReason.MISSING_TARGET_REF,
620 reason=UpdateFailureReason.MISSING_TARGET_REF,
562 old=pull_request, new=None, changes=None)
621 old=pull_request, new=None, changes=None)
563
622
564 # re-compute commit ids
623 # re-compute commit ids
565 old_commit_ids = set(pull_request.revisions)
624 old_commit_ids = set(pull_request.revisions)
566 pre_load = ["author", "branch", "date", "message"]
625 pre_load = ["author", "branch", "date", "message"]
567 commit_ranges = target_repo.compare(
626 commit_ranges = target_repo.compare(
568 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
627 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
569 pre_load=pre_load)
628 pre_load=pre_load)
570
629
571 ancestor = target_repo.get_common_ancestor(
630 ancestor = target_repo.get_common_ancestor(
572 target_commit.raw_id, source_commit.raw_id, source_repo)
631 target_commit.raw_id, source_commit.raw_id, source_repo)
573
632
574 pull_request.source_ref = '%s:%s:%s' % (
633 pull_request.source_ref = '%s:%s:%s' % (
575 source_ref_type, source_ref_name, source_commit.raw_id)
634 source_ref_type, source_ref_name, source_commit.raw_id)
576 pull_request.target_ref = '%s:%s:%s' % (
635 pull_request.target_ref = '%s:%s:%s' % (
577 target_ref_type, target_ref_name, ancestor)
636 target_ref_type, target_ref_name, ancestor)
578 pull_request.revisions = [
637 pull_request.revisions = [
579 commit.raw_id for commit in reversed(commit_ranges)]
638 commit.raw_id for commit in reversed(commit_ranges)]
580 pull_request.updated_on = datetime.datetime.now()
639 pull_request.updated_on = datetime.datetime.now()
581 Session().add(pull_request)
640 Session().add(pull_request)
582 new_commit_ids = set(pull_request.revisions)
641 new_commit_ids = set(pull_request.revisions)
583
642
584 changes = self._calculate_commit_id_changes(
643 changes = self._calculate_commit_id_changes(
585 old_commit_ids, new_commit_ids)
644 old_commit_ids, new_commit_ids)
586
645
587 old_diff_data, new_diff_data = self._generate_update_diffs(
646 old_diff_data, new_diff_data = self._generate_update_diffs(
588 pull_request, pull_request_version)
647 pull_request, pull_request_version)
589
648
590 ChangesetCommentsModel().outdate_comments(
649 ChangesetCommentsModel().outdate_comments(
591 pull_request, old_diff_data=old_diff_data,
650 pull_request, old_diff_data=old_diff_data,
592 new_diff_data=new_diff_data)
651 new_diff_data=new_diff_data)
593
652
594 file_changes = self._calculate_file_changes(
653 file_changes = self._calculate_file_changes(
595 old_diff_data, new_diff_data)
654 old_diff_data, new_diff_data)
596
655
597 # Add an automatic comment to the pull request
656 # Add an automatic comment to the pull request
598 update_comment = ChangesetCommentsModel().create(
657 update_comment = ChangesetCommentsModel().create(
599 text=self._render_update_message(changes, file_changes),
658 text=self._render_update_message(changes, file_changes),
600 repo=pull_request.target_repo,
659 repo=pull_request.target_repo,
601 user=pull_request.author,
660 user=pull_request.author,
602 pull_request=pull_request,
661 pull_request=pull_request,
603 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
662 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
604
663
605 # Update status to "Under Review" for added commits
664 # Update status to "Under Review" for added commits
606 for commit_id in changes.added:
665 for commit_id in changes.added:
607 ChangesetStatusModel().set_status(
666 ChangesetStatusModel().set_status(
608 repo=pull_request.source_repo,
667 repo=pull_request.source_repo,
609 status=ChangesetStatus.STATUS_UNDER_REVIEW,
668 status=ChangesetStatus.STATUS_UNDER_REVIEW,
610 comment=update_comment,
669 comment=update_comment,
611 user=pull_request.author,
670 user=pull_request.author,
612 pull_request=pull_request,
671 pull_request=pull_request,
613 revision=commit_id)
672 revision=commit_id)
614
673
615 log.debug(
674 log.debug(
616 'Updated pull request %s, added_ids: %s, common_ids: %s, '
675 'Updated pull request %s, added_ids: %s, common_ids: %s, '
617 'removed_ids: %s', pull_request.pull_request_id,
676 'removed_ids: %s', pull_request.pull_request_id,
618 changes.added, changes.common, changes.removed)
677 changes.added, changes.common, changes.removed)
619 log.debug('Updated pull request with the following file changes: %s',
678 log.debug('Updated pull request with the following file changes: %s',
620 file_changes)
679 file_changes)
621
680
622 log.info(
681 log.info(
623 "Updated pull request %s from commit %s to commit %s, "
682 "Updated pull request %s from commit %s to commit %s, "
624 "stored new version %s of this pull request.",
683 "stored new version %s of this pull request.",
625 pull_request.pull_request_id, source_ref_id,
684 pull_request.pull_request_id, source_ref_id,
626 pull_request.source_ref_parts.commit_id,
685 pull_request.source_ref_parts.commit_id,
627 pull_request_version.pull_request_version_id)
686 pull_request_version.pull_request_version_id)
628 Session().commit()
687 Session().commit()
629 self._trigger_pull_request_hook(pull_request, pull_request.author,
688 self._trigger_pull_request_hook(pull_request, pull_request.author,
630 'update')
689 'update')
631
690
632 return UpdateResponse(
691 return UpdateResponse(
633 executed=True, reason=UpdateFailureReason.NONE,
692 executed=True, reason=UpdateFailureReason.NONE,
634 old=pull_request, new=pull_request_version, changes=changes)
693 old=pull_request, new=pull_request_version, changes=changes)
635
694
636 def _create_version_from_snapshot(self, pull_request):
695 def _create_version_from_snapshot(self, pull_request):
637 version = PullRequestVersion()
696 version = PullRequestVersion()
638 version.title = pull_request.title
697 version.title = pull_request.title
639 version.description = pull_request.description
698 version.description = pull_request.description
640 version.status = pull_request.status
699 version.status = pull_request.status
641 version.created_on = pull_request.created_on
700 version.created_on = pull_request.created_on
642 version.updated_on = pull_request.updated_on
701 version.updated_on = pull_request.updated_on
643 version.user_id = pull_request.user_id
702 version.user_id = pull_request.user_id
644 version.source_repo = pull_request.source_repo
703 version.source_repo = pull_request.source_repo
645 version.source_ref = pull_request.source_ref
704 version.source_ref = pull_request.source_ref
646 version.target_repo = pull_request.target_repo
705 version.target_repo = pull_request.target_repo
647 version.target_ref = pull_request.target_ref
706 version.target_ref = pull_request.target_ref
648
707
649 version._last_merge_source_rev = pull_request._last_merge_source_rev
708 version._last_merge_source_rev = pull_request._last_merge_source_rev
650 version._last_merge_target_rev = pull_request._last_merge_target_rev
709 version._last_merge_target_rev = pull_request._last_merge_target_rev
651 version._last_merge_status = pull_request._last_merge_status
710 version._last_merge_status = pull_request._last_merge_status
652 version.shadow_merge_ref = pull_request.shadow_merge_ref
711 version.shadow_merge_ref = pull_request.shadow_merge_ref
653 version.merge_rev = pull_request.merge_rev
712 version.merge_rev = pull_request.merge_rev
654
713
655 version.revisions = pull_request.revisions
714 version.revisions = pull_request.revisions
656 version.pull_request = pull_request
715 version.pull_request = pull_request
657 Session().add(version)
716 Session().add(version)
658 Session().flush()
717 Session().flush()
659
718
660 return version
719 return version
661
720
662 def _generate_update_diffs(self, pull_request, pull_request_version):
721 def _generate_update_diffs(self, pull_request, pull_request_version):
663 diff_context = (
722 diff_context = (
664 self.DIFF_CONTEXT +
723 self.DIFF_CONTEXT +
665 ChangesetCommentsModel.needed_extra_diff_context())
724 ChangesetCommentsModel.needed_extra_diff_context())
666 old_diff = self._get_diff_from_pr_or_version(
725 old_diff = self._get_diff_from_pr_or_version(
667 pull_request_version, context=diff_context)
726 pull_request_version, context=diff_context)
668 new_diff = self._get_diff_from_pr_or_version(
727 new_diff = self._get_diff_from_pr_or_version(
669 pull_request, context=diff_context)
728 pull_request, context=diff_context)
670
729
671 old_diff_data = diffs.DiffProcessor(old_diff)
730 old_diff_data = diffs.DiffProcessor(old_diff)
672 old_diff_data.prepare()
731 old_diff_data.prepare()
673 new_diff_data = diffs.DiffProcessor(new_diff)
732 new_diff_data = diffs.DiffProcessor(new_diff)
674 new_diff_data.prepare()
733 new_diff_data.prepare()
675
734
676 return old_diff_data, new_diff_data
735 return old_diff_data, new_diff_data
677
736
678 def _link_comments_to_version(self, pull_request_version):
737 def _link_comments_to_version(self, pull_request_version):
679 """
738 """
680 Link all unlinked comments of this pull request to the given version.
739 Link all unlinked comments of this pull request to the given version.
681
740
682 :param pull_request_version: The `PullRequestVersion` to which
741 :param pull_request_version: The `PullRequestVersion` to which
683 the comments shall be linked.
742 the comments shall be linked.
684
743
685 """
744 """
686 pull_request = pull_request_version.pull_request
745 pull_request = pull_request_version.pull_request
687 comments = ChangesetComment.query().filter(
746 comments = ChangesetComment.query().filter(
688 # TODO: johbo: Should we query for the repo at all here?
747 # TODO: johbo: Should we query for the repo at all here?
689 # Pending decision on how comments of PRs are to be related
748 # Pending decision on how comments of PRs are to be related
690 # to either the source repo, the target repo or no repo at all.
749 # to either the source repo, the target repo or no repo at all.
691 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
750 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
692 ChangesetComment.pull_request == pull_request,
751 ChangesetComment.pull_request == pull_request,
693 ChangesetComment.pull_request_version == None)
752 ChangesetComment.pull_request_version == None)
694
753
695 # TODO: johbo: Find out why this breaks if it is done in a bulk
754 # TODO: johbo: Find out why this breaks if it is done in a bulk
696 # operation.
755 # operation.
697 for comment in comments:
756 for comment in comments:
698 comment.pull_request_version_id = (
757 comment.pull_request_version_id = (
699 pull_request_version.pull_request_version_id)
758 pull_request_version.pull_request_version_id)
700 Session().add(comment)
759 Session().add(comment)
701
760
702 def _calculate_commit_id_changes(self, old_ids, new_ids):
761 def _calculate_commit_id_changes(self, old_ids, new_ids):
703 added = new_ids.difference(old_ids)
762 added = new_ids.difference(old_ids)
704 common = old_ids.intersection(new_ids)
763 common = old_ids.intersection(new_ids)
705 removed = old_ids.difference(new_ids)
764 removed = old_ids.difference(new_ids)
706 return ChangeTuple(added, common, removed)
765 return ChangeTuple(added, common, removed)
707
766
708 def _calculate_file_changes(self, old_diff_data, new_diff_data):
767 def _calculate_file_changes(self, old_diff_data, new_diff_data):
709
768
710 old_files = OrderedDict()
769 old_files = OrderedDict()
711 for diff_data in old_diff_data.parsed_diff:
770 for diff_data in old_diff_data.parsed_diff:
712 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
771 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
713
772
714 added_files = []
773 added_files = []
715 modified_files = []
774 modified_files = []
716 removed_files = []
775 removed_files = []
717 for diff_data in new_diff_data.parsed_diff:
776 for diff_data in new_diff_data.parsed_diff:
718 new_filename = diff_data['filename']
777 new_filename = diff_data['filename']
719 new_hash = md5_safe(diff_data['raw_diff'])
778 new_hash = md5_safe(diff_data['raw_diff'])
720
779
721 old_hash = old_files.get(new_filename)
780 old_hash = old_files.get(new_filename)
722 if not old_hash:
781 if not old_hash:
723 # file is not present in old diff, means it's added
782 # file is not present in old diff, means it's added
724 added_files.append(new_filename)
783 added_files.append(new_filename)
725 else:
784 else:
726 if new_hash != old_hash:
785 if new_hash != old_hash:
727 modified_files.append(new_filename)
786 modified_files.append(new_filename)
728 # now remove a file from old, since we have seen it already
787 # now remove a file from old, since we have seen it already
729 del old_files[new_filename]
788 del old_files[new_filename]
730
789
731 # removed files is when there are present in old, but not in NEW,
790 # removed files is when there are present in old, but not in NEW,
732 # since we remove old files that are present in new diff, left-overs
791 # since we remove old files that are present in new diff, left-overs
733 # if any should be the removed files
792 # if any should be the removed files
734 removed_files.extend(old_files.keys())
793 removed_files.extend(old_files.keys())
735
794
736 return FileChangeTuple(added_files, modified_files, removed_files)
795 return FileChangeTuple(added_files, modified_files, removed_files)
737
796
738 def _render_update_message(self, changes, file_changes):
797 def _render_update_message(self, changes, file_changes):
739 """
798 """
740 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
799 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
741 so it's always looking the same disregarding on which default
800 so it's always looking the same disregarding on which default
742 renderer system is using.
801 renderer system is using.
743
802
744 :param changes: changes named tuple
803 :param changes: changes named tuple
745 :param file_changes: file changes named tuple
804 :param file_changes: file changes named tuple
746
805
747 """
806 """
748 new_status = ChangesetStatus.get_status_lbl(
807 new_status = ChangesetStatus.get_status_lbl(
749 ChangesetStatus.STATUS_UNDER_REVIEW)
808 ChangesetStatus.STATUS_UNDER_REVIEW)
750
809
751 changed_files = (
810 changed_files = (
752 file_changes.added + file_changes.modified + file_changes.removed)
811 file_changes.added + file_changes.modified + file_changes.removed)
753
812
754 params = {
813 params = {
755 'under_review_label': new_status,
814 'under_review_label': new_status,
756 'added_commits': changes.added,
815 'added_commits': changes.added,
757 'removed_commits': changes.removed,
816 'removed_commits': changes.removed,
758 'changed_files': changed_files,
817 'changed_files': changed_files,
759 'added_files': file_changes.added,
818 'added_files': file_changes.added,
760 'modified_files': file_changes.modified,
819 'modified_files': file_changes.modified,
761 'removed_files': file_changes.removed,
820 'removed_files': file_changes.removed,
762 }
821 }
763 renderer = RstTemplateRenderer()
822 renderer = RstTemplateRenderer()
764 return renderer.render('pull_request_update.mako', **params)
823 return renderer.render('pull_request_update.mako', **params)
765
824
766 def edit(self, pull_request, title, description):
825 def edit(self, pull_request, title, description):
767 pull_request = self.__get_pull_request(pull_request)
826 pull_request = self.__get_pull_request(pull_request)
768 if pull_request.is_closed():
827 if pull_request.is_closed():
769 raise ValueError('This pull request is closed')
828 raise ValueError('This pull request is closed')
770 if title:
829 if title:
771 pull_request.title = title
830 pull_request.title = title
772 pull_request.description = description
831 pull_request.description = description
773 pull_request.updated_on = datetime.datetime.now()
832 pull_request.updated_on = datetime.datetime.now()
774 Session().add(pull_request)
833 Session().add(pull_request)
775
834
776 def update_reviewers(self, pull_request, reviewer_data):
835 def update_reviewers(self, pull_request, reviewer_data):
777 """
836 """
778 Update the reviewers in the pull request
837 Update the reviewers in the pull request
779
838
780 :param pull_request: the pr to update
839 :param pull_request: the pr to update
781 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
840 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
782 """
841 """
783
842
784 reviewers_reasons = {}
843 reviewers_reasons = {}
785 for user_id, reasons in reviewer_data:
844 for user_id, reasons in reviewer_data:
786 if isinstance(user_id, (int, basestring)):
845 if isinstance(user_id, (int, basestring)):
787 user_id = self._get_user(user_id).user_id
846 user_id = self._get_user(user_id).user_id
788 reviewers_reasons[user_id] = reasons
847 reviewers_reasons[user_id] = reasons
789
848
790 reviewers_ids = set(reviewers_reasons.keys())
849 reviewers_ids = set(reviewers_reasons.keys())
791 pull_request = self.__get_pull_request(pull_request)
850 pull_request = self.__get_pull_request(pull_request)
792 current_reviewers = PullRequestReviewers.query()\
851 current_reviewers = PullRequestReviewers.query()\
793 .filter(PullRequestReviewers.pull_request ==
852 .filter(PullRequestReviewers.pull_request ==
794 pull_request).all()
853 pull_request).all()
795 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
854 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
796
855
797 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
856 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
798 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
857 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
799
858
800 log.debug("Adding %s reviewers", ids_to_add)
859 log.debug("Adding %s reviewers", ids_to_add)
801 log.debug("Removing %s reviewers", ids_to_remove)
860 log.debug("Removing %s reviewers", ids_to_remove)
802 changed = False
861 changed = False
803 for uid in ids_to_add:
862 for uid in ids_to_add:
804 changed = True
863 changed = True
805 _usr = self._get_user(uid)
864 _usr = self._get_user(uid)
806 reasons = reviewers_reasons[uid]
865 reasons = reviewers_reasons[uid]
807 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
866 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
808 Session().add(reviewer)
867 Session().add(reviewer)
809
868
810 self.notify_reviewers(pull_request, ids_to_add)
869 self.notify_reviewers(pull_request, ids_to_add)
811
870
812 for uid in ids_to_remove:
871 for uid in ids_to_remove:
813 changed = True
872 changed = True
814 reviewer = PullRequestReviewers.query()\
873 reviewer = PullRequestReviewers.query()\
815 .filter(PullRequestReviewers.user_id == uid,
874 .filter(PullRequestReviewers.user_id == uid,
816 PullRequestReviewers.pull_request == pull_request)\
875 PullRequestReviewers.pull_request == pull_request)\
817 .scalar()
876 .scalar()
818 if reviewer:
877 if reviewer:
819 Session().delete(reviewer)
878 Session().delete(reviewer)
820 if changed:
879 if changed:
821 pull_request.updated_on = datetime.datetime.now()
880 pull_request.updated_on = datetime.datetime.now()
822 Session().add(pull_request)
881 Session().add(pull_request)
823
882
824 return ids_to_add, ids_to_remove
883 return ids_to_add, ids_to_remove
825
884
826 def get_url(self, pull_request):
885 def get_url(self, pull_request):
827 return h.url('pullrequest_show',
886 return h.url('pullrequest_show',
828 repo_name=safe_str(pull_request.target_repo.repo_name),
887 repo_name=safe_str(pull_request.target_repo.repo_name),
829 pull_request_id=pull_request.pull_request_id,
888 pull_request_id=pull_request.pull_request_id,
830 qualified=True)
889 qualified=True)
831
890
832 def get_shadow_clone_url(self, pull_request):
891 def get_shadow_clone_url(self, pull_request):
833 """
892 """
834 Returns qualified url pointing to the shadow repository. If this pull
893 Returns qualified url pointing to the shadow repository. If this pull
835 request is closed there is no shadow repository and ``None`` will be
894 request is closed there is no shadow repository and ``None`` will be
836 returned.
895 returned.
837 """
896 """
838 if pull_request.is_closed():
897 if pull_request.is_closed():
839 return None
898 return None
840 else:
899 else:
841 pr_url = urllib.unquote(self.get_url(pull_request))
900 pr_url = urllib.unquote(self.get_url(pull_request))
842 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
901 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
843
902
844 def notify_reviewers(self, pull_request, reviewers_ids):
903 def notify_reviewers(self, pull_request, reviewers_ids):
845 # notification to reviewers
904 # notification to reviewers
846 if not reviewers_ids:
905 if not reviewers_ids:
847 return
906 return
848
907
849 pull_request_obj = pull_request
908 pull_request_obj = pull_request
850 # get the current participants of this pull request
909 # get the current participants of this pull request
851 recipients = reviewers_ids
910 recipients = reviewers_ids
852 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
911 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
853
912
854 pr_source_repo = pull_request_obj.source_repo
913 pr_source_repo = pull_request_obj.source_repo
855 pr_target_repo = pull_request_obj.target_repo
914 pr_target_repo = pull_request_obj.target_repo
856
915
857 pr_url = h.url(
916 pr_url = h.url(
858 'pullrequest_show',
917 'pullrequest_show',
859 repo_name=pr_target_repo.repo_name,
918 repo_name=pr_target_repo.repo_name,
860 pull_request_id=pull_request_obj.pull_request_id,
919 pull_request_id=pull_request_obj.pull_request_id,
861 qualified=True,)
920 qualified=True,)
862
921
863 # set some variables for email notification
922 # set some variables for email notification
864 pr_target_repo_url = h.url(
923 pr_target_repo_url = h.url(
865 'summary_home',
924 'summary_home',
866 repo_name=pr_target_repo.repo_name,
925 repo_name=pr_target_repo.repo_name,
867 qualified=True)
926 qualified=True)
868
927
869 pr_source_repo_url = h.url(
928 pr_source_repo_url = h.url(
870 'summary_home',
929 'summary_home',
871 repo_name=pr_source_repo.repo_name,
930 repo_name=pr_source_repo.repo_name,
872 qualified=True)
931 qualified=True)
873
932
874 # pull request specifics
933 # pull request specifics
875 pull_request_commits = [
934 pull_request_commits = [
876 (x.raw_id, x.message)
935 (x.raw_id, x.message)
877 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
936 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
878
937
879 kwargs = {
938 kwargs = {
880 'user': pull_request.author,
939 'user': pull_request.author,
881 'pull_request': pull_request_obj,
940 'pull_request': pull_request_obj,
882 'pull_request_commits': pull_request_commits,
941 'pull_request_commits': pull_request_commits,
883
942
884 'pull_request_target_repo': pr_target_repo,
943 'pull_request_target_repo': pr_target_repo,
885 'pull_request_target_repo_url': pr_target_repo_url,
944 'pull_request_target_repo_url': pr_target_repo_url,
886
945
887 'pull_request_source_repo': pr_source_repo,
946 'pull_request_source_repo': pr_source_repo,
888 'pull_request_source_repo_url': pr_source_repo_url,
947 'pull_request_source_repo_url': pr_source_repo_url,
889
948
890 'pull_request_url': pr_url,
949 'pull_request_url': pr_url,
891 }
950 }
892
951
893 # pre-generate the subject for notification itself
952 # pre-generate the subject for notification itself
894 (subject,
953 (subject,
895 _h, _e, # we don't care about those
954 _h, _e, # we don't care about those
896 body_plaintext) = EmailNotificationModel().render_email(
955 body_plaintext) = EmailNotificationModel().render_email(
897 notification_type, **kwargs)
956 notification_type, **kwargs)
898
957
899 # create notification objects, and emails
958 # create notification objects, and emails
900 NotificationModel().create(
959 NotificationModel().create(
901 created_by=pull_request.author,
960 created_by=pull_request.author,
902 notification_subject=subject,
961 notification_subject=subject,
903 notification_body=body_plaintext,
962 notification_body=body_plaintext,
904 notification_type=notification_type,
963 notification_type=notification_type,
905 recipients=recipients,
964 recipients=recipients,
906 email_kwargs=kwargs,
965 email_kwargs=kwargs,
907 )
966 )
908
967
909 def delete(self, pull_request):
968 def delete(self, pull_request):
910 pull_request = self.__get_pull_request(pull_request)
969 pull_request = self.__get_pull_request(pull_request)
911 self._cleanup_merge_workspace(pull_request)
970 self._cleanup_merge_workspace(pull_request)
912 Session().delete(pull_request)
971 Session().delete(pull_request)
913
972
914 def close_pull_request(self, pull_request, user):
973 def close_pull_request(self, pull_request, user):
915 pull_request = self.__get_pull_request(pull_request)
974 pull_request = self.__get_pull_request(pull_request)
916 self._cleanup_merge_workspace(pull_request)
975 self._cleanup_merge_workspace(pull_request)
917 pull_request.status = PullRequest.STATUS_CLOSED
976 pull_request.status = PullRequest.STATUS_CLOSED
918 pull_request.updated_on = datetime.datetime.now()
977 pull_request.updated_on = datetime.datetime.now()
919 Session().add(pull_request)
978 Session().add(pull_request)
920 self._trigger_pull_request_hook(
979 self._trigger_pull_request_hook(
921 pull_request, pull_request.author, 'close')
980 pull_request, pull_request.author, 'close')
922 self._log_action('user_closed_pull_request', user, pull_request)
981 self._log_action('user_closed_pull_request', user, pull_request)
923
982
924 def close_pull_request_with_comment(self, pull_request, user, repo,
983 def close_pull_request_with_comment(self, pull_request, user, repo,
925 message=None):
984 message=None):
926 status = ChangesetStatus.STATUS_REJECTED
985 status = ChangesetStatus.STATUS_REJECTED
927
986
928 if not message:
987 if not message:
929 message = (
988 message = (
930 _('Status change %(transition_icon)s %(status)s') % {
989 _('Status change %(transition_icon)s %(status)s') % {
931 'transition_icon': '>',
990 'transition_icon': '>',
932 'status': ChangesetStatus.get_status_lbl(status)})
991 'status': ChangesetStatus.get_status_lbl(status)})
933
992
934 internal_message = _('Closing with') + ' ' + message
993 internal_message = _('Closing with') + ' ' + message
935
994
936 comm = ChangesetCommentsModel().create(
995 comm = ChangesetCommentsModel().create(
937 text=internal_message,
996 text=internal_message,
938 repo=repo.repo_id,
997 repo=repo.repo_id,
939 user=user.user_id,
998 user=user.user_id,
940 pull_request=pull_request.pull_request_id,
999 pull_request=pull_request.pull_request_id,
941 f_path=None,
1000 f_path=None,
942 line_no=None,
1001 line_no=None,
943 status_change=ChangesetStatus.get_status_lbl(status),
1002 status_change=ChangesetStatus.get_status_lbl(status),
944 status_change_type=status,
1003 status_change_type=status,
945 closing_pr=True
1004 closing_pr=True
946 )
1005 )
947
1006
948 ChangesetStatusModel().set_status(
1007 ChangesetStatusModel().set_status(
949 repo.repo_id,
1008 repo.repo_id,
950 status,
1009 status,
951 user.user_id,
1010 user.user_id,
952 comm,
1011 comm,
953 pull_request=pull_request.pull_request_id
1012 pull_request=pull_request.pull_request_id
954 )
1013 )
955 Session().flush()
1014 Session().flush()
956
1015
957 PullRequestModel().close_pull_request(
1016 PullRequestModel().close_pull_request(
958 pull_request.pull_request_id, user)
1017 pull_request.pull_request_id, user)
959
1018
960 def merge_status(self, pull_request):
1019 def merge_status(self, pull_request):
961 if not self._is_merge_enabled(pull_request):
1020 if not self._is_merge_enabled(pull_request):
962 return False, _('Server-side pull request merging is disabled.')
1021 return False, _('Server-side pull request merging is disabled.')
963 if pull_request.is_closed():
1022 if pull_request.is_closed():
964 return False, _('This pull request is closed.')
1023 return False, _('This pull request is closed.')
965 merge_possible, msg = self._check_repo_requirements(
1024 merge_possible, msg = self._check_repo_requirements(
966 target=pull_request.target_repo, source=pull_request.source_repo)
1025 target=pull_request.target_repo, source=pull_request.source_repo)
967 if not merge_possible:
1026 if not merge_possible:
968 return merge_possible, msg
1027 return merge_possible, msg
969
1028
970 try:
1029 try:
971 resp = self._try_merge(pull_request)
1030 resp = self._try_merge(pull_request)
972 log.debug("Merge response: %s", resp)
1031 log.debug("Merge response: %s", resp)
973 status = resp.possible, self.merge_status_message(
1032 status = resp.possible, self.merge_status_message(
974 resp.failure_reason)
1033 resp.failure_reason)
975 except NotImplementedError:
1034 except NotImplementedError:
976 status = False, _('Pull request merging is not supported.')
1035 status = False, _('Pull request merging is not supported.')
977
1036
978 return status
1037 return status
979
1038
980 def _check_repo_requirements(self, target, source):
1039 def _check_repo_requirements(self, target, source):
981 """
1040 """
982 Check if `target` and `source` have compatible requirements.
1041 Check if `target` and `source` have compatible requirements.
983
1042
984 Currently this is just checking for largefiles.
1043 Currently this is just checking for largefiles.
985 """
1044 """
986 target_has_largefiles = self._has_largefiles(target)
1045 target_has_largefiles = self._has_largefiles(target)
987 source_has_largefiles = self._has_largefiles(source)
1046 source_has_largefiles = self._has_largefiles(source)
988 merge_possible = True
1047 merge_possible = True
989 message = u''
1048 message = u''
990
1049
991 if target_has_largefiles != source_has_largefiles:
1050 if target_has_largefiles != source_has_largefiles:
992 merge_possible = False
1051 merge_possible = False
993 if source_has_largefiles:
1052 if source_has_largefiles:
994 message = _(
1053 message = _(
995 'Target repository large files support is disabled.')
1054 'Target repository large files support is disabled.')
996 else:
1055 else:
997 message = _(
1056 message = _(
998 'Source repository large files support is disabled.')
1057 'Source repository large files support is disabled.')
999
1058
1000 return merge_possible, message
1059 return merge_possible, message
1001
1060
1002 def _has_largefiles(self, repo):
1061 def _has_largefiles(self, repo):
1003 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1062 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1004 'extensions', 'largefiles')
1063 'extensions', 'largefiles')
1005 return largefiles_ui and largefiles_ui[0].active
1064 return largefiles_ui and largefiles_ui[0].active
1006
1065
1007 def _try_merge(self, pull_request):
1066 def _try_merge(self, pull_request):
1008 """
1067 """
1009 Try to merge the pull request and return the merge status.
1068 Try to merge the pull request and return the merge status.
1010 """
1069 """
1011 log.debug(
1070 log.debug(
1012 "Trying out if the pull request %s can be merged.",
1071 "Trying out if the pull request %s can be merged.",
1013 pull_request.pull_request_id)
1072 pull_request.pull_request_id)
1014 target_vcs = pull_request.target_repo.scm_instance()
1073 target_vcs = pull_request.target_repo.scm_instance()
1015
1074
1016 # Refresh the target reference.
1075 # Refresh the target reference.
1017 try:
1076 try:
1018 target_ref = self._refresh_reference(
1077 target_ref = self._refresh_reference(
1019 pull_request.target_ref_parts, target_vcs)
1078 pull_request.target_ref_parts, target_vcs)
1020 except CommitDoesNotExistError:
1079 except CommitDoesNotExistError:
1021 merge_state = MergeResponse(
1080 merge_state = MergeResponse(
1022 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1081 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1023 return merge_state
1082 return merge_state
1024
1083
1025 target_locked = pull_request.target_repo.locked
1084 target_locked = pull_request.target_repo.locked
1026 if target_locked and target_locked[0]:
1085 if target_locked and target_locked[0]:
1027 log.debug("The target repository is locked.")
1086 log.debug("The target repository is locked.")
1028 merge_state = MergeResponse(
1087 merge_state = MergeResponse(
1029 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1088 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1030 elif self._needs_merge_state_refresh(pull_request, target_ref):
1089 elif self._needs_merge_state_refresh(pull_request, target_ref):
1031 log.debug("Refreshing the merge status of the repository.")
1090 log.debug("Refreshing the merge status of the repository.")
1032 merge_state = self._refresh_merge_state(
1091 merge_state = self._refresh_merge_state(
1033 pull_request, target_vcs, target_ref)
1092 pull_request, target_vcs, target_ref)
1034 else:
1093 else:
1035 possible = pull_request.\
1094 possible = pull_request.\
1036 _last_merge_status == MergeFailureReason.NONE
1095 _last_merge_status == MergeFailureReason.NONE
1037 merge_state = MergeResponse(
1096 merge_state = MergeResponse(
1038 possible, False, None, pull_request._last_merge_status)
1097 possible, False, None, pull_request._last_merge_status)
1039
1098
1040 return merge_state
1099 return merge_state
1041
1100
1042 def _refresh_reference(self, reference, vcs_repository):
1101 def _refresh_reference(self, reference, vcs_repository):
1043 if reference.type in ('branch', 'book'):
1102 if reference.type in ('branch', 'book'):
1044 name_or_id = reference.name
1103 name_or_id = reference.name
1045 else:
1104 else:
1046 name_or_id = reference.commit_id
1105 name_or_id = reference.commit_id
1047 refreshed_commit = vcs_repository.get_commit(name_or_id)
1106 refreshed_commit = vcs_repository.get_commit(name_or_id)
1048 refreshed_reference = Reference(
1107 refreshed_reference = Reference(
1049 reference.type, reference.name, refreshed_commit.raw_id)
1108 reference.type, reference.name, refreshed_commit.raw_id)
1050 return refreshed_reference
1109 return refreshed_reference
1051
1110
1052 def _needs_merge_state_refresh(self, pull_request, target_reference):
1111 def _needs_merge_state_refresh(self, pull_request, target_reference):
1053 return not(
1112 return not(
1054 pull_request.revisions and
1113 pull_request.revisions and
1055 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1114 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1056 target_reference.commit_id == pull_request._last_merge_target_rev)
1115 target_reference.commit_id == pull_request._last_merge_target_rev)
1057
1116
1058 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1117 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1059 workspace_id = self._workspace_id(pull_request)
1118 workspace_id = self._workspace_id(pull_request)
1060 source_vcs = pull_request.source_repo.scm_instance()
1119 source_vcs = pull_request.source_repo.scm_instance()
1061 use_rebase = self._use_rebase_for_merging(pull_request)
1120 use_rebase = self._use_rebase_for_merging(pull_request)
1062 merge_state = target_vcs.merge(
1121 merge_state = target_vcs.merge(
1063 target_reference, source_vcs, pull_request.source_ref_parts,
1122 target_reference, source_vcs, pull_request.source_ref_parts,
1064 workspace_id, dry_run=True, use_rebase=use_rebase)
1123 workspace_id, dry_run=True, use_rebase=use_rebase)
1065
1124
1066 # Do not store the response if there was an unknown error.
1125 # Do not store the response if there was an unknown error.
1067 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1126 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1068 pull_request._last_merge_source_rev = \
1127 pull_request._last_merge_source_rev = \
1069 pull_request.source_ref_parts.commit_id
1128 pull_request.source_ref_parts.commit_id
1070 pull_request._last_merge_target_rev = target_reference.commit_id
1129 pull_request._last_merge_target_rev = target_reference.commit_id
1071 pull_request._last_merge_status = merge_state.failure_reason
1130 pull_request._last_merge_status = merge_state.failure_reason
1072 pull_request.shadow_merge_ref = merge_state.merge_ref
1131 pull_request.shadow_merge_ref = merge_state.merge_ref
1073 Session().add(pull_request)
1132 Session().add(pull_request)
1074 Session().commit()
1133 Session().commit()
1075
1134
1076 return merge_state
1135 return merge_state
1077
1136
1078 def _workspace_id(self, pull_request):
1137 def _workspace_id(self, pull_request):
1079 workspace_id = 'pr-%s' % pull_request.pull_request_id
1138 workspace_id = 'pr-%s' % pull_request.pull_request_id
1080 return workspace_id
1139 return workspace_id
1081
1140
1082 def merge_status_message(self, status_code):
1141 def merge_status_message(self, status_code):
1083 """
1142 """
1084 Return a human friendly error message for the given merge status code.
1143 Return a human friendly error message for the given merge status code.
1085 """
1144 """
1086 return self.MERGE_STATUS_MESSAGES[status_code]
1145 return self.MERGE_STATUS_MESSAGES[status_code]
1087
1146
1088 def generate_repo_data(self, repo, commit_id=None, branch=None,
1147 def generate_repo_data(self, repo, commit_id=None, branch=None,
1089 bookmark=None):
1148 bookmark=None):
1090 all_refs, selected_ref = \
1149 all_refs, selected_ref = \
1091 self._get_repo_pullrequest_sources(
1150 self._get_repo_pullrequest_sources(
1092 repo.scm_instance(), commit_id=commit_id,
1151 repo.scm_instance(), commit_id=commit_id,
1093 branch=branch, bookmark=bookmark)
1152 branch=branch, bookmark=bookmark)
1094
1153
1095 refs_select2 = []
1154 refs_select2 = []
1096 for element in all_refs:
1155 for element in all_refs:
1097 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1156 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1098 refs_select2.append({'text': element[1], 'children': children})
1157 refs_select2.append({'text': element[1], 'children': children})
1099
1158
1100 return {
1159 return {
1101 'user': {
1160 'user': {
1102 'user_id': repo.user.user_id,
1161 'user_id': repo.user.user_id,
1103 'username': repo.user.username,
1162 'username': repo.user.username,
1104 'firstname': repo.user.firstname,
1163 'firstname': repo.user.firstname,
1105 'lastname': repo.user.lastname,
1164 'lastname': repo.user.lastname,
1106 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1165 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1107 },
1166 },
1108 'description': h.chop_at_smart(repo.description, '\n'),
1167 'description': h.chop_at_smart(repo.description, '\n'),
1109 'refs': {
1168 'refs': {
1110 'all_refs': all_refs,
1169 'all_refs': all_refs,
1111 'selected_ref': selected_ref,
1170 'selected_ref': selected_ref,
1112 'select2_refs': refs_select2
1171 'select2_refs': refs_select2
1113 }
1172 }
1114 }
1173 }
1115
1174
1116 def generate_pullrequest_title(self, source, source_ref, target):
1175 def generate_pullrequest_title(self, source, source_ref, target):
1117 return u'{source}#{at_ref} to {target}'.format(
1176 return u'{source}#{at_ref} to {target}'.format(
1118 source=source,
1177 source=source,
1119 at_ref=source_ref,
1178 at_ref=source_ref,
1120 target=target,
1179 target=target,
1121 )
1180 )
1122
1181
1123 def _cleanup_merge_workspace(self, pull_request):
1182 def _cleanup_merge_workspace(self, pull_request):
1124 # Merging related cleanup
1183 # Merging related cleanup
1125 target_scm = pull_request.target_repo.scm_instance()
1184 target_scm = pull_request.target_repo.scm_instance()
1126 workspace_id = 'pr-%s' % pull_request.pull_request_id
1185 workspace_id = 'pr-%s' % pull_request.pull_request_id
1127
1186
1128 try:
1187 try:
1129 target_scm.cleanup_merge_workspace(workspace_id)
1188 target_scm.cleanup_merge_workspace(workspace_id)
1130 except NotImplementedError:
1189 except NotImplementedError:
1131 pass
1190 pass
1132
1191
1133 def _get_repo_pullrequest_sources(
1192 def _get_repo_pullrequest_sources(
1134 self, repo, commit_id=None, branch=None, bookmark=None):
1193 self, repo, commit_id=None, branch=None, bookmark=None):
1135 """
1194 """
1136 Return a structure with repo's interesting commits, suitable for
1195 Return a structure with repo's interesting commits, suitable for
1137 the selectors in pullrequest controller
1196 the selectors in pullrequest controller
1138
1197
1139 :param commit_id: a commit that must be in the list somehow
1198 :param commit_id: a commit that must be in the list somehow
1140 and selected by default
1199 and selected by default
1141 :param branch: a branch that must be in the list and selected
1200 :param branch: a branch that must be in the list and selected
1142 by default - even if closed
1201 by default - even if closed
1143 :param bookmark: a bookmark that must be in the list and selected
1202 :param bookmark: a bookmark that must be in the list and selected
1144 """
1203 """
1145
1204
1146 commit_id = safe_str(commit_id) if commit_id else None
1205 commit_id = safe_str(commit_id) if commit_id else None
1147 branch = safe_str(branch) if branch else None
1206 branch = safe_str(branch) if branch else None
1148 bookmark = safe_str(bookmark) if bookmark else None
1207 bookmark = safe_str(bookmark) if bookmark else None
1149
1208
1150 selected = None
1209 selected = None
1151
1210
1152 # order matters: first source that has commit_id in it will be selected
1211 # order matters: first source that has commit_id in it will be selected
1153 sources = []
1212 sources = []
1154 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1213 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1155 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1214 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1156
1215
1157 if commit_id:
1216 if commit_id:
1158 ref_commit = (h.short_id(commit_id), commit_id)
1217 ref_commit = (h.short_id(commit_id), commit_id)
1159 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1218 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1160
1219
1161 sources.append(
1220 sources.append(
1162 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1221 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1163 )
1222 )
1164
1223
1165 groups = []
1224 groups = []
1166 for group_key, ref_list, group_name, match in sources:
1225 for group_key, ref_list, group_name, match in sources:
1167 group_refs = []
1226 group_refs = []
1168 for ref_name, ref_id in ref_list:
1227 for ref_name, ref_id in ref_list:
1169 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1228 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1170 group_refs.append((ref_key, ref_name))
1229 group_refs.append((ref_key, ref_name))
1171
1230
1172 if not selected:
1231 if not selected:
1173 if set([commit_id, match]) & set([ref_id, ref_name]):
1232 if set([commit_id, match]) & set([ref_id, ref_name]):
1174 selected = ref_key
1233 selected = ref_key
1175
1234
1176 if group_refs:
1235 if group_refs:
1177 groups.append((group_refs, group_name))
1236 groups.append((group_refs, group_name))
1178
1237
1179 if not selected:
1238 if not selected:
1180 ref = commit_id or branch or bookmark
1239 ref = commit_id or branch or bookmark
1181 if ref:
1240 if ref:
1182 raise CommitDoesNotExistError(
1241 raise CommitDoesNotExistError(
1183 'No commit refs could be found matching: %s' % ref)
1242 'No commit refs could be found matching: %s' % ref)
1184 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1243 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1185 selected = 'branch:%s:%s' % (
1244 selected = 'branch:%s:%s' % (
1186 repo.DEFAULT_BRANCH_NAME,
1245 repo.DEFAULT_BRANCH_NAME,
1187 repo.branches[repo.DEFAULT_BRANCH_NAME]
1246 repo.branches[repo.DEFAULT_BRANCH_NAME]
1188 )
1247 )
1189 elif repo.commit_ids:
1248 elif repo.commit_ids:
1190 rev = repo.commit_ids[0]
1249 rev = repo.commit_ids[0]
1191 selected = 'rev:%s:%s' % (rev, rev)
1250 selected = 'rev:%s:%s' % (rev, rev)
1192 else:
1251 else:
1193 raise EmptyRepositoryError()
1252 raise EmptyRepositoryError()
1194 return groups, selected
1253 return groups, selected
1195
1254
1196 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1255 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1197 pull_request = self.__get_pull_request(pull_request)
1256 pull_request = self.__get_pull_request(pull_request)
1198 return self._get_diff_from_pr_or_version(pull_request, context=context)
1257 return self._get_diff_from_pr_or_version(pull_request, context=context)
1199
1258
1200 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1259 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1201 source_repo = pr_or_version.source_repo
1260 source_repo = pr_or_version.source_repo
1202
1261
1203 # we swap org/other ref since we run a simple diff on one repo
1262 # we swap org/other ref since we run a simple diff on one repo
1204 target_ref_id = pr_or_version.target_ref_parts.commit_id
1263 target_ref_id = pr_or_version.target_ref_parts.commit_id
1205 source_ref_id = pr_or_version.source_ref_parts.commit_id
1264 source_ref_id = pr_or_version.source_ref_parts.commit_id
1206 target_commit = source_repo.get_commit(
1265 target_commit = source_repo.get_commit(
1207 commit_id=safe_str(target_ref_id))
1266 commit_id=safe_str(target_ref_id))
1208 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1267 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1209 vcs_repo = source_repo.scm_instance()
1268 vcs_repo = source_repo.scm_instance()
1210
1269
1211 # TODO: johbo: In the context of an update, we cannot reach
1270 # TODO: johbo: In the context of an update, we cannot reach
1212 # the old commit anymore with our normal mechanisms. It needs
1271 # the old commit anymore with our normal mechanisms. It needs
1213 # some sort of special support in the vcs layer to avoid this
1272 # some sort of special support in the vcs layer to avoid this
1214 # workaround.
1273 # workaround.
1215 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1274 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1216 vcs_repo.alias == 'git'):
1275 vcs_repo.alias == 'git'):
1217 source_commit.raw_id = safe_str(source_ref_id)
1276 source_commit.raw_id = safe_str(source_ref_id)
1218
1277
1219 log.debug('calculating diff between '
1278 log.debug('calculating diff between '
1220 'source_ref:%s and target_ref:%s for repo `%s`',
1279 'source_ref:%s and target_ref:%s for repo `%s`',
1221 target_ref_id, source_ref_id,
1280 target_ref_id, source_ref_id,
1222 safe_unicode(vcs_repo.path))
1281 safe_unicode(vcs_repo.path))
1223
1282
1224 vcs_diff = vcs_repo.get_diff(
1283 vcs_diff = vcs_repo.get_diff(
1225 commit1=target_commit, commit2=source_commit, context=context)
1284 commit1=target_commit, commit2=source_commit, context=context)
1226 return vcs_diff
1285 return vcs_diff
1227
1286
1228 def _is_merge_enabled(self, pull_request):
1287 def _is_merge_enabled(self, pull_request):
1229 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1288 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1230 settings = settings_model.get_general_settings()
1289 settings = settings_model.get_general_settings()
1231 return settings.get('rhodecode_pr_merge_enabled', False)
1290 return settings.get('rhodecode_pr_merge_enabled', False)
1232
1291
1233 def _use_rebase_for_merging(self, pull_request):
1292 def _use_rebase_for_merging(self, pull_request):
1234 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1293 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1235 settings = settings_model.get_general_settings()
1294 settings = settings_model.get_general_settings()
1236 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1295 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1237
1296
1238 def _log_action(self, action, user, pull_request):
1297 def _log_action(self, action, user, pull_request):
1239 action_logger(
1298 action_logger(
1240 user,
1299 user,
1241 '{action}:{pr_id}'.format(
1300 '{action}:{pr_id}'.format(
1242 action=action, pr_id=pull_request.pull_request_id),
1301 action=action, pr_id=pull_request.pull_request_id),
1243 pull_request.target_repo)
1302 pull_request.target_repo)
1244
1303
1245
1304
1246 ChangeTuple = namedtuple('ChangeTuple',
1305 ChangeTuple = namedtuple('ChangeTuple',
1247 ['added', 'common', 'removed'])
1306 ['added', 'common', 'removed'])
1248
1307
1249 FileChangeTuple = namedtuple('FileChangeTuple',
1308 FileChangeTuple = namedtuple('FileChangeTuple',
1250 ['added', 'modified', 'removed'])
1309 ['added', 'modified', 'removed'])
@@ -1,155 +1,78 b''
1 <%namespace name="base" file="/base/base.html"/>
1 <%namespace name="base" file="/base/base.html"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-body">
4 <div class="panel-body">
5 %if c.show_closed:
5 %if c.show_closed:
6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
7 %else:
7 %else:
8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
9 %endif
9 %endif
10 </div>
10 </div>
11 </div>
11 </div>
12
12
13 <div class="panel panel-default">
13 <div class="panel panel-default">
14 <div class="panel-heading">
14 <div class="panel-heading">
15 <h3 class="panel-title">${_('Pull Requests You Opened')}: ${len(c.my_pull_requests)}</h3>
15 <h3 class="panel-title">${_('Pull Requests You Participate In')}: ${c.records_total_participate}</h3>
16 </div>
16 </div>
17 <div class="panel-body">
17 <div class="panel-body">
18 <div class="pullrequestlist">
18 <table id="pull_request_list_table_participate" class="display"></table>
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
72 </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
129 </div>
130 </div>
19 </div>
131 </div>
20 </div>
132
21
133 <script>
22 <script>
134 $('#show_closed').on('click', function(e){
23 $('#show_closed').on('click', function(e){
135 if($(this).is(":checked")){
24 if($(this).is(":checked")){
136 window.location = "${h.url('my_account_pullrequests', pr_show_closed=1)}";
25 window.location = "${h.url('my_account_pullrequests', pr_show_closed=1)}";
137 }
26 }
138 else{
27 else{
139 window.location = "${h.url('my_account_pullrequests')}";
28 window.location = "${h.url('my_account_pullrequests')}";
140 }
29 }
141 });
30 });
142 $('.expand_commit').on('click',function(e){
31 $(document).ready(function() {
143 var target_expand = $(this);
32
144 var cid = target_expand.data('prId');
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')){
50 // participating object list
147 $('#c-'+cid).css({'height': '2.75em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
51 $('#pull_request_list_table_participate').DataTable({
148 target_expand.removeClass('open');
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');
149 }
71 }
150 else {
72 if (data['owned']) {
151 $('#c-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible'});
73 $(row).addClass('owned');
152 target_expand.addClass('open');
74 }
153 }
75 }
154 });
76 });
77 });
155 </script>
78 </script>
@@ -1,299 +1,309 b''
1 ## DATA TABLE RE USABLE ELEMENTS
1 ## DATA TABLE RE USABLE ELEMENTS
2 ## usage:
2 ## usage:
3 ## <%namespace name="dt" file="/data_table/_dt_elements.html"/>
3 ## <%namespace name="dt" file="/data_table/_dt_elements.html"/>
4 <%namespace name="base" file="/base/base.html"/>
4 <%namespace name="base" file="/base/base.html"/>
5
5
6 ## REPOSITORY RENDERERS
6 ## REPOSITORY RENDERERS
7 <%def name="quick_menu(repo_name)">
7 <%def name="quick_menu(repo_name)">
8 <i class="pointer icon-more"></i>
8 <i class="pointer icon-more"></i>
9 <div class="menu_items_container hidden">
9 <div class="menu_items_container hidden">
10 <ul class="menu_items">
10 <ul class="menu_items">
11 <li>
11 <li>
12 <a title="${_('Summary')}" href="${h.url('summary_home',repo_name=repo_name)}">
12 <a title="${_('Summary')}" href="${h.url('summary_home',repo_name=repo_name)}">
13 <span>${_('Summary')}</span>
13 <span>${_('Summary')}</span>
14 </a>
14 </a>
15 </li>
15 </li>
16 <li>
16 <li>
17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
18 <span>${_('Changelog')}</span>
18 <span>${_('Changelog')}</span>
19 </a>
19 </a>
20 </li>
20 </li>
21 <li>
21 <li>
22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
23 <span>${_('Files')}</span>
23 <span>${_('Files')}</span>
24 </a>
24 </a>
25 </li>
25 </li>
26 <li>
26 <li>
27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 <span>${_('Fork')}</span>
28 <span>${_('Fork')}</span>
29 </a>
29 </a>
30 </li>
30 </li>
31 </ul>
31 </ul>
32 </div>
32 </div>
33 </%def>
33 </%def>
34
34
35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 <%
36 <%
37 def get_name(name,short_name=short_name):
37 def get_name(name,short_name=short_name):
38 if short_name:
38 if short_name:
39 return name.split('/')[-1]
39 return name.split('/')[-1]
40 else:
40 else:
41 return name
41 return name
42 %>
42 %>
43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 ##NAME
44 ##NAME
45 <a href="${h.url('edit_repo' if admin else 'summary_home',repo_name=name)}">
45 <a href="${h.url('edit_repo' if admin else 'summary_home',repo_name=name)}">
46
46
47 ##TYPE OF REPO
47 ##TYPE OF REPO
48 %if h.is_hg(rtype):
48 %if h.is_hg(rtype):
49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 %elif h.is_git(rtype):
50 %elif h.is_git(rtype):
51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 %elif h.is_svn(rtype):
52 %elif h.is_svn(rtype):
53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 %endif
54 %endif
55
55
56 ##PRIVATE/PUBLIC
56 ##PRIVATE/PUBLIC
57 %if private and c.visual.show_private_icon:
57 %if private and c.visual.show_private_icon:
58 <i class="icon-lock" title="${_('Private repository')}"></i>
58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 %elif not private and c.visual.show_public_icon:
59 %elif not private and c.visual.show_public_icon:
60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 %else:
61 %else:
62 <span></span>
62 <span></span>
63 %endif
63 %endif
64 ${get_name(name)}
64 ${get_name(name)}
65 </a>
65 </a>
66 %if fork_of:
66 %if fork_of:
67 <a href="${h.url('summary_home',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
67 <a href="${h.url('summary_home',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 %endif
68 %endif
69 %if rstate == 'repo_state_pending':
69 %if rstate == 'repo_state_pending':
70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 %endif
71 %endif
72 </div>
72 </div>
73 </%def>
73 </%def>
74
74
75 <%def name="last_change(last_change)">
75 <%def name="last_change(last_change)">
76 ${h.age_component(last_change)}
76 ${h.age_component(last_change)}
77 </%def>
77 </%def>
78
78
79 <%def name="revision(name,rev,tip,author,last_msg)">
79 <%def name="revision(name,rev,tip,author,last_msg)">
80 <div>
80 <div>
81 %if rev >= 0:
81 %if rev >= 0:
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>
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 %else:
83 %else:
84 ${_('No commits yet')}
84 ${_('No commits yet')}
85 %endif
85 %endif
86 </div>
86 </div>
87 </%def>
87 </%def>
88
88
89 <%def name="rss(name)">
89 <%def name="rss(name)">
90 %if c.rhodecode_user.username != h.DEFAULT_USER:
90 %if c.rhodecode_user.username != h.DEFAULT_USER:
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>
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 %else:
92 %else:
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>
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 %endif
94 %endif
95 </%def>
95 </%def>
96
96
97 <%def name="atom(name)">
97 <%def name="atom(name)">
98 %if c.rhodecode_user.username != h.DEFAULT_USER:
98 %if c.rhodecode_user.username != h.DEFAULT_USER:
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>
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 %else:
100 %else:
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>
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 %endif
102 %endif
103 </%def>
103 </%def>
104
104
105 <%def name="user_gravatar(email, size=16)">
105 <%def name="user_gravatar(email, size=16)">
106 <div class="rc-user tooltip" title="${h.author_string(email)}">
106 <div class="rc-user tooltip" title="${h.author_string(email)}">
107 ${base.gravatar(email, 16)}
107 ${base.gravatar(email, 16)}
108 </div>
108 </div>
109 </%def>
109 </%def>
110
110
111 <%def name="repo_actions(repo_name, super_user=True)">
111 <%def name="repo_actions(repo_name, super_user=True)">
112 <div>
112 <div>
113 <div class="grid_edit">
113 <div class="grid_edit">
114 <a href="${h.url('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
114 <a href="${h.url('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
115 <i class="icon-pencil"></i>Edit</a>
115 <i class="icon-pencil"></i>Edit</a>
116 </div>
116 </div>
117 <div class="grid_delete">
117 <div class="grid_delete">
118 ${h.secure_form(h.url('repo', repo_name=repo_name),method='delete')}
118 ${h.secure_form(h.url('repo', repo_name=repo_name),method='delete')}
119 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
119 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
120 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
120 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
121 ${h.end_form()}
121 ${h.end_form()}
122 </div>
122 </div>
123 </div>
123 </div>
124 </%def>
124 </%def>
125
125
126 <%def name="repo_state(repo_state)">
126 <%def name="repo_state(repo_state)">
127 <div>
127 <div>
128 %if repo_state == 'repo_state_pending':
128 %if repo_state == 'repo_state_pending':
129 <div class="tag tag4">${_('Creating')}</div>
129 <div class="tag tag4">${_('Creating')}</div>
130 %elif repo_state == 'repo_state_created':
130 %elif repo_state == 'repo_state_created':
131 <div class="tag tag1">${_('Created')}</div>
131 <div class="tag tag1">${_('Created')}</div>
132 %else:
132 %else:
133 <div class="tag alert2" title="${repo_state}">invalid</div>
133 <div class="tag alert2" title="${repo_state}">invalid</div>
134 %endif
134 %endif
135 </div>
135 </div>
136 </%def>
136 </%def>
137
137
138
138
139 ## REPO GROUP RENDERERS
139 ## REPO GROUP RENDERERS
140 <%def name="quick_repo_group_menu(repo_group_name)">
140 <%def name="quick_repo_group_menu(repo_group_name)">
141 <i class="pointer icon-more"></i>
141 <i class="pointer icon-more"></i>
142 <div class="menu_items_container hidden">
142 <div class="menu_items_container hidden">
143 <ul class="menu_items">
143 <ul class="menu_items">
144 <li>
144 <li>
145 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
145 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
146 <span class="icon">
146 <span class="icon">
147 <i class="icon-file-text"></i>
147 <i class="icon-file-text"></i>
148 </span>
148 </span>
149 <span>${_('Summary')}</span>
149 <span>${_('Summary')}</span>
150 </a>
150 </a>
151 </li>
151 </li>
152
152
153 </ul>
153 </ul>
154 </div>
154 </div>
155 </%def>
155 </%def>
156
156
157 <%def name="repo_group_name(repo_group_name, children_groups=None)">
157 <%def name="repo_group_name(repo_group_name, children_groups=None)">
158 <div>
158 <div>
159 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
159 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
160 <i class="icon-folder-close" title="${_('Repository group')}"></i>
160 <i class="icon-folder-close" title="${_('Repository group')}"></i>
161 %if children_groups:
161 %if children_groups:
162 ${h.literal(' &raquo; '.join(children_groups))}
162 ${h.literal(' &raquo; '.join(children_groups))}
163 %else:
163 %else:
164 ${repo_group_name}
164 ${repo_group_name}
165 %endif
165 %endif
166 </a>
166 </a>
167 </div>
167 </div>
168 </%def>
168 </%def>
169
169
170 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
170 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
171 <div class="grid_edit">
171 <div class="grid_edit">
172 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
172 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
173 </div>
173 </div>
174 <div class="grid_delete">
174 <div class="grid_delete">
175 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
175 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
176 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
176 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
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)+"');")}
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 ${h.end_form()}
178 ${h.end_form()}
179 </div>
179 </div>
180 </%def>
180 </%def>
181
181
182
182
183 <%def name="user_actions(user_id, username)">
183 <%def name="user_actions(user_id, username)">
184 <div class="grid_edit">
184 <div class="grid_edit">
185 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
185 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
186 <i class="icon-pencil"></i>Edit</a>
186 <i class="icon-pencil"></i>Edit</a>
187 </div>
187 </div>
188 <div class="grid_delete">
188 <div class="grid_delete">
189 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
189 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
190 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
190 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
191 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
191 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
192 ${h.end_form()}
192 ${h.end_form()}
193 </div>
193 </div>
194 </%def>
194 </%def>
195
195
196 <%def name="user_group_actions(user_group_id, user_group_name)">
196 <%def name="user_group_actions(user_group_id, user_group_name)">
197 <div class="grid_edit">
197 <div class="grid_edit">
198 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
198 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
199 </div>
199 </div>
200 <div class="grid_delete">
200 <div class="grid_delete">
201 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
201 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
202 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
202 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
203 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
203 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
204 ${h.end_form()}
204 ${h.end_form()}
205 </div>
205 </div>
206 </%def>
206 </%def>
207
207
208
208
209 <%def name="user_name(user_id, username)">
209 <%def name="user_name(user_id, username)">
210 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
210 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
211 </%def>
211 </%def>
212
212
213 <%def name="user_profile(username)">
213 <%def name="user_profile(username)">
214 ${base.gravatar_with_user(username, 16)}
214 ${base.gravatar_with_user(username, 16)}
215 </%def>
215 </%def>
216
216
217 <%def name="user_group_name(user_group_id, user_group_name)">
217 <%def name="user_group_name(user_group_id, user_group_name)">
218 <div>
218 <div>
219 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
219 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
220 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
220 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
221 </div>
221 </div>
222 </%def>
222 </%def>
223
223
224
224
225 ## GISTS
225 ## GISTS
226
226
227 <%def name="gist_gravatar(full_contact)">
227 <%def name="gist_gravatar(full_contact)">
228 <div class="gist_gravatar">
228 <div class="gist_gravatar">
229 ${base.gravatar(full_contact, 30)}
229 ${base.gravatar(full_contact, 30)}
230 </div>
230 </div>
231 </%def>
231 </%def>
232
232
233 <%def name="gist_access_id(gist_access_id, full_contact)">
233 <%def name="gist_access_id(gist_access_id, full_contact)">
234 <div>
234 <div>
235 <b>
235 <b>
236 <a href="${h.url('gist',gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
236 <a href="${h.url('gist',gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
237 </b>
237 </b>
238 </div>
238 </div>
239 </%def>
239 </%def>
240
240
241 <%def name="gist_author(full_contact, created_on, expires)">
241 <%def name="gist_author(full_contact, created_on, expires)">
242 ${base.gravatar_with_user(full_contact, 16)}
242 ${base.gravatar_with_user(full_contact, 16)}
243 </%def>
243 </%def>
244
244
245
245
246 <%def name="gist_created(created_on)">
246 <%def name="gist_created(created_on)">
247 <div class="created">
247 <div class="created">
248 ${h.age_component(created_on, time_is_local=True)}
248 ${h.age_component(created_on, time_is_local=True)}
249 </div>
249 </div>
250 </%def>
250 </%def>
251
251
252 <%def name="gist_expires(expires)">
252 <%def name="gist_expires(expires)">
253 <div class="created">
253 <div class="created">
254 %if expires == -1:
254 %if expires == -1:
255 ${_('never')}
255 ${_('never')}
256 %else:
256 %else:
257 ${h.age_component(h.time_to_utcdatetime(expires))}
257 ${h.age_component(h.time_to_utcdatetime(expires))}
258 %endif
258 %endif
259 </div>
259 </div>
260 </%def>
260 </%def>
261
261
262 <%def name="gist_type(gist_type)">
262 <%def name="gist_type(gist_type)">
263 %if gist_type != 'public':
263 %if gist_type != 'public':
264 <div class="tag">${_('Private')}</div>
264 <div class="tag">${_('Private')}</div>
265 %endif
265 %endif
266 </%def>
266 </%def>
267
267
268 <%def name="gist_description(gist_description)">
268 <%def name="gist_description(gist_description)">
269 ${gist_description}
269 ${gist_description}
270 </%def>
270 </%def>
271
271
272
272
273 ## PULL REQUESTS GRID RENDERERS
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 <%def name="pullrequest_status(status)">
280 <%def name="pullrequest_status(status)">
275 <div class="${'flag_status %s' % status} pull-left"></div>
281 <div class="${'flag_status %s' % status} pull-left"></div>
276 </%def>
282 </%def>
277
283
278 <%def name="pullrequest_title(title, description)">
284 <%def name="pullrequest_title(title, description)">
279 ${title} <br/>
285 ${title} <br/>
280 ${h.shorter(description, 40)}
286 ${h.shorter(description, 40)}
281 </%def>
287 </%def>
282
288
283 <%def name="pullrequest_comments(comments_nr)">
289 <%def name="pullrequest_comments(comments_nr)">
284 <i class="icon-comment icon-comment-colored"></i> ${comments_nr}
290 <i class="icon-comment icon-comment-colored"></i> ${comments_nr}
285 </%def>
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 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
294 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
295 % if short:
296 #${pull_request_id}
297 % else:
289 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
298 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
299 % endif
290 </a>
300 </a>
291 </%def>
301 </%def>
292
302
293 <%def name="pullrequest_updated_on(updated_on)">
303 <%def name="pullrequest_updated_on(updated_on)">
294 ${h.age_component(h.time_to_utcdatetime(updated_on))}
304 ${h.age_component(h.time_to_utcdatetime(updated_on))}
295 </%def>
305 </%def>
296
306
297 <%def name="pullrequest_author(full_contact)">
307 <%def name="pullrequest_author(full_contact)">
298 ${base.gravatar_with_user(full_contact, 16)}
308 ${base.gravatar_with_user(full_contact, 16)}
299 </%def>
309 </%def>
@@ -1,397 +1,396 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib import helpers as h
23 from rhodecode.lib import helpers as h
24 from rhodecode.lib.auth import check_password
24 from rhodecode.lib.auth import check_password
25 from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys
25 from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.tests import (
27 from rhodecode.tests import (
28 TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
28 TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
29 assert_session_flash)
29 assert_session_flash)
30 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.fixture import Fixture
31 from rhodecode.tests.utils import AssertResponse
31 from rhodecode.tests.utils import AssertResponse
32
32
33 fixture = Fixture()
33 fixture = Fixture()
34
34
35
35
36 class TestMyAccountController(TestController):
36 class TestMyAccountController(TestController):
37 test_user_1 = 'testme'
37 test_user_1 = 'testme'
38 test_user_1_password = '0jd83nHNS/d23n'
38 test_user_1_password = '0jd83nHNS/d23n'
39 destroy_users = set()
39 destroy_users = set()
40
40
41 @classmethod
41 @classmethod
42 def teardown_class(cls):
42 def teardown_class(cls):
43 fixture.destroy_users(cls.destroy_users)
43 fixture.destroy_users(cls.destroy_users)
44
44
45 def test_my_account(self):
45 def test_my_account(self):
46 self.log_user()
46 self.log_user()
47 response = self.app.get(url('my_account'))
47 response = self.app.get(url('my_account'))
48
48
49 response.mustcontain('test_admin')
49 response.mustcontain('test_admin')
50 response.mustcontain('href="/_admin/my_account/edit"')
50 response.mustcontain('href="/_admin/my_account/edit"')
51
51
52 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
52 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
53 response = self.app.get(url('my_account'))
53 response = self.app.get(url('my_account'))
54 assert_response = AssertResponse(response)
54 assert_response = AssertResponse(response)
55 element = assert_response.get_element('.logout #csrf_token')
55 element = assert_response.get_element('.logout #csrf_token')
56 assert element.value == csrf_token
56 assert element.value == csrf_token
57
57
58 def test_my_account_edit(self):
58 def test_my_account_edit(self):
59 self.log_user()
59 self.log_user()
60 response = self.app.get(url('my_account_edit'))
60 response = self.app.get(url('my_account_edit'))
61
61
62 response.mustcontain('value="test_admin')
62 response.mustcontain('value="test_admin')
63
63
64 def test_my_account_my_repos(self):
64 def test_my_account_my_repos(self):
65 self.log_user()
65 self.log_user()
66 response = self.app.get(url('my_account_repos'))
66 response = self.app.get(url('my_account_repos'))
67 repos = Repository.query().filter(
67 repos = Repository.query().filter(
68 Repository.user == User.get_by_username(
68 Repository.user == User.get_by_username(
69 TEST_USER_ADMIN_LOGIN)).all()
69 TEST_USER_ADMIN_LOGIN)).all()
70 for repo in repos:
70 for repo in repos:
71 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
71 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
72
72
73 def test_my_account_my_watched(self):
73 def test_my_account_my_watched(self):
74 self.log_user()
74 self.log_user()
75 response = self.app.get(url('my_account_watched'))
75 response = self.app.get(url('my_account_watched'))
76
76
77 repos = UserFollowing.query().filter(
77 repos = UserFollowing.query().filter(
78 UserFollowing.user == User.get_by_username(
78 UserFollowing.user == User.get_by_username(
79 TEST_USER_ADMIN_LOGIN)).all()
79 TEST_USER_ADMIN_LOGIN)).all()
80 for repo in repos:
80 for repo in repos:
81 response.mustcontain(
81 response.mustcontain(
82 '"name_raw": "%s"' % repo.follows_repository.repo_name)
82 '"name_raw": "%s"' % repo.follows_repository.repo_name)
83
83
84 @pytest.mark.backends("git", "hg")
84 @pytest.mark.backends("git", "hg")
85 def test_my_account_my_pullrequests(self, pr_util):
85 def test_my_account_my_pullrequests(self, pr_util):
86 self.log_user()
86 self.log_user()
87 response = self.app.get(url('my_account_pullrequests'))
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 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 response = self.app.get(url('my_account_pullrequests'))
92 response = self.app.get(url('my_account_pullrequests'))
92 response.mustcontain('There are currently no open pull requests '
93 response.mustcontain('"name_raw": %s' % pr.pull_request_id)
93 'requiring your participation')
94 response.mustcontain('TestMyAccountPR')
94
95 response.mustcontain('#%s: TestMyAccountPR' % pr.pull_request_id)
96
95
97 def test_my_account_my_emails(self):
96 def test_my_account_my_emails(self):
98 self.log_user()
97 self.log_user()
99 response = self.app.get(url('my_account_emails'))
98 response = self.app.get(url('my_account_emails'))
100 response.mustcontain('No additional emails specified')
99 response.mustcontain('No additional emails specified')
101
100
102 def test_my_account_my_emails_add_existing_email(self):
101 def test_my_account_my_emails_add_existing_email(self):
103 self.log_user()
102 self.log_user()
104 response = self.app.get(url('my_account_emails'))
103 response = self.app.get(url('my_account_emails'))
105 response.mustcontain('No additional emails specified')
104 response.mustcontain('No additional emails specified')
106 response = self.app.post(url('my_account_emails'),
105 response = self.app.post(url('my_account_emails'),
107 {'new_email': TEST_USER_REGULAR_EMAIL,
106 {'new_email': TEST_USER_REGULAR_EMAIL,
108 'csrf_token': self.csrf_token})
107 'csrf_token': self.csrf_token})
109 assert_session_flash(response, 'This e-mail address is already taken')
108 assert_session_flash(response, 'This e-mail address is already taken')
110
109
111 def test_my_account_my_emails_add_mising_email_in_form(self):
110 def test_my_account_my_emails_add_mising_email_in_form(self):
112 self.log_user()
111 self.log_user()
113 response = self.app.get(url('my_account_emails'))
112 response = self.app.get(url('my_account_emails'))
114 response.mustcontain('No additional emails specified')
113 response.mustcontain('No additional emails specified')
115 response = self.app.post(url('my_account_emails'),
114 response = self.app.post(url('my_account_emails'),
116 {'csrf_token': self.csrf_token})
115 {'csrf_token': self.csrf_token})
117 assert_session_flash(response, 'Please enter an email address')
116 assert_session_flash(response, 'Please enter an email address')
118
117
119 def test_my_account_my_emails_add_remove(self):
118 def test_my_account_my_emails_add_remove(self):
120 self.log_user()
119 self.log_user()
121 response = self.app.get(url('my_account_emails'))
120 response = self.app.get(url('my_account_emails'))
122 response.mustcontain('No additional emails specified')
121 response.mustcontain('No additional emails specified')
123
122
124 response = self.app.post(url('my_account_emails'),
123 response = self.app.post(url('my_account_emails'),
125 {'new_email': 'foo@barz.com',
124 {'new_email': 'foo@barz.com',
126 'csrf_token': self.csrf_token})
125 'csrf_token': self.csrf_token})
127
126
128 response = self.app.get(url('my_account_emails'))
127 response = self.app.get(url('my_account_emails'))
129
128
130 from rhodecode.model.db import UserEmailMap
129 from rhodecode.model.db import UserEmailMap
131 email_id = UserEmailMap.query().filter(
130 email_id = UserEmailMap.query().filter(
132 UserEmailMap.user == User.get_by_username(
131 UserEmailMap.user == User.get_by_username(
133 TEST_USER_ADMIN_LOGIN)).filter(
132 TEST_USER_ADMIN_LOGIN)).filter(
134 UserEmailMap.email == 'foo@barz.com').one().email_id
133 UserEmailMap.email == 'foo@barz.com').one().email_id
135
134
136 response.mustcontain('foo@barz.com')
135 response.mustcontain('foo@barz.com')
137 response.mustcontain('<input id="del_email_id" name="del_email_id" '
136 response.mustcontain('<input id="del_email_id" name="del_email_id" '
138 'type="hidden" value="%s" />' % email_id)
137 'type="hidden" value="%s" />' % email_id)
139
138
140 response = self.app.post(
139 response = self.app.post(
141 url('my_account_emails'), {
140 url('my_account_emails'), {
142 'del_email_id': email_id, '_method': 'delete',
141 'del_email_id': email_id, '_method': 'delete',
143 'csrf_token': self.csrf_token})
142 'csrf_token': self.csrf_token})
144 assert_session_flash(response, 'Removed email address from user account')
143 assert_session_flash(response, 'Removed email address from user account')
145 response = self.app.get(url('my_account_emails'))
144 response = self.app.get(url('my_account_emails'))
146 response.mustcontain('No additional emails specified')
145 response.mustcontain('No additional emails specified')
147
146
148 @pytest.mark.parametrize(
147 @pytest.mark.parametrize(
149 "name, attrs", [
148 "name, attrs", [
150 ('firstname', {'firstname': 'new_username'}),
149 ('firstname', {'firstname': 'new_username'}),
151 ('lastname', {'lastname': 'new_username'}),
150 ('lastname', {'lastname': 'new_username'}),
152 ('admin', {'admin': True}),
151 ('admin', {'admin': True}),
153 ('admin', {'admin': False}),
152 ('admin', {'admin': False}),
154 ('extern_type', {'extern_type': 'ldap'}),
153 ('extern_type', {'extern_type': 'ldap'}),
155 ('extern_type', {'extern_type': None}),
154 ('extern_type', {'extern_type': None}),
156 # ('extern_name', {'extern_name': 'test'}),
155 # ('extern_name', {'extern_name': 'test'}),
157 # ('extern_name', {'extern_name': None}),
156 # ('extern_name', {'extern_name': None}),
158 ('active', {'active': False}),
157 ('active', {'active': False}),
159 ('active', {'active': True}),
158 ('active', {'active': True}),
160 ('email', {'email': 'some@email.com'}),
159 ('email', {'email': 'some@email.com'}),
161 ])
160 ])
162 def test_my_account_update(self, name, attrs):
161 def test_my_account_update(self, name, attrs):
163 usr = fixture.create_user(self.test_user_1,
162 usr = fixture.create_user(self.test_user_1,
164 password=self.test_user_1_password,
163 password=self.test_user_1_password,
165 email='testme@rhodecode.org',
164 email='testme@rhodecode.org',
166 extern_type='rhodecode',
165 extern_type='rhodecode',
167 extern_name=self.test_user_1,
166 extern_name=self.test_user_1,
168 skip_if_exists=True)
167 skip_if_exists=True)
169 self.destroy_users.add(self.test_user_1)
168 self.destroy_users.add(self.test_user_1)
170
169
171 params = usr.get_api_data() # current user data
170 params = usr.get_api_data() # current user data
172 user_id = usr.user_id
171 user_id = usr.user_id
173 self.log_user(
172 self.log_user(
174 username=self.test_user_1, password=self.test_user_1_password)
173 username=self.test_user_1, password=self.test_user_1_password)
175
174
176 params.update({'password_confirmation': ''})
175 params.update({'password_confirmation': ''})
177 params.update({'new_password': ''})
176 params.update({'new_password': ''})
178 params.update({'extern_type': 'rhodecode'})
177 params.update({'extern_type': 'rhodecode'})
179 params.update({'extern_name': self.test_user_1})
178 params.update({'extern_name': self.test_user_1})
180 params.update({'csrf_token': self.csrf_token})
179 params.update({'csrf_token': self.csrf_token})
181
180
182 params.update(attrs)
181 params.update(attrs)
183 # my account page cannot set language param yet, only for admins
182 # my account page cannot set language param yet, only for admins
184 del params['language']
183 del params['language']
185 response = self.app.post(url('my_account'), params)
184 response = self.app.post(url('my_account'), params)
186
185
187 assert_session_flash(
186 assert_session_flash(
188 response, 'Your account was updated successfully')
187 response, 'Your account was updated successfully')
189
188
190 del params['csrf_token']
189 del params['csrf_token']
191
190
192 updated_user = User.get_by_username(self.test_user_1)
191 updated_user = User.get_by_username(self.test_user_1)
193 updated_params = updated_user.get_api_data()
192 updated_params = updated_user.get_api_data()
194 updated_params.update({'password_confirmation': ''})
193 updated_params.update({'password_confirmation': ''})
195 updated_params.update({'new_password': ''})
194 updated_params.update({'new_password': ''})
196
195
197 params['last_login'] = updated_params['last_login']
196 params['last_login'] = updated_params['last_login']
198 # my account page cannot set language param yet, only for admins
197 # my account page cannot set language param yet, only for admins
199 # but we get this info from API anyway
198 # but we get this info from API anyway
200 params['language'] = updated_params['language']
199 params['language'] = updated_params['language']
201
200
202 if name == 'email':
201 if name == 'email':
203 params['emails'] = [attrs['email']]
202 params['emails'] = [attrs['email']]
204 if name == 'extern_type':
203 if name == 'extern_type':
205 # cannot update this via form, expected value is original one
204 # cannot update this via form, expected value is original one
206 params['extern_type'] = "rhodecode"
205 params['extern_type'] = "rhodecode"
207 if name == 'extern_name':
206 if name == 'extern_name':
208 # cannot update this via form, expected value is original one
207 # cannot update this via form, expected value is original one
209 params['extern_name'] = str(user_id)
208 params['extern_name'] = str(user_id)
210 if name == 'active':
209 if name == 'active':
211 # my account cannot deactivate account
210 # my account cannot deactivate account
212 params['active'] = True
211 params['active'] = True
213 if name == 'admin':
212 if name == 'admin':
214 # my account cannot make you an admin !
213 # my account cannot make you an admin !
215 params['admin'] = False
214 params['admin'] = False
216
215
217 assert params == updated_params
216 assert params == updated_params
218
217
219 def test_my_account_update_err_email_exists(self):
218 def test_my_account_update_err_email_exists(self):
220 self.log_user()
219 self.log_user()
221
220
222 new_email = 'test_regular@mail.com' # already exisitn email
221 new_email = 'test_regular@mail.com' # already exisitn email
223 response = self.app.post(url('my_account'),
222 response = self.app.post(url('my_account'),
224 params={
223 params={
225 'username': 'test_admin',
224 'username': 'test_admin',
226 'new_password': 'test12',
225 'new_password': 'test12',
227 'password_confirmation': 'test122',
226 'password_confirmation': 'test122',
228 'firstname': 'NewName',
227 'firstname': 'NewName',
229 'lastname': 'NewLastname',
228 'lastname': 'NewLastname',
230 'email': new_email,
229 'email': new_email,
231 'csrf_token': self.csrf_token,
230 'csrf_token': self.csrf_token,
232 })
231 })
233
232
234 response.mustcontain('This e-mail address is already taken')
233 response.mustcontain('This e-mail address is already taken')
235
234
236 def test_my_account_update_err(self):
235 def test_my_account_update_err(self):
237 self.log_user('test_regular2', 'test12')
236 self.log_user('test_regular2', 'test12')
238
237
239 new_email = 'newmail.pl'
238 new_email = 'newmail.pl'
240 response = self.app.post(url('my_account'),
239 response = self.app.post(url('my_account'),
241 params={
240 params={
242 'username': 'test_admin',
241 'username': 'test_admin',
243 'new_password': 'test12',
242 'new_password': 'test12',
244 'password_confirmation': 'test122',
243 'password_confirmation': 'test122',
245 'firstname': 'NewName',
244 'firstname': 'NewName',
246 'lastname': 'NewLastname',
245 'lastname': 'NewLastname',
247 'email': new_email,
246 'email': new_email,
248 'csrf_token': self.csrf_token,
247 'csrf_token': self.csrf_token,
249 })
248 })
250
249
251 response.mustcontain('An email address must contain a single @')
250 response.mustcontain('An email address must contain a single @')
252 from rhodecode.model import validators
251 from rhodecode.model import validators
253 msg = validators.ValidUsername(
252 msg = validators.ValidUsername(
254 edit=False, old_data={})._messages['username_exists']
253 edit=False, old_data={})._messages['username_exists']
255 msg = h.html_escape(msg % {'username': 'test_admin'})
254 msg = h.html_escape(msg % {'username': 'test_admin'})
256 response.mustcontain(u"%s" % msg)
255 response.mustcontain(u"%s" % msg)
257
256
258 def test_my_account_auth_tokens(self):
257 def test_my_account_auth_tokens(self):
259 usr = self.log_user('test_regular2', 'test12')
258 usr = self.log_user('test_regular2', 'test12')
260 user = User.get(usr['user_id'])
259 user = User.get(usr['user_id'])
261 response = self.app.get(url('my_account_auth_tokens'))
260 response = self.app.get(url('my_account_auth_tokens'))
262 response.mustcontain(user.api_key)
261 response.mustcontain(user.api_key)
263 response.mustcontain('expires: never')
262 response.mustcontain('expires: never')
264
263
265 @pytest.mark.parametrize("desc, lifetime", [
264 @pytest.mark.parametrize("desc, lifetime", [
266 ('forever', -1),
265 ('forever', -1),
267 ('5mins', 60*5),
266 ('5mins', 60*5),
268 ('30days', 60*60*24*30),
267 ('30days', 60*60*24*30),
269 ])
268 ])
270 def test_my_account_add_auth_tokens(self, desc, lifetime):
269 def test_my_account_add_auth_tokens(self, desc, lifetime):
271 usr = self.log_user('test_regular2', 'test12')
270 usr = self.log_user('test_regular2', 'test12')
272 user = User.get(usr['user_id'])
271 user = User.get(usr['user_id'])
273 response = self.app.post(url('my_account_auth_tokens'),
272 response = self.app.post(url('my_account_auth_tokens'),
274 {'description': desc, 'lifetime': lifetime,
273 {'description': desc, 'lifetime': lifetime,
275 'csrf_token': self.csrf_token})
274 'csrf_token': self.csrf_token})
276 assert_session_flash(response, 'Auth token successfully created')
275 assert_session_flash(response, 'Auth token successfully created')
277 try:
276 try:
278 response = response.follow()
277 response = response.follow()
279 user = User.get(usr['user_id'])
278 user = User.get(usr['user_id'])
280 for auth_token in user.auth_tokens:
279 for auth_token in user.auth_tokens:
281 response.mustcontain(auth_token)
280 response.mustcontain(auth_token)
282 finally:
281 finally:
283 for auth_token in UserApiKeys.query().all():
282 for auth_token in UserApiKeys.query().all():
284 Session().delete(auth_token)
283 Session().delete(auth_token)
285 Session().commit()
284 Session().commit()
286
285
287 def test_my_account_remove_auth_token(self):
286 def test_my_account_remove_auth_token(self):
288 # TODO: without this cleanup it fails when run with the whole
287 # TODO: without this cleanup it fails when run with the whole
289 # test suite, so there must be some interference with other tests.
288 # test suite, so there must be some interference with other tests.
290 UserApiKeys.query().delete()
289 UserApiKeys.query().delete()
291
290
292 usr = self.log_user('test_regular2', 'test12')
291 usr = self.log_user('test_regular2', 'test12')
293 User.get(usr['user_id'])
292 User.get(usr['user_id'])
294 response = self.app.post(url('my_account_auth_tokens'),
293 response = self.app.post(url('my_account_auth_tokens'),
295 {'description': 'desc', 'lifetime': -1,
294 {'description': 'desc', 'lifetime': -1,
296 'csrf_token': self.csrf_token})
295 'csrf_token': self.csrf_token})
297 assert_session_flash(response, 'Auth token successfully created')
296 assert_session_flash(response, 'Auth token successfully created')
298 response = response.follow()
297 response = response.follow()
299
298
300 # now delete our key
299 # now delete our key
301 keys = UserApiKeys.query().all()
300 keys = UserApiKeys.query().all()
302 assert 1 == len(keys)
301 assert 1 == len(keys)
303
302
304 response = self.app.post(
303 response = self.app.post(
305 url('my_account_auth_tokens'),
304 url('my_account_auth_tokens'),
306 {'_method': 'delete', 'del_auth_token': keys[0].api_key,
305 {'_method': 'delete', 'del_auth_token': keys[0].api_key,
307 'csrf_token': self.csrf_token})
306 'csrf_token': self.csrf_token})
308 assert_session_flash(response, 'Auth token successfully deleted')
307 assert_session_flash(response, 'Auth token successfully deleted')
309 keys = UserApiKeys.query().all()
308 keys = UserApiKeys.query().all()
310 assert 0 == len(keys)
309 assert 0 == len(keys)
311
310
312 def test_my_account_reset_main_auth_token(self):
311 def test_my_account_reset_main_auth_token(self):
313 usr = self.log_user('test_regular2', 'test12')
312 usr = self.log_user('test_regular2', 'test12')
314 user = User.get(usr['user_id'])
313 user = User.get(usr['user_id'])
315 api_key = user.api_key
314 api_key = user.api_key
316 response = self.app.get(url('my_account_auth_tokens'))
315 response = self.app.get(url('my_account_auth_tokens'))
317 response.mustcontain(api_key)
316 response.mustcontain(api_key)
318 response.mustcontain('expires: never')
317 response.mustcontain('expires: never')
319
318
320 response = self.app.post(
319 response = self.app.post(
321 url('my_account_auth_tokens'),
320 url('my_account_auth_tokens'),
322 {'_method': 'delete', 'del_auth_token_builtin': api_key,
321 {'_method': 'delete', 'del_auth_token_builtin': api_key,
323 'csrf_token': self.csrf_token})
322 'csrf_token': self.csrf_token})
324 assert_session_flash(response, 'Auth token successfully reset')
323 assert_session_flash(response, 'Auth token successfully reset')
325 response = response.follow()
324 response = response.follow()
326 response.mustcontain(no=[api_key])
325 response.mustcontain(no=[api_key])
327
326
328 def test_valid_change_password(self, user_util):
327 def test_valid_change_password(self, user_util):
329 new_password = 'my_new_valid_password'
328 new_password = 'my_new_valid_password'
330 user = user_util.create_user(password=self.test_user_1_password)
329 user = user_util.create_user(password=self.test_user_1_password)
331 session = self.log_user(user.username, self.test_user_1_password)
330 session = self.log_user(user.username, self.test_user_1_password)
332 form_data = [
331 form_data = [
333 ('current_password', self.test_user_1_password),
332 ('current_password', self.test_user_1_password),
334 ('__start__', 'new_password:mapping'),
333 ('__start__', 'new_password:mapping'),
335 ('new_password', new_password),
334 ('new_password', new_password),
336 ('new_password-confirm', new_password),
335 ('new_password-confirm', new_password),
337 ('__end__', 'new_password:mapping'),
336 ('__end__', 'new_password:mapping'),
338 ('csrf_token', self.csrf_token),
337 ('csrf_token', self.csrf_token),
339 ]
338 ]
340 response = self.app.post(url('my_account_password'), form_data).follow()
339 response = self.app.post(url('my_account_password'), form_data).follow()
341 assert 'Successfully updated password' in response
340 assert 'Successfully updated password' in response
342
341
343 # check_password depends on user being in session
342 # check_password depends on user being in session
344 Session().add(user)
343 Session().add(user)
345 try:
344 try:
346 assert check_password(new_password, user.password)
345 assert check_password(new_password, user.password)
347 finally:
346 finally:
348 Session().expunge(user)
347 Session().expunge(user)
349
348
350 @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [
349 @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [
351 ('', 'abcdef123', 'abcdef123'),
350 ('', 'abcdef123', 'abcdef123'),
352 ('wrong_pw', 'abcdef123', 'abcdef123'),
351 ('wrong_pw', 'abcdef123', 'abcdef123'),
353 (test_user_1_password, test_user_1_password, test_user_1_password),
352 (test_user_1_password, test_user_1_password, test_user_1_password),
354 (test_user_1_password, '', ''),
353 (test_user_1_password, '', ''),
355 (test_user_1_password, 'abcdef123', ''),
354 (test_user_1_password, 'abcdef123', ''),
356 (test_user_1_password, '', 'abcdef123'),
355 (test_user_1_password, '', 'abcdef123'),
357 (test_user_1_password, 'not_the', 'same_pw'),
356 (test_user_1_password, 'not_the', 'same_pw'),
358 (test_user_1_password, 'short', 'short'),
357 (test_user_1_password, 'short', 'short'),
359 ])
358 ])
360 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
359 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
361 user_util):
360 user_util):
362 user = user_util.create_user(password=self.test_user_1_password)
361 user = user_util.create_user(password=self.test_user_1_password)
363 session = self.log_user(user.username, self.test_user_1_password)
362 session = self.log_user(user.username, self.test_user_1_password)
364 old_password_hash = session['password']
363 old_password_hash = session['password']
365 form_data = [
364 form_data = [
366 ('current_password', current_pw),
365 ('current_password', current_pw),
367 ('__start__', 'new_password:mapping'),
366 ('__start__', 'new_password:mapping'),
368 ('new_password', new_pw),
367 ('new_password', new_pw),
369 ('new_password-confirm', confirm_pw),
368 ('new_password-confirm', confirm_pw),
370 ('__end__', 'new_password:mapping'),
369 ('__end__', 'new_password:mapping'),
371 ('csrf_token', self.csrf_token),
370 ('csrf_token', self.csrf_token),
372 ]
371 ]
373 response = self.app.post(url('my_account_password'), form_data)
372 response = self.app.post(url('my_account_password'), form_data)
374 assert 'Error occurred' in response
373 assert 'Error occurred' in response
375
374
376 def test_password_is_updated_in_session_on_password_change(self, user_util):
375 def test_password_is_updated_in_session_on_password_change(self, user_util):
377 old_password = 'abcdef123'
376 old_password = 'abcdef123'
378 new_password = 'abcdef124'
377 new_password = 'abcdef124'
379
378
380 user = user_util.create_user(password=old_password)
379 user = user_util.create_user(password=old_password)
381 session = self.log_user(user.username, old_password)
380 session = self.log_user(user.username, old_password)
382 old_password_hash = session['password']
381 old_password_hash = session['password']
383
382
384 form_data = [
383 form_data = [
385 ('current_password', old_password),
384 ('current_password', old_password),
386 ('__start__', 'new_password:mapping'),
385 ('__start__', 'new_password:mapping'),
387 ('new_password', new_password),
386 ('new_password', new_password),
388 ('new_password-confirm', new_password),
387 ('new_password-confirm', new_password),
389 ('__end__', 'new_password:mapping'),
388 ('__end__', 'new_password:mapping'),
390 ('csrf_token', self.csrf_token),
389 ('csrf_token', self.csrf_token),
391 ]
390 ]
392 self.app.post(url('my_account_password'), form_data)
391 self.app.post(url('my_account_password'), form_data)
393
392
394 response = self.app.get(url('home'))
393 response = self.app.get(url('home'))
395 new_password_hash = response.session['rhodecode_user']['password']
394 new_password_hash = response.session['rhodecode_user']['password']
396
395
397 assert old_password_hash != new_password_hash
396 assert old_password_hash != new_password_hash
General Comments 0
You need to be logged in to leave comments. Login now