Skip to content

Commit 8f7a88e

Browse files
authored
Merge pull request #1220 from OpenTechFund/feature/1191-review-ordering-tables
GH-1191 Make reviewer ordering consistent and tweak styles on tables
2 parents 6ebb4d1 + 6e2aaaf commit 8f7a88e

File tree

9 files changed

+143
-59
lines changed

9 files changed

+143
-59
lines changed

opentech/apply/funds/models/submissions.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,18 @@
77
from django.contrib.postgres.fields import JSONField
88
from django.core.exceptions import PermissionDenied
99
from django.db import models
10-
from django.db.models import Case, Count, IntegerField, OuterRef, Subquery, Sum, Q, Prefetch, When
10+
from django.db.models import (
11+
Case,
12+
Count,
13+
IntegerField,
14+
F,
15+
OuterRef,
16+
Prefetch,
17+
Q,
18+
Subquery,
19+
Sum,
20+
When,
21+
)
1122
from django.db.models.expressions import RawSQL, OrderBy
1223
from django.db.models.functions import Coalesce
1324
from django.dispatch import receiver
@@ -22,17 +33,19 @@
2233

2334
from opentech.apply.activity.messaging import messenger, MESSAGES
2435
from opentech.apply.determinations.models import Determination
25-
from opentech.apply.review.models import Review, ReviewOpinion
36+
from opentech.apply.review.models import ReviewOpinion
2637
from opentech.apply.review.options import MAYBE, AGREE, DISAGREE
2738
from opentech.apply.stream_forms.blocks import UploadableMediaBlock
2839
from opentech.apply.stream_forms.files import StreamFieldDataEncoder
2940
from opentech.apply.stream_forms.models import BaseStreamForm
3041

3142
from .mixins import AccessFormData
3243
from .utils import (
44+
COMMUNITY_REVIEWER_GROUP_NAME,
3345
LIMIT_TO_STAFF,
3446
LIMIT_TO_REVIEWER_GROUPS,
3547
LIMIT_TO_PARTNERS,
48+
PARTNER_GROUP_NAME,
3649
REVIEW_GROUPS,
3750
REVIEWER_GROUP_NAME,
3851
STAFF_GROUP_NAME,
@@ -167,7 +180,9 @@ def for_table(self, user):
167180
output_field=IntegerField(),
168181
),
169182
review_submitted_count=Subquery(
170-
reviewers.reviewed().values('submission').annotate(count=Count('pk')).values('count'),
183+
reviewers.reviewed().values('submission').annotate(
184+
count=Count('pk', distinct=True)
185+
).values('count'),
171186
output_field=IntegerField(),
172187
),
173188
review_recommendation=Case(
@@ -182,10 +197,18 @@ def for_table(self, user):
182197
role_icon=Subquery(roles_for_review[:1].values('role__icon')),
183198
).prefetch_related(
184199
Prefetch(
185-
'reviews', queryset=Review.objects.select_related('author').prefetch_related(
186-
Prefetch('opinions', queryset=ReviewOpinion.objects.select_related('author'))
187-
)
200+
'assigned',
201+
queryset=AssignedReviewers.objects.reviewed().review_order().prefetch_related(
202+
Prefetch('opinions', queryset=ReviewOpinion.objects.select_related('author__reviewer'))
203+
),
204+
to_attr='has_reviewed'
205+
),
206+
Prefetch(
207+
'assigned',
208+
queryset=AssignedReviewers.objects.not_reviewed().staff(),
209+
to_attr='hasnt_reviewed'
188210
)
211+
189212
).select_related(
190213
'page',
191214
'round',
@@ -758,6 +781,43 @@ def get_absolute_url(self):
758781

759782

760783
class AssignedReviewersQuerySet(models.QuerySet):
784+
def review_order(self):
785+
review_order = [
786+
STAFF_GROUP_NAME,
787+
PARTNER_GROUP_NAME,
788+
COMMUNITY_REVIEWER_GROUP_NAME,
789+
REVIEWER_GROUP_NAME,
790+
]
791+
792+
ordering = [
793+
models.When(type__name=review_type, then=models.Value(i))
794+
for i, review_type in enumerate(review_order)
795+
]
796+
return self.exclude(
797+
# Remove people from the list who are opinionated but
798+
# didn't review, they appear elsewhere
799+
opinions__isnull=False,
800+
review__isnull=True,
801+
).annotate(
802+
type_order=models.Case(
803+
*ordering,
804+
output_field=models.IntegerField(),
805+
),
806+
has_review=models.Case(
807+
models.When(review__isnull=True, then=models.Value(1)),
808+
models.When(review__is_draft=True, then=models.Value(1)),
809+
default=models.Value(0),
810+
output_field=models.IntegerField(),
811+
)
812+
).order_by(
813+
'type_order',
814+
'has_review',
815+
F('role__order').asc(nulls_last=True),
816+
).select_related(
817+
'reviewer',
818+
'role',
819+
)
820+
761821
def with_roles(self):
762822
return self.filter(role__isnull=False)
763823

@@ -766,13 +826,14 @@ def without_roles(self):
766826

767827
def reviewed(self):
768828
return self.filter(
769-
Q(opinions__isnull=False) | Q(Q(review__isnull=False) & Q(review__is_draft=False))
829+
Q(opinions__opinion=AGREE) |
830+
Q(Q(review__isnull=False) & Q(review__is_draft=False))
770831
).distinct()
771832

772833
def not_reviewed(self):
773834
return self.filter(
774835
Q(review__isnull=True) | Q(review__is_draft=True),
775-
opinions__isnull=True,
836+
Q(opinions__isnull=True) | Q(opinions__opinion=DISAGREE),
776837
).distinct()
777838

778839
def never_tried_to_review(self):

opentech/apply/funds/templates/funds/includes/review_sidebar.html

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,4 @@
3131
{% endif %}
3232
{% endfor %}
3333
{% endfor %}
34-
35-
{% if reviews_block.external_reviewed or reviews_block.external_not_reviewed %}
36-
{% for review_data in reviews_block.external_reviewed %}
37-
{% include 'funds/includes/review_sidebar_item.html' with review=review_data.review reviewer=review_data.reviewer opinions=review_data.opinions %}
38-
{% endfor %}
39-
40-
{% for review_data in reviews_block.external_not_reviewed %}
41-
{% include 'funds/includes/review_sidebar_item.html' with reviewer=review_data.reviewer missing=True class="is-hidden" %}
42-
{% endfor %}
43-
44-
{% endif %}
4534
</ul>

opentech/apply/funds/templates/funds/includes/review_sidebar_item.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% load wagtailimages_tags %}
22

