##// END OF EJS Templates
pull-requests: added awaiting my review filter for users pull-requests....
super-admin -
r4690:2e951f8d stable
parent child Browse files
Show More
@@ -76,7 +76,11 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b' 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 b''
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 b''
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 b''
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 b''
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 b''
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: {
85 data: {
@@ -23,13 +23,14 b''
23
23
24 <div class="box">
24 <div class="box">
25 <div class="title">
25 <div class="title">
26
26 <ul class="button-links">
27 <ul class="button-links">
27 <li class="btn ${h.is_active('open', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 <li><a class="btn ${h.is_active('open', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'open':1})}">${_('Open')}</a></li>
28 <li class="btn ${h.is_active('my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
29 <li><a class="btn ${h.is_active('my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Created by me')}</a></li>
29 <li class="btn ${h.is_active('awaiting', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 <li><a class="btn ${h.is_active('awaiting', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 <li class="btn ${h.is_active('awaiting_my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 <li><a class="btn ${h.is_active('awaiting_my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 <li class="btn ${h.is_active('closed', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 <li><a class="btn ${h.is_active('closed', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 <li class="btn ${h.is_active('source', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 <li><a class="btn ${h.is_active('source', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 </ul>
34 </ul>
34
35
35 <ul class="links">
36 <ul class="links">
@@ -88,20 +89,50 b''
88 },
89 },
89 dom: 'rtp',
90 dom: 'rtp',
90 pageLength: ${c.visual.dashboard_items},
91 pageLength: ${c.visual.dashboard_items},
91 order: [[ 1, "desc" ]],
92 order: [[ 2, "desc" ]],
92 columns: [
93 columns: [
93 { data: {"_": "status",
94 {
94 "sort": "status"}, title: "", className: "td-status", orderable: false},
95 data: {
95 { data: {"_": "name",
96 "_": "status",
96 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
97 "sort": "status"
97 { data: {"_": "title",
98 }, title: "PR", className: "td-status", orderable: false
98 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
99 },
99 { data: {"_": "author",
100 {
100 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
101 data: {
101 { data: {"_": "comments",
102 "_": "my_status",
102 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
103 "sort": "status"
103 { data: {"_": "updated_on",
104 }, title: "You", className: "td-status", orderable: false
104 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
105 },
106 {
107 data: {
108 "_": "name",
109 "sort": "name_raw"
110 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
111 },
112 {
113 data: {
114 "_": "title",
115 "sort": "title"
116 }, title: "${_('Title')}", className: "td-description"
117 },
118 {
119 data: {
120 "_": "author",
121 "sort": "author_raw"
122 }, title: "${_('Author')}", className: "td-user", orderable: false
123 },
124 {
125 data: {
126 "_": "comments",
127 "sort": "comments_raw"
128 }, title: "", className: "td-comments", orderable: false
129 },
130 {
131 data: {
132 "_": "updated_on",
133 "sort": "updated_on_raw"
134 }, title: "${_('Last Update')}", className: "td-time"
135 }
105 ],
136 ],
106 language: {
137 language: {
107 paginate: DEFAULT_GRID_PAGINATION,
138 paginate: DEFAULT_GRID_PAGINATION,
General Comments 1
Under Review
author

Auto status change to "Under Review"

You need to be logged in to leave comments. Login now