##// 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

The requested changes are too big and content was truncated. Show full diff

@@ -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 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -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