##// END OF EJS Templates
pull-requests: added awaiting my review filter for users pull-requests....
super-admin -
r4690:2e951f8d stable
parent child
Show More
@@ -76,7 +76,11 class TestMyAccountEdit(TestController):
76 'requests requiring your participation.')
76 'requests requiring your participation.')
77
77
78 @pytest.mark.backends("git", "hg")
78 @pytest.mark.backends("git", "hg")
79 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header):
79 @pytest.mark.parametrize('params, expected_title', [
80 ({'closed': 1}, 'Closed'),
81 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 ])
83 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
80 self.log_user()
84 self.log_user()
81 response = self.app.get(route_path('my_account_pullrequests_data'),
85 response = self.app.get(route_path('my_account_pullrequests_data'),
82 extra_environ=xhr_header)
86 extra_environ=xhr_header)
@@ -43,7 +43,7 from rhodecode.model.comment import Comm
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 IntegrityError, or_, in_filter_generator,
44 IntegrityError, or_, in_filter_generator,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup)
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
@@ -654,21 +654,31 class MyAccountView(BaseAppView, DataGri
654 Session().commit()
654 Session().commit()
655 return user.user_data['notification_status']
655 return user.user_data['notification_status']
656
656
657 def _get_pull_requests_list(self, statuses):
657 def _get_pull_requests_list(self, statuses, filter_type=None):
658 draw, start, limit = self._extract_chunk(self.request)
658 draw, start, limit = self._extract_chunk(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
660
660
661 _render = self.request.get_partial_renderer(
661 _render = self.request.get_partial_renderer(
662 'rhodecode:templates/data_table/_dt_elements.mako')
662 'rhodecode:templates/data_table/_dt_elements.mako')
663
663
664 pull_requests = PullRequestModel().get_im_participating_in(
664 if filter_type == 'awaiting_my_review':
665 user_id=self._rhodecode_user.user_id,
665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 statuses=statuses, query=search_q,
666 user_id=self._rhodecode_user.user_id,
667 offset=start, length=limit, order_by=order_by,
667 statuses=statuses, query=search_q,
668 order_dir=order_dir)
668 offset=start, length=limit, order_by=order_by,
669 order_dir=order_dir)
669
670
670 pull_requests_total_count = PullRequestModel().count_im_participating_in(
671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
671 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 else:
674 pull_requests = PullRequestModel().get_im_participating_in(
675 user_id=self._rhodecode_user.user_id,
676 statuses=statuses, query=search_q,
677 offset=start, length=limit, order_by=order_by,
678 order_dir=order_dir)
679
680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
672
682
673 data = []
683 data = []
674 comments_model = CommentsModel()
684 comments_model = CommentsModel()
@@ -678,6 +688,12 class MyAccountView(BaseAppView, DataGri
678 repo_id, pull_request=pr, include_drafts=False, count_only=True)
688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
679 owned = pr.user_id == self._rhodecode_user.user_id
689 owned = pr.user_id == self._rhodecode_user.user_id
680
690
691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 if review_statuses and review_statuses[4]:
694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 my_review_status = statuses[0][1].status
696
681 data.append({
697 data.append({
682 'target_repo': _render('pullrequest_target_repo',
698 'target_repo': _render('pullrequest_target_repo',
683 pr.target_repo.repo_name),
699 pr.target_repo.repo_name),
@@ -688,6 +704,8 class MyAccountView(BaseAppView, DataGri
688 'name_raw': pr.pull_request_id,
704 'name_raw': pr.pull_request_id,
689 'status': _render('pullrequest_status',
705 'status': _render('pullrequest_status',
690 pr.calculated_review_status()),
706 pr.calculated_review_status()),
707 'my_status': _render('pullrequest_status',
708 my_review_status),
691 'title': _render('pullrequest_title', pr.title, pr.description),
709 'title': _render('pullrequest_title', pr.title, pr.description),
692 'description': h.escape(pr.description),
710 'description': h.escape(pr.description),
693 'updated_on': _render('pullrequest_updated_on',
711 'updated_on': _render('pullrequest_updated_on',
@@ -723,7 +741,14 class MyAccountView(BaseAppView, DataGri
723 c.active = 'pullrequests'
741 c.active = 'pullrequests'
724 req_get = self.request.GET
742 req_get = self.request.GET
725
743
726 c.closed = str2bool(req_get.get('pr_show_closed'))
744 c.closed = str2bool(req_get.get('closed'))
745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746
747 c.selected_filter = 'all'
748 if c.closed:
749 c.selected_filter = 'all_closed'
750 if c.awaiting_my_review:
751 c.selected_filter = 'awaiting_my_review'
727
752
728 return self._get_template_context(c)
753 return self._get_template_context(c)
729
754
@@ -732,13 +757,19 class MyAccountView(BaseAppView, DataGri
732 def my_account_pullrequests_data(self):
757 def my_account_pullrequests_data(self):
733 self.load_default_context()
758 self.load_default_context()
734 req_get = self.request.GET
759 req_get = self.request.GET
760
761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
735 closed = str2bool(req_get.get('closed'))
762 closed = str2bool(req_get.get('closed'))
736
763
737 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
738 if closed:
765 if closed:
739 statuses += [PullRequest.STATUS_CLOSED]
766 statuses += [PullRequest.STATUS_CLOSED]
740
767
741 data = self._get_pull_requests_list(statuses=statuses)
768 filter_type = \
769 'awaiting_my_review' if awaiting_my_review \
770 else None
771
772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
742 return data
773 return data
743
774
744 @LoginRequired()
775 @LoginRequired()
@@ -41,7 +41,7 class TestPullRequestList(object):
41
41
42 @pytest.mark.parametrize('params, expected_title', [
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed'),
43 ({'source': 0, 'closed': 1}, 'Closed'),
44 ({'source': 0, 'my': 1}, 'Opened by me'),
44 ({'source': 0, 'my': 1}, 'Created by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 ({'source': 1}, 'From this repo'),
47 ({'source': 1}, 'From this repo'),
@@ -79,21 +79,20 class RepoPullRequestsView(RepoAppView,
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name,
83 statuses=statuses, offset=start, length=limit,
83 search_q=search_q, statuses=statuses,
84 order_by=order_by, order_dir=order_dir)
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name,
87 opened_by=opened_by)
87 search_q=search_q, statuses=statuses)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, self._rhodecode_user.user_id,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 repo_name, self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
95 search_q=search_q, statuses=statuses)
97 else:
96 else:
98 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
@@ -110,6 +109,12 class RepoPullRequestsView(RepoAppView,
110 self.db_repo.repo_id, pull_request=pr,
109 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
110 include_drafts=False, count_only=True)
112
111
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
117
113 data.append({
118 data.append({
114 'name': _render('pullrequest_name',
119 'name': _render('pullrequest_name',
115 pr.pull_request_id, pr.pull_request_state,
120 pr.pull_request_id, pr.pull_request_state,
@@ -118,6 +123,8 class RepoPullRequestsView(RepoAppView,
118 'name_raw': pr.pull_request_id,
123 'name_raw': pr.pull_request_id,
119 'status': _render('pullrequest_status',
124 'status': _render('pullrequest_status',
120 pr.calculated_review_status()),
125 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
127 my_review_status),
121 'title': _render('pullrequest_title', pr.title, pr.description),
128 'title': _render('pullrequest_title', pr.title, pr.description),
122 'description': h.escape(pr.description),
129 'description': h.escape(pr.description),
123 'updated_on': _render('pullrequest_updated_on',
130 'updated_on': _render('pullrequest_updated_on',
@@ -354,7 +354,7 class ChangesetStatusModel(BaseModel):
354 Session().add(new_status)
354 Session().add(new_status)
355 return new_statuses
355 return new_statuses
356
356
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data, user=None):
358
358
359 commit_statuses_map = collections.defaultdict(list)
359 commit_statuses_map = collections.defaultdict(list)
360 for st in commit_statuses:
360 for st in commit_statuses:
@@ -368,6 +368,10 class ChangesetStatusModel(BaseModel):
368 for obj in reviewers_data:
368 for obj in reviewers_data:
369 if not obj.user:
369 if not obj.user:
370 continue
370 continue
371 if user and obj.user.username != user.username:
372 # single user filter
373 continue
374
371 statuses = commit_statuses_map.get(obj.user.username, None)
375 statuses = commit_statuses_map.get(obj.user.username, None)
372 if statuses:
376 if statuses:
373 status_groups = itertools.groupby(
377 status_groups = itertools.groupby(
@@ -376,16 +380,19 class ChangesetStatusModel(BaseModel):
376
380
377 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
381 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
378
382
379 return reviewers
383 if user:
384 return reviewers[0] if reviewers else reviewers
385 else:
386 return reviewers
380
387
381 def reviewers_statuses(self, pull_request):
388 def reviewers_statuses(self, pull_request, user=None):
382 _commit_statuses = self.get_statuses(
389 _commit_statuses = self.get_statuses(
383 pull_request.source_repo,
390 pull_request.source_repo,
384 pull_request=pull_request,
391 pull_request=pull_request,
385 with_revisions=True)
392 with_revisions=True)
386 reviewers = pull_request.get_pull_request_reviewers(
393 reviewers = pull_request.get_pull_request_reviewers(
387 role=PullRequestReviewers.ROLE_REVIEWER)
394 role=PullRequestReviewers.ROLE_REVIEWER)
388 return self.aggregate_votes_by_user(_commit_statuses, reviewers)
395 return self.aggregate_votes_by_user(_commit_statuses, reviewers, user=user)
389
396
390 def calculated_review_status(self, pull_request):
397 def calculated_review_status(self, pull_request):
391 """
398 """
@@ -41,10 +41,10 from sqlalchemy import (
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 Text, Float, PickleType, BigInteger)
43 Text, Float, PickleType, BigInteger)
44 from sqlalchemy.sql.expression import true, false, case
44 from sqlalchemy.sql.expression import true, false, case, null
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 from sqlalchemy.orm import (
46 from sqlalchemy.orm import (
47 relationship, joinedload, class_mapper, validates, aliased)
47 relationship, lazyload, joinedload, class_mapper, validates, aliased)
48 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.declarative import declared_attr
49 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.ext.hybrid import hybrid_property
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
@@ -4479,9 +4479,9 class PullRequest(Base, _PullRequestBase
4479 from rhodecode.model.changeset_status import ChangesetStatusModel
4479 from rhodecode.model.changeset_status import ChangesetStatusModel
4480 return ChangesetStatusModel().calculated_review_status(self)
4480 return ChangesetStatusModel().calculated_review_status(self)
4481
4481
4482 def reviewers_statuses(self):
4482 def reviewers_statuses(self, user=None):
4483 from rhodecode.model.changeset_status import ChangesetStatusModel
4483 from rhodecode.model.changeset_status import ChangesetStatusModel
4484 return ChangesetStatusModel().reviewers_statuses(self)
4484 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4485
4485
4486 def get_pull_request_reviewers(self, role=None):
4486 def get_pull_request_reviewers(self, role=None):
4487 qry = PullRequestReviewers.query()\
4487 qry = PullRequestReviewers.query()\
@@ -56,7 +56,7 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 aliased, null, lazyload, and_, or_, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
@@ -319,7 +319,7 class PullRequestModel(BaseModel):
319
319
320 if search_q:
320 if search_q:
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
322 q = q.join(User)
322 q = q.join(User, User.user_id == PullRequest.user_id)
323 q = q.filter(or_(
323 q = q.filter(or_(
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
325 User.username.ilike(like_expression),
325 User.username.ilike(like_expression),
@@ -405,36 +405,30 class PullRequestModel(BaseModel):
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
408 def count_awaiting_review(self, repo_name, search_q=None, statuses=None):
409 opened_by=None):
410 """
409 """
411 Count the number of pull requests for a specific repository that are
410 Count the number of pull requests for a specific repository that are
412 awaiting review.
411 awaiting review.
413
412
414 :param repo_name: target or source repo
413 :param repo_name: target or source repo
415 :param search_q: filter by text
414 :param search_q: filter by text
416 :param source: boolean flag to specify if repo_name refers to source
417 :param statuses: list of pull request statuses
415 :param statuses: list of pull request statuses
418 :param opened_by: author user of the pull request
419 :returns: int number of pull requests
416 :returns: int number of pull requests
420 """
417 """
421 pull_requests = self.get_awaiting_review(
418 pull_requests = self.get_awaiting_review(
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
419 repo_name, search_q=search_q, statuses=statuses)
423
420
424 return len(pull_requests)
421 return len(pull_requests)
425
422
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
423 def get_awaiting_review(self, repo_name, search_q=None, statuses=None,
427 opened_by=None, offset=0, length=None,
424 offset=0, length=None, order_by=None, order_dir='desc'):
428 order_by=None, order_dir='desc'):
429 """
425 """
430 Get all pull requests for a specific repository that are awaiting
426 Get all pull requests for a specific repository that are awaiting
431 review.
427 review.
432
428
433 :param repo_name: target or source repo
429 :param repo_name: target or source repo
434 :param search_q: filter by text
430 :param search_q: filter by text
435 :param source: boolean flag to specify if repo_name refers to source
436 :param statuses: list of pull request statuses
431 :param statuses: list of pull request statuses
437 :param opened_by: author user of the pull request
438 :param offset: pagination offset
432 :param offset: pagination offset
439 :param length: length of returned list
433 :param length: length of returned list
440 :param order_by: order of the returned list
434 :param order_by: order of the returned list
@@ -442,8 +436,8 class PullRequestModel(BaseModel):
442 :returns: list of pull requests
436 :returns: list of pull requests
443 """
437 """
444 pull_requests = self.get_all(
438 pull_requests = self.get_all(
445 repo_name, search_q=search_q, source=source, statuses=statuses,
439 repo_name, search_q=search_q, statuses=statuses,
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
440 order_by=order_by, order_dir=order_dir)
447
441
448 _filtered_pull_requests = []
442 _filtered_pull_requests = []
449 for pr in pull_requests:
443 for pr in pull_requests:
@@ -456,68 +450,117 class PullRequestModel(BaseModel):
456 else:
450 else:
457 return _filtered_pull_requests
451 return _filtered_pull_requests
458
452
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
453 def _prepare_awaiting_my_review_review_query(
460 opened_by=None, user_id=None):
454 self, repo_name, user_id, search_q=None, statuses=None,
455 order_by=None, order_dir='desc'):
456
457 for_review_statuses = [
458 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
459 ]
460
461 pull_request_alias = aliased(PullRequest)
462 status_alias = aliased(ChangesetStatus)
463 reviewers_alias = aliased(PullRequestReviewers)
464 repo_alias = aliased(Repository)
465
466 last_ver_subq = Session()\
467 .query(func.min(ChangesetStatus.version)) \
468 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
469 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
470 .subquery()
471
472 q = Session().query(pull_request_alias) \
473 .options(lazyload(pull_request_alias.author)) \
474 .join(reviewers_alias,
475 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
476 .join(repo_alias,
477 repo_alias.repo_id == pull_request_alias.target_repo_id) \
478 .outerjoin(status_alias,
479 and_(status_alias.user_id == reviewers_alias.user_id,
480 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
481 .filter(or_(status_alias.version == null(),
482 status_alias.version == last_ver_subq)) \
483 .filter(reviewers_alias.user_id == user_id) \
484 .filter(repo_alias.repo_name == repo_name) \
485 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
486 .group_by(pull_request_alias)
487
488 # closed,opened
489 if statuses:
490 q = q.filter(pull_request_alias.status.in_(statuses))
491
492 if search_q:
493 like_expression = u'%{}%'.format(safe_unicode(search_q))
494 q = q.join(User, User.user_id == pull_request_alias.user_id)
495 q = q.filter(or_(
496 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
497 User.username.ilike(like_expression),
498 pull_request_alias.title.ilike(like_expression),
499 pull_request_alias.description.ilike(like_expression),
500 ))
501
502 if order_by:
503 order_map = {
504 'name_raw': pull_request_alias.pull_request_id,
505 'title': pull_request_alias.title,
506 'updated_on_raw': pull_request_alias.updated_on,
507 'target_repo': pull_request_alias.target_repo_id
508 }
509 if order_dir == 'asc':
510 q = q.order_by(order_map[order_by].asc())
511 else:
512 q = q.order_by(order_map[order_by].desc())
513
514 return q
515
516 def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None):
461 """
517 """
462 Count the number of pull requests for a specific repository that are
518 Count the number of pull requests for a specific repository that are
463 awaiting review from a specific user.
519 awaiting review from a specific user.
464
520
465 :param repo_name: target or source repo
521 :param repo_name: target or source repo
522 :param user_id: reviewer user of the pull request
466 :param search_q: filter by text
523 :param search_q: filter by text
467 :param source: boolean flag to specify if repo_name refers to source
468 :param statuses: list of pull request statuses
524 :param statuses: list of pull request statuses
469 :param opened_by: author user of the pull request
470 :param user_id: reviewer user of the pull request
471 :returns: int number of pull requests
525 :returns: int number of pull requests
472 """
526 """
473 pull_requests = self.get_awaiting_my_review(
527 q = self._prepare_awaiting_my_review_review_query(
474 repo_name, search_q=search_q, source=source, statuses=statuses,
528 repo_name, user_id, search_q=search_q, statuses=statuses)
475 opened_by=opened_by, user_id=user_id)
529 return q.count()
476
530
477 return len(pull_requests)
531 def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None,
478
532 offset=0, length=None, order_by=None, order_dir='desc'):
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
480 opened_by=None, user_id=None, offset=0,
481 length=None, order_by=None, order_dir='desc'):
482 """
533 """
483 Get all pull requests for a specific repository that are awaiting
534 Get all pull requests for a specific repository that are awaiting
484 review from a specific user.
535 review from a specific user.
485
536
486 :param repo_name: target or source repo
537 :param repo_name: target or source repo
538 :param user_id: reviewer user of the pull request
487 :param search_q: filter by text
539 :param search_q: filter by text
488 :param source: boolean flag to specify if repo_name refers to source
489 :param statuses: list of pull request statuses
540 :param statuses: list of pull request statuses
490 :param opened_by: author user of the pull request
491 :param user_id: reviewer user of the pull request
492 :param offset: pagination offset
541 :param offset: pagination offset
493 :param length: length of returned list
542 :param length: length of returned list
494 :param order_by: order of the returned list
543 :param order_by: order of the returned list
495 :param order_dir: 'asc' or 'desc' ordering direction
544 :param order_dir: 'asc' or 'desc' ordering direction
496 :returns: list of pull requests
545 :returns: list of pull requests
497 """
546 """
498 pull_requests = self.get_all(
499 repo_name, search_q=search_q, source=source, statuses=statuses,
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
501
547
502 _my = PullRequestModel().get_not_reviewed(user_id)
548 q = self._prepare_awaiting_my_review_review_query(
503 my_participation = []
549 repo_name, user_id, search_q=search_q, statuses=statuses,
504 for pr in pull_requests:
550 order_by=order_by, order_dir=order_dir)
505 if pr in _my:
551
506 my_participation.append(pr)
507 _filtered_pull_requests = my_participation
508 if length:
552 if length:
509 return _filtered_pull_requests[offset:offset+length]
553 pull_requests = q.limit(length).offset(offset).all()
510 else:
554 else:
511 return _filtered_pull_requests
555 pull_requests = q.all()
556
557 return pull_requests
512
558
513 def get_not_reviewed(self, user_id):
559 def _prepare_im_participating_query(self, user_id=None, statuses=None, query='',
514 return [
560 order_by=None, order_dir='desc'):
515 x.pull_request for x in PullRequestReviewers.query().filter(
561 """
516 PullRequestReviewers.user_id == user_id).all()
562 return a query of pull-requests user is an creator, or he's added as a reviewer
517 ]
563 """
518
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
520 order_by=None, order_dir='desc'):
521 q = PullRequest.query()
564 q = PullRequest.query()
522 if user_id:
565 if user_id:
523 reviewers_subquery = Session().query(
566 reviewers_subquery = Session().query(
@@ -535,7 +578,7 class PullRequestModel(BaseModel):
535
578
536 if query:
579 if query:
537 like_expression = u'%{}%'.format(safe_unicode(query))
580 like_expression = u'%{}%'.format(safe_unicode(query))
538 q = q.join(User)
581 q = q.join(User, User.user_id == PullRequest.user_id)
539 q = q.filter(or_(
582 q = q.filter(or_(
540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
583 cast(PullRequest.pull_request_id, String).ilike(like_expression),
541 User.username.ilike(like_expression),
584 User.username.ilike(like_expression),
@@ -557,17 +600,97 class PullRequestModel(BaseModel):
557 return q
600 return q
558
601
559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
602 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
603 q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query)
561 return q.count()
604 return q.count()
562
605
563 def get_im_participating_in(
606 def get_im_participating_in(
564 self, user_id=None, statuses=None, query='', offset=0,
607 self, user_id=None, statuses=None, query='', offset=0,
565 length=None, order_by=None, order_dir='desc'):
608 length=None, order_by=None, order_dir='desc'):
566 """
609 """
567 Get all Pull requests that i'm participating in, or i have opened
610 Get all Pull requests that i'm participating in as a reviewer, or i have opened
568 """
611 """
569
612
570 q = self._prepare_participating_query(
613 q = self._prepare_im_participating_query(
614 user_id, statuses=statuses, query=query, order_by=order_by,
615 order_dir=order_dir)
616
617 if length:
618 pull_requests = q.limit(length).offset(offset).all()
619 else:
620 pull_requests = q.all()
621
622 return pull_requests
623
624 def _prepare_participating_in_for_review_query(
625 self, user_id, statuses=None, query='', order_by=None, order_dir='desc'):
626
627 for_review_statuses = [
628 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
629 ]
630
631 pull_request_alias = aliased(PullRequest)
632 status_alias = aliased(ChangesetStatus)
633 reviewers_alias = aliased(PullRequestReviewers)
634
635 last_ver_subq = Session()\
636 .query(func.min(ChangesetStatus.version)) \
637 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
638 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
639 .subquery()
640
641 q = Session().query(pull_request_alias) \
642 .options(lazyload(pull_request_alias.author)) \
643 .join(reviewers_alias,
644 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
645 .outerjoin(status_alias,
646 and_(status_alias.user_id == reviewers_alias.user_id,
647 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
648 .filter(or_(status_alias.version == null(),
649 status_alias.version == last_ver_subq)) \
650 .filter(reviewers_alias.user_id == user_id) \
651 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
652 .group_by(pull_request_alias)
653
654 # closed,opened
655 if statuses:
656 q = q.filter(pull_request_alias.status.in_(statuses))
657
658 if query:
659 like_expression = u'%{}%'.format(safe_unicode(query))
660 q = q.join(User, User.user_id == pull_request_alias.user_id)
661 q = q.filter(or_(
662 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
663 User.username.ilike(like_expression),
664 pull_request_alias.title.ilike(like_expression),
665 pull_request_alias.description.ilike(like_expression),
666 ))
667
668 if order_by:
669 order_map = {
670 'name_raw': pull_request_alias.pull_request_id,
671 'title': pull_request_alias.title,
672 'updated_on_raw': pull_request_alias.updated_on,
673 'target_repo': pull_request_alias.target_repo_id
674 }
675 if order_dir == 'asc':
676 q = q.order_by(order_map[order_by].asc())
677 else:
678 q = q.order_by(order_map[order_by].desc())
679
680 return q
681
682 def count_im_participating_in_for_review(self, user_id, statuses=None, query=''):
683 q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query)
684 return q.count()
685
686 def get_im_participating_in_for_review(
687 self, user_id, statuses=None, query='', offset=0,
688 length=None, order_by=None, order_dir='desc'):
689 """
690 Get all Pull requests that needs user approval or rejection
691 """
692
693 q = self._prepare_participating_in_for_review_query(
571 user_id, statuses=statuses, query=query, order_by=order_by,
694 user_id, statuses=statuses, query=query, order_by=order_by,
572 order_dir=order_dir)
695 order_dir=order_dir)
573
696
@@ -618,13 +618,13 input[type="reset"] {
618 text-align: right;
618 text-align: right;
619
619
620 li {
620 li {
621
621 list-style: none;
622
622 text-align: right;
623 display: inline-block;
623 }
624 }
624
625
625 li.active {
626 a.active {
626 background-color: @grey6;
627 border: 2px solid @rcblue;
627 .border ( @border-thickness, @grey4 );
628 }
628 }
629
629
630 }
630 }
@@ -44,7 +44,7
44 padding: @panel-padding;
44 padding: @panel-padding;
45
45
46 &.panel-body-min-height {
46 &.panel-body-min-height {
47 min-height: 150px
47 min-height: 200px
48 }
48 }
49 }
49 }
50
50
@@ -1,17 +1,29
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-body">
4 <div class="panel-heading">
5 <div style="height: 35px">
5 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
6 <%
6 </div>
7 selected_filter = 'all'
8 if c.closed:
9 selected_filter = 'all_closed'
10 %>
11
7
8 <div class="panel-body panel-body-min-height">
9 <div class="title">
12 <ul class="button-links">
10 <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>
11 <li><a class="btn ${h.is_active('all', c.selected_filter)}"
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>
12 href="${h.route_path('my_account_pullrequests', _query={})}">
13 ${_('Open')}
14 </a>
15 </li>
16 <li><a class="btn ${h.is_active('all_closed', c.selected_filter)}"
17 href="${h.route_path('my_account_pullrequests', _query={'closed':1})}">
18 ${_('All + Closed')}
19 </a>
20 </li>
21 <li><a class="btn ${h.is_active('awaiting_my_review', c.selected_filter)}"
22 href="${h.route_path('my_account_pullrequests', _query={'awaiting_my_review':1})}">
23
24 ${_('Awaiting my review')}
25 </a>
26 </li>
15 </ul>
27 </ul>
16
28
17 <div class="grid-quick-filter">
29 <div class="grid-quick-filter">
@@ -20,19 +32,13
20 <i class="icon-search"></i>
32 <i class="icon-search"></i>
21 </li>
33 </li>
22 <li class="grid-filter-box-input">
34 <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=""/>
35 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter"
36 placeholder="${_('quick filter...')}" value=""/>
24 </li>
37 </li>
25 </ul>
38 </ul>
26 </div>
39 </div>
27 </div>
40 </div>
28 </div>
29 </div>
30
41
31 <div class="panel panel-default">
32 <div class="panel-heading">
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
34 </div>
35 <div class="panel-body panel-body-min-height">
36 <table id="pull_request_list_table" class="rctable table-bordered"></table>
42 <table id="pull_request_list_table" class="rctable table-bordered"></table>
37 </div>
43 </div>
38 </div>
44 </div>
@@ -52,6 +58,7
52 "url": "${h.route_path('my_account_pullrequests_data')}",
58 "url": "${h.route_path('my_account_pullrequests_data')}",
53 "data": function (d) {
59 "data": function (d) {
54 d.closed = "${c.closed}";
60 d.closed = "${c.closed}";
61 d.awaiting_my_review = "${c.awaiting_my_review}";
55 },
62 },
56 "dataSrc": function (json) {
63 "dataSrc": function (json) {
57 return json.data;
64 return json.data;
@@ -60,13 +67,19
60
67
61 dom: 'rtp',
68 dom: 'rtp',
62 pageLength: ${c.visual.dashboard_items},
69 pageLength: ${c.visual.dashboard_items},
63 order: [[1, "desc"]],
70 order: [[2, "desc"]],
64 columns: [
71 columns: [
65 {
72 {
66 data: {
73 data: {
67 "_": "status",
74 "_": "status",
68 "sort": "status"
75 "sort": "status"
69 }, title: "", className: "td-status", orderable: false
76 }, title: "PR", className: "td-status", orderable: false
77 },
78 {
79 data: {
80 "_": "my_status",
81 "sort": "status"
82 }, title: "You", className: "td-status", orderable: false
70 },
83 },
71 {
84 {
72 data: