##// END OF EJS Templates
pull-requests: added filters to my account pull requests page.
ergo -
r4318:635c5bc5 default
parent child Browse files
Show More
@@ -1,822 +1,822 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 29 from pyramid.view import view_config
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, NotAnonymous, CSRFRequired,
38 38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
39 39 from rhodecode.lib.channelstream import (
40 40 channelstream_request, ChannelstreamException)
41 41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 42 from rhodecode.model.auth_token import AuthTokenModel
43 43 from rhodecode.model.comment import CommentsModel
44 44 from rhodecode.model.db import (
45 45 IntegrityError, or_, in_filter_generator,
46 46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 47 PullRequest, UserBookmark, RepoGroup)
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.user_group import UserGroupModel
52 52 from rhodecode.model.validation_schema.schemas import user_schema
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class MyAccountView(BaseAppView, DataGridAppView):
58 58 ALLOW_SCOPED_TOKENS = False
59 59 """
60 60 This view has alternative version inside EE, if modified please take a look
61 61 in there as well.
62 62 """
63 63
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context()
66 66 c.user = c.auth_user.get_instance()
67 67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 68
69 69 return c
70 70
71 71 @LoginRequired()
72 72 @NotAnonymous()
73 73 @view_config(
74 74 route_name='my_account_profile', request_method='GET',
75 75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 76 def my_account_profile(self):
77 77 c = self.load_default_context()
78 78 c.active = 'profile'
79 79 c.extern_type = c.user.extern_type
80 80 return self._get_template_context(c)
81 81
82 82 @LoginRequired()
83 83 @NotAnonymous()
84 84 @view_config(
85 85 route_name='my_account_password', request_method='GET',
86 86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
87 87 def my_account_password(self):
88 88 c = self.load_default_context()
89 89 c.active = 'password'
90 90 c.extern_type = c.user.extern_type
91 91
92 92 schema = user_schema.ChangePasswordSchema().bind(
93 93 username=c.user.username)
94 94
95 95 form = forms.Form(
96 96 schema,
97 97 action=h.route_path('my_account_password_update'),
98 98 buttons=(forms.buttons.save, forms.buttons.reset))
99 99
100 100 c.form = form
101 101 return self._get_template_context(c)
102 102
103 103 @LoginRequired()
104 104 @NotAnonymous()
105 105 @CSRFRequired()
106 106 @view_config(
107 107 route_name='my_account_password_update', request_method='POST',
108 108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 109 def my_account_password_update(self):
110 110 _ = self.request.translate
111 111 c = self.load_default_context()
112 112 c.active = 'password'
113 113 c.extern_type = c.user.extern_type
114 114
115 115 schema = user_schema.ChangePasswordSchema().bind(
116 116 username=c.user.username)
117 117
118 118 form = forms.Form(
119 119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120 120
121 121 if c.extern_type != 'rhodecode':
122 122 raise HTTPFound(self.request.route_path('my_account_password'))
123 123
124 124 controls = self.request.POST.items()
125 125 try:
126 126 valid_data = form.validate(controls)
127 127 UserModel().update_user(c.user.user_id, **valid_data)
128 128 c.user.update_userdata(force_password_change=False)
129 129 Session().commit()
130 130 except forms.ValidationFailure as e:
131 131 c.form = e
132 132 return self._get_template_context(c)
133 133
134 134 except Exception:
135 135 log.exception("Exception updating password")
136 136 h.flash(_('Error occurred during update of user password'),
137 137 category='error')
138 138 else:
139 139 instance = c.auth_user.get_instance()
140 140 self.session.setdefault('rhodecode_user', {}).update(
141 141 {'password': md5(instance.password)})
142 142 self.session.save()
143 143 h.flash(_("Successfully updated password"), category='success')
144 144
145 145 raise HTTPFound(self.request.route_path('my_account_password'))
146 146
147 147 @LoginRequired()
148 148 @NotAnonymous()
149 149 @view_config(
150 150 route_name='my_account_auth_tokens', request_method='GET',
151 151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 152 def my_account_auth_tokens(self):
153 153 _ = self.request.translate
154 154
155 155 c = self.load_default_context()
156 156 c.active = 'auth_tokens'
157 157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 158 c.role_values = [
159 159 (x, AuthTokenModel.cls._get_role_name(x))
160 160 for x in AuthTokenModel.cls.ROLES]
161 161 c.role_options = [(c.role_values, _("Role"))]
162 162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 163 c.user.user_id, show_expired=True)
164 164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 165 return self._get_template_context(c)
166 166
167 167 @LoginRequired()
168 168 @NotAnonymous()
169 169 @CSRFRequired()
170 170 @view_config(
171 171 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
172 172 renderer='json_ext')
173 173 def my_account_auth_tokens_view(self):
174 174 _ = self.request.translate
175 175 c = self.load_default_context()
176 176
177 177 auth_token_id = self.request.POST.get('auth_token_id')
178 178
179 179 if auth_token_id:
180 180 token = UserApiKeys.get_or_404(auth_token_id)
181 181 if token.user.user_id != c.user.user_id:
182 182 raise HTTPNotFound()
183 183
184 184 return {
185 185 'auth_token': token.api_key
186 186 }
187 187
188 188 def maybe_attach_token_scope(self, token):
189 189 # implemented in EE edition
190 190 pass
191 191
192 192 @LoginRequired()
193 193 @NotAnonymous()
194 194 @CSRFRequired()
195 195 @view_config(
196 196 route_name='my_account_auth_tokens_add', request_method='POST',)
197 197 def my_account_auth_tokens_add(self):
198 198 _ = self.request.translate
199 199 c = self.load_default_context()
200 200
201 201 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 202 description = self.request.POST.get('description')
203 203 role = self.request.POST.get('role')
204 204
205 205 token = UserModel().add_auth_token(
206 206 user=c.user.user_id,
207 207 lifetime_minutes=lifetime, role=role, description=description,
208 208 scope_callback=self.maybe_attach_token_scope)
209 209 token_data = token.get_api_data()
210 210
211 211 audit_logger.store_web(
212 212 'user.edit.token.add', action_data={
213 213 'data': {'token': token_data, 'user': 'self'}},
214 214 user=self._rhodecode_user, )
215 215 Session().commit()
216 216
217 217 h.flash(_("Auth token successfully created"), category='success')
218 218 return HTTPFound(h.route_path('my_account_auth_tokens'))
219 219
220 220 @LoginRequired()
221 221 @NotAnonymous()
222 222 @CSRFRequired()
223 223 @view_config(
224 224 route_name='my_account_auth_tokens_delete', request_method='POST')
225 225 def my_account_auth_tokens_delete(self):
226 226 _ = self.request.translate
227 227 c = self.load_default_context()
228 228
229 229 del_auth_token = self.request.POST.get('del_auth_token')
230 230
231 231 if del_auth_token:
232 232 token = UserApiKeys.get_or_404(del_auth_token)
233 233 token_data = token.get_api_data()
234 234
235 235 AuthTokenModel().delete(del_auth_token, c.user.user_id)
236 236 audit_logger.store_web(
237 237 'user.edit.token.delete', action_data={
238 238 'data': {'token': token_data, 'user': 'self'}},
239 239 user=self._rhodecode_user,)
240 240 Session().commit()
241 241 h.flash(_("Auth token successfully deleted"), category='success')
242 242
243 243 return HTTPFound(h.route_path('my_account_auth_tokens'))
244 244
245 245 @LoginRequired()
246 246 @NotAnonymous()
247 247 @view_config(
248 248 route_name='my_account_emails', request_method='GET',
249 249 renderer='rhodecode:templates/admin/my_account/my_account.mako')
250 250 def my_account_emails(self):
251 251 _ = self.request.translate
252 252
253 253 c = self.load_default_context()
254 254 c.active = 'emails'
255 255
256 256 c.user_email_map = UserEmailMap.query()\
257 257 .filter(UserEmailMap.user == c.user).all()
258 258
259 259 schema = user_schema.AddEmailSchema().bind(
260 260 username=c.user.username, user_emails=c.user.emails)
261 261
262 262 form = forms.RcForm(schema,
263 263 action=h.route_path('my_account_emails_add'),
264 264 buttons=(forms.buttons.save, forms.buttons.reset))
265 265
266 266 c.form = form
267 267 return self._get_template_context(c)
268 268
269 269 @LoginRequired()
270 270 @NotAnonymous()
271 271 @CSRFRequired()
272 272 @view_config(
273 273 route_name='my_account_emails_add', request_method='POST',
274 274 renderer='rhodecode:templates/admin/my_account/my_account.mako')
275 275 def my_account_emails_add(self):
276 276 _ = self.request.translate
277 277 c = self.load_default_context()
278 278 c.active = 'emails'
279 279
280 280 schema = user_schema.AddEmailSchema().bind(
281 281 username=c.user.username, user_emails=c.user.emails)
282 282
283 283 form = forms.RcForm(
284 284 schema, action=h.route_path('my_account_emails_add'),
285 285 buttons=(forms.buttons.save, forms.buttons.reset))
286 286
287 287 controls = self.request.POST.items()
288 288 try:
289 289 valid_data = form.validate(controls)
290 290 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
291 291 audit_logger.store_web(
292 292 'user.edit.email.add', action_data={
293 293 'data': {'email': valid_data['email'], 'user': 'self'}},
294 294 user=self._rhodecode_user,)
295 295 Session().commit()
296 296 except formencode.Invalid as error:
297 297 h.flash(h.escape(error.error_dict['email']), category='error')
298 298 except forms.ValidationFailure as e:
299 299 c.user_email_map = UserEmailMap.query() \
300 300 .filter(UserEmailMap.user == c.user).all()
301 301 c.form = e
302 302 return self._get_template_context(c)
303 303 except Exception:
304 304 log.exception("Exception adding email")
305 305 h.flash(_('Error occurred during adding email'),
306 306 category='error')
307 307 else:
308 308 h.flash(_("Successfully added email"), category='success')
309 309
310 310 raise HTTPFound(self.request.route_path('my_account_emails'))
311 311
312 312 @LoginRequired()
313 313 @NotAnonymous()
314 314 @CSRFRequired()
315 315 @view_config(
316 316 route_name='my_account_emails_delete', request_method='POST')
317 317 def my_account_emails_delete(self):
318 318 _ = self.request.translate
319 319 c = self.load_default_context()
320 320
321 321 del_email_id = self.request.POST.get('del_email_id')
322 322 if del_email_id:
323 323 email = UserEmailMap.get_or_404(del_email_id).email
324 324 UserModel().delete_extra_email(c.user.user_id, del_email_id)
325 325 audit_logger.store_web(
326 326 'user.edit.email.delete', action_data={
327 327 'data': {'email': email, 'user': 'self'}},
328 328 user=self._rhodecode_user,)
329 329 Session().commit()
330 330 h.flash(_("Email successfully deleted"),
331 331 category='success')
332 332 return HTTPFound(h.route_path('my_account_emails'))
333 333
334 334 @LoginRequired()
335 335 @NotAnonymous()
336 336 @CSRFRequired()
337 337 @view_config(
338 338 route_name='my_account_notifications_test_channelstream',
339 339 request_method='POST', renderer='json_ext')
340 340 def my_account_notifications_test_channelstream(self):
341 341 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
342 342 self._rhodecode_user.username, datetime.datetime.now())
343 343 payload = {
344 344 # 'channel': 'broadcast',
345 345 'type': 'message',
346 346 'timestamp': datetime.datetime.utcnow(),
347 347 'user': 'system',
348 348 'pm_users': [self._rhodecode_user.username],
349 349 'message': {
350 350 'message': message,
351 351 'level': 'info',
352 352 'topic': '/notifications'
353 353 }
354 354 }
355 355
356 356 registry = self.request.registry
357 357 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 358 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 359
360 360 try:
361 361 channelstream_request(channelstream_config, [payload], '/message')
362 362 except ChannelstreamException as e:
363 363 log.exception('Failed to send channelstream data')
364 364 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
365 365 return {"response": 'Channelstream data sent. '
366 366 'You should see a new live message now.'}
367 367
368 368 def _load_my_repos_data(self, watched=False):
369 369
370 370 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
371 371
372 372 if watched:
373 373 # repos user watch
374 374 repo_list = Session().query(
375 375 Repository
376 376 ) \
377 377 .join(
378 378 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
379 379 ) \
380 380 .filter(
381 381 UserFollowing.user_id == self._rhodecode_user.user_id
382 382 ) \
383 383 .filter(or_(
384 384 # generate multiple IN to fix limitation problems
385 385 *in_filter_generator(Repository.repo_id, allowed_ids))
386 386 ) \
387 387 .order_by(Repository.repo_name) \
388 388 .all()
389 389
390 390 else:
391 391 # repos user is owner of
392 392 repo_list = Session().query(
393 393 Repository
394 394 ) \
395 395 .filter(
396 396 Repository.user_id == self._rhodecode_user.user_id
397 397 ) \
398 398 .filter(or_(
399 399 # generate multiple IN to fix limitation problems
400 400 *in_filter_generator(Repository.repo_id, allowed_ids))
401 401 ) \
402 402 .order_by(Repository.repo_name) \
403 403 .all()
404 404
405 405 _render = self.request.get_partial_renderer(
406 406 'rhodecode:templates/data_table/_dt_elements.mako')
407 407
408 408 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
409 409 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
410 410 short_name=False, admin=False)
411 411
412 412 repos_data = []
413 413 for repo in repo_list:
414 414 row = {
415 415 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
416 416 repo.private, repo.archived, repo.fork),
417 417 "name_raw": repo.repo_name.lower(),
418 418 }
419 419
420 420 repos_data.append(row)
421 421
422 422 # json used to render the grid
423 423 return json.dumps(repos_data)
424 424
425 425 @LoginRequired()
426 426 @NotAnonymous()
427 427 @view_config(
428 428 route_name='my_account_repos', request_method='GET',
429 429 renderer='rhodecode:templates/admin/my_account/my_account.mako')
430 430 def my_account_repos(self):
431 431 c = self.load_default_context()
432 432 c.active = 'repos'
433 433
434 434 # json used to render the grid
435 435 c.data = self._load_my_repos_data()
436 436 return self._get_template_context(c)
437 437
438 438 @LoginRequired()
439 439 @NotAnonymous()
440 440 @view_config(
441 441 route_name='my_account_watched', request_method='GET',
442 442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
443 443 def my_account_watched(self):
444 444 c = self.load_default_context()
445 445 c.active = 'watched'
446 446
447 447 # json used to render the grid
448 448 c.data = self._load_my_repos_data(watched=True)
449 449 return self._get_template_context(c)
450 450
451 451 @LoginRequired()
452 452 @NotAnonymous()
453 453 @view_config(
454 454 route_name='my_account_bookmarks', request_method='GET',
455 455 renderer='rhodecode:templates/admin/my_account/my_account.mako')
456 456 def my_account_bookmarks(self):
457 457 c = self.load_default_context()
458 458 c.active = 'bookmarks'
459 459 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
460 460 self._rhodecode_db_user.user_id, cache=False)
461 461 return self._get_template_context(c)
462 462
463 463 def _process_bookmark_entry(self, entry, user_id):
464 464 position = safe_int(entry.get('position'))
465 465 cur_position = safe_int(entry.get('cur_position'))
466 466 if position is None:
467 467 return
468 468
469 469 # check if this is an existing entry
470 470 is_new = False
471 471 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
472 472
473 473 if db_entry and str2bool(entry.get('remove')):
474 474 log.debug('Marked bookmark %s for deletion', db_entry)
475 475 Session().delete(db_entry)
476 476 return
477 477
478 478 if not db_entry:
479 479 # new
480 480 db_entry = UserBookmark()
481 481 is_new = True
482 482
483 483 should_save = False
484 484 default_redirect_url = ''
485 485
486 486 # save repo
487 487 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
488 488 repo = Repository.get(entry['bookmark_repo'])
489 489 perm_check = HasRepoPermissionAny(
490 490 'repository.read', 'repository.write', 'repository.admin')
491 491 if repo and perm_check(repo_name=repo.repo_name):
492 492 db_entry.repository = repo
493 493 should_save = True
494 494 default_redirect_url = '${repo_url}'
495 495 # save repo group
496 496 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
497 497 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
498 498 perm_check = HasRepoGroupPermissionAny(
499 499 'group.read', 'group.write', 'group.admin')
500 500
501 501 if repo_group and perm_check(group_name=repo_group.group_name):
502 502 db_entry.repository_group = repo_group
503 503 should_save = True
504 504 default_redirect_url = '${repo_group_url}'
505 505 # save generic info
506 506 elif entry.get('title') and entry.get('redirect_url'):
507 507 should_save = True
508 508
509 509 if should_save:
510 510 # mark user and position
511 511 db_entry.user_id = user_id
512 512 db_entry.position = position
513 513 db_entry.title = entry.get('title')
514 514 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
515 515 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
516 516
517 517 Session().add(db_entry)
518 518
519 519 @LoginRequired()
520 520 @NotAnonymous()
521 521 @CSRFRequired()
522 522 @view_config(
523 523 route_name='my_account_bookmarks_update', request_method='POST')
524 524 def my_account_bookmarks_update(self):
525 525 _ = self.request.translate
526 526 c = self.load_default_context()
527 527 c.active = 'bookmarks'
528 528
529 529 controls = peppercorn.parse(self.request.POST.items())
530 530 user_id = c.user.user_id
531 531
532 532 # validate positions
533 533 positions = {}
534 534 for entry in controls.get('bookmarks', []):
535 535 position = safe_int(entry['position'])
536 536 if position is None:
537 537 continue
538 538
539 539 if position in positions:
540 540 h.flash(_("Position {} is defined twice. "
541 541 "Please correct this error.").format(position), category='error')
542 542 return HTTPFound(h.route_path('my_account_bookmarks'))
543 543
544 544 entry['position'] = position
545 545 entry['cur_position'] = safe_int(entry.get('cur_position'))
546 546 positions[position] = entry
547 547
548 548 try:
549 549 for entry in positions.values():
550 550 self._process_bookmark_entry(entry, user_id)
551 551
552 552 Session().commit()
553 553 h.flash(_("Update Bookmarks"), category='success')
554 554 except IntegrityError:
555 555 h.flash(_("Failed to update bookmarks. "
556 556 "Make sure an unique position is used."), category='error')
557 557
558 558 return HTTPFound(h.route_path('my_account_bookmarks'))
559 559
560 560 @LoginRequired()
561 561 @NotAnonymous()
562 562 @view_config(
563 563 route_name='my_account_goto_bookmark', request_method='GET',
564 564 renderer='rhodecode:templates/admin/my_account/my_account.mako')
565 565 def my_account_goto_bookmark(self):
566 566
567 567 bookmark_id = self.request.matchdict['bookmark_id']
568 568 user_bookmark = UserBookmark().query()\
569 569 .filter(UserBookmark.user_id == self.request.user.user_id) \
570 570 .filter(UserBookmark.position == bookmark_id).scalar()
571 571
572 572 redirect_url = h.route_path('my_account_bookmarks')
573 573 if not user_bookmark:
574 574 raise HTTPFound(redirect_url)
575 575
576 576 # repository set
577 577 if user_bookmark.repository:
578 578 repo_name = user_bookmark.repository.repo_name
579 579 base_redirect_url = h.route_path(
580 580 'repo_summary', repo_name=repo_name)
581 581 if user_bookmark.redirect_url and \
582 582 '${repo_url}' in user_bookmark.redirect_url:
583 583 redirect_url = string.Template(user_bookmark.redirect_url)\
584 584 .safe_substitute({'repo_url': base_redirect_url})
585 585 else:
586 586 redirect_url = base_redirect_url
587 587 # repository group set
588 588 elif user_bookmark.repository_group:
589 589 repo_group_name = user_bookmark.repository_group.group_name
590 590 base_redirect_url = h.route_path(
591 591 'repo_group_home', repo_group_name=repo_group_name)
592 592 if user_bookmark.redirect_url and \
593 593 '${repo_group_url}' in user_bookmark.redirect_url:
594 594 redirect_url = string.Template(user_bookmark.redirect_url)\
595 595 .safe_substitute({'repo_group_url': base_redirect_url})
596 596 else:
597 597 redirect_url = base_redirect_url
598 598 # custom URL set
599 599 elif user_bookmark.redirect_url:
600 600 server_url = h.route_url('home').rstrip('/')
601 601 redirect_url = string.Template(user_bookmark.redirect_url) \
602 602 .safe_substitute({'server_url': server_url})
603 603
604 604 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
605 605 raise HTTPFound(redirect_url)
606 606
607 607 @LoginRequired()
608 608 @NotAnonymous()
609 609 @view_config(
610 610 route_name='my_account_perms', request_method='GET',
611 611 renderer='rhodecode:templates/admin/my_account/my_account.mako')
612 612 def my_account_perms(self):
613 613 c = self.load_default_context()
614 614 c.active = 'perms'
615 615
616 616 c.perm_user = c.auth_user
617 617 return self._get_template_context(c)
618 618
619 619 @LoginRequired()
620 620 @NotAnonymous()
621 621 @view_config(
622 622 route_name='my_account_notifications', request_method='GET',
623 623 renderer='rhodecode:templates/admin/my_account/my_account.mako')
624 624 def my_notifications(self):
625 625 c = self.load_default_context()
626 626 c.active = 'notifications'
627 627
628 628 return self._get_template_context(c)
629 629
630 630 @LoginRequired()
631 631 @NotAnonymous()
632 632 @CSRFRequired()
633 633 @view_config(
634 634 route_name='my_account_notifications_toggle_visibility',
635 635 request_method='POST', renderer='json_ext')
636 636 def my_notifications_toggle_visibility(self):
637 637 user = self._rhodecode_db_user
638 638 new_status = not user.user_data.get('notification_status', True)
639 639 user.update_userdata(notification_status=new_status)
640 640 Session().commit()
641 641 return user.user_data['notification_status']
642 642
643 643 @LoginRequired()
644 644 @NotAnonymous()
645 645 @view_config(
646 646 route_name='my_account_edit',
647 647 request_method='GET',
648 648 renderer='rhodecode:templates/admin/my_account/my_account.mako')
649 649 def my_account_edit(self):
650 650 c = self.load_default_context()
651 651 c.active = 'profile_edit'
652 652 c.extern_type = c.user.extern_type
653 653 c.extern_name = c.user.extern_name
654 654
655 655 schema = user_schema.UserProfileSchema().bind(
656 656 username=c.user.username, user_emails=c.user.emails)
657 657 appstruct = {
658 658 'username': c.user.username,
659 659 'email': c.user.email,
660 660 'firstname': c.user.firstname,
661 661 'lastname': c.user.lastname,
662 662 'description': c.user.description,
663 663 }
664 664 c.form = forms.RcForm(
665 665 schema, appstruct=appstruct,
666 666 action=h.route_path('my_account_update'),
667 667 buttons=(forms.buttons.save, forms.buttons.reset))
668 668
669 669 return self._get_template_context(c)
670 670
671 671 @LoginRequired()
672 672 @NotAnonymous()
673 673 @CSRFRequired()
674 674 @view_config(
675 675 route_name='my_account_update',
676 676 request_method='POST',
677 677 renderer='rhodecode:templates/admin/my_account/my_account.mako')
678 678 def my_account_update(self):
679 679 _ = self.request.translate
680 680 c = self.load_default_context()
681 681 c.active = 'profile_edit'
682 682 c.perm_user = c.auth_user
683 683 c.extern_type = c.user.extern_type
684 684 c.extern_name = c.user.extern_name
685 685
686 686 schema = user_schema.UserProfileSchema().bind(
687 687 username=c.user.username, user_emails=c.user.emails)
688 688 form = forms.RcForm(
689 689 schema, buttons=(forms.buttons.save, forms.buttons.reset))
690 690
691 691 controls = self.request.POST.items()
692 692 try:
693 693 valid_data = form.validate(controls)
694 694 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
695 695 'new_password', 'password_confirmation']
696 696 if c.extern_type != "rhodecode":
697 697 # forbid updating username for external accounts
698 698 skip_attrs.append('username')
699 699 old_email = c.user.email
700 700 UserModel().update_user(
701 701 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
702 702 **valid_data)
703 703 if old_email != valid_data['email']:
704 704 old = UserEmailMap.query() \
705 705 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
706 706 old.email = old_email
707 707 h.flash(_('Your account was updated successfully'), category='success')
708 708 Session().commit()
709 709 except forms.ValidationFailure as e:
710 710 c.form = e
711 711 return self._get_template_context(c)
712 712 except Exception:
713 713 log.exception("Exception updating user")
714 714 h.flash(_('Error occurred during update of user'),
715 715 category='error')
716 716 raise HTTPFound(h.route_path('my_account_profile'))
717 717
718 718 def _get_pull_requests_list(self, statuses):
719 719 draw, start, limit = self._extract_chunk(self.request)
720 720 search_q, order_by, order_dir = self._extract_ordering(self.request)
721 721 _render = self.request.get_partial_renderer(
722 722 'rhodecode:templates/data_table/_dt_elements.mako')
723 723
724 724 pull_requests = PullRequestModel().get_im_participating_in(
725 725 user_id=self._rhodecode_user.user_id,
726 statuses=statuses,
726 statuses=statuses, query=search_q,
727 727 offset=start, length=limit, order_by=order_by,
728 728 order_dir=order_dir)
729 729
730 730 pull_requests_total_count = PullRequestModel().count_im_participating_in(
731 user_id=self._rhodecode_user.user_id, statuses=statuses)
731 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
732 732
733 733 data = []
734 734 comments_model = CommentsModel()
735 735 for pr in pull_requests:
736 736 repo_id = pr.target_repo_id
737 737 comments = comments_model.get_all_comments(
738 738 repo_id, pull_request=pr)
739 739 owned = pr.user_id == self._rhodecode_user.user_id
740 740
741 741 data.append({
742 742 'target_repo': _render('pullrequest_target_repo',
743 743 pr.target_repo.repo_name),
744 744 'name': _render('pullrequest_name',
745 745 pr.pull_request_id, pr.pull_request_state,
746 746 pr.work_in_progress, pr.target_repo.repo_name,
747 747 short=True),
748 748 'name_raw': pr.pull_request_id,
749 749 'status': _render('pullrequest_status',
750 750 pr.calculated_review_status()),
751 751 'title': _render('pullrequest_title', pr.title, pr.description),
752 752 'description': h.escape(pr.description),
753 753 'updated_on': _render('pullrequest_updated_on',
754 754 h.datetime_to_time(pr.updated_on)),
755 755 'updated_on_raw': h.datetime_to_time(pr.updated_on),
756 756 'created_on': _render('pullrequest_updated_on',
757 757 h.datetime_to_time(pr.created_on)),
758 758 'created_on_raw': h.datetime_to_time(pr.created_on),
759 759 'state': pr.pull_request_state,
760 760 'author': _render('pullrequest_author',
761 761 pr.author.full_contact, ),
762 762 'author_raw': pr.author.full_name,
763 763 'comments': _render('pullrequest_comments', len(comments)),
764 764 'comments_raw': len(comments),
765 765 'closed': pr.is_closed(),
766 766 'owned': owned
767 767 })
768 768
769 769 # json used to render the grid
770 770 data = ({
771 771 'draw': draw,
772 772 'data': data,
773 773 'recordsTotal': pull_requests_total_count,
774 774 'recordsFiltered': pull_requests_total_count,
775 775 })
776 776 return data
777 777
778 778 @LoginRequired()
779 779 @NotAnonymous()
780 780 @view_config(
781 781 route_name='my_account_pullrequests',
782 782 request_method='GET',
783 783 renderer='rhodecode:templates/admin/my_account/my_account.mako')
784 784 def my_account_pullrequests(self):
785 785 c = self.load_default_context()
786 786 c.active = 'pullrequests'
787 787 req_get = self.request.GET
788 788
789 789 c.closed = str2bool(req_get.get('pr_show_closed'))
790 790
791 791 return self._get_template_context(c)
792 792
793 793 @LoginRequired()
794 794 @NotAnonymous()
795 795 @view_config(
796 796 route_name='my_account_pullrequests_data',
797 797 request_method='GET', renderer='json_ext')
798 798 def my_account_pullrequests_data(self):
799 799 self.load_default_context()
800 800 req_get = self.request.GET
801 801 closed = str2bool(req_get.get('closed'))
802 802
803 803 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
804 804 if closed:
805 805 statuses += [PullRequest.STATUS_CLOSED]
806 806
807 807 data = self._get_pull_requests_list(statuses=statuses)
808 808 return data
809 809
810 810 @LoginRequired()
811 811 @NotAnonymous()
812 812 @view_config(
813 813 route_name='my_account_user_group_membership',
814 814 request_method='GET',
815 815 renderer='rhodecode:templates/admin/my_account/my_account.mako')
816 816 def my_account_user_group_membership(self):
817 817 c = self.load_default_context()
818 818 c.active = 'user_group_membership'
819 819 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
820 820 for group in self._rhodecode_db_user.group_member]
821 821 c.user_groups = json.dumps(groups)
822 822 return self._get_template_context(c)
@@ -1,1922 +1,1929 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 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
27 27 import json
28 28 import logging
29 29 import os
30 30
31 31 import datetime
32 32 import urllib
33 33 import collections
34 34
35 35 from pyramid import compat
36 36 from pyramid.threadlocal import get_current_request
37 37
38 38 from rhodecode.translation import lazy_ugettext
39 39 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.compat import OrderedDict
42 42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 43 from rhodecode.lib.markup_renderer import (
44 44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 45 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe, AttributeDict, safe_int
46 46 from rhodecode.lib.vcs.backends.base import (
47 47 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
48 48 TargetRefMissing, SourceRefMissing)
49 49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 50 from rhodecode.lib.vcs.exceptions import (
51 51 CommitDoesNotExistError, EmptyRepositoryError)
52 52 from rhodecode.model import BaseModel
53 53 from rhodecode.model.changeset_status import ChangesetStatusModel
54 54 from rhodecode.model.comment import CommentsModel
55 55 from rhodecode.model.db import (
56 56 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
57 57 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
58 58 from rhodecode.model.meta import Session
59 59 from rhodecode.model.notification import NotificationModel, \
60 60 EmailNotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 # Data structure to hold the response data when updating commits during a pull
69 69 # request update.
70 70 class UpdateResponse(object):
71 71
72 72 def __init__(self, executed, reason, new, old, common_ancestor_id,
73 73 commit_changes, source_changed, target_changed):
74 74
75 75 self.executed = executed
76 76 self.reason = reason
77 77 self.new = new
78 78 self.old = old
79 79 self.common_ancestor_id = common_ancestor_id
80 80 self.changes = commit_changes
81 81 self.source_changed = source_changed
82 82 self.target_changed = target_changed
83 83
84 84
85 85 class PullRequestModel(BaseModel):
86 86
87 87 cls = PullRequest
88 88
89 89 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
90 90
91 91 UPDATE_STATUS_MESSAGES = {
92 92 UpdateFailureReason.NONE: lazy_ugettext(
93 93 'Pull request update successful.'),
94 94 UpdateFailureReason.UNKNOWN: lazy_ugettext(
95 95 'Pull request update failed because of an unknown error.'),
96 96 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
97 97 'No update needed because the source and target have not changed.'),
98 98 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
99 99 'Pull request cannot be updated because the reference type is '
100 100 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
101 101 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 102 'This pull request cannot be updated because the target '
103 103 'reference is missing.'),
104 104 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 105 'This pull request cannot be updated because the source '
106 106 'reference is missing.'),
107 107 }
108 108 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
109 109 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
110 110
111 111 def __get_pull_request(self, pull_request):
112 112 return self._get_instance((
113 113 PullRequest, PullRequestVersion), pull_request)
114 114
115 115 def _check_perms(self, perms, pull_request, user, api=False):
116 116 if not api:
117 117 return h.HasRepoPermissionAny(*perms)(
118 118 user=user, repo_name=pull_request.target_repo.repo_name)
119 119 else:
120 120 return h.HasRepoPermissionAnyApi(*perms)(
121 121 user=user, repo_name=pull_request.target_repo.repo_name)
122 122
123 123 def check_user_read(self, pull_request, user, api=False):
124 124 _perms = ('repository.admin', 'repository.write', 'repository.read',)
125 125 return self._check_perms(_perms, pull_request, user, api)
126 126
127 127 def check_user_merge(self, pull_request, user, api=False):
128 128 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
129 129 return self._check_perms(_perms, pull_request, user, api)
130 130
131 131 def check_user_update(self, pull_request, user, api=False):
132 132 owner = user.user_id == pull_request.user_id
133 133 return self.check_user_merge(pull_request, user, api) or owner
134 134
135 135 def check_user_delete(self, pull_request, user):
136 136 owner = user.user_id == pull_request.user_id
137 137 _perms = ('repository.admin',)
138 138 return self._check_perms(_perms, pull_request, user) or owner
139 139
140 140 def check_user_change_status(self, pull_request, user, api=False):
141 141 reviewer = user.user_id in [x.user_id for x in
142 142 pull_request.reviewers]
143 143 return self.check_user_update(pull_request, user, api) or reviewer
144 144
145 145 def check_user_comment(self, pull_request, user):
146 146 owner = user.user_id == pull_request.user_id
147 147 return self.check_user_read(pull_request, user) or owner
148 148
149 149 def get(self, pull_request):
150 150 return self.__get_pull_request(pull_request)
151 151
152 152 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
153 153 statuses=None, opened_by=None, order_by=None,
154 154 order_dir='desc', only_created=False):
155 155 repo = None
156 156 if repo_name:
157 157 repo = self._get_repo(repo_name)
158 158
159 159 q = PullRequest.query()
160 160
161 161 if search_q:
162 162 like_expression = u'%{}%'.format(safe_unicode(search_q))
163 163 q = q.filter(or_(
164 164 cast(PullRequest.pull_request_id, String).ilike(like_expression),
165 165 PullRequest.title.ilike(like_expression),
166 166 PullRequest.description.ilike(like_expression),
167 167 ))
168 168
169 169 # source or target
170 170 if repo and source:
171 171 q = q.filter(PullRequest.source_repo == repo)
172 172 elif repo:
173 173 q = q.filter(PullRequest.target_repo == repo)
174 174
175 175 # closed,opened
176 176 if statuses:
177 177 q = q.filter(PullRequest.status.in_(statuses))
178 178
179 179 # opened by filter
180 180 if opened_by:
181 181 q = q.filter(PullRequest.user_id.in_(opened_by))
182 182
183 183 # only get those that are in "created" state
184 184 if only_created:
185 185 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
186 186
187 187 if order_by:
188 188 order_map = {
189 189 'name_raw': PullRequest.pull_request_id,
190 190 'id': PullRequest.pull_request_id,
191 191 'title': PullRequest.title,
192 192 'updated_on_raw': PullRequest.updated_on,
193 193 'target_repo': PullRequest.target_repo_id
194 194 }
195 195 if order_dir == 'asc':
196 196 q = q.order_by(order_map[order_by].asc())
197 197 else:
198 198 q = q.order_by(order_map[order_by].desc())
199 199
200 200 return q
201 201
202 202 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
203 203 opened_by=None):
204 204 """
205 205 Count the number of pull requests for a specific repository.
206 206
207 207 :param repo_name: target or source repo
208 208 :param search_q: filter by text
209 209 :param source: boolean flag to specify if repo_name refers to source
210 210 :param statuses: list of pull request statuses
211 211 :param opened_by: author user of the pull request
212 212 :returns: int number of pull requests
213 213 """
214 214 q = self._prepare_get_all_query(
215 215 repo_name, search_q=search_q, source=source, statuses=statuses,
216 216 opened_by=opened_by)
217 217
218 218 return q.count()
219 219
220 220 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
221 221 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
222 222 """
223 223 Get all pull requests for a specific repository.
224 224
225 225 :param repo_name: target or source repo
226 226 :param search_q: filter by text
227 227 :param source: boolean flag to specify if repo_name refers to source
228 228 :param statuses: list of pull request statuses
229 229 :param opened_by: author user of the pull request
230 230 :param offset: pagination offset
231 231 :param length: length of returned list
232 232 :param order_by: order of the returned list
233 233 :param order_dir: 'asc' or 'desc' ordering direction
234 234 :returns: list of pull requests
235 235 """
236 236 q = self._prepare_get_all_query(
237 237 repo_name, search_q=search_q, source=source, statuses=statuses,
238 238 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
239 239
240 240 if length:
241 241 pull_requests = q.limit(length).offset(offset).all()
242 242 else:
243 243 pull_requests = q.all()
244 244
245 245 return pull_requests
246 246
247 247 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
248 248 opened_by=None):
249 249 """
250 250 Count the number of pull requests for a specific repository that are
251 251 awaiting review.
252 252
253 253 :param repo_name: target or source repo
254 254 :param search_q: filter by text
255 255 :param source: boolean flag to specify if repo_name refers to source
256 256 :param statuses: list of pull request statuses
257 257 :param opened_by: author user of the pull request
258 258 :returns: int number of pull requests
259 259 """
260 260 pull_requests = self.get_awaiting_review(
261 261 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
262 262
263 263 return len(pull_requests)
264 264
265 265 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
266 266 opened_by=None, offset=0, length=None,
267 267 order_by=None, order_dir='desc'):
268 268 """
269 269 Get all pull requests for a specific repository that are awaiting
270 270 review.
271 271
272 272 :param repo_name: target or source repo
273 273 :param search_q: filter by text
274 274 :param source: boolean flag to specify if repo_name refers to source
275 275 :param statuses: list of pull request statuses
276 276 :param opened_by: author user of the pull request
277 277 :param offset: pagination offset
278 278 :param length: length of returned list
279 279 :param order_by: order of the returned list
280 280 :param order_dir: 'asc' or 'desc' ordering direction
281 281 :returns: list of pull requests
282 282 """
283 283 pull_requests = self.get_all(
284 284 repo_name, search_q=search_q, source=source, statuses=statuses,
285 285 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
286 286
287 287 _filtered_pull_requests = []
288 288 for pr in pull_requests:
289 289 status = pr.calculated_review_status()
290 290 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 291 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 292 _filtered_pull_requests.append(pr)
293 293 if length:
294 294 return _filtered_pull_requests[offset:offset+length]
295 295 else:
296 296 return _filtered_pull_requests
297 297
298 298 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
299 299 opened_by=None, user_id=None):
300 300 """
301 301 Count the number of pull requests for a specific repository that are
302 302 awaiting review from a specific user.
303 303
304 304 :param repo_name: target or source repo
305 305 :param search_q: filter by text
306 306 :param source: boolean flag to specify if repo_name refers to source
307 307 :param statuses: list of pull request statuses
308 308 :param opened_by: author user of the pull request
309 309 :param user_id: reviewer user of the pull request
310 310 :returns: int number of pull requests
311 311 """
312 312 pull_requests = self.get_awaiting_my_review(
313 313 repo_name, search_q=search_q, source=source, statuses=statuses,
314 314 opened_by=opened_by, user_id=user_id)
315 315
316 316 return len(pull_requests)
317 317
318 318 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
319 319 opened_by=None, user_id=None, offset=0,
320 320 length=None, order_by=None, order_dir='desc'):
321 321 """
322 322 Get all pull requests for a specific repository that are awaiting
323 323 review from a specific user.
324 324
325 325 :param repo_name: target or source repo
326 326 :param search_q: filter by text
327 327 :param source: boolean flag to specify if repo_name refers to source
328 328 :param statuses: list of pull request statuses
329 329 :param opened_by: author user of the pull request
330 330 :param user_id: reviewer user of the pull request
331 331 :param offset: pagination offset
332 332 :param length: length of returned list
333 333 :param order_by: order of the returned list
334 334 :param order_dir: 'asc' or 'desc' ordering direction
335 335 :returns: list of pull requests
336 336 """
337 337 pull_requests = self.get_all(
338 338 repo_name, search_q=search_q, source=source, statuses=statuses,
339 339 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
340 340
341 341 _my = PullRequestModel().get_not_reviewed(user_id)
342 342 my_participation = []
343 343 for pr in pull_requests:
344 344 if pr in _my:
345 345 my_participation.append(pr)
346 346 _filtered_pull_requests = my_participation
347 347 if length:
348 348 return _filtered_pull_requests[offset:offset+length]
349 349 else:
350 350 return _filtered_pull_requests
351 351
352 352 def get_not_reviewed(self, user_id):
353 353 return [
354 354 x.pull_request for x in PullRequestReviewers.query().filter(
355 355 PullRequestReviewers.user_id == user_id).all()
356 356 ]
357 357
358 def _prepare_participating_query(self, user_id=None, statuses=None,
358 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
359 359 order_by=None, order_dir='desc'):
360 360 q = PullRequest.query()
361 361 if user_id:
362 362 reviewers_subquery = Session().query(
363 363 PullRequestReviewers.pull_request_id).filter(
364 364 PullRequestReviewers.user_id == user_id).subquery()
365 365 user_filter = or_(
366 366 PullRequest.user_id == user_id,
367 367 PullRequest.pull_request_id.in_(reviewers_subquery)
368 368 )
369 369 q = PullRequest.query().filter(user_filter)
370 370
371 371 # closed,opened
372 372 if statuses:
373 373 q = q.filter(PullRequest.status.in_(statuses))
374 374
375 if query:
376 like_expression = u'%{}%'.format(safe_unicode(query))
377 q = q.filter(or_(
378 cast(PullRequest.pull_request_id, String).ilike(like_expression),
379 PullRequest.title.ilike(like_expression),
380 PullRequest.description.ilike(like_expression),
381 ))
375 382 if order_by:
376 383 order_map = {
377 384 'name_raw': PullRequest.pull_request_id,
378 385 'title': PullRequest.title,
379 386 'updated_on_raw': PullRequest.updated_on,
380 387 'target_repo': PullRequest.target_repo_id
381 388 }
382 389 if order_dir == 'asc':
383 390 q = q.order_by(order_map[order_by].asc())
384 391 else:
385 392 q = q.order_by(order_map[order_by].desc())
386 393
387 394 return q
388 395
389 def count_im_participating_in(self, user_id=None, statuses=None):
390 q = self._prepare_participating_query(user_id, statuses=statuses)
396 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
397 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
391 398 return q.count()
392 399
393 400 def get_im_participating_in(
394 self, user_id=None, statuses=None, offset=0,
401 self, user_id=None, statuses=None, query='', offset=0,
395 402 length=None, order_by=None, order_dir='desc'):
396 403 """
397 404 Get all Pull requests that i'm participating in, or i have opened
398 405 """
399 406
400 407 q = self._prepare_participating_query(
401 user_id, statuses=statuses, order_by=order_by,
408 user_id, statuses=statuses, query=query, order_by=order_by,
402 409 order_dir=order_dir)
403 410
404 411 if length:
405 412 pull_requests = q.limit(length).offset(offset).all()
406 413 else:
407 414 pull_requests = q.all()
408 415
409 416 return pull_requests
410 417
411 418 def get_versions(self, pull_request):
412 419 """
413 420 returns version of pull request sorted by ID descending
414 421 """
415 422 return PullRequestVersion.query()\
416 423 .filter(PullRequestVersion.pull_request == pull_request)\
417 424 .order_by(PullRequestVersion.pull_request_version_id.asc())\
418 425 .all()
419 426
420 427 def get_pr_version(self, pull_request_id, version=None):
421 428 at_version = None
422 429
423 430 if version and version == 'latest':
424 431 pull_request_ver = PullRequest.get(pull_request_id)
425 432 pull_request_obj = pull_request_ver
426 433 _org_pull_request_obj = pull_request_obj
427 434 at_version = 'latest'
428 435 elif version:
429 436 pull_request_ver = PullRequestVersion.get_or_404(version)
430 437 pull_request_obj = pull_request_ver
431 438 _org_pull_request_obj = pull_request_ver.pull_request
432 439 at_version = pull_request_ver.pull_request_version_id
433 440 else:
434 441 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
435 442 pull_request_id)
436 443
437 444 pull_request_display_obj = PullRequest.get_pr_display_object(
438 445 pull_request_obj, _org_pull_request_obj)
439 446
440 447 return _org_pull_request_obj, pull_request_obj, \
441 448 pull_request_display_obj, at_version
442 449
443 450 def create(self, created_by, source_repo, source_ref, target_repo,
444 451 target_ref, revisions, reviewers, title, description=None,
445 452 description_renderer=None,
446 453 reviewer_data=None, translator=None, auth_user=None):
447 454 translator = translator or get_current_request().translate
448 455
449 456 created_by_user = self._get_user(created_by)
450 457 auth_user = auth_user or created_by_user.AuthUser()
451 458 source_repo = self._get_repo(source_repo)
452 459 target_repo = self._get_repo(target_repo)
453 460
454 461 pull_request = PullRequest()
455 462 pull_request.source_repo = source_repo
456 463 pull_request.source_ref = source_ref
457 464 pull_request.target_repo = target_repo
458 465 pull_request.target_ref = target_ref
459 466 pull_request.revisions = revisions
460 467 pull_request.title = title
461 468 pull_request.description = description
462 469 pull_request.description_renderer = description_renderer
463 470 pull_request.author = created_by_user
464 471 pull_request.reviewer_data = reviewer_data
465 472 pull_request.pull_request_state = pull_request.STATE_CREATING
466 473 Session().add(pull_request)
467 474 Session().flush()
468 475
469 476 reviewer_ids = set()
470 477 # members / reviewers
471 478 for reviewer_object in reviewers:
472 479 user_id, reasons, mandatory, rules = reviewer_object
473 480 user = self._get_user(user_id)
474 481
475 482 # skip duplicates
476 483 if user.user_id in reviewer_ids:
477 484 continue
478 485
479 486 reviewer_ids.add(user.user_id)
480 487
481 488 reviewer = PullRequestReviewers()
482 489 reviewer.user = user
483 490 reviewer.pull_request = pull_request
484 491 reviewer.reasons = reasons
485 492 reviewer.mandatory = mandatory
486 493
487 494 # NOTE(marcink): pick only first rule for now
488 495 rule_id = list(rules)[0] if rules else None
489 496 rule = RepoReviewRule.get(rule_id) if rule_id else None
490 497 if rule:
491 498 review_group = rule.user_group_vote_rule(user_id)
492 499 # we check if this particular reviewer is member of a voting group
493 500 if review_group:
494 501 # NOTE(marcink):
495 502 # can be that user is member of more but we pick the first same,
496 503 # same as default reviewers algo
497 504 review_group = review_group[0]
498 505
499 506 rule_data = {
500 507 'rule_name':
501 508 rule.review_rule_name,
502 509 'rule_user_group_entry_id':
503 510 review_group.repo_review_rule_users_group_id,
504 511 'rule_user_group_name':
505 512 review_group.users_group.users_group_name,
506 513 'rule_user_group_members':
507 514 [x.user.username for x in review_group.users_group.members],
508 515 'rule_user_group_members_id':
509 516 [x.user.user_id for x in review_group.users_group.members],
510 517 }
511 518 # e.g {'vote_rule': -1, 'mandatory': True}
512 519 rule_data.update(review_group.rule_data())
513 520
514 521 reviewer.rule_data = rule_data
515 522
516 523 Session().add(reviewer)
517 524 Session().flush()
518 525
519 526 # Set approval status to "Under Review" for all commits which are
520 527 # part of this pull request.
521 528 ChangesetStatusModel().set_status(
522 529 repo=target_repo,
523 530 status=ChangesetStatus.STATUS_UNDER_REVIEW,
524 531 user=created_by_user,
525 532 pull_request=pull_request
526 533 )
527 534 # we commit early at this point. This has to do with a fact
528 535 # that before queries do some row-locking. And because of that
529 536 # we need to commit and finish transaction before below validate call
530 537 # that for large repos could be long resulting in long row locks
531 538 Session().commit()
532 539
533 540 # prepare workspace, and run initial merge simulation. Set state during that
534 541 # operation
535 542 pull_request = PullRequest.get(pull_request.pull_request_id)
536 543
537 544 # set as merging, for merge simulation, and if finished to created so we mark
538 545 # simulation is working fine
539 546 with pull_request.set_state(PullRequest.STATE_MERGING,
540 547 final_state=PullRequest.STATE_CREATED) as state_obj:
541 548 MergeCheck.validate(
542 549 pull_request, auth_user=auth_user, translator=translator)
543 550
544 551 self.notify_reviewers(pull_request, reviewer_ids)
545 552 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
546 553
547 554 creation_data = pull_request.get_api_data(with_merge_state=False)
548 555 self._log_audit_action(
549 556 'repo.pull_request.create', {'data': creation_data},
550 557 auth_user, pull_request)
551 558
552 559 return pull_request
553 560
554 561 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
555 562 pull_request = self.__get_pull_request(pull_request)
556 563 target_scm = pull_request.target_repo.scm_instance()
557 564 if action == 'create':
558 565 trigger_hook = hooks_utils.trigger_create_pull_request_hook
559 566 elif action == 'merge':
560 567 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
561 568 elif action == 'close':
562 569 trigger_hook = hooks_utils.trigger_close_pull_request_hook
563 570 elif action == 'review_status_change':
564 571 trigger_hook = hooks_utils.trigger_review_pull_request_hook
565 572 elif action == 'update':
566 573 trigger_hook = hooks_utils.trigger_update_pull_request_hook
567 574 elif action == 'comment':
568 575 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
569 576 else:
570 577 return
571 578
572 579 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
573 580 pull_request, action, trigger_hook)
574 581 trigger_hook(
575 582 username=user.username,
576 583 repo_name=pull_request.target_repo.repo_name,
577 584 repo_type=target_scm.alias,
578 585 pull_request=pull_request,
579 586 data=data)
580 587
581 588 def _get_commit_ids(self, pull_request):
582 589 """
583 590 Return the commit ids of the merged pull request.
584 591
585 592 This method is not dealing correctly yet with the lack of autoupdates
586 593 nor with the implicit target updates.
587 594 For example: if a commit in the source repo is already in the target it
588 595 will be reported anyways.
589 596 """
590 597 merge_rev = pull_request.merge_rev
591 598 if merge_rev is None:
592 599 raise ValueError('This pull request was not merged yet')
593 600
594 601 commit_ids = list(pull_request.revisions)
595 602 if merge_rev not in commit_ids:
596 603 commit_ids.append(merge_rev)
597 604
598 605 return commit_ids
599 606
600 607 def merge_repo(self, pull_request, user, extras):
601 608 log.debug("Merging pull request %s", pull_request.pull_request_id)
602 609 extras['user_agent'] = 'internal-merge'
603 610 merge_state = self._merge_pull_request(pull_request, user, extras)
604 611 if merge_state.executed:
605 612 log.debug("Merge was successful, updating the pull request comments.")
606 613 self._comment_and_close_pr(pull_request, user, merge_state)
607 614
608 615 self._log_audit_action(
609 616 'repo.pull_request.merge',
610 617 {'merge_state': merge_state.__dict__},
611 618 user, pull_request)
612 619
613 620 else:
614 621 log.warn("Merge failed, not updating the pull request.")
615 622 return merge_state
616 623
617 624 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
618 625 target_vcs = pull_request.target_repo.scm_instance()
619 626 source_vcs = pull_request.source_repo.scm_instance()
620 627
621 628 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
622 629 pr_id=pull_request.pull_request_id,
623 630 pr_title=pull_request.title,
624 631 source_repo=source_vcs.name,
625 632 source_ref_name=pull_request.source_ref_parts.name,
626 633 target_repo=target_vcs.name,
627 634 target_ref_name=pull_request.target_ref_parts.name,
628 635 )
629 636
630 637 workspace_id = self._workspace_id(pull_request)
631 638 repo_id = pull_request.target_repo.repo_id
632 639 use_rebase = self._use_rebase_for_merging(pull_request)
633 640 close_branch = self._close_branch_before_merging(pull_request)
634 641 user_name = self._user_name_for_merging(pull_request, user)
635 642
636 643 target_ref = self._refresh_reference(
637 644 pull_request.target_ref_parts, target_vcs)
638 645
639 646 callback_daemon, extras = prepare_callback_daemon(
640 647 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
641 648 host=vcs_settings.HOOKS_HOST,
642 649 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
643 650
644 651 with callback_daemon:
645 652 # TODO: johbo: Implement a clean way to run a config_override
646 653 # for a single call.
647 654 target_vcs.config.set(
648 655 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
649 656
650 657 merge_state = target_vcs.merge(
651 658 repo_id, workspace_id, target_ref, source_vcs,
652 659 pull_request.source_ref_parts,
653 660 user_name=user_name, user_email=user.email,
654 661 message=message, use_rebase=use_rebase,
655 662 close_branch=close_branch)
656 663 return merge_state
657 664
658 665 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
659 666 pull_request.merge_rev = merge_state.merge_ref.commit_id
660 667 pull_request.updated_on = datetime.datetime.now()
661 668 close_msg = close_msg or 'Pull request merged and closed'
662 669
663 670 CommentsModel().create(
664 671 text=safe_unicode(close_msg),
665 672 repo=pull_request.target_repo.repo_id,
666 673 user=user.user_id,
667 674 pull_request=pull_request.pull_request_id,
668 675 f_path=None,
669 676 line_no=None,
670 677 closing_pr=True
671 678 )
672 679
673 680 Session().add(pull_request)
674 681 Session().flush()
675 682 # TODO: paris: replace invalidation with less radical solution
676 683 ScmModel().mark_for_invalidation(
677 684 pull_request.target_repo.repo_name)
678 685 self.trigger_pull_request_hook(pull_request, user, 'merge')
679 686
680 687 def has_valid_update_type(self, pull_request):
681 688 source_ref_type = pull_request.source_ref_parts.type
682 689 return source_ref_type in self.REF_TYPES
683 690
684 691 def get_flow_commits(self, pull_request):
685 692
686 693 # source repo
687 694 source_ref_name = pull_request.source_ref_parts.name
688 695 source_ref_type = pull_request.source_ref_parts.type
689 696 source_ref_id = pull_request.source_ref_parts.commit_id
690 697 source_repo = pull_request.source_repo.scm_instance()
691 698
692 699 try:
693 700 if source_ref_type in self.REF_TYPES:
694 701 source_commit = source_repo.get_commit(source_ref_name)
695 702 else:
696 703 source_commit = source_repo.get_commit(source_ref_id)
697 704 except CommitDoesNotExistError:
698 705 raise SourceRefMissing()
699 706
700 707 # target repo
701 708 target_ref_name = pull_request.target_ref_parts.name
702 709 target_ref_type = pull_request.target_ref_parts.type
703 710 target_ref_id = pull_request.target_ref_parts.commit_id
704 711 target_repo = pull_request.target_repo.scm_instance()
705 712
706 713 try:
707 714 if target_ref_type in self.REF_TYPES:
708 715 target_commit = target_repo.get_commit(target_ref_name)
709 716 else:
710 717 target_commit = target_repo.get_commit(target_ref_id)
711 718 except CommitDoesNotExistError:
712 719 raise TargetRefMissing()
713 720
714 721 return source_commit, target_commit
715 722
716 723 def update_commits(self, pull_request, updating_user):
717 724 """
718 725 Get the updated list of commits for the pull request
719 726 and return the new pull request version and the list
720 727 of commits processed by this update action
721 728
722 729 updating_user is the user_object who triggered the update
723 730 """
724 731 pull_request = self.__get_pull_request(pull_request)
725 732 source_ref_type = pull_request.source_ref_parts.type
726 733 source_ref_name = pull_request.source_ref_parts.name
727 734 source_ref_id = pull_request.source_ref_parts.commit_id
728 735
729 736 target_ref_type = pull_request.target_ref_parts.type
730 737 target_ref_name = pull_request.target_ref_parts.name
731 738 target_ref_id = pull_request.target_ref_parts.commit_id
732 739
733 740 if not self.has_valid_update_type(pull_request):
734 741 log.debug("Skipping update of pull request %s due to ref type: %s",
735 742 pull_request, source_ref_type)
736 743 return UpdateResponse(
737 744 executed=False,
738 745 reason=UpdateFailureReason.WRONG_REF_TYPE,
739 746 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
740 747 source_changed=False, target_changed=False)
741 748
742 749 try:
743 750 source_commit, target_commit = self.get_flow_commits(pull_request)
744 751 except SourceRefMissing:
745 752 return UpdateResponse(
746 753 executed=False,
747 754 reason=UpdateFailureReason.MISSING_SOURCE_REF,
748 755 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
749 756 source_changed=False, target_changed=False)
750 757 except TargetRefMissing:
751 758 return UpdateResponse(
752 759 executed=False,
753 760 reason=UpdateFailureReason.MISSING_TARGET_REF,
754 761 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
755 762 source_changed=False, target_changed=False)
756 763
757 764 source_changed = source_ref_id != source_commit.raw_id
758 765 target_changed = target_ref_id != target_commit.raw_id
759 766
760 767 if not (source_changed or target_changed):
761 768 log.debug("Nothing changed in pull request %s", pull_request)
762 769 return UpdateResponse(
763 770 executed=False,
764 771 reason=UpdateFailureReason.NO_CHANGE,
765 772 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
766 773 source_changed=target_changed, target_changed=source_changed)
767 774
768 775 change_in_found = 'target repo' if target_changed else 'source repo'
769 776 log.debug('Updating pull request because of change in %s detected',
770 777 change_in_found)
771 778
772 779 # Finally there is a need for an update, in case of source change
773 780 # we create a new version, else just an update
774 781 if source_changed:
775 782 pull_request_version = self._create_version_from_snapshot(pull_request)
776 783 self._link_comments_to_version(pull_request_version)
777 784 else:
778 785 try:
779 786 ver = pull_request.versions[-1]
780 787 except IndexError:
781 788 ver = None
782 789
783 790 pull_request.pull_request_version_id = \
784 791 ver.pull_request_version_id if ver else None
785 792 pull_request_version = pull_request
786 793
787 794 source_repo = pull_request.source_repo.scm_instance()
788 795 target_repo = pull_request.target_repo.scm_instance()
789 796
790 797 # re-compute commit ids
791 798 old_commit_ids = pull_request.revisions
792 799 pre_load = ["author", "date", "message", "branch"]
793 800 commit_ranges = target_repo.compare(
794 801 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
795 802 pre_load=pre_load)
796 803
797 804 ancestor_commit_id = source_repo.get_common_ancestor(
798 805 source_commit.raw_id, target_commit.raw_id, target_repo)
799 806
800 807 pull_request.source_ref = '%s:%s:%s' % (
801 808 source_ref_type, source_ref_name, source_commit.raw_id)
802 809 pull_request.target_ref = '%s:%s:%s' % (
803 810 target_ref_type, target_ref_name, ancestor_commit_id)
804 811
805 812 pull_request.revisions = [
806 813 commit.raw_id for commit in reversed(commit_ranges)]
807 814 pull_request.updated_on = datetime.datetime.now()
808 815 Session().add(pull_request)
809 816 new_commit_ids = pull_request.revisions
810 817
811 818 old_diff_data, new_diff_data = self._generate_update_diffs(
812 819 pull_request, pull_request_version)
813 820
814 821 # calculate commit and file changes
815 822 commit_changes = self._calculate_commit_id_changes(
816 823 old_commit_ids, new_commit_ids)
817 824 file_changes = self._calculate_file_changes(
818 825 old_diff_data, new_diff_data)
819 826
820 827 # set comments as outdated if DIFFS changed
821 828 CommentsModel().outdate_comments(
822 829 pull_request, old_diff_data=old_diff_data,
823 830 new_diff_data=new_diff_data)
824 831
825 832 valid_commit_changes = (commit_changes.added or commit_changes.removed)
826 833 file_node_changes = (
827 834 file_changes.added or file_changes.modified or file_changes.removed)
828 835 pr_has_changes = valid_commit_changes or file_node_changes
829 836
830 837 # Add an automatic comment to the pull request, in case
831 838 # anything has changed
832 839 if pr_has_changes:
833 840 update_comment = CommentsModel().create(
834 841 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
835 842 repo=pull_request.target_repo,
836 843 user=pull_request.author,
837 844 pull_request=pull_request,
838 845 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
839 846
840 847 # Update status to "Under Review" for added commits
841 848 for commit_id in commit_changes.added:
842 849 ChangesetStatusModel().set_status(
843 850 repo=pull_request.source_repo,
844 851 status=ChangesetStatus.STATUS_UNDER_REVIEW,
845 852 comment=update_comment,
846 853 user=pull_request.author,
847 854 pull_request=pull_request,
848 855 revision=commit_id)
849 856
850 857 # send update email to users
851 858 try:
852 859 self.notify_users(pull_request=pull_request, updating_user=updating_user,
853 860 ancestor_commit_id=ancestor_commit_id,
854 861 commit_changes=commit_changes,
855 862 file_changes=file_changes)
856 863 except Exception:
857 864 log.exception('Failed to send email notification to users')
858 865
859 866 log.debug(
860 867 'Updated pull request %s, added_ids: %s, common_ids: %s, '
861 868 'removed_ids: %s', pull_request.pull_request_id,
862 869 commit_changes.added, commit_changes.common, commit_changes.removed)
863 870 log.debug(
864 871 'Updated pull request with the following file changes: %s',
865 872 file_changes)
866 873
867 874 log.info(
868 875 "Updated pull request %s from commit %s to commit %s, "
869 876 "stored new version %s of this pull request.",
870 877 pull_request.pull_request_id, source_ref_id,
871 878 pull_request.source_ref_parts.commit_id,
872 879 pull_request_version.pull_request_version_id)
873 880 Session().commit()
874 881 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
875 882
876 883 return UpdateResponse(
877 884 executed=True, reason=UpdateFailureReason.NONE,
878 885 old=pull_request, new=pull_request_version,
879 886 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
880 887 source_changed=source_changed, target_changed=target_changed)
881 888
882 889 def _create_version_from_snapshot(self, pull_request):
883 890 version = PullRequestVersion()
884 891 version.title = pull_request.title
885 892 version.description = pull_request.description
886 893 version.status = pull_request.status
887 894 version.pull_request_state = pull_request.pull_request_state
888 895 version.created_on = datetime.datetime.now()
889 896 version.updated_on = pull_request.updated_on
890 897 version.user_id = pull_request.user_id
891 898 version.source_repo = pull_request.source_repo
892 899 version.source_ref = pull_request.source_ref
893 900 version.target_repo = pull_request.target_repo
894 901 version.target_ref = pull_request.target_ref
895 902
896 903 version._last_merge_source_rev = pull_request._last_merge_source_rev
897 904 version._last_merge_target_rev = pull_request._last_merge_target_rev
898 905 version.last_merge_status = pull_request.last_merge_status
899 906 version.last_merge_metadata = pull_request.last_merge_metadata
900 907 version.shadow_merge_ref = pull_request.shadow_merge_ref
901 908 version.merge_rev = pull_request.merge_rev
902 909 version.reviewer_data = pull_request.reviewer_data
903 910
904 911 version.revisions = pull_request.revisions
905 912 version.pull_request = pull_request
906 913 Session().add(version)
907 914 Session().flush()
908 915
909 916 return version
910 917
911 918 def _generate_update_diffs(self, pull_request, pull_request_version):
912 919
913 920 diff_context = (
914 921 self.DIFF_CONTEXT +
915 922 CommentsModel.needed_extra_diff_context())
916 923 hide_whitespace_changes = False
917 924 source_repo = pull_request_version.source_repo
918 925 source_ref_id = pull_request_version.source_ref_parts.commit_id
919 926 target_ref_id = pull_request_version.target_ref_parts.commit_id
920 927 old_diff = self._get_diff_from_pr_or_version(
921 928 source_repo, source_ref_id, target_ref_id,
922 929 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
923 930
924 931 source_repo = pull_request.source_repo
925 932 source_ref_id = pull_request.source_ref_parts.commit_id
926 933 target_ref_id = pull_request.target_ref_parts.commit_id
927 934
928 935 new_diff = self._get_diff_from_pr_or_version(
929 936 source_repo, source_ref_id, target_ref_id,
930 937 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
931 938
932 939 old_diff_data = diffs.DiffProcessor(old_diff)
933 940 old_diff_data.prepare()
934 941 new_diff_data = diffs.DiffProcessor(new_diff)
935 942 new_diff_data.prepare()
936 943
937 944 return old_diff_data, new_diff_data
938 945
939 946 def _link_comments_to_version(self, pull_request_version):
940 947 """
941 948 Link all unlinked comments of this pull request to the given version.
942 949
943 950 :param pull_request_version: The `PullRequestVersion` to which
944 951 the comments shall be linked.
945 952
946 953 """
947 954 pull_request = pull_request_version.pull_request
948 955 comments = ChangesetComment.query()\
949 956 .filter(
950 957 # TODO: johbo: Should we query for the repo at all here?
951 958 # Pending decision on how comments of PRs are to be related
952 959 # to either the source repo, the target repo or no repo at all.
953 960 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
954 961 ChangesetComment.pull_request == pull_request,
955 962 ChangesetComment.pull_request_version == None)\
956 963 .order_by(ChangesetComment.comment_id.asc())
957 964
958 965 # TODO: johbo: Find out why this breaks if it is done in a bulk
959 966 # operation.
960 967 for comment in comments:
961 968 comment.pull_request_version_id = (
962 969 pull_request_version.pull_request_version_id)
963 970 Session().add(comment)
964 971
965 972 def _calculate_commit_id_changes(self, old_ids, new_ids):
966 973 added = [x for x in new_ids if x not in old_ids]
967 974 common = [x for x in new_ids if x in old_ids]
968 975 removed = [x for x in old_ids if x not in new_ids]
969 976 total = new_ids
970 977 return ChangeTuple(added, common, removed, total)
971 978
972 979 def _calculate_file_changes(self, old_diff_data, new_diff_data):
973 980
974 981 old_files = OrderedDict()
975 982 for diff_data in old_diff_data.parsed_diff:
976 983 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
977 984
978 985 added_files = []
979 986 modified_files = []
980 987 removed_files = []
981 988 for diff_data in new_diff_data.parsed_diff:
982 989 new_filename = diff_data['filename']
983 990 new_hash = md5_safe(diff_data['raw_diff'])
984 991
985 992 old_hash = old_files.get(new_filename)
986 993 if not old_hash:
987 994 # file is not present in old diff, we have to figure out from parsed diff
988 995 # operation ADD/REMOVE
989 996 operations_dict = diff_data['stats']['ops']
990 997 if diffs.DEL_FILENODE in operations_dict:
991 998 removed_files.append(new_filename)
992 999 else:
993 1000 added_files.append(new_filename)
994 1001 else:
995 1002 if new_hash != old_hash:
996 1003 modified_files.append(new_filename)
997 1004 # now remove a file from old, since we have seen it already
998 1005 del old_files[new_filename]
999 1006
1000 1007 # removed files is when there are present in old, but not in NEW,
1001 1008 # since we remove old files that are present in new diff, left-overs
1002 1009 # if any should be the removed files
1003 1010 removed_files.extend(old_files.keys())
1004 1011
1005 1012 return FileChangeTuple(added_files, modified_files, removed_files)
1006 1013
1007 1014 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1008 1015 """
1009 1016 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1010 1017 so it's always looking the same disregarding on which default
1011 1018 renderer system is using.
1012 1019
1013 1020 :param ancestor_commit_id: ancestor raw_id
1014 1021 :param changes: changes named tuple
1015 1022 :param file_changes: file changes named tuple
1016 1023
1017 1024 """
1018 1025 new_status = ChangesetStatus.get_status_lbl(
1019 1026 ChangesetStatus.STATUS_UNDER_REVIEW)
1020 1027
1021 1028 changed_files = (
1022 1029 file_changes.added + file_changes.modified + file_changes.removed)
1023 1030
1024 1031 params = {
1025 1032 'under_review_label': new_status,
1026 1033 'added_commits': changes.added,
1027 1034 'removed_commits': changes.removed,
1028 1035 'changed_files': changed_files,
1029 1036 'added_files': file_changes.added,
1030 1037 'modified_files': file_changes.modified,
1031 1038 'removed_files': file_changes.removed,
1032 1039 'ancestor_commit_id': ancestor_commit_id
1033 1040 }
1034 1041 renderer = RstTemplateRenderer()
1035 1042 return renderer.render('pull_request_update.mako', **params)
1036 1043
1037 1044 def edit(self, pull_request, title, description, description_renderer, user):
1038 1045 pull_request = self.__get_pull_request(pull_request)
1039 1046 old_data = pull_request.get_api_data(with_merge_state=False)
1040 1047 if pull_request.is_closed():
1041 1048 raise ValueError('This pull request is closed')
1042 1049 if title:
1043 1050 pull_request.title = title
1044 1051 pull_request.description = description
1045 1052 pull_request.updated_on = datetime.datetime.now()
1046 1053 pull_request.description_renderer = description_renderer
1047 1054 Session().add(pull_request)
1048 1055 self._log_audit_action(
1049 1056 'repo.pull_request.edit', {'old_data': old_data},
1050 1057 user, pull_request)
1051 1058
1052 1059 def update_reviewers(self, pull_request, reviewer_data, user):
1053 1060 """
1054 1061 Update the reviewers in the pull request
1055 1062
1056 1063 :param pull_request: the pr to update
1057 1064 :param reviewer_data: list of tuples
1058 1065 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1059 1066 """
1060 1067 pull_request = self.__get_pull_request(pull_request)
1061 1068 if pull_request.is_closed():
1062 1069 raise ValueError('This pull request is closed')
1063 1070
1064 1071 reviewers = {}
1065 1072 for user_id, reasons, mandatory, rules in reviewer_data:
1066 1073 if isinstance(user_id, (int, compat.string_types)):
1067 1074 user_id = self._get_user(user_id).user_id
1068 1075 reviewers[user_id] = {
1069 1076 'reasons': reasons, 'mandatory': mandatory}
1070 1077
1071 1078 reviewers_ids = set(reviewers.keys())
1072 1079 current_reviewers = PullRequestReviewers.query()\
1073 1080 .filter(PullRequestReviewers.pull_request ==
1074 1081 pull_request).all()
1075 1082 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1076 1083
1077 1084 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1078 1085 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1079 1086
1080 1087 log.debug("Adding %s reviewers", ids_to_add)
1081 1088 log.debug("Removing %s reviewers", ids_to_remove)
1082 1089 changed = False
1083 1090 added_audit_reviewers = []
1084 1091 removed_audit_reviewers = []
1085 1092
1086 1093 for uid in ids_to_add:
1087 1094 changed = True
1088 1095 _usr = self._get_user(uid)
1089 1096 reviewer = PullRequestReviewers()
1090 1097 reviewer.user = _usr
1091 1098 reviewer.pull_request = pull_request
1092 1099 reviewer.reasons = reviewers[uid]['reasons']
1093 1100 # NOTE(marcink): mandatory shouldn't be changed now
1094 1101 # reviewer.mandatory = reviewers[uid]['reasons']
1095 1102 Session().add(reviewer)
1096 1103 added_audit_reviewers.append(reviewer.get_dict())
1097 1104
1098 1105 for uid in ids_to_remove:
1099 1106 changed = True
1100 1107 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1101 1108 # that prevents and fixes cases that we added the same reviewer twice.
1102 1109 # this CAN happen due to the lack of DB checks
1103 1110 reviewers = PullRequestReviewers.query()\
1104 1111 .filter(PullRequestReviewers.user_id == uid,
1105 1112 PullRequestReviewers.pull_request == pull_request)\
1106 1113 .all()
1107 1114
1108 1115 for obj in reviewers:
1109 1116 added_audit_reviewers.append(obj.get_dict())
1110 1117 Session().delete(obj)
1111 1118
1112 1119 if changed:
1113 1120 Session().expire_all()
1114 1121 pull_request.updated_on = datetime.datetime.now()
1115 1122 Session().add(pull_request)
1116 1123
1117 1124 # finally store audit logs
1118 1125 for user_data in added_audit_reviewers:
1119 1126 self._log_audit_action(
1120 1127 'repo.pull_request.reviewer.add', {'data': user_data},
1121 1128 user, pull_request)
1122 1129 for user_data in removed_audit_reviewers:
1123 1130 self._log_audit_action(
1124 1131 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1125 1132 user, pull_request)
1126 1133
1127 1134 self.notify_reviewers(pull_request, ids_to_add)
1128 1135 return ids_to_add, ids_to_remove
1129 1136
1130 1137 def get_url(self, pull_request, request=None, permalink=False):
1131 1138 if not request:
1132 1139 request = get_current_request()
1133 1140
1134 1141 if permalink:
1135 1142 return request.route_url(
1136 1143 'pull_requests_global',
1137 1144 pull_request_id=pull_request.pull_request_id,)
1138 1145 else:
1139 1146 return request.route_url('pullrequest_show',
1140 1147 repo_name=safe_str(pull_request.target_repo.repo_name),
1141 1148 pull_request_id=pull_request.pull_request_id,)
1142 1149
1143 1150 def get_shadow_clone_url(self, pull_request, request=None):
1144 1151 """
1145 1152 Returns qualified url pointing to the shadow repository. If this pull
1146 1153 request is closed there is no shadow repository and ``None`` will be
1147 1154 returned.
1148 1155 """
1149 1156 if pull_request.is_closed():
1150 1157 return None
1151 1158 else:
1152 1159 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1153 1160 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1154 1161
1155 1162 def notify_reviewers(self, pull_request, reviewers_ids):
1156 1163 # notification to reviewers
1157 1164 if not reviewers_ids:
1158 1165 return
1159 1166
1160 1167 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1161 1168
1162 1169 pull_request_obj = pull_request
1163 1170 # get the current participants of this pull request
1164 1171 recipients = reviewers_ids
1165 1172 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1166 1173
1167 1174 pr_source_repo = pull_request_obj.source_repo
1168 1175 pr_target_repo = pull_request_obj.target_repo
1169 1176
1170 1177 pr_url = h.route_url('pullrequest_show',
1171 1178 repo_name=pr_target_repo.repo_name,
1172 1179 pull_request_id=pull_request_obj.pull_request_id,)
1173 1180
1174 1181 # set some variables for email notification
1175 1182 pr_target_repo_url = h.route_url(
1176 1183 'repo_summary', repo_name=pr_target_repo.repo_name)
1177 1184
1178 1185 pr_source_repo_url = h.route_url(
1179 1186 'repo_summary', repo_name=pr_source_repo.repo_name)
1180 1187
1181 1188 # pull request specifics
1182 1189 pull_request_commits = [
1183 1190 (x.raw_id, x.message)
1184 1191 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1185 1192
1186 1193 kwargs = {
1187 1194 'user': pull_request.author,
1188 1195 'pull_request': pull_request_obj,
1189 1196 'pull_request_commits': pull_request_commits,
1190 1197
1191 1198 'pull_request_target_repo': pr_target_repo,
1192 1199 'pull_request_target_repo_url': pr_target_repo_url,
1193 1200
1194 1201 'pull_request_source_repo': pr_source_repo,
1195 1202 'pull_request_source_repo_url': pr_source_repo_url,
1196 1203
1197 1204 'pull_request_url': pr_url,
1198 1205 }
1199 1206
1200 1207 # pre-generate the subject for notification itself
1201 1208 (subject,
1202 1209 _h, _e, # we don't care about those
1203 1210 body_plaintext) = EmailNotificationModel().render_email(
1204 1211 notification_type, **kwargs)
1205 1212
1206 1213 # create notification objects, and emails
1207 1214 NotificationModel().create(
1208 1215 created_by=pull_request.author,
1209 1216 notification_subject=subject,
1210 1217 notification_body=body_plaintext,
1211 1218 notification_type=notification_type,
1212 1219 recipients=recipients,
1213 1220 email_kwargs=kwargs,
1214 1221 )
1215 1222
1216 1223 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1217 1224 commit_changes, file_changes):
1218 1225
1219 1226 updating_user_id = updating_user.user_id
1220 1227 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1221 1228 # NOTE(marcink): send notification to all other users except to
1222 1229 # person who updated the PR
1223 1230 recipients = reviewers.difference(set([updating_user_id]))
1224 1231
1225 1232 log.debug('Notify following recipients about pull-request update %s', recipients)
1226 1233
1227 1234 pull_request_obj = pull_request
1228 1235
1229 1236 # send email about the update
1230 1237 changed_files = (
1231 1238 file_changes.added + file_changes.modified + file_changes.removed)
1232 1239
1233 1240 pr_source_repo = pull_request_obj.source_repo
1234 1241 pr_target_repo = pull_request_obj.target_repo
1235 1242
1236 1243 pr_url = h.route_url('pullrequest_show',
1237 1244 repo_name=pr_target_repo.repo_name,
1238 1245 pull_request_id=pull_request_obj.pull_request_id,)
1239 1246
1240 1247 # set some variables for email notification
1241 1248 pr_target_repo_url = h.route_url(
1242 1249 'repo_summary', repo_name=pr_target_repo.repo_name)
1243 1250
1244 1251 pr_source_repo_url = h.route_url(
1245 1252 'repo_summary', repo_name=pr_source_repo.repo_name)
1246 1253
1247 1254 email_kwargs = {
1248 1255 'date': datetime.datetime.now(),
1249 1256 'updating_user': updating_user,
1250 1257
1251 1258 'pull_request': pull_request_obj,
1252 1259
1253 1260 'pull_request_target_repo': pr_target_repo,
1254 1261 'pull_request_target_repo_url': pr_target_repo_url,
1255 1262
1256 1263 'pull_request_source_repo': pr_source_repo,
1257 1264 'pull_request_source_repo_url': pr_source_repo_url,
1258 1265
1259 1266 'pull_request_url': pr_url,
1260 1267
1261 1268 'ancestor_commit_id': ancestor_commit_id,
1262 1269 'added_commits': commit_changes.added,
1263 1270 'removed_commits': commit_changes.removed,
1264 1271 'changed_files': changed_files,
1265 1272 'added_files': file_changes.added,
1266 1273 'modified_files': file_changes.modified,
1267 1274 'removed_files': file_changes.removed,
1268 1275 }
1269 1276
1270 1277 (subject,
1271 1278 _h, _e, # we don't care about those
1272 1279 body_plaintext) = EmailNotificationModel().render_email(
1273 1280 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1274 1281
1275 1282 # create notification objects, and emails
1276 1283 NotificationModel().create(
1277 1284 created_by=updating_user,
1278 1285 notification_subject=subject,
1279 1286 notification_body=body_plaintext,
1280 1287 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1281 1288 recipients=recipients,
1282 1289 email_kwargs=email_kwargs,
1283 1290 )
1284 1291
1285 1292 def delete(self, pull_request, user):
1286 1293 pull_request = self.__get_pull_request(pull_request)
1287 1294 old_data = pull_request.get_api_data(with_merge_state=False)
1288 1295 self._cleanup_merge_workspace(pull_request)
1289 1296 self._log_audit_action(
1290 1297 'repo.pull_request.delete', {'old_data': old_data},
1291 1298 user, pull_request)
1292 1299 Session().delete(pull_request)
1293 1300
1294 1301 def close_pull_request(self, pull_request, user):
1295 1302 pull_request = self.__get_pull_request(pull_request)
1296 1303 self._cleanup_merge_workspace(pull_request)
1297 1304 pull_request.status = PullRequest.STATUS_CLOSED
1298 1305 pull_request.updated_on = datetime.datetime.now()
1299 1306 Session().add(pull_request)
1300 1307 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1301 1308
1302 1309 pr_data = pull_request.get_api_data(with_merge_state=False)
1303 1310 self._log_audit_action(
1304 1311 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1305 1312
1306 1313 def close_pull_request_with_comment(
1307 1314 self, pull_request, user, repo, message=None, auth_user=None):
1308 1315
1309 1316 pull_request_review_status = pull_request.calculated_review_status()
1310 1317
1311 1318 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1312 1319 # approved only if we have voting consent
1313 1320 status = ChangesetStatus.STATUS_APPROVED
1314 1321 else:
1315 1322 status = ChangesetStatus.STATUS_REJECTED
1316 1323 status_lbl = ChangesetStatus.get_status_lbl(status)
1317 1324
1318 1325 default_message = (
1319 1326 'Closing with status change {transition_icon} {status}.'
1320 1327 ).format(transition_icon='>', status=status_lbl)
1321 1328 text = message or default_message
1322 1329
1323 1330 # create a comment, and link it to new status
1324 1331 comment = CommentsModel().create(
1325 1332 text=text,
1326 1333 repo=repo.repo_id,
1327 1334 user=user.user_id,
1328 1335 pull_request=pull_request.pull_request_id,
1329 1336 status_change=status_lbl,
1330 1337 status_change_type=status,
1331 1338 closing_pr=True,
1332 1339 auth_user=auth_user,
1333 1340 )
1334 1341
1335 1342 # calculate old status before we change it
1336 1343 old_calculated_status = pull_request.calculated_review_status()
1337 1344 ChangesetStatusModel().set_status(
1338 1345 repo.repo_id,
1339 1346 status,
1340 1347 user.user_id,
1341 1348 comment=comment,
1342 1349 pull_request=pull_request.pull_request_id
1343 1350 )
1344 1351
1345 1352 Session().flush()
1346 1353
1347 1354 self.trigger_pull_request_hook(pull_request, user, 'comment',
1348 1355 data={'comment': comment})
1349 1356
1350 1357 # we now calculate the status of pull request again, and based on that
1351 1358 # calculation trigger status change. This might happen in cases
1352 1359 # that non-reviewer admin closes a pr, which means his vote doesn't
1353 1360 # change the status, while if he's a reviewer this might change it.
1354 1361 calculated_status = pull_request.calculated_review_status()
1355 1362 if old_calculated_status != calculated_status:
1356 1363 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1357 1364 data={'status': calculated_status})
1358 1365
1359 1366 # finally close the PR
1360 1367 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1361 1368
1362 1369 return comment, status
1363 1370
1364 1371 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1365 1372 _ = translator or get_current_request().translate
1366 1373
1367 1374 if not self._is_merge_enabled(pull_request):
1368 1375 return None, False, _('Server-side pull request merging is disabled.')
1369 1376
1370 1377 if pull_request.is_closed():
1371 1378 return None, False, _('This pull request is closed.')
1372 1379
1373 1380 merge_possible, msg = self._check_repo_requirements(
1374 1381 target=pull_request.target_repo, source=pull_request.source_repo,
1375 1382 translator=_)
1376 1383 if not merge_possible:
1377 1384 return None, merge_possible, msg
1378 1385
1379 1386 try:
1380 1387 merge_response = self._try_merge(
1381 1388 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1382 1389 log.debug("Merge response: %s", merge_response)
1383 1390 return merge_response, merge_response.possible, merge_response.merge_status_message
1384 1391 except NotImplementedError:
1385 1392 return None, False, _('Pull request merging is not supported.')
1386 1393
1387 1394 def _check_repo_requirements(self, target, source, translator):
1388 1395 """
1389 1396 Check if `target` and `source` have compatible requirements.
1390 1397
1391 1398 Currently this is just checking for largefiles.
1392 1399 """
1393 1400 _ = translator
1394 1401 target_has_largefiles = self._has_largefiles(target)
1395 1402 source_has_largefiles = self._has_largefiles(source)
1396 1403 merge_possible = True
1397 1404 message = u''
1398 1405
1399 1406 if target_has_largefiles != source_has_largefiles:
1400 1407 merge_possible = False
1401 1408 if source_has_largefiles:
1402 1409 message = _(
1403 1410 'Target repository large files support is disabled.')
1404 1411 else:
1405 1412 message = _(
1406 1413 'Source repository large files support is disabled.')
1407 1414
1408 1415 return merge_possible, message
1409 1416
1410 1417 def _has_largefiles(self, repo):
1411 1418 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1412 1419 'extensions', 'largefiles')
1413 1420 return largefiles_ui and largefiles_ui[0].active
1414 1421
1415 1422 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1416 1423 """
1417 1424 Try to merge the pull request and return the merge status.
1418 1425 """
1419 1426 log.debug(
1420 1427 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1421 1428 pull_request.pull_request_id, force_shadow_repo_refresh)
1422 1429 target_vcs = pull_request.target_repo.scm_instance()
1423 1430 # Refresh the target reference.
1424 1431 try:
1425 1432 target_ref = self._refresh_reference(
1426 1433 pull_request.target_ref_parts, target_vcs)
1427 1434 except CommitDoesNotExistError:
1428 1435 merge_state = MergeResponse(
1429 1436 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1430 1437 metadata={'target_ref': pull_request.target_ref_parts})
1431 1438 return merge_state
1432 1439
1433 1440 target_locked = pull_request.target_repo.locked
1434 1441 if target_locked and target_locked[0]:
1435 1442 locked_by = 'user:{}'.format(target_locked[0])
1436 1443 log.debug("The target repository is locked by %s.", locked_by)
1437 1444 merge_state = MergeResponse(
1438 1445 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1439 1446 metadata={'locked_by': locked_by})
1440 1447 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1441 1448 pull_request, target_ref):
1442 1449 log.debug("Refreshing the merge status of the repository.")
1443 1450 merge_state = self._refresh_merge_state(
1444 1451 pull_request, target_vcs, target_ref)
1445 1452 else:
1446 1453 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1447 1454 metadata = {
1448 1455 'unresolved_files': '',
1449 1456 'target_ref': pull_request.target_ref_parts,
1450 1457 'source_ref': pull_request.source_ref_parts,
1451 1458 }
1452 1459 if pull_request.last_merge_metadata:
1453 1460 metadata.update(pull_request.last_merge_metadata)
1454 1461
1455 1462 if not possible and target_ref.type == 'branch':
1456 1463 # NOTE(marcink): case for mercurial multiple heads on branch
1457 1464 heads = target_vcs._heads(target_ref.name)
1458 1465 if len(heads) != 1:
1459 1466 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1460 1467 metadata.update({
1461 1468 'heads': heads
1462 1469 })
1463 1470
1464 1471 merge_state = MergeResponse(
1465 1472 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1466 1473
1467 1474 return merge_state
1468 1475
1469 1476 def _refresh_reference(self, reference, vcs_repository):
1470 1477 if reference.type in self.UPDATABLE_REF_TYPES:
1471 1478 name_or_id = reference.name
1472 1479 else:
1473 1480 name_or_id = reference.commit_id
1474 1481
1475 1482 refreshed_commit = vcs_repository.get_commit(name_or_id)
1476 1483 refreshed_reference = Reference(
1477 1484 reference.type, reference.name, refreshed_commit.raw_id)
1478 1485 return refreshed_reference
1479 1486
1480 1487 def _needs_merge_state_refresh(self, pull_request, target_reference):
1481 1488 return not(
1482 1489 pull_request.revisions and
1483 1490 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1484 1491 target_reference.commit_id == pull_request._last_merge_target_rev)
1485 1492
1486 1493 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1487 1494 workspace_id = self._workspace_id(pull_request)
1488 1495 source_vcs = pull_request.source_repo.scm_instance()
1489 1496 repo_id = pull_request.target_repo.repo_id
1490 1497 use_rebase = self._use_rebase_for_merging(pull_request)
1491 1498 close_branch = self._close_branch_before_merging(pull_request)
1492 1499 merge_state = target_vcs.merge(
1493 1500 repo_id, workspace_id,
1494 1501 target_reference, source_vcs, pull_request.source_ref_parts,
1495 1502 dry_run=True, use_rebase=use_rebase,
1496 1503 close_branch=close_branch)
1497 1504
1498 1505 # Do not store the response if there was an unknown error.
1499 1506 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1500 1507 pull_request._last_merge_source_rev = \
1501 1508 pull_request.source_ref_parts.commit_id
1502 1509 pull_request._last_merge_target_rev = target_reference.commit_id
1503 1510 pull_request.last_merge_status = merge_state.failure_reason
1504 1511 pull_request.last_merge_metadata = merge_state.metadata
1505 1512
1506 1513 pull_request.shadow_merge_ref = merge_state.merge_ref
1507 1514 Session().add(pull_request)
1508 1515 Session().commit()
1509 1516
1510 1517 return merge_state
1511 1518
1512 1519 def _workspace_id(self, pull_request):
1513 1520 workspace_id = 'pr-%s' % pull_request.pull_request_id
1514 1521 return workspace_id
1515 1522
1516 1523 def generate_repo_data(self, repo, commit_id=None, branch=None,
1517 1524 bookmark=None, translator=None):
1518 1525 from rhodecode.model.repo import RepoModel
1519 1526
1520 1527 all_refs, selected_ref = \
1521 1528 self._get_repo_pullrequest_sources(
1522 1529 repo.scm_instance(), commit_id=commit_id,
1523 1530 branch=branch, bookmark=bookmark, translator=translator)
1524 1531
1525 1532 refs_select2 = []
1526 1533 for element in all_refs:
1527 1534 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1528 1535 refs_select2.append({'text': element[1], 'children': children})
1529 1536
1530 1537 return {
1531 1538 'user': {
1532 1539 'user_id': repo.user.user_id,
1533 1540 'username': repo.user.username,
1534 1541 'firstname': repo.user.first_name,
1535 1542 'lastname': repo.user.last_name,
1536 1543 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1537 1544 },
1538 1545 'name': repo.repo_name,
1539 1546 'link': RepoModel().get_url(repo),
1540 1547 'description': h.chop_at_smart(repo.description_safe, '\n'),
1541 1548 'refs': {
1542 1549 'all_refs': all_refs,
1543 1550 'selected_ref': selected_ref,
1544 1551 'select2_refs': refs_select2
1545 1552 }
1546 1553 }
1547 1554
1548 1555 def generate_pullrequest_title(self, source, source_ref, target):
1549 1556 return u'{source}#{at_ref} to {target}'.format(
1550 1557 source=source,
1551 1558 at_ref=source_ref,
1552 1559 target=target,
1553 1560 )
1554 1561
1555 1562 def _cleanup_merge_workspace(self, pull_request):
1556 1563 # Merging related cleanup
1557 1564 repo_id = pull_request.target_repo.repo_id
1558 1565 target_scm = pull_request.target_repo.scm_instance()
1559 1566 workspace_id = self._workspace_id(pull_request)
1560 1567
1561 1568 try:
1562 1569 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1563 1570 except NotImplementedError:
1564 1571 pass
1565 1572
1566 1573 def _get_repo_pullrequest_sources(
1567 1574 self, repo, commit_id=None, branch=None, bookmark=None,
1568 1575 translator=None):
1569 1576 """
1570 1577 Return a structure with repo's interesting commits, suitable for
1571 1578 the selectors in pullrequest controller
1572 1579
1573 1580 :param commit_id: a commit that must be in the list somehow
1574 1581 and selected by default
1575 1582 :param branch: a branch that must be in the list and selected
1576 1583 by default - even if closed
1577 1584 :param bookmark: a bookmark that must be in the list and selected
1578 1585 """
1579 1586 _ = translator or get_current_request().translate
1580 1587
1581 1588 commit_id = safe_str(commit_id) if commit_id else None
1582 1589 branch = safe_unicode(branch) if branch else None
1583 1590 bookmark = safe_unicode(bookmark) if bookmark else None
1584 1591
1585 1592 selected = None
1586 1593
1587 1594 # order matters: first source that has commit_id in it will be selected
1588 1595 sources = []
1589 1596 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1590 1597 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1591 1598
1592 1599 if commit_id:
1593 1600 ref_commit = (h.short_id(commit_id), commit_id)
1594 1601 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1595 1602
1596 1603 sources.append(
1597 1604 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1598 1605 )
1599 1606
1600 1607 groups = []
1601 1608
1602 1609 for group_key, ref_list, group_name, match in sources:
1603 1610 group_refs = []
1604 1611 for ref_name, ref_id in ref_list:
1605 1612 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1606 1613 group_refs.append((ref_key, ref_name))
1607 1614
1608 1615 if not selected:
1609 1616 if set([commit_id, match]) & set([ref_id, ref_name]):
1610 1617 selected = ref_key
1611 1618
1612 1619 if group_refs:
1613 1620 groups.append((group_refs, group_name))
1614 1621
1615 1622 if not selected:
1616 1623 ref = commit_id or branch or bookmark
1617 1624 if ref:
1618 1625 raise CommitDoesNotExistError(
1619 1626 u'No commit refs could be found matching: {}'.format(ref))
1620 1627 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1621 1628 selected = u'branch:{}:{}'.format(
1622 1629 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1623 1630 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1624 1631 )
1625 1632 elif repo.commit_ids:
1626 1633 # make the user select in this case
1627 1634 selected = None
1628 1635 else:
1629 1636 raise EmptyRepositoryError()
1630 1637 return groups, selected
1631 1638
1632 1639 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1633 1640 hide_whitespace_changes, diff_context):
1634 1641
1635 1642 return self._get_diff_from_pr_or_version(
1636 1643 source_repo, source_ref_id, target_ref_id,
1637 1644 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1638 1645
1639 1646 def _get_diff_from_pr_or_version(
1640 1647 self, source_repo, source_ref_id, target_ref_id,
1641 1648 hide_whitespace_changes, diff_context):
1642 1649
1643 1650 target_commit = source_repo.get_commit(
1644 1651 commit_id=safe_str(target_ref_id))
1645 1652 source_commit = source_repo.get_commit(
1646 1653 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1647 1654 if isinstance(source_repo, Repository):
1648 1655 vcs_repo = source_repo.scm_instance()
1649 1656 else:
1650 1657 vcs_repo = source_repo
1651 1658
1652 1659 # TODO: johbo: In the context of an update, we cannot reach
1653 1660 # the old commit anymore with our normal mechanisms. It needs
1654 1661 # some sort of special support in the vcs layer to avoid this
1655 1662 # workaround.
1656 1663 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1657 1664 vcs_repo.alias == 'git'):
1658 1665 source_commit.raw_id = safe_str(source_ref_id)
1659 1666
1660 1667 log.debug('calculating diff between '
1661 1668 'source_ref:%s and target_ref:%s for repo `%s`',
1662 1669 target_ref_id, source_ref_id,
1663 1670 safe_unicode(vcs_repo.path))
1664 1671
1665 1672 vcs_diff = vcs_repo.get_diff(
1666 1673 commit1=target_commit, commit2=source_commit,
1667 1674 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1668 1675 return vcs_diff
1669 1676
1670 1677 def _is_merge_enabled(self, pull_request):
1671 1678 return self._get_general_setting(
1672 1679 pull_request, 'rhodecode_pr_merge_enabled')
1673 1680
1674 1681 def _use_rebase_for_merging(self, pull_request):
1675 1682 repo_type = pull_request.target_repo.repo_type
1676 1683 if repo_type == 'hg':
1677 1684 return self._get_general_setting(
1678 1685 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1679 1686 elif repo_type == 'git':
1680 1687 return self._get_general_setting(
1681 1688 pull_request, 'rhodecode_git_use_rebase_for_merging')
1682 1689
1683 1690 return False
1684 1691
1685 1692 def _user_name_for_merging(self, pull_request, user):
1686 1693 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1687 1694 if env_user_name_attr and hasattr(user, env_user_name_attr):
1688 1695 user_name_attr = env_user_name_attr
1689 1696 else:
1690 1697 user_name_attr = 'short_contact'
1691 1698
1692 1699 user_name = getattr(user, user_name_attr)
1693 1700 return user_name
1694 1701
1695 1702 def _close_branch_before_merging(self, pull_request):
1696 1703 repo_type = pull_request.target_repo.repo_type
1697 1704 if repo_type == 'hg':
1698 1705 return self._get_general_setting(
1699 1706 pull_request, 'rhodecode_hg_close_branch_before_merging')
1700 1707 elif repo_type == 'git':
1701 1708 return self._get_general_setting(
1702 1709 pull_request, 'rhodecode_git_close_branch_before_merging')
1703 1710
1704 1711 return False
1705 1712
1706 1713 def _get_general_setting(self, pull_request, settings_key, default=False):
1707 1714 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1708 1715 settings = settings_model.get_general_settings()
1709 1716 return settings.get(settings_key, default)
1710 1717
1711 1718 def _log_audit_action(self, action, action_data, user, pull_request):
1712 1719 audit_logger.store(
1713 1720 action=action,
1714 1721 action_data=action_data,
1715 1722 user=user,
1716 1723 repo=pull_request.target_repo)
1717 1724
1718 1725 def get_reviewer_functions(self):
1719 1726 """
1720 1727 Fetches functions for validation and fetching default reviewers.
1721 1728 If available we use the EE package, else we fallback to CE
1722 1729 package functions
1723 1730 """
1724 1731 try:
1725 1732 from rc_reviewers.utils import get_default_reviewers_data
1726 1733 from rc_reviewers.utils import validate_default_reviewers
1727 1734 except ImportError:
1728 1735 from rhodecode.apps.repository.utils import get_default_reviewers_data
1729 1736 from rhodecode.apps.repository.utils import validate_default_reviewers
1730 1737
1731 1738 return get_default_reviewers_data, validate_default_reviewers
1732 1739
1733 1740
1734 1741 class MergeCheck(object):
1735 1742 """
1736 1743 Perform Merge Checks and returns a check object which stores information
1737 1744 about merge errors, and merge conditions
1738 1745 """
1739 1746 TODO_CHECK = 'todo'
1740 1747 PERM_CHECK = 'perm'
1741 1748 REVIEW_CHECK = 'review'
1742 1749 MERGE_CHECK = 'merge'
1743 1750 WIP_CHECK = 'wip'
1744 1751
1745 1752 def __init__(self):
1746 1753 self.review_status = None
1747 1754 self.merge_possible = None
1748 1755 self.merge_msg = ''
1749 1756 self.merge_response = None
1750 1757 self.failed = None
1751 1758 self.errors = []
1752 1759 self.error_details = OrderedDict()
1753 1760 self.source_commit = AttributeDict()
1754 1761 self.target_commit = AttributeDict()
1755 1762
1756 1763 def __repr__(self):
1757 1764 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1758 1765 self.merge_possible, self.failed, self.errors)
1759 1766
1760 1767 def push_error(self, error_type, message, error_key, details):
1761 1768 self.failed = True
1762 1769 self.errors.append([error_type, message])
1763 1770 self.error_details[error_key] = dict(
1764 1771 details=details,
1765 1772 error_type=error_type,
1766 1773 message=message
1767 1774 )
1768 1775
1769 1776 @classmethod
1770 1777 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1771 1778 force_shadow_repo_refresh=False):
1772 1779 _ = translator
1773 1780 merge_check = cls()
1774 1781
1775 1782 # title has WIP:
1776 1783 if pull_request.work_in_progress:
1777 1784 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1778 1785
1779 1786 msg = _('WIP marker in title prevents from accidental merge.')
1780 1787 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1781 1788 if fail_early:
1782 1789 return merge_check
1783 1790
1784 1791 # permissions to merge
1785 1792 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1786 1793 if not user_allowed_to_merge:
1787 1794 log.debug("MergeCheck: cannot merge, approval is pending.")
1788 1795
1789 1796 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1790 1797 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1791 1798 if fail_early:
1792 1799 return merge_check
1793 1800
1794 1801 # permission to merge into the target branch
1795 1802 target_commit_id = pull_request.target_ref_parts.commit_id
1796 1803 if pull_request.target_ref_parts.type == 'branch':
1797 1804 branch_name = pull_request.target_ref_parts.name
1798 1805 else:
1799 1806 # for mercurial we can always figure out the branch from the commit
1800 1807 # in case of bookmark
1801 1808 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1802 1809 branch_name = target_commit.branch
1803 1810
1804 1811 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1805 1812 pull_request.target_repo.repo_name, branch_name)
1806 1813 if branch_perm and branch_perm == 'branch.none':
1807 1814 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1808 1815 branch_name, rule)
1809 1816 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1810 1817 if fail_early:
1811 1818 return merge_check
1812 1819
1813 1820 # review status, must be always present
1814 1821 review_status = pull_request.calculated_review_status()
1815 1822 merge_check.review_status = review_status
1816 1823
1817 1824 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1818 1825 if not status_approved:
1819 1826 log.debug("MergeCheck: cannot merge, approval is pending.")
1820 1827
1821 1828 msg = _('Pull request reviewer approval is pending.')
1822 1829
1823 1830 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1824 1831
1825 1832 if fail_early:
1826 1833 return merge_check
1827 1834
1828 1835 # left over TODOs
1829 1836 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1830 1837 if todos:
1831 1838 log.debug("MergeCheck: cannot merge, {} "
1832 1839 "unresolved TODOs left.".format(len(todos)))
1833 1840
1834 1841 if len(todos) == 1:
1835 1842 msg = _('Cannot merge, {} TODO still not resolved.').format(
1836 1843 len(todos))
1837 1844 else:
1838 1845 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1839 1846 len(todos))
1840 1847
1841 1848 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1842 1849
1843 1850 if fail_early:
1844 1851 return merge_check
1845 1852
1846 1853 # merge possible, here is the filesystem simulation + shadow repo
1847 1854 merge_response, merge_status, msg = PullRequestModel().merge_status(
1848 1855 pull_request, translator=translator,
1849 1856 force_shadow_repo_refresh=force_shadow_repo_refresh)
1850 1857
1851 1858 merge_check.merge_possible = merge_status
1852 1859 merge_check.merge_msg = msg
1853 1860 merge_check.merge_response = merge_response
1854 1861
1855 1862 source_ref_id = pull_request.source_ref_parts.commit_id
1856 1863 target_ref_id = pull_request.target_ref_parts.commit_id
1857 1864
1858 1865 try:
1859 1866 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
1860 1867 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
1861 1868 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
1862 1869 merge_check.source_commit.current_raw_id = source_commit.raw_id
1863 1870 merge_check.source_commit.previous_raw_id = source_ref_id
1864 1871
1865 1872 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
1866 1873 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
1867 1874 merge_check.target_commit.current_raw_id = target_commit.raw_id
1868 1875 merge_check.target_commit.previous_raw_id = target_ref_id
1869 1876 except (SourceRefMissing, TargetRefMissing):
1870 1877 pass
1871 1878
1872 1879 if not merge_status:
1873 1880 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1874 1881 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1875 1882
1876 1883 if fail_early:
1877 1884 return merge_check
1878 1885
1879 1886 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1880 1887 return merge_check
1881 1888
1882 1889 @classmethod
1883 1890 def get_merge_conditions(cls, pull_request, translator):
1884 1891 _ = translator
1885 1892 merge_details = {}
1886 1893
1887 1894 model = PullRequestModel()
1888 1895 use_rebase = model._use_rebase_for_merging(pull_request)
1889 1896
1890 1897 if use_rebase:
1891 1898 merge_details['merge_strategy'] = dict(
1892 1899 details={},
1893 1900 message=_('Merge strategy: rebase')
1894 1901 )
1895 1902 else:
1896 1903 merge_details['merge_strategy'] = dict(
1897 1904 details={},
1898 1905 message=_('Merge strategy: explicit merge commit')
1899 1906 )
1900 1907
1901 1908 close_branch = model._close_branch_before_merging(pull_request)
1902 1909 if close_branch:
1903 1910 repo_type = pull_request.target_repo.repo_type
1904 1911 close_msg = ''
1905 1912 if repo_type == 'hg':
1906 1913 close_msg = _('Source branch will be closed after merge.')
1907 1914 elif repo_type == 'git':
1908 1915 close_msg = _('Source branch will be deleted after merge.')
1909 1916
1910 1917 merge_details['close_branch'] = dict(
1911 1918 details={},
1912 1919 message=close_msg
1913 1920 )
1914 1921
1915 1922 return merge_details
1916 1923
1917 1924
1918 1925 ChangeTuple = collections.namedtuple(
1919 1926 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1920 1927
1921 1928 FileChangeTuple = collections.namedtuple(
1922 1929 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,91 +1,144 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-body">
5 %if c.closed:
6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
7 %else:
8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
9 %endif
5 <div style="height: 35px">
6 <%
7 selected_filter = 'all'
8 if c.closed:
9 selected_filter = 'all_closed'
10 %>
11
12 <ul class="button-links">
13 <li class="btn ${h.is_active('all', selected_filter)}"><a href="${h.route_path('my_account_pullrequests')}">${_('All')}</a></li>
14 <li class="btn ${h.is_active('all_closed', selected_filter)}"><a href="${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}">${_('All + Closed')}</a></li>
15 </ul>
16
17 <div class="grid-quick-filter">
18 <ul class="grid-filter-box">
19 <li class="grid-filter-box-icon">
20 <i class="icon-search"></i>
21 </li>
22 <li class="grid-filter-box-input">
23 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
24 </li>
25 </ul>
26 </div>
27 </div>
10 28 </div>
11 29 </div>
12 30
13 31 <div class="panel panel-default">
14 32 <div class="panel-heading">
15 33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
16 34 </div>
17 35 <div class="panel-body panel-body-min-height">
18 36 <table id="pull_request_list_table" class="display"></table>
19 37 </div>
20 38 </div>
21 39
22 40 <script type="text/javascript">
23 $(document).ready(function() {
41 $(document).ready(function () {
24 42
25 $('#show_closed').on('click', function(e){
26 if($(this).is(":checked")){
27 window.location = "${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}";
28 }
29 else{
30 window.location = "${h.route_path('my_account_pullrequests')}";
31 }
32 });
33
34 var $pullRequestListTable = $('#pull_request_list_table');
43 var $pullRequestListTable = $('#pull_request_list_table');
35 44
36 45 // participating object list
37 46 $pullRequestListTable.DataTable({
38 processing: true,
39 serverSide: true,
40 ajax: {
41 "url": "${h.route_path('my_account_pullrequests_data')}",
42 "data": function (d) {
43 d.closed = "${c.closed}";
44 }
45 },
46 dom: 'rtp',
47 pageLength: ${c.visual.dashboard_items},
48 order: [[ 2, "desc" ]],
49 columns: [
50 { data: {"_": "target_repo",
51 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
52 { data: {"_": "status",
53 "sort": "status"}, title: "", className: "td-status", orderable: false},
54 { data: {"_": "name",
55 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
56 { data: {"_": "title",
57 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
58 { data: {"_": "author",
59 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
60 { data: {"_": "comments",
61 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
62 { data: {"_": "updated_on",
63 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
64 ],
65 language: {
47 processing: true,
48 serverSide: true,
49 ajax: {
50 "url": "${h.route_path('my_account_pullrequests_data')}",
51 "data": function (d) {
52 d.closed = "${c.closed}";
53 },
54 "dataSrc": function (json) {
55 return json.data;
56 }
57 },
58
59 dom: 'rtp',
60 pageLength: ${c.visual.dashboard_items},
61 order: [[2, "desc"]],
62 columns: [
63 {
64 data: {
65 "_": "target_repo",
66 "sort": "target_repo"
67 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
68 },
69 {
70 data: {
71 "_": "status",
72 "sort": "status"
73 }, title: "", className: "td-status", orderable: false
74 },
75 {
76 data: {
77 "_": "name",
78 "sort": "name_raw"
79 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
80 },
81 {
82 data: {
83 "_": "title",
84 "sort": "title"
85 }, title: "${_('Title')}", className: "td-description"
86 },
87 {
88 data: {
89 "_": "author",
90 "sort": "author_raw"
91 }, title: "${_('Author')}", className: "td-user", orderable: false
92 },
93 {
94 data: {
95 "_": "comments",
96 "sort": "comments_raw"
97 }, title: "", className: "td-comments", orderable: false
98 },
99 {
100 data: {
101 "_": "updated_on",
102 "sort": "updated_on_raw"
103 }, title: "${_('Last Update')}", className: "td-time"
104 }
105 ],
106 language: {
66 107 paginate: DEFAULT_GRID_PAGINATION,
67 108 sProcessing: _gettext('loading...'),
68 109 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
69 },
70 "drawCallback": function( settings, json ) {
71 timeagoActivate();
72 tooltipActivate();
73 },
74 "createdRow": function ( row, data, index ) {
75 if (data['closed']) {
76 $(row).addClass('closed');
77 }
78 if (data['owned']) {
79 $(row).addClass('owned');
80 }
81 }
110 },
111 "drawCallback": function (settings, json) {
112 timeagoActivate();
113 tooltipActivate();
114 },
115 "createdRow": function (row, data, index) {
116 if (data['closed']) {
117 $(row).addClass('closed');
118 }
119 if (data['owned']) {
120 $(row).addClass('owned');
121 }
122 }
82 123 });
83 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
124 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
84 125 $pullRequestListTable.css('opacity', 1);
85 126 });
86 127
87 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
128 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
88 129 $pullRequestListTable.css('opacity', 0.3);
89 130 });
131
132 // filter
133 $('#q_filter').on('keyup',
134 $.debounce(250, function () {
135 $pullRequestListTable.DataTable().search(
136 $('#q_filter').val()
137 ).draw();
138 })
139 );
140
90 141 });
142
143
91 144 </script>
General Comments 0
You need to be logged in to leave comments. Login now