##// 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 my account controller for RhodeCode admin
24 24 """
25 25
26 26 import logging
27 27
28 28 import formencode
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, session
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from sqlalchemy.orm import joinedload
34 34
35 35 from rhodecode import forms
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib import auth
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
40 40 from rhodecode.lib.base import BaseController, render
41 41 from rhodecode.lib.utils import jsonify
42 from rhodecode.lib.utils2 import safe_int, md5
42 from rhodecode.lib.utils2 import safe_int, md5, str2bool
43 43 from rhodecode.lib.ext_json import json
44 44
45 45 from rhodecode.model.validation_schema.schemas import user_schema
46 46 from rhodecode.model.db import (
47 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
48 UserFollowing)
47 Repository, PullRequest, UserEmailMap, User, UserFollowing)
49 48 from rhodecode.model.forms import UserForm
50 49 from rhodecode.model.scm import RepoList
51 50 from rhodecode.model.user import UserModel
52 51 from rhodecode.model.repo import RepoModel
53 52 from rhodecode.model.auth_token import AuthTokenModel
54 53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel
55 from rhodecode.model.comment import ChangesetCommentsModel
55 56
56 57 log = logging.getLogger(__name__)
57 58
58 59
59 60 class MyAccountController(BaseController):
60 61 """REST Controller styled on the Atom Publishing Protocol"""
61 62 # To properly map this controller, ensure your config/routing.py
62 63 # file has a resource setup:
63 64 # map.resource('setting', 'settings', controller='admin/settings',
64 65 # path_prefix='/admin', name_prefix='admin_')
65 66
66 67 @LoginRequired()
67 68 @NotAnonymous()
68 69 def __before__(self):
69 70 super(MyAccountController, self).__before__()
70 71
71 72 def __load_data(self):
72 73 c.user = User.get(c.rhodecode_user.user_id)
73 74 if c.user.username == User.DEFAULT_USER:
74 75 h.flash(_("You can't edit this user since it's"
75 76 " crucial for entire application"), category='warning')
76 77 return redirect(url('users'))
77 78
78 79 def _load_my_repos_data(self, watched=False):
79 80 if watched:
80 81 admin = False
81 82 follows_repos = Session().query(UserFollowing)\
82 83 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
83 84 .options(joinedload(UserFollowing.follows_repository))\
84 85 .all()
85 86 repo_list = [x.follows_repository for x in follows_repos]
86 87 else:
87 88 admin = True
88 89 repo_list = Repository.get_all_repos(
89 90 user_id=c.rhodecode_user.user_id)
90 91 repo_list = RepoList(repo_list, perm_set=[
91 92 'repository.read', 'repository.write', 'repository.admin'])
92 93
93 94 repos_data = RepoModel().get_repos_as_dict(
94 95 repo_list=repo_list, admin=admin)
95 96 # json used to render the grid
96 97 return json.dumps(repos_data)
97 98
98 99 @auth.CSRFRequired()
99 100 def my_account_update(self):
100 101 """
101 102 POST /_admin/my_account Updates info of my account
102 103 """
103 104 # url('my_account')
104 105 c.active = 'profile_edit'
105 106 self.__load_data()
106 107 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
107 108 ip_addr=self.ip_addr)
108 109 c.extern_type = c.user.extern_type
109 110 c.extern_name = c.user.extern_name
110 111
111 112 defaults = c.user.get_dict()
112 113 update = False
113 114 _form = UserForm(edit=True,
114 115 old_data={'user_id': c.rhodecode_user.user_id,
115 116 'email': c.rhodecode_user.email})()
116 117 form_result = {}
117 118 try:
118 119 post_data = dict(request.POST)
119 120 post_data['new_password'] = ''
120 121 post_data['password_confirmation'] = ''
121 122 form_result = _form.to_python(post_data)
122 123 # skip updating those attrs for my account
123 124 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
124 125 'new_password', 'password_confirmation']
125 126 # TODO: plugin should define if username can be updated
126 127 if c.extern_type != "rhodecode":
127 128 # forbid updating username for external accounts
128 129 skip_attrs.append('username')
129 130
130 131 UserModel().update_user(
131 132 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
132 133 h.flash(_('Your account was updated successfully'),
133 134 category='success')
134 135 Session().commit()
135 136 update = True
136 137
137 138 except formencode.Invalid as errors:
138 139 return htmlfill.render(
139 140 render('admin/my_account/my_account.html'),
140 141 defaults=errors.value,
141 142 errors=errors.error_dict or {},
142 143 prefix_error=False,
143 144 encoding="UTF-8",
144 145 force_defaults=False)
145 146 except Exception:
146 147 log.exception("Exception updating user")
147 148 h.flash(_('Error occurred during update of user %s')
148 149 % form_result.get('username'), category='error')
149 150
150 151 if update:
151 152 return redirect('my_account')
152 153
153 154 return htmlfill.render(
154 155 render('admin/my_account/my_account.html'),
155 156 defaults=defaults,
156 157 encoding="UTF-8",
157 158 force_defaults=False
158 159 )
159 160
160 161 def my_account(self):
161 162 """
162 163 GET /_admin/my_account Displays info about my account
163 164 """
164 165 # url('my_account')
165 166 c.active = 'profile'
166 167 self.__load_data()
167 168
168 169 defaults = c.user.get_dict()
169 170 return htmlfill.render(
170 171 render('admin/my_account/my_account.html'),
171 172 defaults=defaults, encoding="UTF-8", force_defaults=False)
172 173
173 174 def my_account_edit(self):
174 175 """
175 176 GET /_admin/my_account/edit Displays edit form of my account
176 177 """
177 178 c.active = 'profile_edit'
178 179 self.__load_data()
179 180 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
180 181 ip_addr=self.ip_addr)
181 182 c.extern_type = c.user.extern_type
182 183 c.extern_name = c.user.extern_name
183 184
184 185 defaults = c.user.get_dict()
185 186 return htmlfill.render(
186 187 render('admin/my_account/my_account.html'),
187 188 defaults=defaults,
188 189 encoding="UTF-8",
189 190 force_defaults=False
190 191 )
191 192
192 193 @auth.CSRFRequired(except_methods=['GET'])
193 194 def my_account_password(self):
194 195 c.active = 'password'
195 196 self.__load_data()
196 197
197 198 schema = user_schema.ChangePasswordSchema().bind(
198 199 username=c.rhodecode_user.username)
199 200
200 201 form = forms.Form(schema,
201 202 buttons=(forms.buttons.save, forms.buttons.reset))
202 203
203 204 if request.method == 'POST':
204 205 controls = request.POST.items()
205 206 try:
206 207 valid_data = form.validate(controls)
207 208 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
208 209 instance = c.rhodecode_user.get_instance()
209 210 instance.update_userdata(force_password_change=False)
210 211 Session().commit()
211 212 except forms.ValidationFailure as e:
212 213 request.session.flash(
213 214 _('Error occurred during update of user password'),
214 215 queue='error')
215 216 form = e
216 217 except Exception:
217 218 log.exception("Exception updating password")
218 219 request.session.flash(
219 220 _('Error occurred during update of user password'),
220 221 queue='error')
221 222 else:
222 223 session.setdefault('rhodecode_user', {}).update(
223 224 {'password': md5(instance.password)})
224 225 session.save()
225 226 request.session.flash(
226 227 _("Successfully updated password"), queue='success')
227 228 return redirect(url('my_account_password'))
228 229
229 230 c.form = form
230 231 return render('admin/my_account/my_account.html')
231 232
232 233 def my_account_repos(self):
233 234 c.active = 'repos'
234 235 self.__load_data()
235 236
236 237 # json used to render the grid
237 238 c.data = self._load_my_repos_data()
238 239 return render('admin/my_account/my_account.html')
239 240
240 241 def my_account_watched(self):
241 242 c.active = 'watched'
242 243 self.__load_data()
243 244
244 245 # json used to render the grid
245 246 c.data = self._load_my_repos_data(watched=True)
246 247 return render('admin/my_account/my_account.html')
247 248
248 249 def my_account_perms(self):
249 250 c.active = 'perms'
250 251 self.__load_data()
251 252 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
252 253 ip_addr=self.ip_addr)
253 254
254 255 return render('admin/my_account/my_account.html')
255 256
256 257 def my_account_emails(self):
257 258 c.active = 'emails'
258 259 self.__load_data()
259 260
260 261 c.user_email_map = UserEmailMap.query()\
261 262 .filter(UserEmailMap.user == c.user).all()
262 263 return render('admin/my_account/my_account.html')
263 264
264 265 @auth.CSRFRequired()
265 266 def my_account_emails_add(self):
266 267 email = request.POST.get('new_email')
267 268
268 269 try:
269 270 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
270 271 Session().commit()
271 272 h.flash(_("Added new email address `%s` for user account") % email,
272 273 category='success')
273 274 except formencode.Invalid as error:
274 275 msg = error.error_dict['email']
275 276 h.flash(msg, category='error')
276 277 except Exception:
277 278 log.exception("Exception in my_account_emails")
278 279 h.flash(_('An error occurred during email saving'),
279 280 category='error')
280 281 return redirect(url('my_account_emails'))
281 282
282 283 @auth.CSRFRequired()
283 284 def my_account_emails_delete(self):
284 285 email_id = request.POST.get('del_email_id')
285 286 user_model = UserModel()
286 287 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
287 288 Session().commit()
288 289 h.flash(_("Removed email address from user account"),
289 290 category='success')
290 291 return redirect(url('my_account_emails'))
291 292
293 def _extract_ordering(self, request):
294 column_index = safe_int(request.GET.get('order[0][column]'))
295 order_dir = request.GET.get('order[0][dir]', 'desc')
296 order_by = request.GET.get(
297 'columns[%s][data][sort]' % column_index, 'name_raw')
298 return order_by, order_dir
299
300 def _get_pull_requests_list(self, statuses):
301 start = safe_int(request.GET.get('start'), 0)
302 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
303 order_by, order_dir = self._extract_ordering(request)
304
305 pull_requests = PullRequestModel().get_im_participating_in(
306 user_id=c.rhodecode_user.user_id,
307 statuses=statuses,
308 offset=start, length=length, order_by=order_by,
309 order_dir=order_dir)
310
311 pull_requests_total_count = PullRequestModel().count_im_participating_in(
312 user_id=c.rhodecode_user.user_id, statuses=statuses)
313
314 from rhodecode.lib.utils import PartialRenderer
315 _render = PartialRenderer('data_table/_dt_elements.html')
316 data = []
317 for pr in pull_requests:
318 repo_id = pr.target_repo_id
319 comments = ChangesetCommentsModel().get_all_comments(
320 repo_id, pull_request=pr)
321 owned = pr.user_id == c.rhodecode_user.user_id
322 status = pr.calculated_review_status()
323
324 data.append({
325 'target_repo': _render('pullrequest_target_repo',
326 pr.target_repo.repo_name),
327 'name': _render('pullrequest_name',
328 pr.pull_request_id, pr.target_repo.repo_name,
329 short=True),
330 'name_raw': pr.pull_request_id,
331 'status': _render('pullrequest_status', status),
332 'title': _render(
333 'pullrequest_title', pr.title, pr.description),
334 'description': h.escape(pr.description),
335 'updated_on': _render('pullrequest_updated_on',
336 h.datetime_to_time(pr.updated_on)),
337 'updated_on_raw': h.datetime_to_time(pr.updated_on),
338 'created_on': _render('pullrequest_updated_on',
339 h.datetime_to_time(pr.created_on)),
340 'created_on_raw': h.datetime_to_time(pr.created_on),
341 'author': _render('pullrequest_author',
342 pr.author.full_contact, ),
343 'author_raw': pr.author.full_name,
344 'comments': _render('pullrequest_comments', len(comments)),
345 'comments_raw': len(comments),
346 'closed': pr.is_closed(),
347 'owned': owned
348 })
349 # json used to render the grid
350 data = ({
351 'data': data,
352 'recordsTotal': pull_requests_total_count,
353 'recordsFiltered': pull_requests_total_count,
354 })
355 return data
356
292 357 def my_account_pullrequests(self):
293 358 c.active = 'pullrequests'
294 359 self.__load_data()
295 c.show_closed = request.GET.get('pr_show_closed')
296
297 def _filter(pr):
298 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
299 if not c.show_closed:
300 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
301 return s
360 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
302 361
303 c.my_pull_requests = _filter(
304 PullRequest.query().filter(
305 PullRequest.user_id == c.rhodecode_user.user_id).all())
306 my_prs = [
307 x.pull_request for x in PullRequestReviewers.query().filter(
308 PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
309 c.participate_in_pull_requests = _filter(my_prs)
362 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
363 if c.show_closed:
364 statuses += [PullRequest.STATUS_CLOSED]
365 data = self._get_pull_requests_list(statuses)
366 if not request.is_xhr:
367 c.data_participate = json.dumps(data['data'])
368 c.records_total_participate = data['recordsTotal']
310 369 return render('admin/my_account/my_account.html')
370 else:
371 return json.dumps(data)
311 372
312 373 def my_account_auth_tokens(self):
313 374 c.active = 'auth_tokens'
314 375 self.__load_data()
315 376 show_expired = True
316 377 c.lifetime_values = [
317 378 (str(-1), _('forever')),
318 379 (str(5), _('5 minutes')),
319 380 (str(60), _('1 hour')),
320 381 (str(60 * 24), _('1 day')),
321 382 (str(60 * 24 * 30), _('1 month')),
322 383 ]
323 384 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
324 385 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
325 386 for x in AuthTokenModel.cls.ROLES]
326 387 c.role_options = [(c.role_values, _("Role"))]
327 388 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
328 389 c.rhodecode_user.user_id, show_expired=show_expired)
329 390 return render('admin/my_account/my_account.html')
330 391
331 392 @auth.CSRFRequired()
332 393 def my_account_auth_tokens_add(self):
333 394 lifetime = safe_int(request.POST.get('lifetime'), -1)
334 395 description = request.POST.get('description')
335 396 role = request.POST.get('role')
336 397 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
337 398 role)
338 399 Session().commit()
339 400 h.flash(_("Auth token successfully created"), category='success')
340 401 return redirect(url('my_account_auth_tokens'))
341 402
342 403 @auth.CSRFRequired()
343 404 def my_account_auth_tokens_delete(self):
344 405 auth_token = request.POST.get('del_auth_token')
345 406 user_id = c.rhodecode_user.user_id
346 407 if request.POST.get('del_auth_token_builtin'):
347 408 user = User.get(user_id)
348 409 if user:
349 410 user.api_key = generate_auth_token(user.username)
350 411 Session().add(user)
351 412 Session().commit()
352 413 h.flash(_("Auth token successfully reset"), category='success')
353 414 elif auth_token:
354 415 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
355 416 Session().commit()
356 417 h.flash(_("Auth token successfully deleted"), category='success')
357 418
358 419 return redirect(url('my_account_auth_tokens'))
359 420
360 421 def my_notifications(self):
361 422 c.active = 'notifications'
362 423 return render('admin/my_account/my_account.html')
363 424
364 425 @auth.CSRFRequired()
365 426 @jsonify
366 427 def my_notifications_toggle_visibility(self):
367 428 user = c.rhodecode_user.get_instance()
368 429 new_status = not user.user_data.get('notification_status', True)
369 430 user.update_userdata(notification_status=new_status)
370 431 Session().commit()
371 432 return user.user_data['notification_status']
1 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26 from collections import namedtuple
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31
32 32 from pylons.i18n.translation import _
33 33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 35
35 36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 37 from rhodecode.lib.compat import OrderedDict
37 38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 39 from rhodecode.lib.markup_renderer import (
39 40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 41 from rhodecode.lib.utils import action_logger
41 42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 43 from rhodecode.lib.vcs.backends.base import (
43 44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 46 from rhodecode.lib.vcs.exceptions import (
46 47 CommitDoesNotExistError, EmptyRepositoryError)
47 48 from rhodecode.model import BaseModel
48 49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 50 from rhodecode.model.comment import ChangesetCommentsModel
50 51 from rhodecode.model.db import (
51 52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 53 PullRequestVersion, ChangesetComment)
53 54 from rhodecode.model.meta import Session
54 55 from rhodecode.model.notification import NotificationModel, \
55 56 EmailNotificationModel
56 57 from rhodecode.model.scm import ScmModel
57 58 from rhodecode.model.settings import VcsSettingsModel
58 59
59 60
60 61 log = logging.getLogger(__name__)
61 62
62 63
63 64 # Data structure to hold the response data when updating commits during a pull
64 65 # request update.
65 66 UpdateResponse = namedtuple(
66 67 'UpdateResponse', 'executed, reason, new, old, changes')
67 68
68 69
69 70 class PullRequestModel(BaseModel):
70 71
71 72 cls = PullRequest
72 73
73 74 DIFF_CONTEXT = 3
74 75
75 76 MERGE_STATUS_MESSAGES = {
76 77 MergeFailureReason.NONE: lazy_ugettext(
77 78 'This pull request can be automatically merged.'),
78 79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 80 'This pull request cannot be merged because of an unhandled'
80 81 ' exception.'),
81 82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 83 'This pull request cannot be merged because of conflicts.'),
83 84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 85 'This pull request could not be merged because push to target'
85 86 ' failed.'),
86 87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 88 'This pull request cannot be merged because the target is not a'
88 89 ' head.'),
89 90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 91 'This pull request cannot be merged because the source contains'
91 92 ' more branches than the target.'),
92 93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 94 'This pull request cannot be merged because the target has'
94 95 ' multiple heads.'),
95 96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 97 'This pull request cannot be merged because the target repository'
97 98 ' is locked.'),
98 99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 100 'This pull request cannot be merged because the target or the '
100 101 'source reference is missing.'),
101 102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 103 'This pull request cannot be merged because the target '
103 104 'reference is missing.'),
104 105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 106 'This pull request cannot be merged because the source '
106 107 'reference is missing.'),
107 108 }
108 109
109 110 UPDATE_STATUS_MESSAGES = {
110 111 UpdateFailureReason.NONE: lazy_ugettext(
111 112 'Pull request update successful.'),
112 113 UpdateFailureReason.UNKNOWN: lazy_ugettext(
113 114 'Pull request update failed because of an unknown error.'),
114 115 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
115 116 'No update needed because the source reference is already '
116 117 'up to date.'),
117 118 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
118 119 'Pull request cannot be updated because the reference type is '
119 120 'not supported for an update.'),
120 121 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
121 122 'This pull request cannot be updated because the target '
122 123 'reference is missing.'),
123 124 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
124 125 'This pull request cannot be updated because the source '
125 126 'reference is missing.'),
126 127 }
127 128
128 129 def __get_pull_request(self, pull_request):
129 130 return self._get_instance(PullRequest, pull_request)
130 131
131 132 def _check_perms(self, perms, pull_request, user, api=False):
132 133 if not api:
133 134 return h.HasRepoPermissionAny(*perms)(
134 135 user=user, repo_name=pull_request.target_repo.repo_name)
135 136 else:
136 137 return h.HasRepoPermissionAnyApi(*perms)(
137 138 user=user, repo_name=pull_request.target_repo.repo_name)
138 139
139 140 def check_user_read(self, pull_request, user, api=False):
140 141 _perms = ('repository.admin', 'repository.write', 'repository.read',)
141 142 return self._check_perms(_perms, pull_request, user, api)
142 143
143 144 def check_user_merge(self, pull_request, user, api=False):
144 145 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
145 146 return self._check_perms(_perms, pull_request, user, api)
146 147
147 148 def check_user_update(self, pull_request, user, api=False):
148 149 owner = user.user_id == pull_request.user_id
149 150 return self.check_user_merge(pull_request, user, api) or owner
150 151
151 152 def check_user_change_status(self, pull_request, user, api=False):
152 153 reviewer = user.user_id in [x.user_id for x in
153 154 pull_request.reviewers]
154 155 return self.check_user_update(pull_request, user, api) or reviewer
155 156
156 157 def get(self, pull_request):
157 158 return self.__get_pull_request(pull_request)
158 159
159 160 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
160 161 opened_by=None, order_by=None,
161 162 order_dir='desc'):
163 repo = None
164 if repo_name:
162 165 repo = self._get_repo(repo_name)
166
163 167 q = PullRequest.query()
168
164 169 # source or target
165 if source:
170 if repo and source:
166 171 q = q.filter(PullRequest.source_repo == repo)
167 else:
172 elif repo:
168 173 q = q.filter(PullRequest.target_repo == repo)
169 174
170 175 # closed,opened
171 176 if statuses:
172 177 q = q.filter(PullRequest.status.in_(statuses))
173 178
174 179 # opened by filter
175 180 if opened_by:
176 181 q = q.filter(PullRequest.user_id.in_(opened_by))
177 182
178 183 if order_by:
179 184 order_map = {
180 185 'name_raw': PullRequest.pull_request_id,
181 186 'title': PullRequest.title,
182 'updated_on_raw': PullRequest.updated_on
187 'updated_on_raw': PullRequest.updated_on,
188 'target_repo': PullRequest.target_repo_id
183 189 }
184 190 if order_dir == 'asc':
185 191 q = q.order_by(order_map[order_by].asc())
186 192 else:
187 193 q = q.order_by(order_map[order_by].desc())
188 194
189 195 return q
190 196
191 197 def count_all(self, repo_name, source=False, statuses=None,
192 198 opened_by=None):
193 199 """
194 200 Count the number of pull requests for a specific repository.
195 201
196 202 :param repo_name: target or source repo
197 203 :param source: boolean flag to specify if repo_name refers to source
198 204 :param statuses: list of pull request statuses
199 205 :param opened_by: author user of the pull request
200 206 :returns: int number of pull requests
201 207 """
202 208 q = self._prepare_get_all_query(
203 209 repo_name, source=source, statuses=statuses, opened_by=opened_by)
204 210
205 211 return q.count()
206 212
207 213 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
208 214 offset=0, length=None, order_by=None, order_dir='desc'):
209 215 """
210 216 Get all pull requests for a specific repository.
211 217
212 218 :param repo_name: target or source repo
213 219 :param source: boolean flag to specify if repo_name refers to source
214 220 :param statuses: list of pull request statuses
215 221 :param opened_by: author user of the pull request
216 222 :param offset: pagination offset
217 223 :param length: length of returned list
218 224 :param order_by: order of the returned list
219 225 :param order_dir: 'asc' or 'desc' ordering direction
220 226 :returns: list of pull requests
221 227 """
222 228 q = self._prepare_get_all_query(
223 229 repo_name, source=source, statuses=statuses, opened_by=opened_by,
224 230 order_by=order_by, order_dir=order_dir)
225 231
226 232 if length:
227 233 pull_requests = q.limit(length).offset(offset).all()
228 234 else:
229 235 pull_requests = q.all()
230 236
231 237 return pull_requests
232 238
233 239 def count_awaiting_review(self, repo_name, source=False, statuses=None,
234 240 opened_by=None):
235 241 """
236 242 Count the number of pull requests for a specific repository that are
237 243 awaiting review.
238 244
239 245 :param repo_name: target or source repo
240 246 :param source: boolean flag to specify if repo_name refers to source
241 247 :param statuses: list of pull request statuses
242 248 :param opened_by: author user of the pull request
243 249 :returns: int number of pull requests
244 250 """
245 251 pull_requests = self.get_awaiting_review(
246 252 repo_name, source=source, statuses=statuses, opened_by=opened_by)
247 253
248 254 return len(pull_requests)
249 255
250 256 def get_awaiting_review(self, repo_name, source=False, statuses=None,
251 257 opened_by=None, offset=0, length=None,
252 258 order_by=None, order_dir='desc'):
253 259 """
254 260 Get all pull requests for a specific repository that are awaiting
255 261 review.
256 262
257 263 :param repo_name: target or source repo
258 264 :param source: boolean flag to specify if repo_name refers to source
259 265 :param statuses: list of pull request statuses
260 266 :param opened_by: author user of the pull request
261 267 :param offset: pagination offset
262 268 :param length: length of returned list
263 269 :param order_by: order of the returned list
264 270 :param order_dir: 'asc' or 'desc' ordering direction
265 271 :returns: list of pull requests
266 272 """
267 273 pull_requests = self.get_all(
268 274 repo_name, source=source, statuses=statuses, opened_by=opened_by,
269 275 order_by=order_by, order_dir=order_dir)
270 276
271 277 _filtered_pull_requests = []
272 278 for pr in pull_requests:
273 279 status = pr.calculated_review_status()
274 280 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
275 281 ChangesetStatus.STATUS_UNDER_REVIEW]:
276 282 _filtered_pull_requests.append(pr)
277 283 if length:
278 284 return _filtered_pull_requests[offset:offset+length]
279 285 else:
280 286 return _filtered_pull_requests
281 287
282 288 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
283 289 opened_by=None, user_id=None):
284 290 """
285 291 Count the number of pull requests for a specific repository that are
286 292 awaiting review from a specific user.
287 293
288 294 :param repo_name: target or source repo
289 295 :param source: boolean flag to specify if repo_name refers to source
290 296 :param statuses: list of pull request statuses
291 297 :param opened_by: author user of the pull request
292 298 :param user_id: reviewer user of the pull request
293 299 :returns: int number of pull requests
294 300 """
295 301 pull_requests = self.get_awaiting_my_review(
296 302 repo_name, source=source, statuses=statuses, opened_by=opened_by,
297 303 user_id=user_id)
298 304
299 305 return len(pull_requests)
300 306
301 307 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 308 opened_by=None, user_id=None, offset=0,
303 309 length=None, order_by=None, order_dir='desc'):
304 310 """
305 311 Get all pull requests for a specific repository that are awaiting
306 312 review from a specific user.
307 313
308 314 :param repo_name: target or source repo
309 315 :param source: boolean flag to specify if repo_name refers to source
310 316 :param statuses: list of pull request statuses
311 317 :param opened_by: author user of the pull request
312 318 :param user_id: reviewer user of the pull request
313 319 :param offset: pagination offset
314 320 :param length: length of returned list
315 321 :param order_by: order of the returned list
316 322 :param order_dir: 'asc' or 'desc' ordering direction
317 323 :returns: list of pull requests
318 324 """
319 325 pull_requests = self.get_all(
320 326 repo_name, source=source, statuses=statuses, opened_by=opened_by,
321 327 order_by=order_by, order_dir=order_dir)
322 328
323 329 _my = PullRequestModel().get_not_reviewed(user_id)
324 330 my_participation = []
325 331 for pr in pull_requests:
326 332 if pr in _my:
327 333 my_participation.append(pr)
328 334 _filtered_pull_requests = my_participation
329 335 if length:
330 336 return _filtered_pull_requests[offset:offset+length]
331 337 else:
332 338 return _filtered_pull_requests
333 339
334 340 def get_not_reviewed(self, user_id):
335 341 return [
336 342 x.pull_request for x in PullRequestReviewers.query().filter(
337 343 PullRequestReviewers.user_id == user_id).all()
338 344 ]
339 345
346 def _prepare_participating_query(self, user_id=None, statuses=None,
347 order_by=None, order_dir='desc'):
348 q = PullRequest.query()
349 if user_id:
350 reviewers_subquery = Session().query(
351 PullRequestReviewers.pull_request_id).filter(
352 PullRequestReviewers.user_id == user_id).subquery()
353 user_filter= or_(
354 PullRequest.user_id == user_id,
355 PullRequest.pull_request_id.in_(reviewers_subquery)
356 )
357 q = PullRequest.query().filter(user_filter)
358
359 # closed,opened
360 if statuses:
361 q = q.filter(PullRequest.status.in_(statuses))
362
363 if order_by:
364 order_map = {
365 'name_raw': PullRequest.pull_request_id,
366 'title': PullRequest.title,
367 'updated_on_raw': PullRequest.updated_on,
368 'target_repo': PullRequest.target_repo_id
369 }
370 if order_dir == 'asc':
371 q = q.order_by(order_map[order_by].asc())
372 else:
373 q = q.order_by(order_map[order_by].desc())
374
375 return q
376
377 def count_im_participating_in(self, user_id=None, statuses=None):
378 q = self._prepare_participating_query(user_id, statuses=statuses)
379 return q.count()
380
381 def get_im_participating_in(
382 self, user_id=None, statuses=None, offset=0,
383 length=None, order_by=None, order_dir='desc'):
384 """
385 Get all Pull requests that i'm participating in, or i have opened
386 """
387
388 q = self._prepare_participating_query(
389 user_id, statuses=statuses, order_by=order_by,
390 order_dir=order_dir)
391
392 if length:
393 pull_requests = q.limit(length).offset(offset).all()
394 else:
395 pull_requests = q.all()
396
397 return pull_requests
398
340 399 def get_versions(self, pull_request):
341 400 """
342 401 returns version of pull request sorted by ID descending
343 402 """
344 403 return PullRequestVersion.query()\
345 404 .filter(PullRequestVersion.pull_request == pull_request)\
346 405 .order_by(PullRequestVersion.pull_request_version_id.asc())\
347 406 .all()
348 407
349 408 def create(self, created_by, source_repo, source_ref, target_repo,
350 409 target_ref, revisions, reviewers, title, description=None):
351 410 created_by_user = self._get_user(created_by)
352 411 source_repo = self._get_repo(source_repo)
353 412 target_repo = self._get_repo(target_repo)
354 413
355 414 pull_request = PullRequest()
356 415 pull_request.source_repo = source_repo
357 416 pull_request.source_ref = source_ref
358 417 pull_request.target_repo = target_repo
359 418 pull_request.target_ref = target_ref
360 419 pull_request.revisions = revisions
361 420 pull_request.title = title
362 421 pull_request.description = description
363 422 pull_request.author = created_by_user
364 423
365 424 Session().add(pull_request)
366 425 Session().flush()
367 426
368 427 reviewer_ids = set()
369 428 # members / reviewers
370 429 for reviewer_object in reviewers:
371 430 if isinstance(reviewer_object, tuple):
372 431 user_id, reasons = reviewer_object
373 432 else:
374 433 user_id, reasons = reviewer_object, []
375 434
376 435 user = self._get_user(user_id)
377 436 reviewer_ids.add(user.user_id)
378 437
379 438 reviewer = PullRequestReviewers(user, pull_request, reasons)
380 439 Session().add(reviewer)
381 440
382 441 # Set approval status to "Under Review" for all commits which are
383 442 # part of this pull request.
384 443 ChangesetStatusModel().set_status(
385 444 repo=target_repo,
386 445 status=ChangesetStatus.STATUS_UNDER_REVIEW,
387 446 user=created_by_user,
388 447 pull_request=pull_request
389 448 )
390 449
391 450 self.notify_reviewers(pull_request, reviewer_ids)
392 451 self._trigger_pull_request_hook(
393 452 pull_request, created_by_user, 'create')
394 453
395 454 return pull_request
396 455
397 456 def _trigger_pull_request_hook(self, pull_request, user, action):
398 457 pull_request = self.__get_pull_request(pull_request)
399 458 target_scm = pull_request.target_repo.scm_instance()
400 459 if action == 'create':
401 460 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
402 461 elif action == 'merge':
403 462 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
404 463 elif action == 'close':
405 464 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
406 465 elif action == 'review_status_change':
407 466 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
408 467 elif action == 'update':
409 468 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
410 469 else:
411 470 return
412 471
413 472 trigger_hook(
414 473 username=user.username,
415 474 repo_name=pull_request.target_repo.repo_name,
416 475 repo_alias=target_scm.alias,
417 476 pull_request=pull_request)
418 477
419 478 def _get_commit_ids(self, pull_request):
420 479 """
421 480 Return the commit ids of the merged pull request.
422 481
423 482 This method is not dealing correctly yet with the lack of autoupdates
424 483 nor with the implicit target updates.
425 484 For example: if a commit in the source repo is already in the target it
426 485 will be reported anyways.
427 486 """
428 487 merge_rev = pull_request.merge_rev
429 488 if merge_rev is None:
430 489 raise ValueError('This pull request was not merged yet')
431 490
432 491 commit_ids = list(pull_request.revisions)
433 492 if merge_rev not in commit_ids:
434 493 commit_ids.append(merge_rev)
435 494
436 495 return commit_ids
437 496
438 497 def merge(self, pull_request, user, extras):
439 498 log.debug("Merging pull request %s", pull_request.pull_request_id)
440 499 merge_state = self._merge_pull_request(pull_request, user, extras)
441 500 if merge_state.executed:
442 501 log.debug(
443 502 "Merge was successful, updating the pull request comments.")
444 503 self._comment_and_close_pr(pull_request, user, merge_state)
445 504 self._log_action('user_merged_pull_request', user, pull_request)
446 505 else:
447 506 log.warn("Merge failed, not updating the pull request.")
448 507 return merge_state
449 508
450 509 def _merge_pull_request(self, pull_request, user, extras):
451 510 target_vcs = pull_request.target_repo.scm_instance()
452 511 source_vcs = pull_request.source_repo.scm_instance()
453 512 target_ref = self._refresh_reference(
454 513 pull_request.target_ref_parts, target_vcs)
455 514
456 515 message = _(
457 516 'Merge pull request #%(pr_id)s from '
458 517 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
459 518 'pr_id': pull_request.pull_request_id,
460 519 'source_repo': source_vcs.name,
461 520 'source_ref_name': pull_request.source_ref_parts.name,
462 521 'pr_title': pull_request.title
463 522 }
464 523
465 524 workspace_id = self._workspace_id(pull_request)
466 525 use_rebase = self._use_rebase_for_merging(pull_request)
467 526
468 527 callback_daemon, extras = prepare_callback_daemon(
469 528 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
470 529 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
471 530
472 531 with callback_daemon:
473 532 # TODO: johbo: Implement a clean way to run a config_override
474 533 # for a single call.
475 534 target_vcs.config.set(
476 535 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
477 536 merge_state = target_vcs.merge(
478 537 target_ref, source_vcs, pull_request.source_ref_parts,
479 538 workspace_id, user_name=user.username,
480 539 user_email=user.email, message=message, use_rebase=use_rebase)
481 540 return merge_state
482 541
483 542 def _comment_and_close_pr(self, pull_request, user, merge_state):
484 543 pull_request.merge_rev = merge_state.merge_ref.commit_id
485 544 pull_request.updated_on = datetime.datetime.now()
486 545
487 546 ChangesetCommentsModel().create(
488 547 text=unicode(_('Pull request merged and closed')),
489 548 repo=pull_request.target_repo.repo_id,
490 549 user=user.user_id,
491 550 pull_request=pull_request.pull_request_id,
492 551 f_path=None,
493 552 line_no=None,
494 553 closing_pr=True
495 554 )
496 555
497 556 Session().add(pull_request)
498 557 Session().flush()
499 558 # TODO: paris: replace invalidation with less radical solution
500 559 ScmModel().mark_for_invalidation(
501 560 pull_request.target_repo.repo_name)
502 561 self._trigger_pull_request_hook(pull_request, user, 'merge')
503 562
504 563 def has_valid_update_type(self, pull_request):
505 564 source_ref_type = pull_request.source_ref_parts.type
506 565 return source_ref_type in ['book', 'branch', 'tag']
507 566
508 567 def update_commits(self, pull_request):
509 568 """
510 569 Get the updated list of commits for the pull request
511 570 and return the new pull request version and the list
512 571 of commits processed by this update action
513 572 """
514 573 pull_request = self.__get_pull_request(pull_request)
515 574 source_ref_type = pull_request.source_ref_parts.type
516 575 source_ref_name = pull_request.source_ref_parts.name
517 576 source_ref_id = pull_request.source_ref_parts.commit_id
518 577
519 578 if not self.has_valid_update_type(pull_request):
520 579 log.debug(
521 580 "Skipping update of pull request %s due to ref type: %s",
522 581 pull_request, source_ref_type)
523 582 return UpdateResponse(
524 583 executed=False,
525 584 reason=UpdateFailureReason.WRONG_REF_TPYE,
526 585 old=pull_request, new=None, changes=None)
527 586
528 587 source_repo = pull_request.source_repo.scm_instance()
529 588 try:
530 589 source_commit = source_repo.get_commit(commit_id=source_ref_name)
531 590 except CommitDoesNotExistError:
532 591 return UpdateResponse(
533 592 executed=False,
534 593 reason=UpdateFailureReason.MISSING_SOURCE_REF,
535 594 old=pull_request, new=None, changes=None)
536 595
537 596 if source_ref_id == source_commit.raw_id:
538 597 log.debug("Nothing changed in pull request %s", pull_request)
539 598 return UpdateResponse(
540 599 executed=False,
541 600 reason=UpdateFailureReason.NO_CHANGE,
542 601 old=pull_request, new=None, changes=None)
543 602
544 603 # Finally there is a need for an update
545 604 pull_request_version = self._create_version_from_snapshot(pull_request)
546 605 self._link_comments_to_version(pull_request_version)
547 606
548 607 target_ref_type = pull_request.target_ref_parts.type
549 608 target_ref_name = pull_request.target_ref_parts.name
550 609 target_ref_id = pull_request.target_ref_parts.commit_id
551 610 target_repo = pull_request.target_repo.scm_instance()
552 611
553 612 try:
554 613 if target_ref_type in ('tag', 'branch', 'book'):
555 614 target_commit = target_repo.get_commit(target_ref_name)
556 615 else:
557 616 target_commit = target_repo.get_commit(target_ref_id)
558 617 except CommitDoesNotExistError:
559 618 return UpdateResponse(
560 619 executed=False,
561 620 reason=UpdateFailureReason.MISSING_TARGET_REF,
562 621 old=pull_request, new=None, changes=None)
563 622
564 623 # re-compute commit ids
565 624 old_commit_ids = set(pull_request.revisions)
566 625 pre_load = ["author", "branch", "date", "message"]
567 626 commit_ranges = target_repo.compare(
568 627 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
569 628 pre_load=pre_load)
570 629
571 630 ancestor = target_repo.get_common_ancestor(
572 631 target_commit.raw_id, source_commit.raw_id, source_repo)
573 632
574 633 pull_request.source_ref = '%s:%s:%s' % (
575 634 source_ref_type, source_ref_name, source_commit.raw_id)
576 635 pull_request.target_ref = '%s:%s:%s' % (
577 636 target_ref_type, target_ref_name, ancestor)
578 637 pull_request.revisions = [
579 638 commit.raw_id for commit in reversed(commit_ranges)]
580 639 pull_request.updated_on = datetime.datetime.now()
581 640 Session().add(pull_request)
582 641 new_commit_ids = set(pull_request.revisions)
583 642
584 643 changes = self._calculate_commit_id_changes(
585 644 old_commit_ids, new_commit_ids)
586 645
587 646 old_diff_data, new_diff_data = self._generate_update_diffs(
588 647 pull_request, pull_request_version)
589 648
590 649 ChangesetCommentsModel().outdate_comments(
591 650 pull_request, old_diff_data=old_diff_data,
592 651 new_diff_data=new_diff_data)
593 652
594 653 file_changes = self._calculate_file_changes(
595 654 old_diff_data, new_diff_data)
596 655
597 656 # Add an automatic comment to the pull request
598 657 update_comment = ChangesetCommentsModel().create(
599 658 text=self._render_update_message(changes, file_changes),
600 659 repo=pull_request.target_repo,
601 660 user=pull_request.author,
602 661 pull_request=pull_request,
603 662 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
604 663
605 664 # Update status to "Under Review" for added commits
606 665 for commit_id in changes.added:
607 666 ChangesetStatusModel().set_status(
608 667 repo=pull_request.source_repo,
609 668 status=ChangesetStatus.STATUS_UNDER_REVIEW,
610 669 comment=update_comment,
611 670 user=pull_request.author,
612 671 pull_request=pull_request,
613 672 revision=commit_id)
614 673
615 674 log.debug(
616 675 'Updated pull request %s, added_ids: %s, common_ids: %s, '
617 676 'removed_ids: %s', pull_request.pull_request_id,
618 677 changes.added, changes.common, changes.removed)
619 678 log.debug('Updated pull request with the following file changes: %s',
620 679 file_changes)
621 680
622 681 log.info(
623 682 "Updated pull request %s from commit %s to commit %s, "
624 683 "stored new version %s of this pull request.",
625 684 pull_request.pull_request_id, source_ref_id,
626 685 pull_request.source_ref_parts.commit_id,
627 686 pull_request_version.pull_request_version_id)
628 687 Session().commit()
629 688 self._trigger_pull_request_hook(pull_request, pull_request.author,
630 689 'update')
631 690
632 691 return UpdateResponse(
633 692 executed=True, reason=UpdateFailureReason.NONE,
634 693 old=pull_request, new=pull_request_version, changes=changes)
635 694
636 695 def _create_version_from_snapshot(self, pull_request):
637 696 version = PullRequestVersion()
638 697 version.title = pull_request.title
639 698 version.description = pull_request.description
640 699 version.status = pull_request.status
641 700 version.created_on = pull_request.created_on
642 701 version.updated_on = pull_request.updated_on
643 702 version.user_id = pull_request.user_id
644 703 version.source_repo = pull_request.source_repo
645 704 version.source_ref = pull_request.source_ref
646 705 version.target_repo = pull_request.target_repo
647 706 version.target_ref = pull_request.target_ref
648 707
649 708 version._last_merge_source_rev = pull_request._last_merge_source_rev
650 709 version._last_merge_target_rev = pull_request._last_merge_target_rev
651 710 version._last_merge_status = pull_request._last_merge_status
652 711 version.shadow_merge_ref = pull_request.shadow_merge_ref
653 712 version.merge_rev = pull_request.merge_rev
654 713
655 714 version.revisions = pull_request.revisions
656 715 version.pull_request = pull_request
657 716 Session().add(version)
658 717 Session().flush()
659 718
660 719 return version
661 720
662 721 def _generate_update_diffs(self, pull_request, pull_request_version):
663 722 diff_context = (
664 723 self.DIFF_CONTEXT +
665 724 ChangesetCommentsModel.needed_extra_diff_context())
666 725 old_diff = self._get_diff_from_pr_or_version(
667 726 pull_request_version, context=diff_context)
668 727 new_diff = self._get_diff_from_pr_or_version(
669 728 pull_request, context=diff_context)
670 729
671 730 old_diff_data = diffs.DiffProcessor(old_diff)
672 731 old_diff_data.prepare()
673 732 new_diff_data = diffs.DiffProcessor(new_diff)
674 733 new_diff_data.prepare()
675 734
676 735 return old_diff_data, new_diff_data
677 736
678 737 def _link_comments_to_version(self, pull_request_version):
679 738 """
680 739 Link all unlinked comments of this pull request to the given version.
681 740
682 741 :param pull_request_version: The `PullRequestVersion` to which
683 742 the comments shall be linked.
684 743
685 744 """
686 745 pull_request = pull_request_version.pull_request
687 746 comments = ChangesetComment.query().filter(
688 747 # TODO: johbo: Should we query for the repo at all here?
689 748 # Pending decision on how comments of PRs are to be related
690 749 # to either the source repo, the target repo or no repo at all.
691 750 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
692 751 ChangesetComment.pull_request == pull_request,
693 752 ChangesetComment.pull_request_version == None)
694 753
695 754 # TODO: johbo: Find out why this breaks if it is done in a bulk
696 755 # operation.
697 756 for comment in comments:
698 757 comment.pull_request_version_id = (
699 758 pull_request_version.pull_request_version_id)
700 759 Session().add(comment)
701 760
702 761 def _calculate_commit_id_changes(self, old_ids, new_ids):
703 762 added = new_ids.difference(old_ids)
704 763 common = old_ids.intersection(new_ids)
705 764 removed = old_ids.difference(new_ids)
706 765 return ChangeTuple(added, common, removed)
707 766
708 767 def _calculate_file_changes(self, old_diff_data, new_diff_data):
709 768
710 769 old_files = OrderedDict()
711 770 for diff_data in old_diff_data.parsed_diff:
712 771 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
713 772
714 773 added_files = []
715 774 modified_files = []
716 775 removed_files = []
717 776 for diff_data in new_diff_data.parsed_diff:
718 777 new_filename = diff_data['filename']
719 778 new_hash = md5_safe(diff_data['raw_diff'])
720 779
721 780 old_hash = old_files.get(new_filename)
722 781 if not old_hash:
723 782 # file is not present in old diff, means it's added
724 783 added_files.append(new_filename)
725 784 else:
726 785 if new_hash != old_hash:
727 786 modified_files.append(new_filename)
728 787 # now remove a file from old, since we have seen it already
729 788 del old_files[new_filename]
730 789
731 790 # removed files is when there are present in old, but not in NEW,
732 791 # since we remove old files that are present in new diff, left-overs
733 792 # if any should be the removed files
734 793 removed_files.extend(old_files.keys())
735 794
736 795 return FileChangeTuple(added_files, modified_files, removed_files)
737 796
738 797 def _render_update_message(self, changes, file_changes):
739 798 """
740 799 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
741 800 so it's always looking the same disregarding on which default
742 801 renderer system is using.
743 802
744 803 :param changes: changes named tuple
745 804 :param file_changes: file changes named tuple
746 805
747 806 """
748 807 new_status = ChangesetStatus.get_status_lbl(
749 808 ChangesetStatus.STATUS_UNDER_REVIEW)
750 809
751 810 changed_files = (
752 811 file_changes.added + file_changes.modified + file_changes.removed)
753 812
754 813 params = {
755 814 'under_review_label': new_status,
756 815 'added_commits': changes.added,
757 816 'removed_commits': changes.removed,
758 817 'changed_files': changed_files,
759 818 'added_files': file_changes.added,
760 819 'modified_files': file_changes.modified,
761 820 'removed_files': file_changes.removed,
762 821 }
763 822 renderer = RstTemplateRenderer()
764 823 return renderer.render('pull_request_update.mako', **params)
765 824
766 825 def edit(self, pull_request, title, description):
767 826 pull_request = self.__get_pull_request(pull_request)
768 827 if pull_request.is_closed():
769 828 raise ValueError('This pull request is closed')
770 829 if title:
771 830 pull_request.title = title
772 831 pull_request.description = description
773 832 pull_request.updated_on = datetime.datetime.now()
774 833 Session().add(pull_request)
775 834
776 835 def update_reviewers(self, pull_request, reviewer_data):
777 836 """
778 837 Update the reviewers in the pull request
779 838
780 839 :param pull_request: the pr to update
781 840 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
782 841 """
783 842
784 843 reviewers_reasons = {}
785 844 for user_id, reasons in reviewer_data:
786 845 if isinstance(user_id, (int, basestring)):
787 846 user_id = self._get_user(user_id).user_id
788 847 reviewers_reasons[user_id] = reasons
789 848
790 849 reviewers_ids = set(reviewers_reasons.keys())
791 850 pull_request = self.__get_pull_request(pull_request)
792 851 current_reviewers = PullRequestReviewers.query()\
793 852 .filter(PullRequestReviewers.pull_request ==
794 853 pull_request).all()
795 854 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
796 855
797 856 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
798 857 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
799 858
800 859 log.debug("Adding %s reviewers", ids_to_add)
801 860 log.debug("Removing %s reviewers", ids_to_remove)
802 861 changed = False
803 862 for uid in ids_to_add:
804 863 changed = True
805 864 _usr = self._get_user(uid)
806 865 reasons = reviewers_reasons[uid]
807 866 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
808 867 Session().add(reviewer)
809 868
810 869 self.notify_reviewers(pull_request, ids_to_add)
811 870
812 871 for uid in ids_to_remove:
813 872 changed = True
814 873 reviewer = PullRequestReviewers.query()\
815 874 .filter(PullRequestReviewers.user_id == uid,
816 875 PullRequestReviewers.pull_request == pull_request)\
817 876 .scalar()
818 877 if reviewer:
819 878 Session().delete(reviewer)
820 879 if changed:
821 880 pull_request.updated_on = datetime.datetime.now()
822 881 Session().add(pull_request)
823 882
824 883 return ids_to_add, ids_to_remove
825 884
826 885 def get_url(self, pull_request):
827 886 return h.url('pullrequest_show',
828 887 repo_name=safe_str(pull_request.target_repo.repo_name),
829 888 pull_request_id=pull_request.pull_request_id,
830 889 qualified=True)
831 890
832 891 def get_shadow_clone_url(self, pull_request):
833 892 """
834 893 Returns qualified url pointing to the shadow repository. If this pull
835 894 request is closed there is no shadow repository and ``None`` will be
836 895 returned.
837 896 """
838 897 if pull_request.is_closed():
839 898 return None
840 899 else:
841 900 pr_url = urllib.unquote(self.get_url(pull_request))
842 901 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
843 902
844 903 def notify_reviewers(self, pull_request, reviewers_ids):
845 904 # notification to reviewers
846 905 if not reviewers_ids:
847 906 return
848 907
849 908 pull_request_obj = pull_request
850 909 # get the current participants of this pull request
851 910 recipients = reviewers_ids
852 911 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
853 912
854 913 pr_source_repo = pull_request_obj.source_repo
855 914 pr_target_repo = pull_request_obj.target_repo
856 915
857 916 pr_url = h.url(
858 917 'pullrequest_show',
859 918 repo_name=pr_target_repo.repo_name,
860 919 pull_request_id=pull_request_obj.pull_request_id,
861 920 qualified=True,)
862 921
863 922 # set some variables for email notification
864 923 pr_target_repo_url = h.url(
865 924 'summary_home',
866 925 repo_name=pr_target_repo.repo_name,
867 926 qualified=True)
868 927
869 928 pr_source_repo_url = h.url(
870 929 'summary_home',
871 930 repo_name=pr_source_repo.repo_name,
872 931 qualified=True)
873 932
874 933 # pull request specifics
875 934 pull_request_commits = [
876 935 (x.raw_id, x.message)
877 936 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
878 937
879 938 kwargs = {
880 939 'user': pull_request.author,
881 940 'pull_request': pull_request_obj,
882 941 'pull_request_commits': pull_request_commits,
883 942
884 943 'pull_request_target_repo': pr_target_repo,
885 944 'pull_request_target_repo_url': pr_target_repo_url,
886 945
887 946 'pull_request_source_repo': pr_source_repo,
888 947 'pull_request_source_repo_url': pr_source_repo_url,
889 948
890 949 'pull_request_url': pr_url,
891 950 }
892 951
893 952 # pre-generate the subject for notification itself
894 953 (subject,
895 954 _h, _e, # we don't care about those
896 955 body_plaintext) = EmailNotificationModel().render_email(
897 956 notification_type, **kwargs)
898 957
899 958 # create notification objects, and emails
900 959 NotificationModel().create(
901 960 created_by=pull_request.author,
902 961 notification_subject=subject,
903 962 notification_body=body_plaintext,
904 963 notification_type=notification_type,
905 964 recipients=recipients,
906 965 email_kwargs=kwargs,
907 966 )
908 967
909 968 def delete(self, pull_request):
910 969 pull_request = self.__get_pull_request(pull_request)
911 970 self._cleanup_merge_workspace(pull_request)
912 971 Session().delete(pull_request)
913 972
914 973 def close_pull_request(self, pull_request, user):
915 974 pull_request = self.__get_pull_request(pull_request)
916 975 self._cleanup_merge_workspace(pull_request)
917 976 pull_request.status = PullRequest.STATUS_CLOSED
918 977 pull_request.updated_on = datetime.datetime.now()
919 978 Session().add(pull_request)
920 979 self._trigger_pull_request_hook(
921 980 pull_request, pull_request.author, 'close')
922 981 self._log_action('user_closed_pull_request', user, pull_request)
923 982
924 983 def close_pull_request_with_comment(self, pull_request, user, repo,
925 984 message=None):
926 985 status = ChangesetStatus.STATUS_REJECTED
927 986
928 987 if not message:
929 988 message = (
930 989 _('Status change %(transition_icon)s %(status)s') % {
931 990 'transition_icon': '>',
932 991 'status': ChangesetStatus.get_status_lbl(status)})
933 992
934 993 internal_message = _('Closing with') + ' ' + message
935 994
936 995 comm = ChangesetCommentsModel().create(
937 996 text=internal_message,
938 997 repo=repo.repo_id,
939 998 user=user.user_id,
940 999 pull_request=pull_request.pull_request_id,
941 1000 f_path=None,
942 1001 line_no=None,
943 1002 status_change=ChangesetStatus.get_status_lbl(status),
944 1003 status_change_type=status,
945 1004 closing_pr=True
946 1005 )
947 1006
948 1007 ChangesetStatusModel().set_status(
949 1008 repo.repo_id,
950 1009 status,
951 1010 user.user_id,
952 1011 comm,
953 1012 pull_request=pull_request.pull_request_id
954 1013 )
955 1014 Session().flush()
956 1015
957 1016 PullRequestModel().close_pull_request(
958 1017 pull_request.pull_request_id, user)
959 1018
960 1019 def merge_status(self, pull_request):
961 1020 if not self._is_merge_enabled(pull_request):
962 1021 return False, _('Server-side pull request merging is disabled.')
963 1022 if pull_request.is_closed():
964 1023 return False, _('This pull request is closed.')
965 1024 merge_possible, msg = self._check_repo_requirements(
966 1025 target=pull_request.target_repo, source=pull_request.source_repo)
967 1026 if not merge_possible:
968 1027 return merge_possible, msg
969 1028
970 1029 try:
971 1030 resp = self._try_merge(pull_request)
972 1031 log.debug("Merge response: %s", resp)
973 1032 status = resp.possible, self.merge_status_message(
974 1033 resp.failure_reason)
975 1034 except NotImplementedError:
976 1035 status = False, _('Pull request merging is not supported.')
977 1036
978 1037 return status
979 1038
980 1039 def _check_repo_requirements(self, target, source):
981 1040 """
982 1041 Check if `target` and `source` have compatible requirements.
983 1042
984 1043 Currently this is just checking for largefiles.
985 1044 """
986 1045 target_has_largefiles = self._has_largefiles(target)
987 1046 source_has_largefiles = self._has_largefiles(source)
988 1047 merge_possible = True
989 1048 message = u''
990 1049
991 1050 if target_has_largefiles != source_has_largefiles:
992 1051 merge_possible = False
993 1052 if source_has_largefiles:
994 1053 message = _(
995 1054 'Target repository large files support is disabled.')
996 1055 else:
997 1056 message = _(
998 1057 'Source repository large files support is disabled.')
999 1058
1000 1059 return merge_possible, message
1001 1060
1002 1061 def _has_largefiles(self, repo):
1003 1062 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1004 1063 'extensions', 'largefiles')
1005 1064 return largefiles_ui and largefiles_ui[0].active
1006 1065
1007 1066 def _try_merge(self, pull_request):
1008 1067 """
1009 1068 Try to merge the pull request and return the merge status.
1010 1069 """
1011 1070 log.debug(
1012 1071 "Trying out if the pull request %s can be merged.",
1013 1072 pull_request.pull_request_id)
1014 1073 target_vcs = pull_request.target_repo.scm_instance()
1015 1074
1016 1075 # Refresh the target reference.
1017 1076 try:
1018 1077 target_ref = self._refresh_reference(
1019 1078 pull_request.target_ref_parts, target_vcs)
1020 1079 except CommitDoesNotExistError:
1021 1080 merge_state = MergeResponse(
1022 1081 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1023 1082 return merge_state
1024 1083
1025 1084 target_locked = pull_request.target_repo.locked
1026 1085 if target_locked and target_locked[0]:
1027 1086 log.debug("The target repository is locked.")
1028 1087 merge_state = MergeResponse(
1029 1088 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1030 1089 elif self._needs_merge_state_refresh(pull_request, target_ref):
1031 1090 log.debug("Refreshing the merge status of the repository.")
1032 1091 merge_state = self._refresh_merge_state(
1033 1092 pull_request, target_vcs, target_ref)
1034 1093 else:
1035 1094 possible = pull_request.\
1036 1095 _last_merge_status == MergeFailureReason.NONE
1037 1096 merge_state = MergeResponse(
1038 1097 possible, False, None, pull_request._last_merge_status)
1039 1098
1040 1099 return merge_state
1041 1100
1042 1101 def _refresh_reference(self, reference, vcs_repository):
1043 1102 if reference.type in ('branch', 'book'):
1044 1103 name_or_id = reference.name
1045 1104 else:
1046 1105 name_or_id = reference.commit_id
1047 1106 refreshed_commit = vcs_repository.get_commit(name_or_id)
1048 1107 refreshed_reference = Reference(
1049 1108 reference.type, reference.name, refreshed_commit.raw_id)
1050 1109 return refreshed_reference
1051 1110
1052 1111 def _needs_merge_state_refresh(self, pull_request, target_reference):
1053 1112 return not(
1054 1113 pull_request.revisions and
1055 1114 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1056 1115 target_reference.commit_id == pull_request._last_merge_target_rev)
1057 1116
1058 1117 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1059 1118 workspace_id = self._workspace_id(pull_request)
1060 1119 source_vcs = pull_request.source_repo.scm_instance()
1061 1120 use_rebase = self._use_rebase_for_merging(pull_request)
1062 1121 merge_state = target_vcs.merge(
1063 1122 target_reference, source_vcs, pull_request.source_ref_parts,
1064 1123 workspace_id, dry_run=True, use_rebase=use_rebase)
1065 1124
1066 1125 # Do not store the response if there was an unknown error.
1067 1126 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1068 1127 pull_request._last_merge_source_rev = \
1069 1128 pull_request.source_ref_parts.commit_id
1070 1129 pull_request._last_merge_target_rev = target_reference.commit_id
1071 1130 pull_request._last_merge_status = merge_state.failure_reason
1072 1131 pull_request.shadow_merge_ref = merge_state.merge_ref
1073 1132 Session().add(pull_request)
1074 1133 Session().commit()
1075 1134
1076 1135 return merge_state
1077 1136
1078 1137 def _workspace_id(self, pull_request):
1079 1138 workspace_id = 'pr-%s' % pull_request.pull_request_id
1080 1139 return workspace_id
1081 1140
1082 1141 def merge_status_message(self, status_code):
1083 1142 """
1084 1143 Return a human friendly error message for the given merge status code.
1085 1144 """
1086 1145 return self.MERGE_STATUS_MESSAGES[status_code]
1087 1146
1088 1147 def generate_repo_data(self, repo, commit_id=None, branch=None,
1089 1148 bookmark=None):
1090 1149 all_refs, selected_ref = \
1091 1150 self._get_repo_pullrequest_sources(
1092 1151 repo.scm_instance(), commit_id=commit_id,
1093 1152 branch=branch, bookmark=bookmark)
1094 1153
1095 1154 refs_select2 = []
1096 1155 for element in all_refs:
1097 1156 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1098 1157 refs_select2.append({'text': element[1], 'children': children})
1099 1158
1100 1159 return {
1101 1160 'user': {
1102 1161 'user_id': repo.user.user_id,
1103 1162 'username': repo.user.username,
1104 1163 'firstname': repo.user.firstname,
1105 1164 'lastname': repo.user.lastname,
1106 1165 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1107 1166 },
1108 1167 'description': h.chop_at_smart(repo.description, '\n'),
1109 1168 'refs': {
1110 1169 'all_refs': all_refs,
1111 1170 'selected_ref': selected_ref,
1112 1171 'select2_refs': refs_select2
1113 1172 }
1114 1173 }
1115 1174
1116 1175 def generate_pullrequest_title(self, source, source_ref, target):
1117 1176 return u'{source}#{at_ref} to {target}'.format(
1118 1177 source=source,
1119 1178 at_ref=source_ref,
1120 1179 target=target,
1121 1180 )
1122 1181
1123 1182 def _cleanup_merge_workspace(self, pull_request):
1124 1183 # Merging related cleanup
1125 1184 target_scm = pull_request.target_repo.scm_instance()
1126 1185 workspace_id = 'pr-%s' % pull_request.pull_request_id
1127 1186
1128 1187 try:
1129 1188 target_scm.cleanup_merge_workspace(workspace_id)
1130 1189 except NotImplementedError:
1131 1190 pass
1132 1191
1133 1192 def _get_repo_pullrequest_sources(
1134 1193 self, repo, commit_id=None, branch=None, bookmark=None):
1135 1194 """
1136 1195 Return a structure with repo's interesting commits, suitable for
1137 1196 the selectors in pullrequest controller
1138 1197
1139 1198 :param commit_id: a commit that must be in the list somehow
1140 1199 and selected by default
1141 1200 :param branch: a branch that must be in the list and selected
1142 1201 by default - even if closed
1143 1202 :param bookmark: a bookmark that must be in the list and selected
1144 1203 """
1145 1204
1146 1205 commit_id = safe_str(commit_id) if commit_id else None
1147 1206 branch = safe_str(branch) if branch else None
1148 1207 bookmark = safe_str(bookmark) if bookmark else None
1149 1208
1150 1209 selected = None
1151 1210
1152 1211 # order matters: first source that has commit_id in it will be selected
1153 1212 sources = []
1154 1213 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1155 1214 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1156 1215
1157 1216 if commit_id:
1158 1217 ref_commit = (h.short_id(commit_id), commit_id)
1159 1218 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1160 1219
1161 1220 sources.append(
1162 1221 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1163 1222 )
1164 1223
1165 1224 groups = []
1166 1225 for group_key, ref_list, group_name, match in sources:
1167 1226 group_refs = []
1168 1227 for ref_name, ref_id in ref_list:
1169 1228 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1170 1229 group_refs.append((ref_key, ref_name))
1171 1230
1172 1231 if not selected:
1173 1232 if set([commit_id, match]) & set([ref_id, ref_name]):
1174 1233 selected = ref_key
1175 1234
1176 1235 if group_refs:
1177 1236 groups.append((group_refs, group_name))
1178 1237
1179 1238 if not selected:
1180 1239 ref = commit_id or branch or bookmark
1181 1240 if ref:
1182 1241 raise CommitDoesNotExistError(
1183 1242 'No commit refs could be found matching: %s' % ref)
1184 1243 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1185 1244 selected = 'branch:%s:%s' % (
1186 1245 repo.DEFAULT_BRANCH_NAME,
1187 1246 repo.branches[repo.DEFAULT_BRANCH_NAME]
1188 1247 )
1189 1248 elif repo.commit_ids:
1190 1249 rev = repo.commit_ids[0]
1191 1250 selected = 'rev:%s:%s' % (rev, rev)
1192 1251 else:
1193 1252 raise EmptyRepositoryError()
1194 1253 return groups, selected
1195 1254
1196 1255 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1197 1256 pull_request = self.__get_pull_request(pull_request)
1198 1257 return self._get_diff_from_pr_or_version(pull_request, context=context)
1199 1258
1200 1259 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1201 1260 source_repo = pr_or_version.source_repo
1202 1261
1203 1262 # we swap org/other ref since we run a simple diff on one repo
1204 1263 target_ref_id = pr_or_version.target_ref_parts.commit_id
1205 1264 source_ref_id = pr_or_version.source_ref_parts.commit_id
1206 1265 target_commit = source_repo.get_commit(
1207 1266 commit_id=safe_str(target_ref_id))
1208 1267 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1209 1268 vcs_repo = source_repo.scm_instance()
1210 1269
1211 1270 # TODO: johbo: In the context of an update, we cannot reach
1212 1271 # the old commit anymore with our normal mechanisms. It needs
1213 1272 # some sort of special support in the vcs layer to avoid this
1214 1273 # workaround.
1215 1274 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1216 1275 vcs_repo.alias == 'git'):
1217 1276 source_commit.raw_id = safe_str(source_ref_id)
1218 1277
1219 1278 log.debug('calculating diff between '
1220 1279 'source_ref:%s and target_ref:%s for repo `%s`',
1221 1280 target_ref_id, source_ref_id,
1222 1281 safe_unicode(vcs_repo.path))
1223 1282
1224 1283 vcs_diff = vcs_repo.get_diff(
1225 1284 commit1=target_commit, commit2=source_commit, context=context)
1226 1285 return vcs_diff
1227 1286
1228 1287 def _is_merge_enabled(self, pull_request):
1229 1288 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1230 1289 settings = settings_model.get_general_settings()
1231 1290 return settings.get('rhodecode_pr_merge_enabled', False)
1232 1291
1233 1292 def _use_rebase_for_merging(self, pull_request):
1234 1293 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1235 1294 settings = settings_model.get_general_settings()
1236 1295 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1237 1296
1238 1297 def _log_action(self, action, user, pull_request):
1239 1298 action_logger(
1240 1299 user,
1241 1300 '{action}:{pr_id}'.format(
1242 1301 action=action, pr_id=pull_request.pull_request_id),
1243 1302 pull_request.target_repo)
1244 1303
1245 1304
1246 1305 ChangeTuple = namedtuple('ChangeTuple',
1247 1306 ['added', 'common', 'removed'])
1248 1307
1249 1308 FileChangeTuple = namedtuple('FileChangeTuple',
1250 1309 ['added', 'modified', 'removed'])
@@ -1,155 +1,78 b''
1 1 <%namespace name="base" file="/base/base.html"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-body">
5 5 %if c.show_closed:
6 6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
7 7 %else:
8 8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
9 9 %endif
10 10 </div>
11 11 </div>
12 12
13 13 <div class="panel panel-default">
14 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 16 </div>
17 17 <div class="panel-body">
18 <div class="pullrequestlist">
19 %if c.my_pull_requests:
20 <table class="rctable">
21 <thead>
22 <th class="td-status"></th>
23 <th>${_('Target Repo')}</th>
24 <th>${_('Author')}</th>
25 <th></th>
26 <th>${_('Title')}</th>
27 <th class="td-time">${_('Last Update')}</th>
28 <th></th>
29 </thead>
30 %for pull_request in c.my_pull_requests:
31 <tr class="${'closed' if pull_request.is_closed() else ''} prwrapper">
32 <td class="td-status">
33 <div class="${'flag_status %s' % pull_request.calculated_review_status()} pull-left"></div>
34 </td>
35 <td class="truncate-wrap td-componentname">
36 <div class="truncate">
37 ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))}
38 </div>
39 </td>
40 <td class="user">
41 ${base.gravatar_with_user(pull_request.author.email, 16)}
42 </td>
43 <td class="td-message expand_commit" data-pr-id="m${pull_request.pull_request_id}" title="${_('Expand commit message')}">
44 <div class="show_more_col">
45 <i class="show_more"></i>&nbsp;
46 </div>
47 </td>
48 <td class="mid td-description">
49 <div class="log-container truncate-wrap">
50 <div class="message truncate" id="c-m${pull_request.pull_request_id}"><a href="${h.url('pullrequest_show',repo_name=pull_request.target_repo.repo_name,pull_request_id=pull_request.pull_request_id)}">#${pull_request.pull_request_id}: ${pull_request.title}</a>\
51 %if pull_request.is_closed():
52 &nbsp;(${_('Closed')})\
53 %endif
54 <br/>${pull_request.description}</div>
55 </div>
56 </td>
57 <td class="td-time">
58 ${h.age_component(pull_request.updated_on)}
59 </td>
60 <td class="td-action repolist_actions">
61 ${h.secure_form(url('pullrequest_delete', repo_name=pull_request.target_repo.repo_name, pull_request_id=pull_request.pull_request_id),method='delete')}
62 ${h.submit('remove_%s' % pull_request.pull_request_id, _('Delete'),
63 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
64 ${h.end_form()}
65 </td>
66 </tr>
67 %endfor
68 </table>
69 %else:
70 <h2><span class="empty_data">${_('You currently have no open pull requests.')}</span></h2>
71 %endif
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>
18 <table id="pull_request_list_table_participate" class="display"></table>
130 19 </div>
131 20 </div>
132 21
133 22 <script>
134 23 $('#show_closed').on('click', function(e){
135 24 if($(this).is(":checked")){
136 25 window.location = "${h.url('my_account_pullrequests', pr_show_closed=1)}";
137 26 }
138 27 else{
139 28 window.location = "${h.url('my_account_pullrequests')}";
140 29 }
141 30 });
142 $('.expand_commit').on('click',function(e){
143 var target_expand = $(this);
144 var cid = target_expand.data('prId');
31 $(document).ready(function() {
32
33 var columnsDefs = [
34 { data: {"_": "status",
35 "sort": "status"}, title: "", className: "td-status", orderable: false},
36 { data: {"_": "target_repo",
37 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
38 { data: {"_": "name",
39 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
40 { data: {"_": "author",
41 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
42 { data: {"_": "title",
43 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
44 { data: {"_": "comments",
45 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
46 { data: {"_": "updated_on",
47 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
48 ];
145 49
146 if (target_expand.hasClass('open')){
147 $('#c-'+cid).css({'height': '2.75em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
148 target_expand.removeClass('open');
50 // participating object list
51 $('#pull_request_list_table_participate').DataTable({
52 data: ${c.data_participate|n},
53 processing: true,
54 serverSide: true,
55 deferLoading: ${c.records_total_participate},
56 ajax: "",
57 dom: 'tp',
58 pageLength: ${c.visual.dashboard_items},
59 order: [[ 2, "desc" ]],
60 columns: columnsDefs,
61 language: {
62 paginate: DEFAULT_GRID_PAGINATION,
63 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
64 },
65 "drawCallback": function( settings, json ) {
66 timeagoActivate();
67 },
68 "createdRow": function ( row, data, index ) {
69 if (data['closed']) {
70 $(row).addClass('closed');
149 71 }
150 else {
151 $('#c-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible'});
152 target_expand.addClass('open');
72 if (data['owned']) {
73 $(row).addClass('owned');
74 }
153 75 }
154 76 });
77 });
155 78 </script>
@@ -1,299 +1,309 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.html"/>
4 4 <%namespace name="base" file="/base/base.html"/>
5 5
6 6 ## REPOSITORY RENDERERS
7 7 <%def name="quick_menu(repo_name)">
8 8 <i class="pointer icon-more"></i>
9 9 <div class="menu_items_container hidden">
10 10 <ul class="menu_items">
11 11 <li>
12 12 <a title="${_('Summary')}" href="${h.url('summary_home',repo_name=repo_name)}">
13 13 <span>${_('Summary')}</span>
14 14 </a>
15 15 </li>
16 16 <li>
17 17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
18 18 <span>${_('Changelog')}</span>
19 19 </a>
20 20 </li>
21 21 <li>
22 22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
23 23 <span>${_('Files')}</span>
24 24 </a>
25 25 </li>
26 26 <li>
27 27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 28 <span>${_('Fork')}</span>
29 29 </a>
30 30 </li>
31 31 </ul>
32 32 </div>
33 33 </%def>
34 34
35 35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 36 <%
37 37 def get_name(name,short_name=short_name):
38 38 if short_name:
39 39 return name.split('/')[-1]
40 40 else:
41 41 return name
42 42 %>
43 43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 44 ##NAME
45 45 <a href="${h.url('edit_repo' if admin else 'summary_home',repo_name=name)}">
46 46
47 47 ##TYPE OF REPO
48 48 %if h.is_hg(rtype):
49 49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 50 %elif h.is_git(rtype):
51 51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 52 %elif h.is_svn(rtype):
53 53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 54 %endif
55 55
56 56 ##PRIVATE/PUBLIC
57 57 %if private and c.visual.show_private_icon:
58 58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 59 %elif not private and c.visual.show_public_icon:
60 60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 61 %else:
62 62 <span></span>
63 63 %endif
64 64 ${get_name(name)}
65 65 </a>
66 66 %if fork_of:
67 67 <a href="${h.url('summary_home',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 68 %endif
69 69 %if rstate == 'repo_state_pending':
70 70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 71 %endif
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="last_change(last_change)">
76 76 ${h.age_component(last_change)}
77 77 </%def>
78 78
79 79 <%def name="revision(name,rev,tip,author,last_msg)">
80 80 <div>
81 81 %if rev >= 0:
82 82 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
83 83 %else:
84 84 ${_('No commits yet')}
85 85 %endif
86 86 </div>
87 87 </%def>
88 88
89 89 <%def name="rss(name)">
90 90 %if c.rhodecode_user.username != h.DEFAULT_USER:
91 91 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
92 92 %else:
93 93 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
94 94 %endif
95 95 </%def>
96 96
97 97 <%def name="atom(name)">
98 98 %if c.rhodecode_user.username != h.DEFAULT_USER:
99 99 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
100 100 %else:
101 101 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
102 102 %endif
103 103 </%def>
104 104
105 105 <%def name="user_gravatar(email, size=16)">
106 106 <div class="rc-user tooltip" title="${h.author_string(email)}">
107 107 ${base.gravatar(email, 16)}
108 108 </div>
109 109 </%def>
110 110
111 111 <%def name="repo_actions(repo_name, super_user=True)">
112 112 <div>
113 113 <div class="grid_edit">
114 114 <a href="${h.url('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
115 115 <i class="icon-pencil"></i>Edit</a>
116 116 </div>
117 117 <div class="grid_delete">
118 118 ${h.secure_form(h.url('repo', repo_name=repo_name),method='delete')}
119 119 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
120 120 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
121 121 ${h.end_form()}
122 122 </div>
123 123 </div>
124 124 </%def>
125 125
126 126 <%def name="repo_state(repo_state)">
127 127 <div>
128 128 %if repo_state == 'repo_state_pending':
129 129 <div class="tag tag4">${_('Creating')}</div>
130 130 %elif repo_state == 'repo_state_created':
131 131 <div class="tag tag1">${_('Created')}</div>
132 132 %else:
133 133 <div class="tag alert2" title="${repo_state}">invalid</div>
134 134 %endif
135 135 </div>
136 136 </%def>
137 137
138 138
139 139 ## REPO GROUP RENDERERS
140 140 <%def name="quick_repo_group_menu(repo_group_name)">
141 141 <i class="pointer icon-more"></i>
142 142 <div class="menu_items_container hidden">
143 143 <ul class="menu_items">
144 144 <li>
145 145 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
146 146 <span class="icon">
147 147 <i class="icon-file-text"></i>
148 148 </span>
149 149 <span>${_('Summary')}</span>
150 150 </a>
151 151 </li>
152 152
153 153 </ul>
154 154 </div>
155 155 </%def>
156 156
157 157 <%def name="repo_group_name(repo_group_name, children_groups=None)">
158 158 <div>
159 159 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
160 160 <i class="icon-folder-close" title="${_('Repository group')}"></i>
161 161 %if children_groups:
162 162 ${h.literal(' &raquo; '.join(children_groups))}
163 163 %else:
164 164 ${repo_group_name}
165 165 %endif
166 166 </a>
167 167 </div>
168 168 </%def>
169 169
170 170 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
171 171 <div class="grid_edit">
172 172 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
173 173 </div>
174 174 <div class="grid_delete">
175 175 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
176 176 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
177 177 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
178 178 ${h.end_form()}
179 179 </div>
180 180 </%def>
181 181
182 182
183 183 <%def name="user_actions(user_id, username)">
184 184 <div class="grid_edit">
185 185 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
186 186 <i class="icon-pencil"></i>Edit</a>
187 187 </div>
188 188 <div class="grid_delete">
189 189 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
190 190 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
191 191 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
192 192 ${h.end_form()}
193 193 </div>
194 194 </%def>
195 195
196 196 <%def name="user_group_actions(user_group_id, user_group_name)">
197 197 <div class="grid_edit">
198 198 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
199 199 </div>
200 200 <div class="grid_delete">
201 201 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
202 202 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
203 203 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
204 204 ${h.end_form()}
205 205 </div>
206 206 </%def>
207 207
208 208
209 209 <%def name="user_name(user_id, username)">
210 210 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
211 211 </%def>
212 212
213 213 <%def name="user_profile(username)">
214 214 ${base.gravatar_with_user(username, 16)}
215 215 </%def>
216 216
217 217 <%def name="user_group_name(user_group_id, user_group_name)">
218 218 <div>
219 219 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
220 220 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
221 221 </div>
222 222 </%def>
223 223
224 224
225 225 ## GISTS
226 226
227 227 <%def name="gist_gravatar(full_contact)">
228 228 <div class="gist_gravatar">
229 229 ${base.gravatar(full_contact, 30)}
230 230 </div>
231 231 </%def>
232 232
233 233 <%def name="gist_access_id(gist_access_id, full_contact)">
234 234 <div>
235 235 <b>
236 236 <a href="${h.url('gist',gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
237 237 </b>
238 238 </div>
239 239 </%def>
240 240
241 241 <%def name="gist_author(full_contact, created_on, expires)">
242 242 ${base.gravatar_with_user(full_contact, 16)}
243 243 </%def>
244 244
245 245
246 246 <%def name="gist_created(created_on)">
247 247 <div class="created">
248 248 ${h.age_component(created_on, time_is_local=True)}
249 249 </div>
250 250 </%def>
251 251
252 252 <%def name="gist_expires(expires)">
253 253 <div class="created">
254 254 %if expires == -1:
255 255 ${_('never')}
256 256 %else:
257 257 ${h.age_component(h.time_to_utcdatetime(expires))}
258 258 %endif
259 259 </div>
260 260 </%def>
261 261
262 262 <%def name="gist_type(gist_type)">
263 263 %if gist_type != 'public':
264 264 <div class="tag">${_('Private')}</div>
265 265 %endif
266 266 </%def>
267 267
268 268 <%def name="gist_description(gist_description)">
269 269 ${gist_description}
270 270 </%def>
271 271
272 272
273 273 ## PULL REQUESTS GRID RENDERERS
274
275 <%def name="pullrequest_target_repo(repo_name)">
276 <div class="truncate">
277 ${h.link_to(repo_name,h.url('summary_home',repo_name=repo_name))}
278 </div>
279 </%def>
274 280 <%def name="pullrequest_status(status)">
275 281 <div class="${'flag_status %s' % status} pull-left"></div>
276 282 </%def>
277 283
278 284 <%def name="pullrequest_title(title, description)">
279 285 ${title} <br/>
280 286 ${h.shorter(description, 40)}
281 287 </%def>
282 288
283 289 <%def name="pullrequest_comments(comments_nr)">
284 290 <i class="icon-comment icon-comment-colored"></i> ${comments_nr}
285 291 </%def>
286 292
287 <%def name="pullrequest_name(pull_request_id, target_repo_name)">
293 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
288 294 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
295 % if short:
296 #${pull_request_id}
297 % else:
289 298 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
299 % endif
290 300 </a>
291 301 </%def>
292 302
293 303 <%def name="pullrequest_updated_on(updated_on)">
294 304 ${h.age_component(h.time_to_utcdatetime(updated_on))}
295 305 </%def>
296 306
297 307 <%def name="pullrequest_author(full_contact)">
298 308 ${base.gravatar_with_user(full_contact, 16)}
299 309 </%def>
@@ -1,397 +1,396 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib import helpers as h
24 24 from rhodecode.lib.auth import check_password
25 25 from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests import (
28 28 TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
29 29 assert_session_flash)
30 30 from rhodecode.tests.fixture import Fixture
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33 fixture = Fixture()
34 34
35 35
36 36 class TestMyAccountController(TestController):
37 37 test_user_1 = 'testme'
38 38 test_user_1_password = '0jd83nHNS/d23n'
39 39 destroy_users = set()
40 40
41 41 @classmethod
42 42 def teardown_class(cls):
43 43 fixture.destroy_users(cls.destroy_users)
44 44
45 45 def test_my_account(self):
46 46 self.log_user()
47 47 response = self.app.get(url('my_account'))
48 48
49 49 response.mustcontain('test_admin')
50 50 response.mustcontain('href="/_admin/my_account/edit"')
51 51
52 52 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
53 53 response = self.app.get(url('my_account'))
54 54 assert_response = AssertResponse(response)
55 55 element = assert_response.get_element('.logout #csrf_token')
56 56 assert element.value == csrf_token
57 57
58 58 def test_my_account_edit(self):
59 59 self.log_user()
60 60 response = self.app.get(url('my_account_edit'))
61 61
62 62 response.mustcontain('value="test_admin')
63 63
64 64 def test_my_account_my_repos(self):
65 65 self.log_user()
66 66 response = self.app.get(url('my_account_repos'))
67 67 repos = Repository.query().filter(
68 68 Repository.user == User.get_by_username(
69 69 TEST_USER_ADMIN_LOGIN)).all()
70 70 for repo in repos:
71 71 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
72 72
73 73 def test_my_account_my_watched(self):
74 74 self.log_user()
75 75 response = self.app.get(url('my_account_watched'))
76 76
77 77 repos = UserFollowing.query().filter(
78 78 UserFollowing.user == User.get_by_username(
79 79 TEST_USER_ADMIN_LOGIN)).all()
80 80 for repo in repos:
81 81 response.mustcontain(
82 82 '"name_raw": "%s"' % repo.follows_repository.repo_name)
83 83
84 84 @pytest.mark.backends("git", "hg")
85 85 def test_my_account_my_pullrequests(self, pr_util):
86 86 self.log_user()
87 87 response = self.app.get(url('my_account_pullrequests'))
88 response.mustcontain('You currently have no open pull requests.')
88 response.mustcontain('There are currently no open pull '
89 'requests requiring your participation.')
89 90
90 91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 92 response = self.app.get(url('my_account_pullrequests'))
92 response.mustcontain('There are currently no open pull requests '
93 'requiring your participation')
94
95 response.mustcontain('#%s: TestMyAccountPR' % pr.pull_request_id)
93 response.mustcontain('"name_raw": %s' % pr.pull_request_id)
94 response.mustcontain('TestMyAccountPR')
96 95
97 96 def test_my_account_my_emails(self):
98 97 self.log_user()
99 98 response = self.app.get(url('my_account_emails'))
100 99 response.mustcontain('No additional emails specified')
101 100
102 101 def test_my_account_my_emails_add_existing_email(self):
103 102 self.log_user()
104 103 response = self.app.get(url('my_account_emails'))
105 104 response.mustcontain('No additional emails specified')
106 105 response = self.app.post(url('my_account_emails'),
107 106 {'new_email': TEST_USER_REGULAR_EMAIL,
108 107 'csrf_token': self.csrf_token})
109 108 assert_session_flash(response, 'This e-mail address is already taken')
110 109
111 110 def test_my_account_my_emails_add_mising_email_in_form(self):
112 111 self.log_user()
113 112 response = self.app.get(url('my_account_emails'))
114 113 response.mustcontain('No additional emails specified')
115 114 response = self.app.post(url('my_account_emails'),
116 115 {'csrf_token': self.csrf_token})
117 116 assert_session_flash(response, 'Please enter an email address')
118 117
119 118 def test_my_account_my_emails_add_remove(self):
120 119 self.log_user()
121 120 response = self.app.get(url('my_account_emails'))
122 121 response.mustcontain('No additional emails specified')
123 122
124 123 response = self.app.post(url('my_account_emails'),
125 124 {'new_email': 'foo@barz.com',
126 125 'csrf_token': self.csrf_token})
127 126
128 127 response = self.app.get(url('my_account_emails'))
129 128
130 129 from rhodecode.model.db import UserEmailMap
131 130 email_id = UserEmailMap.query().filter(
132 131 UserEmailMap.user == User.get_by_username(
133 132 TEST_USER_ADMIN_LOGIN)).filter(
134 133 UserEmailMap.email == 'foo@barz.com').one().email_id
135 134
136 135 response.mustcontain('foo@barz.com')
137 136 response.mustcontain('<input id="del_email_id" name="del_email_id" '
138 137 'type="hidden" value="%s" />' % email_id)
139 138
140 139 response = self.app.post(
141 140 url('my_account_emails'), {
142 141 'del_email_id': email_id, '_method': 'delete',
143 142 'csrf_token': self.csrf_token})
144 143 assert_session_flash(response, 'Removed email address from user account')
145 144 response = self.app.get(url('my_account_emails'))
146 145 response.mustcontain('No additional emails specified')
147 146
148 147 @pytest.mark.parametrize(
149 148 "name, attrs", [
150 149 ('firstname', {'firstname': 'new_username'}),
151 150 ('lastname', {'lastname': 'new_username'}),
152 151 ('admin', {'admin': True}),
153 152 ('admin', {'admin': False}),
154 153 ('extern_type', {'extern_type': 'ldap'}),
155 154 ('extern_type', {'extern_type': None}),
156 155 # ('extern_name', {'extern_name': 'test'}),
157 156 # ('extern_name', {'extern_name': None}),
158 157 ('active', {'active': False}),
159 158 ('active', {'active': True}),
160 159 ('email', {'email': 'some@email.com'}),
161 160 ])
162 161 def test_my_account_update(self, name, attrs):
163 162 usr = fixture.create_user(self.test_user_1,
164 163 password=self.test_user_1_password,
165 164 email='testme@rhodecode.org',
166 165 extern_type='rhodecode',
167 166 extern_name=self.test_user_1,
168 167 skip_if_exists=True)
169 168 self.destroy_users.add(self.test_user_1)
170 169
171 170 params = usr.get_api_data() # current user data
172 171 user_id = usr.user_id
173 172 self.log_user(
174 173 username=self.test_user_1, password=self.test_user_1_password)
175 174
176 175 params.update({'password_confirmation': ''})
177 176 params.update({'new_password': ''})
178 177 params.update({'extern_type': 'rhodecode'})
179 178 params.update({'extern_name': self.test_user_1})
180 179 params.update({'csrf_token': self.csrf_token})
181 180
182 181 params.update(attrs)
183 182 # my account page cannot set language param yet, only for admins
184 183 del params['language']
185 184 response = self.app.post(url('my_account'), params)
186 185
187 186 assert_session_flash(
188 187 response, 'Your account was updated successfully')
189 188
190 189 del params['csrf_token']
191 190
192 191 updated_user = User.get_by_username(self.test_user_1)
193 192 updated_params = updated_user.get_api_data()
194 193 updated_params.update({'password_confirmation': ''})
195 194 updated_params.update({'new_password': ''})
196 195
197 196 params['last_login'] = updated_params['last_login']
198 197 # my account page cannot set language param yet, only for admins
199 198 # but we get this info from API anyway
200 199 params['language'] = updated_params['language']
201 200
202 201 if name == 'email':
203 202 params['emails'] = [attrs['email']]
204 203 if name == 'extern_type':
205 204 # cannot update this via form, expected value is original one
206 205 params['extern_type'] = "rhodecode"
207 206 if name == 'extern_name':
208 207 # cannot update this via form, expected value is original one
209 208 params['extern_name'] = str(user_id)
210 209 if name == 'active':
211 210 # my account cannot deactivate account
212 211 params['active'] = True
213 212 if name == 'admin':
214 213 # my account cannot make you an admin !
215 214 params['admin'] = False
216 215
217 216 assert params == updated_params
218 217
219 218 def test_my_account_update_err_email_exists(self):
220 219 self.log_user()
221 220
222 221 new_email = 'test_regular@mail.com' # already exisitn email
223 222 response = self.app.post(url('my_account'),
224 223 params={
225 224 'username': 'test_admin',
226 225 'new_password': 'test12',
227 226 'password_confirmation': 'test122',
228 227 'firstname': 'NewName',
229 228 'lastname': 'NewLastname',
230 229 'email': new_email,
231 230 'csrf_token': self.csrf_token,
232 231 })
233 232
234 233 response.mustcontain('This e-mail address is already taken')
235 234
236 235 def test_my_account_update_err(self):
237 236 self.log_user('test_regular2', 'test12')
238 237
239 238 new_email = 'newmail.pl'
240 239 response = self.app.post(url('my_account'),
241 240 params={
242 241 'username': 'test_admin',
243 242 'new_password': 'test12',
244 243 'password_confirmation': 'test122',
245 244 'firstname': 'NewName',
246 245 'lastname': 'NewLastname',
247 246 'email': new_email,
248 247 'csrf_token': self.csrf_token,
249 248 })
250 249
251 250 response.mustcontain('An email address must contain a single @')
252 251 from rhodecode.model import validators
253 252 msg = validators.ValidUsername(
254 253 edit=False, old_data={})._messages['username_exists']
255 254 msg = h.html_escape(msg % {'username': 'test_admin'})
256 255 response.mustcontain(u"%s" % msg)
257 256
258 257 def test_my_account_auth_tokens(self):
259 258 usr = self.log_user('test_regular2', 'test12')
260 259 user = User.get(usr['user_id'])
261 260 response = self.app.get(url('my_account_auth_tokens'))
262 261 response.mustcontain(user.api_key)
263 262 response.mustcontain('expires: never')
264 263
265 264 @pytest.mark.parametrize("desc, lifetime", [
266 265 ('forever', -1),
267 266 ('5mins', 60*5),
268 267 ('30days', 60*60*24*30),
269 268 ])
270 269 def test_my_account_add_auth_tokens(self, desc, lifetime):
271 270 usr = self.log_user('test_regular2', 'test12')
272 271 user = User.get(usr['user_id'])
273 272 response = self.app.post(url('my_account_auth_tokens'),
274 273 {'description': desc, 'lifetime': lifetime,
275 274 'csrf_token': self.csrf_token})
276 275 assert_session_flash(response, 'Auth token successfully created')
277 276 try:
278 277 response = response.follow()
279 278 user = User.get(usr['user_id'])
280 279 for auth_token in user.auth_tokens:
281 280 response.mustcontain(auth_token)
282 281 finally:
283 282 for auth_token in UserApiKeys.query().all():
284 283 Session().delete(auth_token)
285 284 Session().commit()
286 285
287 286 def test_my_account_remove_auth_token(self):
288 287 # TODO: without this cleanup it fails when run with the whole
289 288 # test suite, so there must be some interference with other tests.
290 289 UserApiKeys.query().delete()
291 290
292 291 usr = self.log_user('test_regular2', 'test12')
293 292 User.get(usr['user_id'])
294 293 response = self.app.post(url('my_account_auth_tokens'),
295 294 {'description': 'desc', 'lifetime': -1,
296 295 'csrf_token': self.csrf_token})
297 296 assert_session_flash(response, 'Auth token successfully created')
298 297 response = response.follow()
299 298
300 299 # now delete our key
301 300 keys = UserApiKeys.query().all()
302 301 assert 1 == len(keys)
303 302
304 303 response = self.app.post(
305 304 url('my_account_auth_tokens'),
306 305 {'_method': 'delete', 'del_auth_token': keys[0].api_key,
307 306 'csrf_token': self.csrf_token})
308 307 assert_session_flash(response, 'Auth token successfully deleted')
309 308 keys = UserApiKeys.query().all()
310 309 assert 0 == len(keys)
311 310
312 311 def test_my_account_reset_main_auth_token(self):
313 312 usr = self.log_user('test_regular2', 'test12')
314 313 user = User.get(usr['user_id'])
315 314 api_key = user.api_key
316 315 response = self.app.get(url('my_account_auth_tokens'))
317 316 response.mustcontain(api_key)
318 317 response.mustcontain('expires: never')
319 318
320 319 response = self.app.post(
321 320 url('my_account_auth_tokens'),
322 321 {'_method': 'delete', 'del_auth_token_builtin': api_key,
323 322 'csrf_token': self.csrf_token})
324 323 assert_session_flash(response, 'Auth token successfully reset')
325 324 response = response.follow()
326 325 response.mustcontain(no=[api_key])
327 326
328 327 def test_valid_change_password(self, user_util):
329 328 new_password = 'my_new_valid_password'
330 329 user = user_util.create_user(password=self.test_user_1_password)
331 330 session = self.log_user(user.username, self.test_user_1_password)
332 331 form_data = [
333 332 ('current_password', self.test_user_1_password),
334 333 ('__start__', 'new_password:mapping'),
335 334 ('new_password', new_password),
336 335 ('new_password-confirm', new_password),
337 336 ('__end__', 'new_password:mapping'),
338 337 ('csrf_token', self.csrf_token),
339 338 ]
340 339 response = self.app.post(url('my_account_password'), form_data).follow()
341 340 assert 'Successfully updated password' in response
342 341
343 342 # check_password depends on user being in session
344 343 Session().add(user)
345 344 try:
346 345 assert check_password(new_password, user.password)
347 346 finally:
348 347 Session().expunge(user)
349 348
350 349 @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [
351 350 ('', 'abcdef123', 'abcdef123'),
352 351 ('wrong_pw', 'abcdef123', 'abcdef123'),
353 352 (test_user_1_password, test_user_1_password, test_user_1_password),
354 353 (test_user_1_password, '', ''),
355 354 (test_user_1_password, 'abcdef123', ''),
356 355 (test_user_1_password, '', 'abcdef123'),
357 356 (test_user_1_password, 'not_the', 'same_pw'),
358 357 (test_user_1_password, 'short', 'short'),
359 358 ])
360 359 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
361 360 user_util):
362 361 user = user_util.create_user(password=self.test_user_1_password)
363 362 session = self.log_user(user.username, self.test_user_1_password)
364 363 old_password_hash = session['password']
365 364 form_data = [
366 365 ('current_password', current_pw),
367 366 ('__start__', 'new_password:mapping'),
368 367 ('new_password', new_pw),
369 368 ('new_password-confirm', confirm_pw),
370 369 ('__end__', 'new_password:mapping'),
371 370 ('csrf_token', self.csrf_token),
372 371 ]
373 372 response = self.app.post(url('my_account_password'), form_data)
374 373 assert 'Error occurred' in response
375 374
376 375 def test_password_is_updated_in_session_on_password_change(self, user_util):
377 376 old_password = 'abcdef123'
378 377 new_password = 'abcdef124'
379 378
380 379 user = user_util.create_user(password=old_password)
381 380 session = self.log_user(user.username, old_password)
382 381 old_password_hash = session['password']
383 382
384 383 form_data = [
385 384 ('current_password', old_password),
386 385 ('__start__', 'new_password:mapping'),
387 386 ('new_password', new_password),
388 387 ('new_password-confirm', new_password),
389 388 ('__end__', 'new_password:mapping'),
390 389 ('csrf_token', self.csrf_token),
391 390 ]
392 391 self.app.post(url('my_account_password'), form_data)
393 392
394 393 response = self.app.get(url('home'))
395 394 new_password_hash = response.session['rhodecode_user']['password']
396 395
397 396 assert old_password_hash != new_password_hash
General Comments 0
You need to be logged in to leave comments. Login now