Skip to content

Commit 571c5c2

Browse files
rantamatti-lamppu
authored andcommitted
Add ApplicationSectionStatusChoice.REJECTED
refs. TILA-3380
1 parent 2406d13 commit 571c5c2

11 files changed

Lines changed: 114 additions & 34 deletions

File tree

applications/admin/forms/application_section.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ class ApplicationSectionAdminForm(forms.ModelForm):
2929
"%(in_allocation)s: Application round has closed, but the section is not fully allocated. <br>"
3030
"%(handled)s: Application round is no longer in allocation, section's applied reservations "
3131
"per week has been fulfilled, or all reservation unit options rejected or locked. <br>"
32+
"%(rejected)s: All applied slots for this application section have been locked or rejected. <br>"
3233
)
3334
% {
3435
"unallocated": ApplicationSectionStatusChoice.UNALLOCATED.label,
3536
"in_allocation": ApplicationSectionStatusChoice.IN_ALLOCATION.label,
3637
"handled": ApplicationSectionStatusChoice.HANDLED.label,
38+
"rejected": ApplicationSectionStatusChoice.REJECTED.label,
3739
},
3840
)
3941

applications/choices.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ class ApplicationSectionStatusChoice(models.TextChoices):
252252
HANDLED = "HANDLED", _("Handled")
253253
"""Application section has been handled fully in the allocation process"""
254254

255+
REJECTED = "REJECTED", _("Rejected")
256+
"""All applied slots for this application section have been locked or rejected."""
257+
255258
@DynamicClassAttribute
256259
def can_allocate(self) -> bool:
257260
return self in [

applications/models/application_section.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,29 @@ def status() -> ApplicationSectionStatusChoice:
150150
models.Q(application__application_round__application_period_end__gte=Now()),
151151
then=models.Value(ApplicationSectionStatusChoice.UNALLOCATED.value),
152152
),
153+
models.When(
154+
# Application round has moved to handled stage
155+
# AND there are no allocated reservation unit options
156+
# OR Application round has NOT moved to rejected stage
157+
# AND all reservation unit options have been locked or rejected
158+
condition=(
159+
models.Q(
160+
models.Q(application__application_round__handled_date__isnull=False)
161+
& models.Q(L(allocations=0))
162+
)
163+
| models.Q(
164+
models.Q(application__application_round__handled_date__isnull=True)
165+
& models.Q(L(usable_reservation_unit_options=0))
166+
)
167+
),
168+
then=models.Value(ApplicationSectionStatusChoice.REJECTED.value),
169+
),
153170
models.When(
154171
# Application round has moved to handled stage
155172
# OR number of allocations equals the number of applied reservations per week
156-
# OR all reservation unit options have been locked or rejected
157173
condition=(
158174
models.Q(application__application_round__handled_date__isnull=False)
159175
| models.Q(L(allocations__gte=models.F("applied_reservations_per_week")))
160-
| models.Q(L(usable_reservation_unit_options=0))
161176
),
162177
then=models.Value(ApplicationSectionStatusChoice.HANDLED.value),
163178
),
@@ -182,12 +197,15 @@ def _(self) -> ApplicationSectionStatusChoice:
182197
total_allocations = sum(option.num_of_allocations for option in reservation_unit_options)
183198
all_locked_or_rejected = all(option.locked or option.rejected for option in reservation_unit_options)
184199

185-
if self.application.application_round.status.past_allocation:
186-
return ApplicationSectionStatusChoice.HANDLED
187200
if total_allocations >= self.applied_reservations_per_week:
188201
return ApplicationSectionStatusChoice.HANDLED
189202
if all_locked_or_rejected:
190-
return ApplicationSectionStatusChoice.HANDLED
203+
return ApplicationSectionStatusChoice.REJECTED
204+
205+
if self.application.application_round.status.past_allocation:
206+
if total_allocations >= 1:
207+
return ApplicationSectionStatusChoice.HANDLED
208+
return ApplicationSectionStatusChoice.REJECTED
191209

192210
return ApplicationSectionStatusChoice.IN_ALLOCATION
193211

@@ -248,9 +266,13 @@ def status_sort_order() -> int:
248266
then=models.Value(2),
249267
),
250268
models.When(
251-
models.Q(L(status=ApplicationSectionStatusChoice.HANDLED.value)),
269+
models.Q(L(status=ApplicationSectionStatusChoice.REJECTED.value)),
252270
then=models.Value(3),
253271
),
254-
default=models.Value(6),
272+
models.When(
273+
models.Q(L(status=ApplicationSectionStatusChoice.HANDLED.value)),
274+
then=models.Value(4),
275+
),
276+
default=models.Value(5),
255277
output_field=models.IntegerField(),
256278
)

locale/fi/LC_MESSAGES/django.po

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -502,24 +502,20 @@ msgstr "Teksti, joka näytetään hakijoille heidän täyttäessään hakemuksia
502502
msgid "Application this section is in."
503503
msgstr "Hakemus jolle tämä osa kuuluu."
504504

505-
#: applications/admin/forms/application_section.py
506-
#, python-format
507505
msgid ""
508506
"%(unallocated)s: Section has been created, but application round is still "
509507
"open. <br>%(in_allocation)s: Application round has closed, but the section "
510508
"is not fully allocated. <br>%(handled)s: Application round is no longer in "
511509
"allocation, section's applied reservations per week has been fulfilled, or "
512-
"all reservation unit options rejected or locked. <br>%(failed)s: At least "
513-
"one reservation was not possible for some allocation. <br>%(reserved)s: All "
514-
"allocations have successful reservations. <br>"
510+
"all reservation unit options rejected or locked. <br>%(rejected)s: All "
511+
"applied slots for this application section have been locked or rejected. <br>"
515512
msgstr ""
516513
"%(unallocated)s: Hakemuksen osa luotu, mutta kausivarauskierroksen hakuaika "
517514
"on vielä auki. <br>%(in_allocation)s: Hakuaika on sulkeutunut, mutta osaa ei "
518515
"ole vielä kokonaan käsitelty. <br>%(handled)s: Kausivarauskierros on "
519516
"käsitelty, tai osan toistokerrat on täytetty, tai kaikki varausyksikkö-"
520-
"vaihtoehdot on hylätty tai lukittu. <br>%(failed)s: Ainakin yhtä varausta ei "
521-
"ollut mahdollista luoda jollekin vuorolle. <br>%(reserved)s: Kaikki "
522-
"varaukset jaetuille vuoroille on luotu. <br>"
517+
"vaihtoehdot on hylätty tai lukittu. <br>%(rejected)s: Kaikki "
518+
"haetut toistokerrat tälle hakemuksen osalle ovat lukittu tai hylätty. <br>"
523519

524520
#: applications/admin/forms/application_section.py reservations/admin.py
525521
#: reservations/models/reservation_statistic.py
@@ -709,7 +705,7 @@ msgstr "Varausyksikkö tälle vaihtoehdolle."
709705
msgid "Preferred order"
710706
msgstr "Suosittu järjestys"
711707

712-
#: applications/admin/forms/reservation_unit_option.py
708+
#: applications/admin/forms/reservation_unit_option.py applications/choices.py
713709
msgid "Rejected"
714710
msgstr "Hylätty"
715711

@@ -871,10 +867,6 @@ msgstr "Peruttu"
871867
msgid "Unallocated"
872868
msgstr "Vastaanotettu"
873869

874-
#: applications/choices.py
875-
msgid "Reserved"
876-
msgstr "Varattu"
877-
878870
#: applications/choices.py
879871
msgid "Internal"
880872
msgstr "Sisäinen"

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ lint.ignore = [
229229
"N801", # Allow whatever class names in tests
230230
"N802", # Allow whatever function names in tests
231231
"N815", # Allow whatever variable names in tests
232+
"S311", # Pseudo-random generators are fine here.
232233
]
233234

234235
[tool.ruff.lint.isort]

tests/factories/application_section.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import random
23
from collections.abc import Iterable
34
from typing import Any
45

@@ -47,6 +48,8 @@ def create_in_status(cls, status: ApplicationSectionStatusChoice, **kwargs: Any)
4748
return cls.create_in_status_in_allocation(**kwargs)
4849
case ApplicationSectionStatusChoice.HANDLED:
4950
return cls.create_in_status_handled(**kwargs)
51+
case ApplicationSectionStatusChoice.REJECTED:
52+
return cls.create_in_status_rejected(**kwargs)
5053

5154
@classmethod
5255
def create_in_status_unallocated(cls, **kwargs: Any) -> ApplicationSection:
@@ -111,6 +114,31 @@ def create_in_status_handled(cls, **kwargs: Any) -> ApplicationSection:
111114

112115
return cls.create(**kwargs)
113116

