Skip to content

Commit 7416eb4

Browse files
committed
Add endpoints for rejecting and restoring all application options
1 parent 9b65a39 commit 7416eb4

9 files changed

Lines changed: 227 additions & 3 deletions

File tree

api/graphql/extensions/error_codes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@
3939
HELSINKI_PROFILE_USER_MISSING_PROFILE_ID = "HELSINKI_PROFILE_USER_MISSING_PROFILE_ID"
4040

4141
CANNOT_REJECT_SECTION_OPTIONS = "CANNOT_REJECT_SECTION_OPTIONS"
42+
CANNOT_REJECT_APPLICATION_OPTIONS = "CANNOT_REJECT_APPLICATION_OPTIONS"

api/graphql/mutations.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
ApplicationCreateMutation,
2424
ApplicationSendMutation,
2525
ApplicationUpdateMutation,
26+
RejectAllApplicationOptionsMutation,
27+
RestoreAllApplicationOptionsMutation,
2628
)
2729
from api.graphql.types.application_section.mutations import (
2830
ApplicationSectionCreateMutation,
@@ -104,6 +106,7 @@
104106
"RecurringReservationCreateMutation",
105107
"RecurringReservationUpdateMutation",
106108
"RefreshOrderMutation",
109+
"RejectAllApplicationOptionsMutation",
107110
"RejectAllSectionOptionsMutation",
108111
"ReservationAdjustTimeMutation",
109112
"ReservationApproveMutation",
@@ -128,6 +131,7 @@
128131
"ResourceCreateMutation",
129132
"ResourceDeleteMutation",
130133
"ResourceUpdateMutation",
134+
"RestoreAllApplicationOptionsMutation",
131135
"RestoreAllSectionOptionsMutation",
132136
"SpaceCreateMutation",
133137
"SpaceDeleteMutation",

api/graphql/schema.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
RecurringReservationCreateMutation,
4545
RecurringReservationUpdateMutation,
4646
RefreshOrderMutation,
47+
RejectAllApplicationOptionsMutation,
4748
RejectAllSectionOptionsMutation,
4849
ReservationAdjustTimeMutation,
4950
ReservationApproveMutation,
@@ -68,6 +69,7 @@
6869
ResourceCreateMutation,
6970
ResourceDeleteMutation,
7071
ResourceUpdateMutation,
72+
RestoreAllApplicationOptionsMutation,
7173
RestoreAllSectionOptionsMutation,
7274
SpaceCreateMutation,
7375
SpaceDeleteMutation,
@@ -266,6 +268,8 @@ class Mutation(graphene.ObjectType):
266268
update_reservation_unit_option = ReservationUnitOptionUpdateMutation.Field()
267269
reject_all_section_options = RejectAllSectionOptionsMutation.Field()
268270
restore_all_section_options = RestoreAllSectionOptionsMutation.Field()
271+
reject_all_application_options = RejectAllApplicationOptionsMutation.Field()
272+
restore_all_application_options = RestoreAllApplicationOptionsMutation.Field()
269273
#
270274
# Reservable entities
271275
update_unit = UnitUpdateMutation.Field()

api/graphql/types/application/mutations.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
from graphene_django_extensions import CreateMutation, UpdateMutation
22

3-
from api.graphql.types.application.permissions import ApplicationPermission
3+
from api.graphql.types.application.permissions import ApplicationPermission, UpdateAllApplicationOptionsPermission
44
from api.graphql.types.application.serializers import (
55
ApplicationCancelSerializer,
66
ApplicationCreateSerializer,
77
ApplicationSendSerializer,
88
ApplicationUpdateSerializer,
9+
RejectAllApplicationOptionsSerializer,
10+
RestoreAllApplicationOptionsSerializer,
911
)
1012

1113
__all__ = [
1214
"ApplicationCancelMutation",
1315
"ApplicationCreateMutation",
1416
"ApplicationSendMutation",
1517
"ApplicationUpdateMutation",
18+
"RejectAllApplicationOptionsMutation",
19+
"RestoreAllApplicationOptionsMutation",
1620
]
1721

1822

@@ -38,3 +42,15 @@ class ApplicationCancelMutation(UpdateMutation):
3842
class Meta:
3943
serializer_class = ApplicationCancelSerializer
4044
permission_classes = [ApplicationPermission]
45+
46+
47+
class RejectAllApplicationOptionsMutation(UpdateMutation):
48+
class Meta:
49+
serializer_class = RejectAllApplicationOptionsSerializer
50+
permission_classes = [UpdateAllApplicationOptionsPermission]
51+
52+
53+
class RestoreAllApplicationOptionsMutation(UpdateMutation):
54+
class Meta:
55+
serializer_class = RestoreAllApplicationOptionsSerializer
56+
permission_classes = [UpdateAllApplicationOptionsPermission]

api/graphql/types/application/permissions.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
from typing import TYPE_CHECKING, Any
44

5+
from django.db.models import Prefetch
56
from graphene_django_extensions.permissions import BasePermission
67

8+
from applications.models import ReservationUnitOption
79
from common.typing import AnyUser
8-
from permissions.helpers import can_modify_application, can_read_application
10+
from permissions.helpers import (
11+
can_modify_application,
12+
can_read_application,
13+
has_general_permission,
14+
has_unit_permission,
15+
)
916

1017
if TYPE_CHECKING:
1118
from applications.models import Application
@@ -23,3 +30,26 @@ def has_node_permission(cls, instance: Application, user: AnyUser, filters: dict
2330
@classmethod
2431
def has_update_permission(cls, instance: Application, user: AnyUser, input_data: dict[str, Any]) -> bool:
2532
return can_modify_application(user, instance)
33+
34+
35+
class UpdateAllApplicationOptionsPermission(BasePermission):
36+
@classmethod
37+
def has_update_permission(cls, instance: Application, user: AnyUser, input_data: dict[str, Any]) -> bool:
38+
if user.is_anonymous:
39+
return False
40+
if user.is_superuser:
41+
return True
42+
if has_general_permission(user, required_permission="can_handle_applications"):
43+
return True
44+
45+
units = [
46+
unit_id
47+
for section in instance.application_sections.all().prefetch_related(
48+
Prefetch(
49+
"reservation_unit_options",
50+
ReservationUnitOption.objects.all().select_related("reservation_unit__unit"),
51+
)
52+
)
53+
for unit_id in section.reservation_unit_options.all().values_list("reservation_unit__unit__id", flat=True)
54+
]
55+
return has_unit_permission(user, "can_handle_applications", units)

api/graphql/types/application/serializers.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
from django.utils import timezone
55
from graphene_django_extensions import NestingModelSerializer
66
from rest_framework import serializers
7+
from rest_framework.exceptions import ValidationError
78
from rest_framework.settings import api_settings
89

10+
from api.graphql.extensions import error_codes
911
from api.graphql.types.address.serializers import AddressSerializer
1012
from api.graphql.types.application_section.serializers import ApplicationSectionForApplicationSerializer
1113
from api.graphql.types.organisation.serializers import OrganisationSerializer
1214
from api.graphql.types.person.serializers import PersonSerializer
1315
from applications.choices import ApplicationStatusChoice
14-
from applications.models import Application
16+
from applications.models import AllocatedTimeSlot, Application, ReservationUnitOption
1517
from email_notification.helpers.application_email_notification_sender import ApplicationEmailNotificationSender
1618
from permissions.helpers import can_validate_unit_applications
1719

@@ -126,3 +128,42 @@ def save(self, **kwargs: Any) -> Application:
126128
self.instance.cancelled_date = timezone.now()
127129
self.instance.save()
128130
return self.instance
131+
132+
133+
class RejectAllApplicationOptionsSerializer(NestingModelSerializer):
134+
instance: Application
135+
136+
class Meta:
137+
model = Application
138+
fields = [
139+
"pk",
140+
]
141+
142+
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
143+
slots_exist = AllocatedTimeSlot.objects.filter(
144+
reservation_unit_option__application_section__application=self.instance,
145+
).exists()
146+
147+
if slots_exist:
148+
msg = "Application has allocated time slots and cannot be rejected."
149+
raise ValidationError(msg, code=error_codes.CANNOT_REJECT_APPLICATION_OPTIONS)
150+
151+
return data
152+
153+
def save(self, **kwargs: Any) -> Application:
154+
ReservationUnitOption.objects.filter(application_section__application=self.instance).update(rejected=True)
155+
return self.instance
156+
157+
158+
class RestoreAllApplicationOptionsSerializer(NestingModelSerializer):
159+
instance: Application
160+
161+
class Meta:
162+
model = Application
163+
fields = [
164+
"pk",
165+
]
166+
167+
def save(self, **kwargs: Any) -> Application:
168+
ReservationUnitOption.objects.filter(application_section__application=self.instance).update(rejected=False)
169+
return self.instance

tests/test_graphql_api/test_application/helpers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
"cancelApplication",
3535
"ApplicationCancelMutation",
3636
)
37+
REJECT_MUTATION = build_mutation(
38+
"rejectAllApplicationOptions",
39+
"RejectAllApplicationOptionsMutation",
40+
)
41+
RESTORE_MUTATION = build_mutation(
42+
"restoreAllApplicationOptions",
43+
"RestoreAllApplicationOptionsMutation",
44+
)
3745

3846

3947
def get_application_create_data(application_round: ApplicationRound, create_sections: bool = False) -> dict[str, Any]:
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import pytest
2+
3+
from applications.choices import Weekday
4+
from tests.factories import ApplicationFactory, UserFactory
5+
6+
from .helpers import REJECT_MUTATION
7+
8+
# Applied to all tests
9+
pytestmark = [
10+
pytest.mark.django_db,
11+
]
12+
13+
14+
def test_application__reject_all_options(graphql):
15+
application = ApplicationFactory.create_in_status_in_allocation()
16+
section = application.application_sections.first()
17+
option = section.reservation_unit_options.first()
18+
assert option.rejected is False
19+
20+
graphql.login_with_superuser()
21+
response = graphql(REJECT_MUTATION, input_data={"pk": application.pk})
22+
23+
assert response.has_errors is False, response
24+
25+
option.refresh_from_db()
26+
assert option.rejected is True
27+
28+
29+
def test_application__reject_all_options__has_allocations(graphql):
30+
application = ApplicationFactory.create_in_status_in_allocation(
31+
application_sections__reservation_unit_options__rejected=False,
32+
application_sections__reservation_unit_options__allocated_time_slots__day_of_the_week=Weekday.MONDAY,
33+
)
34+
section = application.application_sections.first()
35+
option = section.reservation_unit_options.first()
36+
assert option.rejected is False
37+
assert option.allocated_time_slots.exists()
38+
39+
graphql.login_with_superuser()
40+
response = graphql(REJECT_MUTATION, input_data={"pk": application.pk})
41+
42+
assert response.error_message() == "Mutation was unsuccessful.", response
43+
assert response.field_error_messages() == [
44+
"Application has allocated time slots and cannot be rejected.",
45+
]
46+
47+
48+
def test_application__reject_all_options__general_admin(graphql):
49+
application = ApplicationFactory.create_in_status_in_allocation()
50+
51+
admin = UserFactory.create_with_general_permissions(perms=["can_handle_applications"])
52+
graphql.force_login(admin)
53+
54+
response = graphql(REJECT_MUTATION, input_data={"pk": application.pk})
55+
56+
assert response.has_errors is False
57+
58+
59+
def test_application__reject_all_options__unit_admin(graphql):
60+
application = ApplicationFactory.create_in_status_in_allocation()
61+
62+
section = application.application_sections.first()
63+
unit = section.reservation_unit_options.first().reservation_unit.unit
64+
admin = UserFactory.create_with_unit_permissions(unit=unit, perms=["can_handle_applications"])
65+
graphql.force_login(admin)
66+
67+
response = graphql(REJECT_MUTATION, input_data={"pk": application.pk})
68+
69+
assert response.has_errors is False
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pytest
2+
3+
from tests.factories import ApplicationFactory, UserFactory
4+
5+
from .helpers import RESTORE_MUTATION
6+
7+
# Applied to all tests
8+
pytestmark = [
9+
pytest.mark.django_db,
10+
]
11+
12+
13+
def test_application__restore_all_options(graphql):
14+
application = ApplicationFactory.create_in_status_in_allocation(
15+
application_sections__reservation_unit_options__rejected=True
16+
)
17+
section = application.application_sections.first()
18+
option = section.reservation_unit_options.first()
19+
assert option.rejected is True
20+
21+
graphql.login_with_superuser()
22+
response = graphql(RESTORE_MUTATION, input_data={"pk": application.pk})
23+
24+
assert response.has_errors is False, response
25+
26+
option.refresh_from_db()
27+
assert option.rejected is False
28+
29+
30+
def test_application__restore_all_options__general_admin(graphql):
31+
application = ApplicationFactory.create_in_status_in_allocation()
32+
33+
admin = UserFactory.create_with_general_permissions(perms=["can_handle_applications"])
34+
graphql.force_login(admin)
35+
36+
response = graphql(RESTORE_MUTATION, input_data={"pk": application.pk})
37+
38+
assert response.has_errors is False
39+
40+
41+
def test_application__restore_all_options__unit_admin(graphql):
42+
application = ApplicationFactory.create_in_status_in_allocation()
43+
44+
section = application.application_sections.first()
45+
unit = section.reservation_unit_options.first().reservation_unit.unit
46+
admin = UserFactory.create_with_unit_permissions(unit=unit, perms=["can_handle_applications"])
47+
graphql.force_login(admin)
48+
49+
response = graphql(RESTORE_MUTATION, input_data={"pk": application.pk})
50+
51+
assert response.has_errors is False

0 commit comments

Comments
 (0)