33
<li class="reviews-sidebar__item {% if hidden and not reviewer.review %}is-hidden {% endif %}{% if not reviewer.review %}no-response {% endif %}">
4-
{% if not reviewer.review %}
4+
{% if not reviewer.review or reviewer.review.is_draft %}
55
<div class="reviews-sidebar__name">
66
<span>{{ reviewer}}</span>
77
{% if reviewer.role %}{% image reviewer.role.icon max-12x12 %}{% endif %}

opentech/apply/funds/templates/funds/tables/table.html

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends 'django_tables2/table.html' %}
2-
{% load django_tables2 table_tags review_tags %}
2+
{% load django_tables2 table_tags review_tags wagtailimages_tags %}
33

44
{% block table.tbody.row %}
55
<tr {{ row.attrs.as_html }}>
@@ -36,20 +36,27 @@
3636
<td>
3737
<strong>{{ submission.screening_status|default:"Awaiting Screen status" }}</strong>
3838
</td>
39+
3940
<td>
4041
<ul class="list list--no-margin">
41-
{% for review in submission.reviews.submitted %}
42+
{% for reviewer in submission.has_reviewed %}
4243
<li class="list__item list__item--reviewer">
43-
<span class="list__item--reviewer-name">{{ review.author }}</span>
44-
<span class="list__item list__item--reviewer-outcome">{{ review.get_recommendation_display }}</span>
44+
<span class="list__item--reviewer-name">
45+
{{ reviewer }}
46+
{% if reviewer.role %}{% image reviewer.role.icon max-12x12 %}{% endif %}
47+
</span>
48+
<span class="list__item list__item--reviewer-outcome">{{ reviewer.review.get_recommendation_display }}</span>
4549
</li>
46-
{% for opinion in review.opinions.all %}
50+
{% for opinion in reviewer.review.opinions.all %}
4751
{% if forloop.first %}
4852
<ul class="list list--opinion">
4953
{% endif %}
5054

