Skip to content

Commit ece9c7f

Browse files
committed
Add Ready for Review state.
1 parent 27fd600 commit ece9c7f

5 files changed

Lines changed: 76 additions & 18 deletions

File tree

docs/references/workflows.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ Funding organization wants an advisory board or external partners to give their
128128
Once an application is submitted (`INITIAL_STATE`) — it can transition into the following:
129129

130130
- A request for more information (`ext_int_more_info`) — opens editing permissions to applicant again to revise their application to provide the information requested by the screeners.
131-
- Open external review (`ext_int_external_review`) — can transition only between closing the review period (`ext_int_post_external_review_discussion`) and reverting back to the internal screening phase.
131+
- Ready for review (`ext_int_ready_for_review`) — a staff only holding phase between screening and the external review, where reviewers can be assigned before they are given access to the application. Can transition to opening the external review (`ext_int_external_review`), mark ready for determination (`ext_int_determination`), dismiss (`ext_int_rejected`) or revert back to screening (`INITIAL_STATE`). Reviewers can see the application in this phase but can not review it yet.
132+
- Open external review (`ext_int_external_review`) — can transition only between closing the review period (`ext_int_post_external_review_discussion`) and reverting back to ready for review (`ext_int_ready_for_review`).
132133
- `ext_int_post_external_review_discussion` — after the external review is closed, you can request more information (`ext_int_post_external_review_more_info`), open the internal review (`ext_int_internal_review`), mark ready for determination (`ext_int_determination`), revert back to opening the external review (`ext_int_external_review`), accept (`ext_int_accepted`), waitlist (`ext_int_waitlisted`) or dismiss (`ext_int_rejected`)
133134
- `ext_int_post_external_review_more_info` — opens editing permissions to applicant again to revise their application to provide the information requested by the external reviewers.
134135
- Open internal review (`ext_int_internal_review`) — can transition only between closing the review period (`ext_int_post_review_discussion`) and reverting back to discussion (`ext_int_post_external_review_discussion`)
@@ -137,13 +138,14 @@ Once an application is submitted (`INITIAL_STATE`) — it can transition into th
137138
- Ready for determination (`ext_int_determination`) — can revert back to discussion (“Ready for Discussion (revert)” — `ext_int_post_review_discussion`), accept (`ext_int_accepted`), waitlist (`ext_int_waitlisted`) or dismiss (`ext_int_rejected`)
138139
- Waitlisted (`ext_int_waitlisted`) — the application is kept in play without a decision. Can transition to accept (`ext_int_accepted`), dismiss (`ext_int_rejected`), or revert back to ready for determination (`ext_int_determination`). Staff can still edit this submission. Waitlisting is a plain status change — unlike Accept and Dismiss it does not open the determination form and does not create a determination record.
139140
- Accepted (`ext_int_accepted`) — application accepted. Staff can still edit this submission.
140-
- Dismissed (`ext_int_rejected`) — application rejected. Editing and reviewing permissions removed from all roles, the applicant can still view the submission.
141+
- Dismissed (`ext_int_rejected`) — application rejected, shown as "Not Accepted" to the applicant. Editing and reviewing permissions removed from all roles, the applicant can still view the submission.
141142

142143
There is no "Accepted but additional info required" state in this workflow.
143144

144145
If the `TRANSITION_AFTER_REVIEWS` setting is set to a number, submitting a review can move the application on automatically:
145146

146-
- Need screening → External Review, as soon as the first review is submitted. The configured number is not taken into account for this step, and only staff can review during screening.
147+
- Need screening → Ready for Review, as soon as the first review is submitted. The configured number is not taken into account for this step, and only staff can review during screening.
148+
- Ready for Review → External Review, once that many reviews have been submitted in total. Only staff can review in this phase.
147149
- External Review → Ready for Discussion, once that many reviews have been submitted by users in the Reviewer group.
148150
- Internal Review → Ready for Discussion, once that many reviews have been submitted in total.
149151

@@ -156,9 +158,9 @@ Everything from the first discussion onwards is hidden from the applicant. The s
156158
1. **Draft** (`draft`)
157159
2. **Application Received** (`INITIAL_STATE`, "Need screening" to staff)
158160
3. **Application Review** (`ext_int_external_review`, "External Review" to staff)
159-
4. **Application Outcome** — shown as Accepted (`ext_int_accepted`), Waitlisted (`ext_int_waitlisted`) or Dismissed (`ext_int_rejected`) once the application reaches that step
161+
4. **Application Outcome** — shown as Accepted (`ext_int_accepted`), Waitlisted (`ext_int_waitlisted`) or Not Accepted (`ext_int_rejected`, "Dismissed" to staff) once the application reaches that step
160162

