aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site/apps')
-rw-r--r--pydis_site/apps/api/admin.py4
-rw-r--r--pydis_site/apps/api/migrations/0036_alter_nominations_api.py42
-rw-r--r--pydis_site/apps/api/models/bot/nomination.py14
-rw-r--r--pydis_site/apps/api/serializers.py16
-rw-r--r--pydis_site/apps/api/tests/test_nominations.py87
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py4
-rw-r--r--pydis_site/apps/api/viewsets/bot/nomination.py47
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)