5155
<li class="list__item list__item--reviewer list__item--opinion">
52-
<span class="list__item--reviewer-name">{{ opinion.author }}</span>
56+
<span class="list__item--reviewer-name">
57+
{{ opinion.author }}
58+
{% if opinion.author.role %}{% image opinion.author.role.icon max-12x12 %}{% endif %}
59+
</span>
5360
<span class="list__item list__item--reviewer-outcome {{ opinion.get_opinion_display|lower }}">{{ opinion.get_opinion_display }}</span>
5461
</li>
5562

@@ -58,9 +65,12 @@
5865
{% endif %}
5966
{% endfor %}
6067
{% endfor %}
61-
{% for reviewer in submission.staff_not_reviewed %}
68+
{% for reviewer in submission.hasnt_reviewed %}
6269
<li class="list__item list__item--reviewer">
63-
<span class="list__item--reviewer-name">{{ reviewer }}</span>
70+
<span class="list__item--reviewer-name">
71+
{{ reviewer }}
72+
{% if reviewer.role %}{% image reviewer.role.icon max-12x12 %}{% endif %}
73+
</span>
6474
<span class="list__item list__item--reviewer-outcome">&mdash;</span>
6575
</li>
6676
{% endfor %}

opentech/apply/funds/tests/test_models.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,23 @@ def test_disagree_review_is_maybe(self):
535535
submission = qs[0]
536536
self.assertEqual(submission.opinion_disagree, 1)
537537
self.assertEqual(submission.review_count, 2)
538-
self.assertEqual(submission.review_submitted_count, 2)
538+
# Reviewers that disagree are not counted
539+
self.assertEqual(submission.review_submitted_count, 1)
540+
self.assertEqual(submission.review_recommendation, MAYBE)
541+
542+
def test_opinionated_slash_confused_reviewer(self):
543+
staff = StaffFactory()
544+
submission = ApplicationSubmissionFactory()
545+
review_one = ReviewFactory(submission=submission)
546+
review_two = ReviewFactory(submission=submission)
547+
ReviewOpinionFactory(opinion_disagree=True, review=review_one, author__reviewer=staff)
548+
ReviewOpinionFactory(opinion_agree=True, review=review_two, author__reviewer=staff)
549+
qs = ApplicationSubmission.objects.for_table(user=staff)
550+
submission = qs[0]
551+
self.assertEqual(submission.opinion_disagree, 1)
552+
self.assertEqual(submission.review_count, 3)
553+
# Reviewers that disagree are not counted
554+
self.assertEqual(submission.review_submitted_count, 3)
539555
self.assertEqual(submission.review_recommendation, MAYBE)
540556

541557
def test_dont_double_count_review_and_opinion(self):
@@ -570,7 +586,8 @@ def test_submissions_dont_conflict(self):
570586
self.assertEqual(submission, submission_one)
571587
self.assertEqual(submission.opinion_disagree, 1)
572588
self.assertEqual(submission.review_count, 2)
573-
self.assertEqual(submission.review_submitted_count, 2)
589+
# Reviewers that disagree are not counted
590+
self.assertEqual(submission.review_submitted_count, 1)
574591
self.assertEqual(submission.review_recommendation, MAYBE)
575592

576593
submission = qs[1]