117+
@classmethod
118+
def create_in_status_rejected(cls, **kwargs: Any) -> ApplicationSection:
119+
"""
120+
Create a rejected application section:
121+
- in an application in allocation
122+
- in an application round in allocation
123+
- without any allocated reservation unit options
124+
"""
125+
from .application import ApplicationFactory
126+
127+
kwargs["applied_reservations_per_week"] = 1
128+
129+
if "application" not in kwargs:
130+
sub_kwargs = cls.pop_sub_kwargs("application", kwargs)
131+
sub_kwargs["application_sections"] = []
132+
kwargs["application"] = ApplicationFactory.create_in_status_in_allocation(**sub_kwargs)
133+
134+
if not cls.has_sub_kwargs("reservation_unit_options", kwargs):
135+
# All options are either locked or rejected
136+
is_rejected = random.choice([True, False])
137+
kwargs["reservation_unit_options__rejected"] = is_rejected
138+
kwargs["reservation_unit_options__locked"] = not is_rejected
139+
140+
return cls.create(**kwargs)
141+
114142
@factory.post_generation
115143
def reservation_unit_options(
116144
self,

tests/test_factories/test_application_round.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
2+
from lookup_property import L
23

34
from applications.choices import ApplicationRoundStatusChoice
5+
from applications.models import ApplicationRound
46
from tests.factories import ApplicationRoundFactory
57

68
# Applied to all tests
@@ -11,5 +13,6 @@
1113

1214
@pytest.mark.parametrize("status", ApplicationRoundStatusChoice.values)
1315
def test_application_round_factory_create_in_status(status):
14-
application = ApplicationRoundFactory.create_in_status(status=status)
15-
assert application.status == status
16+
application_round = ApplicationRoundFactory.create_in_status(status=status)
17+
assert application_round.status == status
18+
assert ApplicationRound.objects.filter(L(status=status)).exists()
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
2+
from lookup_property import L
23

3-
from applications.choices import ApplicationSectionStatusChoice
4+
from applications.choices import ApplicationSectionStatusChoice, ApplicationStatusChoice
5+
from applications.models import ApplicationSection
46
from tests.factories import ApplicationSectionFactory
57

68
# Applied to all tests
@@ -10,13 +12,17 @@
1012

1113

1214
@pytest.mark.parametrize(
13-
"status",
15+
("status", "application_status"),
1416
[
15-
ApplicationSectionStatusChoice.UNALLOCATED,
16-
ApplicationSectionStatusChoice.IN_ALLOCATION,
17-
ApplicationSectionStatusChoice.HANDLED,
17+
(ApplicationSectionStatusChoice.UNALLOCATED, ApplicationStatusChoice.DRAFT),
18+
(ApplicationSectionStatusChoice.IN_ALLOCATION, ApplicationStatusChoice.IN_ALLOCATION),
19+
(ApplicationSectionStatusChoice.HANDLED, ApplicationStatusChoice.HANDLED),
20+
(ApplicationSectionStatusChoice.REJECTED, ApplicationStatusChoice.HANDLED),
1821
],
1922
)
20-
def test_application_section_factory_create_in_status(status):
23+
def test_application_section_factory_create_in_status(status, application_status):
2124
section = ApplicationSectionFactory.create_in_status(status=status)
2225
assert section.status == status
26+
assert ApplicationSection.objects.filter(L(status=status.value)).exists()
27+
28+
assert section.application.status == application_status

tests/test_graphql_api/test_allocated_time_slot/test_create.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,12 @@ def test_allocated_time_slot__create__application_not_yet_in_allocation(graphql)
124124
]
125125

126126

127-
def test_allocated_time_slot__create__application_not_in_allocation_anymore(graphql):
127+
def test_allocated_time_slot__create__application_not_in_allocation_anymore__HANDLED(graphql):
128128
# given:
129129
# - There is an open application that is already allocated
130130
# - A superuser is using the system
131131
application_round = ApplicationRoundFactory.create_in_status_handled()
132-
application = ApplicationFactory.create_application_ready_for_allocation(application_round)
132+
application = ApplicationFactory.create_application_ready_for_allocation(application_round, pre_allocated=True)
133133
section = application.application_sections.first()
134134
option = section.reservation_unit_options.first()
135135
graphql.login_user_based_on_type(UserType.SUPERUSER)
@@ -146,6 +146,28 @@ def test_allocated_time_slot__create__application_not_in_allocation_anymore(grap
146146
assert response.field_error_messages() == ["Cannot allocate to application section in status: 'HANDLED'"]
147147

148148

149+
def test_allocated_time_slot__create__application_not_in_allocation_anymore__REJECTED(graphql):
150+
# given:
151+
# - There is an open application that is already allocated
152+
# - A superuser is using the system
153+
application_round = ApplicationRoundFactory.create_in_status_handled()
154+
application = ApplicationFactory.create_application_ready_for_allocation(application_round)
155+
section = application.application_sections.first()
156+
option = section.reservation_unit_options.first()
157+
graphql.login_user_based_on_type(UserType.SUPERUSER)
158+
159+
assert application.status == ApplicationStatusChoice.HANDLED
160+
161+
# when:
162+
# - The user tries to make an allocation for a reservation unit option
163+
input_data = allocation_create_data(option)
164+
response = graphql(CREATE_ALLOCATION, input_data=input_data)
165+
166+
# then:
167+
# - The response complains about the application being in the wrong status
168+
assert response.field_error_messages() == ["Cannot allocate to application section in status: 'REJECTED'"]
169+
170+
149171
@pytest.mark.parametrize("force", [True, False])
150172
def test_allocated_time_slot__create__approve_duration_longer_than_section_maximum(graphql, force):
151173
# given:

tests/test_graphql_api/test_application_section/test_delete.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
[
1717
ApplicationSectionStatusChoice.IN_ALLOCATION,
1818
ApplicationSectionStatusChoice.HANDLED,
19+
ApplicationSectionStatusChoice.REJECTED,
1920
],
2021
)
2122
def test_cannot_delete_application_event_not_unallocated(graphql, status):

0 commit comments

Comments
 (0)