161-
The hidden phases (`ext_int_post_external_review_discussion`, `ext_int_internal_review`, `ext_int_post_review_discussion` and `ext_int_determination`) do not add a step of their own — to the applicant the application stays on "Application Review" until an outcome is reached. The three "More information required" states are visible to the applicant, since they need to edit and resubmit their application.
163+
The hidden phases (`ext_int_ready_for_review`, `ext_int_post_external_review_discussion`, `ext_int_internal_review`, `ext_int_post_review_discussion` and `ext_int_determination`) do not add a step of their own — to the applicant the application stays on "Application Review" until an outcome is reached. The three "More information required" states are visible to the applicant, since they need to edit and resubmit their application.
162164

163165
### 💡 Concept and Proposal
164166

hypha/apply/funds/migrations/0137_alter_anonymizedsubmission_status_and_more.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 5.2.17 on 2026-08-26 12:22
1+
# Generated by Django 5.2.17 on 2026-08-27 12:07
22

33
from django.db import migrations, models
44

@@ -54,6 +54,7 @@ class Migration(migrations.Migration):
5454
),
5555
("ext_int_post_review_discussion", "Ready for Discussion"),
5656
("ext_int_post_review_more_info", "More information required"),
57+
("ext_int_ready_for_review", "Ready for Review"),
5758
("ext_int_rejected", "Dismissed"),
5859
("ext_int_waitlisted", "Waitlisted"),
5960
("ext_internal_review", "Internal Review"),
@@ -140,6 +141,7 @@ class Migration(migrations.Migration):
140141
),
141142
("ext_int_post_review_discussion", "Ready for Discussion"),
142143
("ext_int_post_review_more_info", "More information required"),
144+
("ext_int_ready_for_review", "Ready for Review"),
143145
("ext_int_rejected", "Dismissed"),
144146
("ext_int_waitlisted", "Waitlisted"),
145147
("ext_internal_review", "Internal Review"),

hypha/apply/funds/tests/test_workflows.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"""Tests for the workflow registry, focused on the external-then-internal workflow."""
22

3+
from types import SimpleNamespace
4+
35
import pytest
46
from django.test import SimpleTestCase
57

68
from hypha.apply.users.tests.factories import StaffFactory
79

810
from ..workflows import (
911
DETERMINATION_OUTCOMES,
12+
INITIAL_STATE,
1013
WORKFLOWS,
1114
ext_or_higher_statuses,
1215
ext_review_statuses,
@@ -33,21 +36,49 @@ def test_phases_and_steps(self):
3336
"draft": 0,
3437
"in_discussion": 1,
3538
"ext_int_more_info": 1,
36-
"ext_int_external_review": 2,
37-
"ext_int_post_external_review_discussion": 3,
38-
"ext_int_post_external_review_more_info": 3,
39-
"ext_int_internal_review": 4,
40-
"ext_int_post_review_discussion": 5,
41-
"ext_int_post_review_more_info": 5,
42-
"ext_int_determination": 6,
43-
"ext_int_accepted": 7,
44-
"ext_int_waitlisted": 7,
45-
"ext_int_rejected": 7,
39+
"ext_int_ready_for_review": 2,
40+
"ext_int_external_review": 3,
41+
"ext_int_post_external_review_discussion": 4,
42+
"ext_int_post_external_review_more_info": 4,
43+
"ext_int_internal_review": 5,
44+
"ext_int_post_review_discussion": 6,
45+
"ext_int_post_review_more_info": 6,
46+
"ext_int_determination": 7,
47+
"ext_int_accepted": 8,
48+
"ext_int_waitlisted": 8,
49+
"ext_int_rejected": 8,
4650
}
4751
self.assertEqual(
4852
{name: phase.step for name, phase in self.workflow.items()}, expected
4953
)
5054

