diff options
-rw-r--r-- | pydis_site/apps/api/admin.py | 4 | ||||
-rw-r--r-- | pydis_site/apps/api/migrations/0036_alter_nominations_api.py | 42 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/nomination.py | 14 | ||||
-rw-r--r-- | pydis_site/apps/api/serializers.py | 16 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_nominations.py | 87 | ||||
-rw-r--r-- | pydis_site/apps/api/viewsets/bot/infraction.py | 4 | ||||
-rw-r--r-- | pydis_site/apps/api/viewsets/bot/nomination.py | 47 |
7 files changed, 176 insertions, 38 deletions
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 3ae7f3c5..1caa1d8b 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -3,7 +3,8 @@ from django.contrib import admin from .models import ( BotSetting, DeletedMessage, DocumentationLink, Infraction, - MessageDeletionContext, OffTopicChannelName, + MessageDeletionContext, Nomination, + OffTopicChannelName, Role, SnakeFact, SnakeIdiom, SnakeName, SpecialSnake, Tag, @@ -16,6 +17,7 @@ admin.site.register(DeletedMessage) admin.site.register(DocumentationLink) admin.site.register(Infraction) admin.site.register(MessageDeletionContext) +admin.site.register(Nomination) admin.site.register(OffTopicChannelName) admin.site.register(Role) admin.site.register(SnakeFact) diff --git a/pydis_site/apps/api/migrations/0036_alter_nominations_api.py b/pydis_site/apps/api/migrations/0036_alter_nominations_api.py new file mode 100644 index 00000000..f31be14c --- /dev/null +++ b/pydis_site/apps/api/migrations/0036_alter_nominations_api.py @@ -0,0 +1,42 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0035_create_table_log_entry'), + ] + + operations = [ + migrations.AlterField( + model_name='nomination', + name='user', + field=models.ForeignKey(help_text='The nominated user.', on_delete=django.db.models.deletion.CASCADE, related_name='nomination', to='api.User'), + ), + migrations.AlterField( + model_name='nomination', + name='author', + field=models.ForeignKey(help_text='The staff member that nominated this user.', on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', to='api.User'), + ), + migrations.RenameField( + model_name='nomination', + old_name='author', + new_name='actor', + ), + migrations.AddField( + model_name='nomination', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AddField( + model_name='nomination', + name='unnominate_reason', + field=models.TextField(default='', help_text='Why the nomination was ended.'), + ), + migrations.AddField( + model_name='nomination', + name='unwatched_at', + field=models.DateTimeField(help_text='When the nomination was ended.', null=True), + ), + ] diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 5ebb9759..227a40ec 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -11,7 +11,7 @@ class Nomination(ModelReprMixin, models.Model): default=True, help_text="Whether this nomination is still relevant." ) - author = models.ForeignKey( + actor = models.ForeignKey( User, on_delete=models.CASCADE, help_text="The staff member that nominated this user.", @@ -20,14 +20,22 @@ class Nomination(ModelReprMixin, models.Model): reason = models.TextField( help_text="Why this user was nominated." ) - user = models.OneToOneField( + user = models.ForeignKey( User, on_delete=models.CASCADE, help_text="The nominated user.", - primary_key=True, related_name='nomination' ) inserted_at = models.DateTimeField( auto_now_add=True, help_text="The creation date of this nomination." ) + unnominate_reason = models.TextField( + help_text="Why the nomination was ended.", + default="" + ) + unwatched_at = models.DateTimeField( + auto_now_add=False, + help_text="When the nomination was ended.", + null=True + ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 905a7f82..d58f1fa7 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -270,12 +270,24 @@ class UserSerializer(BulkSerializerMixin, ModelSerializer): class NominationSerializer(ModelSerializer): """A class providing (de-)serialization of `Nomination` instances.""" - author = PrimaryKeyRelatedField(queryset=User.objects.all()) + actor = PrimaryKeyRelatedField(queryset=User.objects.all()) user = PrimaryKeyRelatedField(queryset=User.objects.all()) class Meta: """Metadata defined for the Django REST Framework.""" model = Nomination - fields = ('active', 'author', 'reason', 'user', 'inserted_at') + fields = ( + 'id', 'active', 'actor', 'reason', 'user', + 'inserted_at', 'unnominate_reason', 'unwatched_at') depth = 1 + + def validate(self, attrs): + active = attrs.get("active") + + unnominate_reason = attrs.get("unnominate_reason") + if active and unnominate_reason: + raise ValidationError( + {'unnominate_reason': "An active nomination can't have an unnominate reason"} + ) + return attrs diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 1f03d1b0..99a3fe69 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -1,41 +1,72 @@ +from datetime import datetime as dt, timedelta, timezone + from django_hosts.resolvers import reverse from .base import APISubdomainTestCase from ..models import Nomination, User -class NominationTests(APISubdomainTestCase): +# class NominationTests(APISubdomainTestCase): +# @classmethod +# def setUpTestData(cls): # noqa +# cls.actor = User.objects.create( +# id=5152, +# name='Ro Bert', +# discriminator=256, +# avatar_hash=None +# ) +# cls.user = cls.actor + +# cls.nomination = Nomination.objects.create( +# actor=cls.actor, +# reason="he's good", +# user=cls.actor +# ) + +# def test_returns_400_on_attempt_to_update_frozen_field(self): +# url = reverse('bot:nomination-detail', args=(self.user.id,), host='api') +# response = self.client.put( +# url, +# data={'inserted_at': 'something bad'} +# ) +# self.assertEqual(response.status_code, 400) +# self.assertEqual(response.json(), { +# 'inserted_at': ['This field cannot be updated.'] +# }) + +# def test_returns_200_on_successful_update(self): +# url = reverse('bot:nomination-detail', args=(self.user.id,), host='api') +# response = self.client.patch( +# url, +# data={'reason': 'there are many like it, but this test is mine'} +# ) +# self.assertEqual(response.status_code, 200) + + +class CreationTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): # noqa - cls.author = User.objects.create( - id=5152, - name='Ro Bert', - discriminator=256, + cls.user = User.objects.create( + id=1234, + name='joe dart', + discriminator=1111, avatar_hash=None ) - cls.user = cls.author - cls.nomination = Nomination.objects.create( - author=cls.author, - reason="he's good", - user=cls.author - ) + def test_accepts_valid_data(self): + url = reverse('bot:nomination-list', host='api') + data = { + 'actor': self.user.id, + 'reason': 'Joe Dart on Fender Bass', + 'user': self.user.id, + } - def test_returns_400_on_attempt_to_update_frozen_field(self): - url = reverse('bot:nomination-detail', args=(self.user.id,), host='api') - response = self.client.put( - url, - data={'inserted_at': 'something bad'} - ) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'inserted_at': ['This field cannot be updated.'] - }) - - def test_returns_200_on_successful_update(self): - url = reverse('bot:nomination-detail', args=(self.user.id,), host='api') - response = self.client.patch( - url, - data={'reason': 'there are many like it, but this test is mine'} + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + + nomination = Nomination.objects.get(id=response.json()['id']) + self.assertAlmostEqual( + nomination.inserted_at, + dt.now(timezone.utc), + delta=timedelta(seconds=2) ) - self.assertEqual(response.status_code, 200) diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index 4be153e1..8a279bc4 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -1,7 +1,7 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.filters import SearchFilter +from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.mixins import ( CreateModelMixin, ListModelMixin, @@ -117,7 +117,7 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge serializer_class = InfractionSerializer queryset = Infraction.objects.all() - filter_backends = (DjangoFilterBackend, SearchFilter) + filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type') search_fields = ('$reason',) frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 62f5dd48..c3615dd9 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -1,4 +1,9 @@ +from django.utils import timezone +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import status +from rest_framework.decorators import action from rest_framework.exceptions import ValidationError +from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet @@ -10,8 +15,22 @@ class NominationViewSet(ModelViewSet): """View providing CRUD operations on helper nominations done through the bot.""" serializer_class = NominationSerializer - queryset = Nomination.objects.prefetch_related('author', 'user') - frozen_fields = ('author', 'inserted_at', 'user') + queryset = Nomination.objects.prefetch_related('actor', 'user') + filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) + filter_fields = ('user__id', 'actor__id', 'active') + frozen_fields = ('id', 'actor', 'inserted_at', 'user', 'unwatched_at', 'active') + frozen_on_create = ('unwatched_at', 'unnominate_reason') + + def create(self, request, *args, **kwargs): + for field in request.data: + if field in self.frozen_on_create: + raise ValidationError({field: ['This field cannot be updated.']}) + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def update(self, request, *args, **kwargs): """ @@ -30,3 +49,27 @@ class NominationViewSet(ModelViewSet): serializer.save() return Response(serializer.data) + + @action(detail=True, methods=['patch']) + def end_nomination(self, request, pk=None): + for field in request.data: + if field in self.frozen_fields: + raise ValidationError({field: ['This field cannot be updated.']}) + + if "unnominate_reason" not in request.data: + raise ValidationError( + {'unnominate_reason': ['This field is required when ending a nomination']} + ) + + instance = self.get_object() + if not instance.active: + raise ValidationError({'active': ['A nomination must be active to be ended.']}) + + instance.active = False + instance.unwatched_at = timezone.now() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + instance.save() + + return Response(serializer.data) |