opentech/apply/review/templates/review/review_list.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ <h5>For <a href="{% url "funds:submissions:detail" submission.id %}">{{ submissi
2323
<tr class="reviews-list__tr">
2424
<th class="reviews-list__th">{{ answers.question }}</th>
2525
{% for answer in answers.answers %}
26-
{% if answers.question == "Opinions" %}
26+
{% if forloop.parentloop.first %}
27+
<td class="reviews-list__td reviews-list__td--author">{{ answer|safe }}</td>
28+
{% elif answers.question == "Opinions"%}
2729
<td class="reviews-list__td">{{ answer }}</td>
2830
{% else %}
2931
<td class="reviews-list__td">{{ answer|bleach }}</td>

opentech/apply/review/views.py

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from django.contrib.auth.decorators import login_required
22
from django.contrib.auth.mixins import UserPassesTestMixin
33
from django.core.exceptions import PermissionDenied
4-
from django.db import models
54
from django.http import HttpResponseRedirect
65
from django.shortcuts import get_object_or_404
76
from django.template.loader import get_template
@@ -17,36 +16,18 @@
1716
from opentech.apply.review.forms import ReviewModelForm, ReviewOpinionForm
1817
from opentech.apply.stream_forms.models import BaseStreamForm
1918
from opentech.apply.users.decorators import staff_required
20-
from opentech.apply.users.groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME, PARTNER_GROUP_NAME, COMMUNITY_REVIEWER_GROUP_NAME
19+
from opentech.apply.users.groups import REVIEWER_GROUP_NAME
2120
from opentech.apply.utils.views import CreateOrUpdateView
21+
from opentech.apply.utils.image import generate_image_tag
2222

2323
from .models import Review
2424
from .options import DISAGREE
2525

2626

2727
class ReviewContextMixin:
2828
def get_context_data(self, **kwargs):
29-
review_order = [
30-
STAFF_GROUP_NAME,
31-
COMMUNITY_REVIEWER_GROUP_NAME,
32-
PARTNER_GROUP_NAME,
33-
REVIEWER_GROUP_NAME,
34-
]
35-
36-
ordering = [models.When(type__name=review_type, then=models.Value(i)) for i, review_type in enumerate(review_order)]
37-
assigned_reviewers = self.object.assigned.annotate(
38-
type_order=models.Case(
39-
*ordering,
40-
output_field=models.IntegerField(),
41-
)
42-
).order_by(
43-
'role__order',
44-
'review',
45-
'type_order'
46-
).select_related(
47-
'reviewer',
48-
'role',
49-
)
29+
assigned_reviewers = self.object.assigned.review_order()
30+
5031
if not self.object.stage.has_external_review:
5132
assigned_reviewers = assigned_reviewers.staff()
5233

@@ -286,9 +267,17 @@ def get_context_data(self, **kwargs):
286267
review_data['comments'] = {'question': 'Comments', 'answers': list()}
287268

288269
responses = self.object_list.count()
270+
ordered_reviewers = AssignedReviewers.objects.filter(submission=self.submission).reviewed().review_order()
271+
272+
reviews = {review.author: review for review in self.object_list}
273+
for i, reviewer in enumerate(ordered_reviewers):
274+
review = reviews[reviewer]
275+
author = '<a href="{}"><span>{}</span></a>'.format(review.get_absolute_url(), review.author)
276+
if review.author.role:
277+
author += generate_image_tag(review.author.role.icon, '12x12')
278+
author = f'<div>{author}</div>'
289279

290-
for i, review in enumerate(self.object_list):
291-
review_data['title']['answers'].append('<a href="{}">{}</a>'.format(review.get_absolute_url(), review.author))
280+
review_data['title']['answers'].append(author)
292281
opinions_template = get_template('review/includes/review_opinions_list.html')
293282
opinions_html = opinions_template.render({'opinions': review.opinions.select_related('author').all()})
294283
review_data['opinions']['answers'].append(opinions_html)

opentech/static_src/src/sass/apply/components/_list.scss

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.list {
22
$root: &;
3+
max-width: 290px;
34

45
&--no-margin {
56
margin: 0;
@@ -12,7 +13,6 @@
1213
border-bottom: 1px solid $color--mid-grey;
1314
margin: 10px 0;
1415
padding: 5px 0;
15-
max-width: 150px;
1616

1717
#{$root}__item--opinion:first-child {
1818
span:last-child {
@@ -39,14 +39,19 @@
3939
&--reviewer {
4040
display: flex;
4141
justify-content: space-between;
42-
max-width: 150px;
4342
}
4443

4544
&--reviewer-name {
46-
max-width: 100px;
45+
max-width: 190px;
4746
overflow: hidden;
4847
font-weight: $weight--bold;
4948
text-overflow: ellipsis;
49+
display: flex;
50+
align-items: center;
51+
52+
> img {
53+
margin-left: 10px;
54+
}
5055

5156
// show truncated emails on hover
5257
&:hover {

opentech/static_src/src/sass/apply/components/_reviews-list.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@
2121
max-width: 340px;
2222
min-width: 240px;
2323
padding: 20px;
24+
25+
&--author {
26+
> div {
27+
display: flex;
28+
align-items: center;
29+
30+
img {
31+
margin-left: 7px;
32+
}
33+
}
34+
}
2435
}
2536

2637
&__th,

0 commit comments

Comments
 (0)