55+
def test_ready_for_review_comes_before_external_review(self):
56+
self.assertLess(
57+
self.workflow["ext_int_ready_for_review"].step,
58+
self.workflow["ext_int_external_review"].step,
59+
)
60+
# Screening can only reach the external review through the new phase.
61+
self.assertNotIn(
62+
"ext_int_external_review", self.workflow[INITIAL_STATE].transitions
63+
)
64+
self.assertIn(
65+
"ext_int_ready_for_review", self.workflow[INITIAL_STATE].transitions
66+
)
67+
68+
def test_ready_for_review_is_hidden_from_the_applicant(self):
69+
applicant = SimpleNamespace(
70+
is_apply_staff=False, is_applicant=True, is_reviewer=False
71+
)
72+
reviewer = SimpleNamespace(
73+
is_apply_staff=False, is_applicant=False, is_reviewer=True
74+
)
75+
phase = self.workflow["ext_int_ready_for_review"]
76+
self.assertEqual(phase.display_name, "Ready for Review")
77+
self.assertFalse(phase.permissions.can_view(applicant))
78+
self.assertTrue(phase.permissions.can_view(reviewer))
79+
# Reviewers only get to review once the external review is opened.
80+
self.assertFalse(phase.permissions.can_review(reviewer))
81+
5182
def test_external_review_comes_before_internal_review(self):
5283
self.assertLess(
5384
self.workflow["ext_int_external_review"].step,
@@ -78,13 +109,20 @@ def test_waitlist_offered_from_discussion_and_determination(self):
78109
"ext_int_waitlisted", self.workflow[phase_name].transitions
79110
)
80111

112+
def test_dismissed_is_softened_for_the_applicant(self):
113+
rejected = self.workflow["ext_int_rejected"]
114+
self.assertEqual(rejected.display_name, "Dismissed")
115+
self.assertEqual(rejected.public_name, "Not Accepted")
116+
81117
def test_outcome_phases_are_terminal(self):
82118
for phase_name in ["ext_int_accepted", "ext_int_rejected"]:
83119
with self.subTest(phase=phase_name):
84120
self.assertEqual(self.workflow[phase_name].transitions, {})
85121

86122
def test_picked_up_by_derived_status_sets(self):
87123
self.assertIn("ext_int_external_review", ext_review_statuses)
124+
self.assertNotIn("ext_int_ready_for_review", ext_review_statuses)
125+
self.assertNotIn("ext_int_ready_for_review", ext_or_higher_statuses)
88126
self.assertIn("ext_int_internal_review", ext_or_higher_statuses)
89127
self.assertIn("ext_int_post_review_discussion", DETERMINATION_RESPONSE_PHASES)
90128

@@ -97,6 +135,7 @@ def test_walk_the_whole_workflow():
97135
assert submission.status == "in_discussion"
98136

99137
chain = [
138+
"ext_int_ready_for_review",
100139
"ext_int_external_review",
101140
"ext_int_post_external_review_discussion",
102141
"ext_int_internal_review",

hypha/apply/funds/workflows/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"Need screening": "bg-cyan-200",
1818
"Ready for Determination": "bg-blue-200",
1919
"Ready for Discussion": "bg-blue-100",
20+
"Ready for Review": "bg-yellow-100",
2021
"Invited for Proposal": "bg-green-100",
2122
"Internal Review": "bg-yellow-200",
2223
"External Review": "bg-yellow-200",

hypha/apply/funds/workflows/definitions/single_stage_ext_int.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
INITIAL_STATE: {
3232
"transitions": {
3333
"ext_int_more_info": _("Request More Information"),
34-
"ext_int_external_review": _("Open External Review"),
34+
"ext_int_ready_for_review": _("Ready for Review"),
3535
"ext_int_determination": _("Ready for Determination"),
3636
"ext_int_rejected": _("Dismiss"),
3737
},
@@ -59,11 +59,24 @@
5959
"permissions": applicant_edit_permissions,
6060
},
6161
},
62+
{
63+
"ext_int_ready_for_review": {
64+
"transitions": {
65+
"ext_int_external_review": _("Open External Review"),
66+
"ext_int_determination": _("Ready for Determination"),
67+
"ext_int_rejected": _("Dismiss"),
68+
INITIAL_STATE: _("Need screening (revert)"),
69+
},
70+
"display": _("Ready for Review"),
71+
"stage": RequestExtInt,
72+
"permissions": hidden_from_applicant_permissions,
73+
},
74+
},
6275
{
6376
"ext_int_external_review": {
6477
"transitions": {
6578
"ext_int_post_external_review_discussion": _("Close Review"),
66-
INITIAL_STATE: _("Need screening (revert)"),
79+
"ext_int_ready_for_review": _("Ready for Review (revert)"),
6780
},
6881
"display": _("External Review"),
6982
"public": _("Application Review"),
@@ -183,6 +196,7 @@
183196
},
184197
"ext_int_rejected": {
185198
"display": _("Dismissed"),
199+
"public": _("Not Accepted"),
186200
"stage": RequestExtInt,
187201
"permissions": no_permissions,
188202
},

0 commit comments

Comments
 (0)