diff --git a/app/kontres/migrations/0006_rename_alcohol_agreement_reservation_serves_alcohol.py b/app/kontres/migrations/0006_rename_alcohol_agreement_reservation_serves_alcohol.py new file mode 100644 index 00000000..fdc3de0a --- /dev/null +++ b/app/kontres/migrations/0006_rename_alcohol_agreement_reservation_serves_alcohol.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2024-03-18 15:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("kontres", "0005_bookableitem_allows_alcohol_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="reservation", + old_name="alcohol_agreement", + new_name="serves_alcohol", + ), + ] diff --git a/app/kontres/models/reservation.py b/app/kontres/models/reservation.py index 1ceb5af5..50d15921 100644 --- a/app/kontres/models/reservation.py +++ b/app/kontres/models/reservation.py @@ -45,7 +45,7 @@ class Reservation(BaseModel, BasePermissionModel): null=True, blank=True, ) - alcohol_agreement = models.BooleanField(default=False) + serves_alcohol = models.BooleanField(default=False) sober_watch = models.ForeignKey( User, on_delete=models.SET_NULL, diff --git a/app/kontres/serializer/reservation_seralizer.py b/app/kontres/serializer/reservation_seralizer.py index c178c1dc..af52fc37 100644 --- a/app/kontres/serializer/reservation_seralizer.py +++ b/app/kontres/serializer/reservation_seralizer.py @@ -31,6 +31,11 @@ class ReservationSerializer(serializers.ModelSerializer): ) author_detail = UserSerializer(source="author", read_only=True) + sober_watch = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), write_only=True, required=False + ) + sober_watch_detail = UserSerializer(source="sober_watch", read_only=True) + class Meta: model = Reservation fields = "__all__" @@ -56,23 +61,20 @@ def validate(self, data): return data def validate_alcohol(self, data): - if not data.get( - "alcohol_agreement", - self.instance.alcohol_agreement if self.instance else False, - ): - raise serializers.ValidationError( - "Du må godta at dere vil følge reglene for alkoholbruk." - ) - sober_watch = data.get( - "sober_watch", self.instance.sober_watch if self.instance else None - ) - if ( - not sober_watch - or not User.objects.filter(user_id=sober_watch.user_id).exists() + if data.get( + "serves_alcohol", + self.instance.serves_alcohol if self.instance else False, ): - raise serializers.ValidationError( - "Du må velge en edruvakt for reservasjonen." + sober_watch = data.get( + "sober_watch", self.instance.sober_watch if self.instance else None ) + if ( + not sober_watch + or not User.objects.filter(user_id=sober_watch.user_id).exists() + ): + raise serializers.ValidationError( + "Du må velge en edruvakt for reservasjonen." + ) def validate_group(self, value): user = self.context["request"].user @@ -104,7 +106,6 @@ def validate_state_change(self, data, user): "state": "Du har ikke rettigheter til å endre reservasjonsstatusen." } ) - pass def validate_time_and_overlapping(self, data): @@ -140,6 +141,7 @@ def validate_time_and_overlapping(self, data): bookable_item=bookable_item, end_time__gt=start_time, start_time__lt=end_time, + state=ReservationStateEnum.CONFIRMED, ) # Exclude the current instance if updating if self.instance: @@ -149,4 +151,3 @@ def validate_time_and_overlapping(self, data): raise serializers.ValidationError( "Det er en reservasjonsoverlapp for det gitte tidsrommet." ) - pass diff --git a/app/kontres/views/bookable_item.py b/app/kontres/views/bookable_item.py index 246937ca..cb27ed72 100644 --- a/app/kontres/views/bookable_item.py +++ b/app/kontres/views/bookable_item.py @@ -1,3 +1,6 @@ +from rest_framework import status +from rest_framework.response import Response + from app.common.permissions import BasicViewPermission from app.common.viewsets import BaseViewSet from app.kontres.models.bookable_item import BookableItem @@ -10,3 +13,9 @@ class BookableItemViewSet(BaseViewSet): queryset = BookableItem.objects.all() serializer_class = BookableItemSerializer permission_classes = [BasicViewPermission] + + def destroy(self, request, *args, **kwargs): + super().destroy(self, request, *args, **kwargs) + return Response( + {"detail": "Gjenstanden ble slettet."}, status=status.HTTP_204_NO_CONTENT + ) diff --git a/app/kontres/views/reservation.py b/app/kontres/views/reservation.py index 49e67e44..cfd75f96 100644 --- a/app/kontres/views/reservation.py +++ b/app/kontres/views/reservation.py @@ -63,4 +63,6 @@ def update(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): super().destroy(self, request, *args, **kwargs) - return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + {"detail": "Reservasjonen ble slettet."}, status=status.HTTP_204_NO_CONTENT + ) diff --git a/app/tests/kontres/test_bookable_item_integration.py b/app/tests/kontres/test_bookable_item_integration.py index ec1cbab9..e51a4bb5 100644 --- a/app/tests/kontres/test_bookable_item_integration.py +++ b/app/tests/kontres/test_bookable_item_integration.py @@ -19,6 +19,7 @@ def test_admin_can_delete_bookable_item(admin_user, bookable_item): f"/kontres/bookable_items/{bookable_item.id}/", format="json" ) assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.data["detail"] == "Gjenstanden ble slettet." @pytest.mark.django_db @@ -78,3 +79,14 @@ def test_admin_can_edit_bookable_item(admin_user, bookable_item): ) assert response.status_code == status.HTTP_200_OK assert response.data["name"] == "test" + + +@pytest.mark.django_db +def test_get_returns_empty_list_when_no_bookable_items(member): + client = get_api_client(user=member) + response = client.get("/kontres/bookable_items/", format="json") + + assert response.status_code == status.HTTP_200_OK + assert ( + response.data == [] + ), "Expected an empty list when there are no bookable items" diff --git a/app/tests/kontres/test_reservation_integration.py b/app/tests/kontres/test_reservation_integration.py index 5666ab2f..9f651bc1 100644 --- a/app/tests/kontres/test_reservation_integration.py +++ b/app/tests/kontres/test_reservation_integration.py @@ -37,7 +37,9 @@ def test_member_can_create_reservation(member, bookable_item): @pytest.mark.django_db -def test_member_can_create_reservation_with_alcohol_agreement(member, bookable_item): +def test_member_can_create_reservation_with_alcohol_and_sober_watch( + member, bookable_item +): client = get_api_client(user=member) bookable_item.allows_alcohol = True @@ -49,42 +51,15 @@ def test_member_can_create_reservation_with_alcohol_agreement(member, bookable_i "bookable_item": bookable_item.id, "start_time": "2030-10-10T10:00:00Z", "end_time": "2030-10-10T11:00:00Z", - "alcohol_agreement": True, + "serves_alcohol": True, "sober_watch": member.user_id, }, format="json", ) assert response.status_code == 201, response.data - assert response.data.get("alcohol_agreement") is True - assert response.data.get("sober_watch") == str(member.user_id) - - -@pytest.mark.django_db -def test_reservation_creation_fails_without_alcohol_agreement(member, bookable_item): - client = get_api_client(user=member) - - bookable_item.allows_alcohol = True - bookable_item.save() - - response = client.post( - "/kontres/reservations/", - { - "bookable_item": bookable_item.id, - "start_time": "2030-10-10T10:00:00Z", - "end_time": "2030-10-10T11:00:00Z", - # Notice the absence of "alcohol_agreement": True, - "sober_watch": member.user_id, - }, - format="json", - ) - - assert response.status_code == 400 - expected_error_message = "Du må godta at dere vil følge reglene for alkoholbruk." - actual_error_messages = response.data.get("non_field_errors", []) - assert any( - expected_error_message in error for error in actual_error_messages - ), f"Expected specific alcohol agreement validation error: {expected_error_message}" + assert response.data.get("serves_alcohol") is True + assert response.data["sober_watch_detail"]["user_id"] == str(member.user_id) @pytest.mark.django_db @@ -100,14 +75,15 @@ def test_reservation_creation_fails_without_sober_watch(member, bookable_item): "bookable_item": bookable_item.id, "start_time": "2030-10-10T10:00:00Z", "end_time": "2030-10-10T11:00:00Z", - "alcohol_agreement": True, + "serves_alcohol": True, # Notice the absence of "sober_watch", }, format="json", ) assert response.status_code == 400 - expected_error_message = "Du må velge en edruvakt for reservasjonen." + print(response.data) + expected_error_message = "Du må velge en edruvakt for reservasjonen." actual_error_messages = response.data.get("non_field_errors", []) assert any( expected_error_message in error for error in actual_error_messages @@ -186,6 +162,7 @@ def test_member_deleting_own_reservation(member, reservation): client = get_api_client(user=member) response = client.delete(f"/kontres/reservations/{reservation.id}/", format="json") assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.data["detail"] == "Reservasjonen ble slettet." @pytest.mark.django_db @@ -501,7 +478,9 @@ def test_unauthenticated_request_cannot_create_reservation(bookable_item): @pytest.mark.django_db -def test_creating_overlapping_reservation(member, bookable_item, admin_user): +def test_creating_overlapping_reservation_should_not_work_when_confirmed( + member, bookable_item, admin_user +): # Create a confirmed reservation using the ReservationFactory existing_confirmed_reservation = ReservationFactory( bookable_item=bookable_item, @@ -532,6 +511,105 @@ def test_creating_overlapping_reservation(member, bookable_item, admin_user): assert response.status_code == status.HTTP_400_BAD_REQUEST +@pytest.mark.django_db +def test_updating_to_overlapping_reservation_should_not_work_when_confirmed( + member, bookable_item, admin_user +): + # Create two initial reservations, one confirmed and one pending + confirmed_reservation = ReservationFactory( + bookable_item=bookable_item, + start_time=timezone.now() + timezone.timedelta(hours=1), + end_time=timezone.now() + timezone.timedelta(hours=2), + state=ReservationStateEnum.CONFIRMED, + ) + + pending_reservation = ReservationFactory( + bookable_item=bookable_item, + start_time=confirmed_reservation.start_time + + timezone.timedelta(minutes=30), # Overlapping time + end_time=confirmed_reservation.end_time + timezone.timedelta(minutes=30), + state=ReservationStateEnum.PENDING, + ) + + # Now attempt to update the pending reservation to confirmed, which should overlap with the confirmed_reservation + client = get_api_client(user=member) + response = client.put( + f"/kontres/reservations/{pending_reservation.id}/", + {"state": ReservationStateEnum.CONFIRMED}, + format="json", + ) + + # The system should not allow this, as it would overlap with another confirmed reservation + assert ( + response.status_code == status.HTTP_400_BAD_REQUEST + ), "Should not update reservation to confirmed due to overlap" + + +@pytest.mark.django_db +def test_creating_overlapping_reservation_should_work_when_cancelled( + member, bookable_item, admin_user +): + existing_confirmed_reservation = ReservationFactory( + bookable_item=bookable_item, + start_time=timezone.now() + timezone.timedelta(hours=1), + end_time=timezone.now() + timezone.timedelta(hours=2), + state=ReservationStateEnum.CANCELLED, # Set the reservation as declined + ) + + # Now attempt to create an overlapping reservation + client = get_api_client(user=member) + overlapping_start_time = ( + existing_confirmed_reservation.start_time + timezone.timedelta(minutes=30) + ) + response = client.post( + "/kontres/reservations/", + { + "author": member.user_id, + "bookable_item": bookable_item.id, + "start_time": overlapping_start_time, + "end_time": existing_confirmed_reservation.end_time + + timezone.timedelta(hours=1), + "state": ReservationStateEnum.PENDING, + }, + format="json", + ) + + assert response.status_code == status.HTTP_201_CREATED + + +@pytest.mark.django_db +def test_creating_overlapping_reservation_should_work_when_pending( + member, bookable_item, admin_user +): + # Create a confirmed reservation using the ReservationFactory + existing_confirmed_reservation = ReservationFactory( + bookable_item=bookable_item, + start_time=timezone.now() + timezone.timedelta(hours=1), + end_time=timezone.now() + timezone.timedelta(hours=2), + state=ReservationStateEnum.PENDING, # Set the reservation as declined + ) + + # Now attempt to create an overlapping reservation + client = get_api_client(user=member) + overlapping_start_time = ( + existing_confirmed_reservation.start_time + timezone.timedelta(minutes=30) + ) + response = client.post( + "/kontres/reservations/", + { + "author": member.user_id, + "bookable_item": bookable_item.id, + "start_time": overlapping_start_time, + "end_time": existing_confirmed_reservation.end_time + + timezone.timedelta(hours=1), + "state": ReservationStateEnum.PENDING, + }, + format="json", + ) + + assert response.status_code == status.HTTP_201_CREATED + + @pytest.mark.django_db def test_retrieve_specific_reservation_within_its_date_range(member, bookable_item): client = get_api_client(user=member)