From 3d0726e5f36f0d1efa9948e38fdc0198d21965b6 Mon Sep 17 00:00:00 2001 From: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:16:59 +0200 Subject: [PATCH] Early april update (#789) * Feat(kontres)/add image to bookable item (#785) * added optional image to bookable item model * added update method in serializer to handle new images * linting * remove update method for images * Feat(kontres)/add approved by (#786) * added approved by field * endpoint will now set approved by * serializer will return full user object in approved_by_detail * created test for approved by * migration * remove unnecessary code * removed write-only field in approved-by context * Create minutes for Codex (#787) * init * format * Feat(minute)/viewset (#788) * added richer reponse on post and put * added to admin panel * added filter for minute --------- Co-authored-by: Erik Skjellevik <98759397+eriskjel@users.noreply.github.com> --- CHANGELOG.md | 3 + app/content/admin/admin.py | 7 + app/content/enums.py | 5 + app/content/factories/__init__.py | 1 + app/content/factories/minute_factory.py | 14 ++ app/content/filters/__init__.py | 1 + app/content/filters/minute.py | 15 ++ app/content/migrations/0059_minute.py | 47 +++++++ app/content/migrations/0060_minute_tag.py | 22 +++ app/content/models/__init__.py | 1 + app/content/models/minute.py | 53 +++++++ app/content/serializers/__init__.py | 6 + app/content/serializers/minute.py | 45 ++++++ app/content/urls.py | 2 + app/content/views/__init__.py | 1 + app/content/views/minute.py | 66 +++++++++ ...okableitem_image_bookableitem_image_alt.py | 23 +++ .../0008_reservation_approved_by.py | 27 ++++ app/kontres/models/bookable_item.py | 4 +- app/kontres/models/reservation.py | 7 + .../serializer/reservation_seralizer.py | 2 + app/kontres/views/reservation.py | 12 +- app/tests/conftest.py | 12 ++ app/tests/content/test_minute_integration.py | 133 ++++++++++++++++++ .../kontres/test_reservation_integration.py | 19 +++ 25 files changed, 525 insertions(+), 3 deletions(-) create mode 100644 app/content/factories/minute_factory.py create mode 100644 app/content/filters/minute.py create mode 100644 app/content/migrations/0059_minute.py create mode 100644 app/content/migrations/0060_minute_tag.py create mode 100644 app/content/models/minute.py create mode 100644 app/content/serializers/minute.py create mode 100644 app/content/views/minute.py create mode 100644 app/kontres/migrations/0007_bookableitem_image_bookableitem_image_alt.py create mode 100644 app/kontres/migrations/0008_reservation_approved_by.py create mode 100644 app/tests/content/test_minute_integration.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b526f705c..17dd794be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ ## Neste versjon +## Versjon 2023.04.08 +- ✨ **Codex** Index brukere kan nå opprette dokumenter og møtereferater i Codex. + ## Versjon 2023.03.11 - 🦟 **Vipps** Brukere som kommer fra venteliste vil nå få en payment countdown startet, slik at de blir kastet ut hvis de ikke betaler. - ⚡ **Venteliste** Brukere vil nå se sin reelle ventelisteplass som tar hensyn til prioriteringer. diff --git a/app/content/admin/admin.py b/app/content/admin/admin.py index 8afc11f84..0ac3488fc 100644 --- a/app/content/admin/admin.py +++ b/app/content/admin/admin.py @@ -251,3 +251,10 @@ def object_link(self, obj): object_link.admin_order_field = "object_repr" object_link.short_description = "object" + + +@admin.register(models.Minute) +class MinuteAdmin(admin.ModelAdmin): + list_display = ("title", "author", "created_at", "updated_at") + search_fields = ("title", "content", "author__user_id") + list_filter = ("author",) diff --git a/app/content/enums.py b/app/content/enums.py index 3f0ced1d3..5d2332a87 100644 --- a/app/content/enums.py +++ b/app/content/enums.py @@ -18,3 +18,8 @@ class CategoryEnum(ChoiceEnum): KURS = "Kurs" ANNET = "Annet" FADDERUKA = "Fadderuka" + + +class MinuteTagEnum(models.TextChoices): + MINUTE = "Møtereferat" + DOCUMENT = "Dokument" diff --git a/app/content/factories/__init__.py b/app/content/factories/__init__.py index 5c27302ed..1a28282eb 100644 --- a/app/content/factories/__init__.py +++ b/app/content/factories/__init__.py @@ -11,3 +11,4 @@ from app.content.factories.priority_pool_factory import PriorityPoolFactory from app.content.factories.qr_code_factory import QRCodeFactory from app.content.factories.logentry_factory import LogEntryFactory +from app.content.factories.minute_factory import MinuteFactory diff --git a/app/content/factories/minute_factory.py b/app/content/factories/minute_factory.py new file mode 100644 index 000000000..84377af9d --- /dev/null +++ b/app/content/factories/minute_factory.py @@ -0,0 +1,14 @@ +import factory +from factory.django import DjangoModelFactory + +from app.content.factories.user_factory import UserFactory +from app.content.models.minute import Minute + + +class MinuteFactory(DjangoModelFactory): + class Meta: + model = Minute + + title = factory.Faker("sentence", nb_words=4) + content = factory.Faker("text") + author = factory.SubFactory(UserFactory) diff --git a/app/content/filters/__init__.py b/app/content/filters/__init__.py index ae6e76129..d442c2664 100644 --- a/app/content/filters/__init__.py +++ b/app/content/filters/__init__.py @@ -1,3 +1,4 @@ from app.content.filters.cheatsheet import CheatsheetFilter from app.content.filters.event import EventFilter from app.content.filters.user import UserFilter +from app.content.filters.minute import MinuteFilter diff --git a/app/content/filters/minute.py b/app/content/filters/minute.py new file mode 100644 index 000000000..db956ab22 --- /dev/null +++ b/app/content/filters/minute.py @@ -0,0 +1,15 @@ +from django_filters.rest_framework import FilterSet, OrderingFilter + +from app.content.models import Minute + + +class MinuteFilter(FilterSet): + """Filters minutes""" + + ordering = OrderingFilter( + fields=("created_at", "updated_at", "title", "author", "tag") + ) + + class Meta: + model = Minute + fields = ["author", "title", "tag"] diff --git a/app/content/migrations/0059_minute.py b/app/content/migrations/0059_minute.py new file mode 100644 index 000000000..977bc07cf --- /dev/null +++ b/app/content/migrations/0059_minute.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.5 on 2024-04-08 17:56 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0058_merge_20231217_2155"), + ] + + operations = [ + migrations.CreateModel( + name="Minute", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=200)), + ("content", models.TextField(blank=True, default="")), + ( + "author", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="meeting_minutes", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/app/content/migrations/0060_minute_tag.py b/app/content/migrations/0060_minute_tag.py new file mode 100644 index 000000000..b2d57d897 --- /dev/null +++ b/app/content/migrations/0060_minute_tag.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.5 on 2024-04-08 19:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0059_minute"), + ] + + operations = [ + migrations.AddField( + model_name="minute", + name="tag", + field=models.CharField( + choices=[("Møtereferat", "Minute"), ("Dokument", "Document")], + default="Møtereferat", + max_length=50, + ), + ), + ] diff --git a/app/content/models/__init__.py b/app/content/models/__init__.py index 44b062437..b96468577 100644 --- a/app/content/models/__init__.py +++ b/app/content/models/__init__.py @@ -14,3 +14,4 @@ get_strike_strike_size, ) from app.content.models.qr_code import QRCode +from app.content.models.minute import Minute diff --git a/app/content/models/minute.py b/app/content/models/minute.py new file mode 100644 index 000000000..2aa8d26ad --- /dev/null +++ b/app/content/models/minute.py @@ -0,0 +1,53 @@ +from django.db import models + +from app.common.enums import AdminGroup +from app.common.permissions import BasePermissionModel +from app.content.enums import MinuteTagEnum +from app.content.models.user import User +from app.util.models import BaseModel + + +class Minute(BaseModel, BasePermissionModel): + write_access = (AdminGroup.INDEX,) + read_access = (AdminGroup.INDEX,) + + title = models.CharField(max_length=200) + content = models.TextField(default="", blank=True) + tag = models.CharField( + max_length=50, choices=MinuteTagEnum.choices, default=MinuteTagEnum.MINUTE + ) + author = models.ForeignKey( + User, + blank=True, + null=True, + default=None, + on_delete=models.SET_NULL, + related_name="meeting_minutes", + ) + + @classmethod + def has_update_permission(cls, request): + return cls.has_write_permission(request) + + @classmethod + def has_destroy_permission(cls, request): + return cls.has_write_permission(request) + + @classmethod + def has_retrieve_permission(cls, request): + return cls.has_read_permission(request) + + def has_object_read_permission(self, request): + return self.has_read_permission(request) + + def has_object_update_permission(self, request): + return self.has_write_permission(request) + + def has_object_destroy_permission(self, request): + return self.has_write_permission(request) + + def has_object_retrieve_permission(self, request): + return self.has_read_permission(request) + + def __str__(self): + return self.title diff --git a/app/content/serializers/__init__.py b/app/content/serializers/__init__.py index c587de35c..53ae7b21e 100644 --- a/app/content/serializers/__init__.py +++ b/app/content/serializers/__init__.py @@ -31,3 +31,9 @@ DefaultUserSerializer, UserPermissionsSerializer, ) +from app.content.serializers.minute import ( + MinuteCreateSerializer, + MinuteSerializer, + MinuteUpdateSerializer, + MinuteListSerializer, +) diff --git a/app/content/serializers/minute.py b/app/content/serializers/minute.py new file mode 100644 index 000000000..f490195d8 --- /dev/null +++ b/app/content/serializers/minute.py @@ -0,0 +1,45 @@ +from rest_framework import serializers + +from app.content.models import Minute, User + + +class SimpleUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ("user_id", "first_name", "last_name", "image") + + +class MinuteCreateSerializer(serializers.ModelSerializer): + class Meta: + model = Minute + fields = ("title", "content", "tag") + + def create(self, validated_data): + author = self.context["request"].user + minute = Minute.objects.create(**validated_data, author=author) + return minute + + +class MinuteSerializer(serializers.ModelSerializer): + author = SimpleUserSerializer(read_only=True) + + class Meta: + model = Minute + fields = ("id", "title", "content", "author", "created_at", "updated_at", "tag") + + +class MinuteUpdateSerializer(serializers.ModelSerializer): + class Meta: + model = Minute + fields = ("id", "title", "content", "tag") + + def update(self, instance, validated_data): + return super().update(instance, validated_data) + + +class MinuteListSerializer(serializers.ModelSerializer): + author = SimpleUserSerializer(read_only=True) + + class Meta: + model = Minute + fields = ("id", "title", "author", "created_at", "updated_at", "tag") diff --git a/app/content/urls.py b/app/content/urls.py index a1f515085..1a783c067 100644 --- a/app/content/urls.py +++ b/app/content/urls.py @@ -6,6 +6,7 @@ CheatsheetViewSet, EventViewSet, LogEntryViewSet, + MinuteViewSet, NewsViewSet, PageViewSet, QRCodeViewSet, @@ -42,6 +43,7 @@ router.register("pages", PageViewSet) router.register("strikes", StrikeViewSet, basename="strikes") router.register("log-entries", LogEntryViewSet, basename="log-entries") +router.register("minutes", MinuteViewSet, basename="minutes") urlpatterns = [ re_path(r"", include(router.urls)), diff --git a/app/content/views/__init__.py b/app/content/views/__init__.py index 9a89d3abc..517d59b3c 100644 --- a/app/content/views/__init__.py +++ b/app/content/views/__init__.py @@ -13,3 +13,4 @@ from app.content.views.toddel import ToddelViewSet from app.content.views.qr_code import QRCodeViewSet from app.content.views.logentry import LogEntryViewSet +from app.content.views.minute import MinuteViewSet diff --git a/app/content/views/minute.py b/app/content/views/minute.py new file mode 100644 index 000000000..3cc14914b --- /dev/null +++ b/app/content/views/minute.py @@ -0,0 +1,66 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, status +from rest_framework.response import Response + +from app.common.pagination import BasePagination +from app.common.permissions import BasicViewPermission +from app.common.viewsets import BaseViewSet +from app.content.filters import MinuteFilter +from app.content.models import Minute +from app.content.serializers import ( + MinuteCreateSerializer, + MinuteListSerializer, + MinuteSerializer, + MinuteUpdateSerializer, +) + + +class MinuteViewSet(BaseViewSet): + serializer_class = MinuteSerializer + permission_classes = [BasicViewPermission] + pagination_class = BasePagination + queryset = Minute.objects.all() + + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + filterset_class = MinuteFilter + search_fields = [ + "title", + "author__first_name", + "author__last_name", + "author__user_id", + ] + + def get_serializer_class(self): + if hasattr(self, "action") and self.action == "list": + return MinuteListSerializer + return super().get_serializer_class() + + def create(self, request, *args, **kwargs): + data = request.data + serializer = MinuteCreateSerializer(data=data, context={"request": request}) + if serializer.is_valid(): + minute = super().perform_create(serializer) + serializer = MinuteSerializer(minute) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response( + {"detail": serializer.errors}, status=status.HTTP_400_BAD_REQUEST + ) + + def update(self, request, *args, **kwargs): + minute = self.get_object() + serializer = MinuteUpdateSerializer( + minute, data=request.data, context={"request": request} + ) + if serializer.is_valid(): + minute = super().perform_update(serializer) + serializer = MinuteSerializer(minute) + return Response(serializer.data, status=status.HTTP_200_OK) + + return Response( + {"detail": serializer.errors}, status=status.HTTP_400_BAD_REQUEST + ) + + def destroy(self, request, *args, **kwargs): + super().destroy(request, *args, **kwargs) + return Response({"detail": "The minute was deleted"}, status=status.HTTP_200_OK) diff --git a/app/kontres/migrations/0007_bookableitem_image_bookableitem_image_alt.py b/app/kontres/migrations/0007_bookableitem_image_bookableitem_image_alt.py new file mode 100644 index 000000000..52bfc06bc --- /dev/null +++ b/app/kontres/migrations/0007_bookableitem_image_bookableitem_image_alt.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.5 on 2024-03-22 12:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("kontres", "0006_rename_alcohol_agreement_reservation_serves_alcohol"), + ] + + operations = [ + migrations.AddField( + model_name="bookableitem", + name="image", + field=models.URLField(blank=True, max_length=600, null=True), + ), + migrations.AddField( + model_name="bookableitem", + name="image_alt", + field=models.CharField(blank=True, max_length=200, null=True), + ), + ] diff --git a/app/kontres/migrations/0008_reservation_approved_by.py b/app/kontres/migrations/0008_reservation_approved_by.py new file mode 100644 index 000000000..ce4954faa --- /dev/null +++ b/app/kontres/migrations/0008_reservation_approved_by.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.5 on 2024-04-06 09:39 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("kontres", "0007_bookableitem_image_bookableitem_image_alt"), + ] + + operations = [ + migrations.AddField( + model_name="reservation", + name="approved_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="approved_reservations", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/app/kontres/models/bookable_item.py b/app/kontres/models/bookable_item.py index 6ad0ece1c..b6ed96699 100644 --- a/app/kontres/models/bookable_item.py +++ b/app/kontres/models/bookable_item.py @@ -4,10 +4,10 @@ from app.common.enums import AdminGroup, Groups from app.common.permissions import BasePermissionModel, check_has_access -from app.util.models import BaseModel +from app.util.models import BaseModel, OptionalImage -class BookableItem(BaseModel, BasePermissionModel): +class BookableItem(BaseModel, BasePermissionModel, OptionalImage): write_access = AdminGroup.admin() read_access = [Groups.TIHLDE] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) diff --git a/app/kontres/models/reservation.py b/app/kontres/models/reservation.py index 50d159216..f73fbc0a1 100644 --- a/app/kontres/models/reservation.py +++ b/app/kontres/models/reservation.py @@ -53,6 +53,13 @@ class Reservation(BaseModel, BasePermissionModel): null=True, blank=True, ) + approved_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + related_name="approved_reservations", + null=True, + blank=True, + ) def __str__(self): return f"{self.state} - Reservation request by {self.author.first_name} {self.author.last_name} to book {self.bookable_item.name}. Created at {self.created_at}" diff --git a/app/kontres/serializer/reservation_seralizer.py b/app/kontres/serializer/reservation_seralizer.py index af52fc37d..98bf66ac6 100644 --- a/app/kontres/serializer/reservation_seralizer.py +++ b/app/kontres/serializer/reservation_seralizer.py @@ -36,6 +36,8 @@ class ReservationSerializer(serializers.ModelSerializer): ) sober_watch_detail = UserSerializer(source="sober_watch", read_only=True) + approved_by_detail = UserSerializer(source="approved_by", read_only=True) + class Meta: model = Reservation fields = "__all__" diff --git a/app/kontres/views/reservation.py b/app/kontres/views/reservation.py index cfd75f96f..d3ab071fe 100644 --- a/app/kontres/views/reservation.py +++ b/app/kontres/views/reservation.py @@ -58,7 +58,17 @@ def update(self, request, *args, **kwargs): reservation = self.get_object() serializer = self.get_serializer(reservation, data=request.data, partial=True) serializer.is_valid(raise_exception=True) - serializer.save() + + # Check if the state is being updated to CONFIRMED and set approved_by + if ( + "state" in serializer.validated_data + and serializer.validated_data["state"] == ReservationStateEnum.CONFIRMED + and reservation.state != ReservationStateEnum.CONFIRMED + ): + serializer.save(approved_by=request.user) + else: + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, *args, **kwargs): diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 02d22d5ed..3d864bb04 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -14,6 +14,7 @@ from app.content.factories import ( CheatsheetFactory, EventFactory, + MinuteFactory, NewsFactory, PageFactory, ParentPageFactory, @@ -124,6 +125,12 @@ def plask_member(member): return member +@pytest.fixture() +def index_member(member): + add_user_to_group_with_name(member, AdminGroup.INDEX) + return member + + @pytest.fixture() def member_client(member): return get_api_client(user=member) @@ -281,3 +288,8 @@ def event_with_priority_pool(priority_group): event = EventFactory(limit=1) PriorityPoolFactory(event=event, groups=(priority_group,)) return event + + +@pytest.fixture() +def minute(user): + return MinuteFactory(author=user) diff --git a/app/tests/content/test_minute_integration.py b/app/tests/content/test_minute_integration.py new file mode 100644 index 000000000..a0b925735 --- /dev/null +++ b/app/tests/content/test_minute_integration.py @@ -0,0 +1,133 @@ +from rest_framework import status + +import pytest + +from app.util.test_utils import get_api_client + +API_MINUTE_BASE_URL = "/minutes/" + + +def get_minute_detail_url(minute): + return f"{API_MINUTE_BASE_URL}{minute.id}/" + + +def get_minute_post_data(): + return {"title": "Test Minute", "content": "This is a test minute."} + + +def get_minute_put_data(): + return {"title": "Test Minute update", "content": "This is a test minute update."} + + +@pytest.mark.django_db +def test_create_minute_as_member(member): + """A member should be not able to create a minute""" + url = API_MINUTE_BASE_URL + client = get_api_client(user=member) + data = get_minute_post_data() + response = client.post(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_create_minute_as_index_member(index_member): + """An index member should be able to create a minute""" + url = API_MINUTE_BASE_URL + client = get_api_client(user=index_member) + data = get_minute_post_data() + response = client.post(url, data) + + assert response.status_code == status.HTTP_201_CREATED + + +@pytest.mark.django_db +def test_update_minute_as_member(member, minute): + """A member should not be able to update a minute""" + url = get_minute_detail_url(minute) + client = get_api_client(user=member) + data = get_minute_put_data() + response = client.put(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_update_minute_as_index_member(index_member, minute): + """An index member should be able to update a minute""" + minute.author = index_member + minute.save() + url = get_minute_detail_url(minute) + client = get_api_client(user=index_member) + data = get_minute_put_data() + response = client.put(url, data) + + assert response.status_code == status.HTTP_200_OK + assert response.data["title"] == data["title"] + + +@pytest.mark.django_db +def test_delete_minute_as_member(member, minute): + """A member should not be able to delete a minute""" + url = get_minute_detail_url(minute) + client = get_api_client(user=member) + response = client.delete(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_delete_minute_as_index_member(index_member, minute): + """An index member should be able to delete a minute""" + minute.author = index_member + minute.save() + url = get_minute_detail_url(minute) + client = get_api_client(user=index_member) + response = client.delete(url) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_list_minutes_as_member(member): + """A member should not be able to list minutes""" + url = API_MINUTE_BASE_URL + client = get_api_client(user=member) + response = client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_list_minutes_as_index_member(index_member, minute): + """An index member should be able to list minutes""" + minute.author = index_member + minute.save() + url = API_MINUTE_BASE_URL + client = get_api_client(user=index_member) + response = client.get(url) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_retrieve_minute_as_member(member, minute): + """A member should not be able to retrieve a minute""" + url = get_minute_detail_url(minute) + client = get_api_client(user=member) + response = client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_retrieve_minute_as_index_member(index_member, minute): + """An index member should be able to retrieve a minute""" + minute.author = index_member + minute.save() + url = get_minute_detail_url(minute) + client = get_api_client(user=index_member) + response = client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data["id"] == minute.id diff --git a/app/tests/kontres/test_reservation_integration.py b/app/tests/kontres/test_reservation_integration.py index 9f651bc16..7668c9968 100644 --- a/app/tests/kontres/test_reservation_integration.py +++ b/app/tests/kontres/test_reservation_integration.py @@ -237,6 +237,25 @@ def test_admin_can_edit_reservation_to_confirmed(reservation, admin_user): assert response.data["state"] == ReservationStateEnum.CONFIRMED +@pytest.mark.django_db +def test_admin_can_approve_reservation_and_approved_by_is_set(reservation, admin_user): + client = get_api_client(user=admin_user) + assert reservation.state == ReservationStateEnum.PENDING + assert reservation.approved_by is None + + response = client.put( + f"/kontres/reservations/{reservation.id}/", + {"state": "CONFIRMED"}, + format="json", + ) + + reservation.refresh_from_db() + + assert response.status_code == 200 + assert reservation.state == ReservationStateEnum.CONFIRMED + assert response.data["approved_by_detail"]["user_id"] == str(admin_user.user_id) + + @pytest.mark.django_db def test_admin_can_edit_reservation_to_cancelled(reservation, admin_user): client = get_api_client(user=admin_user)