diff options
Diffstat (limited to 'pydis_site')
| -rw-r--r-- | pydis_site/apps/api/serializers.py | 34 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_nominations.py | 2 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_offensive_message.py | 8 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/infraction.py | 5 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/nomination.py | 5 | 
5 files changed, 40 insertions, 14 deletions
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 2186b02c..87fd6190 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -2,6 +2,7 @@  from datetime import timedelta  from typing import Any +from django.db import models  from django.db.models.query import QuerySet  from django.db.utils import IntegrityError  from rest_framework.exceptions import NotFound @@ -35,6 +36,30 @@ from .models import (      User  ) +class FrozenFieldsMixin: +    """ +    Serializer mixin that allows adding non-updateable fields to a serializer. + +    To use, inherit from the mixin and specify the fields that should only be +    written to on creation in the `frozen_fields` attribute of the `Meta` class +    in a serializer. + +    See also the DRF discussion for this feature at +    https://github.com/encode/django-rest-framework/discussions/8606, which may +    eventually provide an official way to implement this. +    """ + +    def update(self, instance: models.Model, validated_data: dict) -> models.Model: +        """Validate that no frozen fields were changed and update the instance.""" +        for field_name in getattr(self.Meta, 'frozen_fields', ()): +            if field_name in validated_data: +                raise ValidationError( +                    { +                        field_name: ["This field cannot be updated."] +                    } +                ) +        return super().update(instance, validated_data) +  class BotSettingSerializer(ModelSerializer):      """A class providing (de-)serialization of `BotSetting` instances.""" @@ -426,7 +451,7 @@ class FilterListSerializer(ModelSerializer):  #  endregion -class InfractionSerializer(ModelSerializer): +class InfractionSerializer(FrozenFieldsMixin, ModelSerializer):      """A class providing (de-)serialization of `Infraction` instances."""      class Meta: @@ -447,6 +472,7 @@ class InfractionSerializer(ModelSerializer):              'dm_sent',              'jump_url'          ) +        frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden')      def validate(self, attrs: dict) -> dict:          """Validate data constraints for the given data and abort if it is invalid.""" @@ -683,7 +709,7 @@ class NominationEntrySerializer(ModelSerializer):          fields = ('nomination', 'actor', 'reason', 'inserted_at') -class NominationSerializer(ModelSerializer): +class NominationSerializer(FrozenFieldsMixin, ModelSerializer):      """A class providing (de-)serialization of `Nomination` instances."""      entries = NominationEntrySerializer(many=True, read_only=True) @@ -703,9 +729,10 @@ class NominationSerializer(ModelSerializer):              'entries',              'thread_id'          ) +        frozen_fields = ('id', 'inserted_at', 'user', 'ended_at') -class OffensiveMessageSerializer(ModelSerializer): +class OffensiveMessageSerializer(FrozenFieldsMixin, ModelSerializer):      """A class providing (de-)serialization of `OffensiveMessage` instances."""      class Meta: @@ -713,3 +740,4 @@ class OffensiveMessageSerializer(ModelSerializer):          model = OffensiveMessage          fields = ('id', 'channel_id', 'delete_date') +        frozen_fields = ('id', 'channel_id') diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 7fe2f0a8..e4dfe36a 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -254,7 +254,7 @@ class NominationTests(AuthenticatedAPITestCase):      def test_returns_400_on_frozen_field_update(self):          url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          data = { -            'user': "Theo Katzman" +            'user': 1234          }          response = self.client.patch(url, data=data) diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py index d01231f1..2dc60bc3 100644 --- a/pydis_site/apps/api/tests/test_offensive_message.py +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -156,6 +156,14 @@ class UpdateOffensiveMessageTestCase(AuthenticatedAPITestCase):              delta=datetime.timedelta(seconds=1),          ) +    def test_updating_write_once_fields(self): +        """Fields such as the channel ID may not be updated.""" +        url = reverse('api:bot:offensivemessage-detail', args=(self.message.id,)) +        data = {'channel_id': self.message.channel_id + 1} +        response = self.client.patch(url, data=data) +        self.assertEqual(response.status_code, 400) +        self.assertEqual(response.json(), {'channel_id': ["This field cannot be updated."]}) +      def test_updating_nonexistent_message(self):          url = reverse('api:bot:offensivemessage-detail', args=(self.message.id + 1,))          data = {'delete_date': self.in_one_week} diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index 26cae3ad..09c05a74 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -157,14 +157,9 @@ class InfractionViewSet(      filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)      filterset_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type')      search_fields = ('$reason',) -    frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden')      def partial_update(self, request: HttpRequest, *_args, **_kwargs) -> Response:          """Method that handles the nuts and bolts of updating an Infraction.""" -        for field in request.data: -            if field in self.frozen_fields: -                raise ValidationError({field: ['This field cannot be updated.']}) -          instance = self.get_object()          serializer = self.get_serializer(instance, data=request.data, partial=True)          serializer.is_valid(raise_exception=True) diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 78687e0e..953513e0 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -173,7 +173,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge      queryset = Nomination.objects.all()      filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)      filterset_fields = ('user__id', 'active') -    frozen_fields = ('id', 'inserted_at', 'user', 'ended_at')      frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at', 'reviewed')      def create(self, request: HttpRequest, *args, **kwargs) -> Response: @@ -238,10 +237,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge          Called by the Django Rest Framework in response to the corresponding HTTP request.          """ -        for field in request.data: -            if field in self.frozen_fields: -                raise ValidationError({field: ['This field cannot be updated.']}) -          instance = self.get_object()          serializer = self.get_serializer(instance, data=request.data, partial=True)          serializer.is_valid(raise_exception=True)  |