aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api
diff options
context:
space:
mode:
authorGravatar jchristgit <[email protected]>2023-12-11 18:09:39 +0100
committerGravatar GitHub <[email protected]>2023-12-11 17:09:39 +0000
commite8f8161e41a4735897b3038e202107f5d55ec96e (patch)
tree51998b09ecd506df7562f8eaa564c7154f4bdddc /pydis_site/apps/api
parentImplement editing of offensive message records (#1165) (diff)
Unify frozen fields logic into serializer mixin (#1169)
Additionally, implement frozen fields on the offensive message serializer.
Diffstat (limited to 'pydis_site/apps/api')
-rw-r--r--pydis_site/apps/api/serializers.py34
-rw-r--r--pydis_site/apps/api/tests/test_nominations.py2
-rw-r--r--pydis_site/apps/api/tests/test_offensive_message.py8
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py5
-rw-r--r--pydis_site/apps/api/viewsets/bot/nomination.py5
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)