diff options
Diffstat (limited to '')
| -rw-r--r-- | pydis_site/apps/api/admin.py | 2 | ||||
| -rw-r--r-- | pydis_site/apps/api/migrations/0049_offensivemessage.py | 25 | ||||
| -rw-r--r-- | pydis_site/apps/api/models/__init__.py | 1 | ||||
| -rw-r--r-- | pydis_site/apps/api/models/bot/__init__.py | 1 | ||||
| -rw-r--r-- | pydis_site/apps/api/models/bot/offensive_message.py | 48 | ||||
| -rw-r--r-- | pydis_site/apps/api/serializers.py | 11 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_models.py | 6 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_offensive_message.py | 155 | ||||
| -rw-r--r-- | pydis_site/apps/api/tests/test_validators.py | 12 | ||||
| -rw-r--r-- | pydis_site/apps/api/urls.py | 7 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/__init__.py | 1 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/__init__.py | 1 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/offensive_message.py | 61 | 
13 files changed, 330 insertions, 1 deletions
| diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 059f52eb..0333fefc 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -12,6 +12,7 @@ from .models import (      MessageDeletionContext,      Nomination,      OffTopicChannelName, +    OffensiveMessage,      Role,      Tag,      User @@ -60,6 +61,7 @@ admin.site.register(Infraction)  admin.site.register(LogEntry, LogEntryAdmin)  admin.site.register(MessageDeletionContext)  admin.site.register(Nomination) +admin.site.register(OffensiveMessage)  admin.site.register(OffTopicChannelName)  admin.site.register(Role)  admin.site.register(Tag) diff --git a/pydis_site/apps/api/migrations/0049_offensivemessage.py b/pydis_site/apps/api/migrations/0049_offensivemessage.py new file mode 100644 index 00000000..fe4a1961 --- /dev/null +++ b/pydis_site/apps/api/migrations/0049_offensivemessage.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.6 on 2019-11-07 18:08 + +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.offensive_message +import pydis_site.apps.api.models.utils + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0048_add_infractions_unique_constraints_active'), +    ] + +    operations = [ +        migrations.CreateModel( +            name='OffensiveMessage', +            fields=[ +                ('id', models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')])), +                ('channel_id', models.BigIntegerField(help_text='The channel ID that the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), +                ('delete_date', models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator])), +            ], +            bases=(pydis_site.apps.api.models.utils.ModelReprMixin, models.Model), +        ), +    ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index a4656bc3..450d18cd 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -7,6 +7,7 @@ from .bot import (      Message,      MessageDeletionContext,      Nomination, +    OffensiveMessage,      OffTopicChannelName,      Reminder,      Role, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 46219ea2..8ae47746 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -7,6 +7,7 @@ from .message import Message  from .message_deletion_context import MessageDeletionContext  from .nomination import Nomination  from .off_topic_channel_name import OffTopicChannelName +from .offensive_message import OffensiveMessage  from .reminder import Reminder  from .role import Role  from .tag import Tag diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py new file mode 100644 index 00000000..b466d9c2 --- /dev/null +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -0,0 +1,48 @@ +import datetime + +from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.utils import ModelReprMixin + + +def future_date_validator(date: datetime.date) -> None: +    """Raise ValidationError if the date isn't a future date.""" +    if date < datetime.datetime.now(datetime.timezone.utc): +        raise ValidationError("Date must be a future date") + + +class OffensiveMessage(ModelReprMixin, models.Model): +    """A message that triggered a filter and that will be deleted one week after it was sent.""" + +    id = models.BigIntegerField( +        primary_key=True, +        help_text="The message ID as taken from Discord.", +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Message IDs cannot be negative." +            ), +        ) +    ) +    channel_id = models.BigIntegerField( +        help_text=( +            "The channel ID that the message was " +            "sent in, taken from Discord." +        ), +        validators=( +            MinValueValidator( +                limit_value=0, +                message="Channel IDs cannot be negative." +            ), +        ) +    ) +    delete_date = models.DateTimeField( +        help_text="The date on which the message will be auto-deleted.", +        validators=(future_date_validator,) +    ) + +    def __str__(self): +        """Return some info on this message, for display purposes only.""" +        return f"Message {self.id}, will be deleted at {self.delete_date}" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 4e7cd863..52a82eac 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -8,6 +8,7 @@ from .models import (      DocumentationLink, Infraction,      LogEntry, MessageDeletionContext,      Nomination, OffTopicChannelName, +    OffensiveMessage,      Reminder, Role,      Tag, User  ) @@ -247,3 +248,13 @@ class NominationSerializer(ModelSerializer):          fields = (              'id', 'active', 'actor', 'reason', 'user',              'inserted_at', 'end_reason', 'ended_at') + + +class OffensiveMessageSerializer(ModelSerializer): +    """A class providing (de-)serialization of `OffensiveMessage` instances.""" + +    class Meta: +        """Metadata defined for the Django REST Framework.""" + +        model = OffensiveMessage +        fields = ('id', 'channel_id', 'delete_date') diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index b4a766d0..a97d3251 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -12,6 +12,7 @@ from ..models import (      ModelReprMixin,      Nomination,      OffTopicChannelName, +    OffensiveMessage,      Reminder,      Role,      Tag, @@ -69,6 +70,11 @@ class StringDunderMethodTests(SimpleTestCase):              DocumentationLink(                  'test', 'http://example.com', 'http://example.com'              ), +            OffensiveMessage( +                id=602951077675139072, +                channel_id=291284109232308226, +                delete_date=dt(3000, 1, 1) +            ),              OffTopicChannelName(name='bob-the-builders-playground'),              Role(                  id=5, name='test role', diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py new file mode 100644 index 00000000..d5896714 --- /dev/null +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -0,0 +1,155 @@ +import datetime + +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..models import OffensiveMessage + + +class CreationTests(APISubdomainTestCase): +    def test_accept_valid_data(self): +        url = reverse('bot:offensivemessage-list', host='api') +        delete_at = datetime.datetime.now() + datetime.timedelta(days=1) +        data = { +            'id': '602951077675139072', +            'channel_id': '291284109232308226', +            'delete_date': delete_at.isoformat()[:-1] +        } + +        aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc) + +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 201) + +        offensive_message = OffensiveMessage.objects.get(id=response.json()['id']) +        self.assertAlmostEqual( +            aware_delete_at, +            offensive_message.delete_date, +            delta=datetime.timedelta(seconds=1) +        ) +        self.assertEqual(data['id'], str(offensive_message.id)) +        self.assertEqual(data['channel_id'], str(offensive_message.channel_id)) + +    def test_returns_400_on_non_future_date(self): +        url = reverse('bot:offensivemessage-list', host='api') +        delete_at = datetime.datetime.now() - datetime.timedelta(days=1) +        data = { +            'id': '602951077675139072', +            'channel_id': '291284109232308226', +            'delete_date': delete_at.isoformat()[:-1] +        } +        response = self.client.post(url, data=data) +        self.assertEqual(response.status_code, 400) +        self.assertEqual(response.json(), { +            'delete_date': ['Date must be a future date'] +        }) + +    def test_returns_400_on_negative_id_or_channel_id(self): +        url = reverse('bot:offensivemessage-list', host='api') +        delete_at = datetime.datetime.now() + datetime.timedelta(days=1) +        data = { +            'id': '602951077675139072', +            'channel_id': '291284109232308226', +            'delete_date': delete_at.isoformat()[:-1] +        } +        cases = ( +            ('id', '-602951077675139072'), +            ('channel_id', '-291284109232308226') +        ) + +        for field, invalid_value in cases: +            with self.subTest(fied=field, invalid_value=invalid_value): +                test_data = data.copy() +                test_data.update({field: invalid_value}) + +                response = self.client.post(url, test_data) +                self.assertEqual(response.status_code, 400) +                self.assertEqual(response.json(), { +                    field: ['Ensure this value is greater than or equal to 0.'] +                }) + + +class ListTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls):  # noqa +        delete_at = datetime.datetime.now() + datetime.timedelta(days=1) +        aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc) + +        cls.messages = [ +            { +                'id': 602951077675139072, +                'channel_id': 91284109232308226, +            }, +            { +                'id': 645298201494159401, +                'channel_id': 592000283102674944 +            } +        ] + +        cls.of1 = OffensiveMessage.objects.create( +            **cls.messages[0], +            delete_date=aware_delete_at.isoformat() +        ) +        cls.of2 = OffensiveMessage.objects.create( +            **cls.messages[1], +            delete_date=aware_delete_at.isoformat() +        ) + +        # Expected API answer : +        cls.messages[0]['delete_date'] = delete_at.isoformat() + 'Z' +        cls.messages[1]['delete_date'] = delete_at.isoformat() + 'Z' + +    def test_get_data(self): +        url = reverse('bot:offensivemessage-list', host='api') + +        response = self.client.get(url) +        self.assertEqual(response.status_code, 200) + +        self.assertEqual(response.json(), self.messages) + + +class DeletionTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls):  # noqa +        delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) + +        cls.valid_offensive_message = OffensiveMessage.objects.create( +            id=602951077675139072, +            channel_id=291284109232308226, +            delete_date=delete_at.isoformat() +        ) + +    def test_delete_data(self): +        url = reverse( +            'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) +        ) + +        response = self.client.delete(url) +        self.assertEqual(response.status_code, 204) + +        self.assertFalse( +            OffensiveMessage.objects.filter(id=self.valid_offensive_message.id).exists() +        ) + + +class NotAllowedMethodsTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls):  # noqa +        delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) + +        cls.valid_offensive_message = OffensiveMessage.objects.create( +            id=602951077675139072, +            channel_id=291284109232308226, +            delete_date=delete_at.isoformat() +        ) + +    def test_returns_405_for_patch_and_put_requests(self): +        url = reverse( +            'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) +        ) +        not_allowed_methods = (self.client.patch, self.client.put) + +        for method in not_allowed_methods: +            with self.subTest(method=method): +                response = method(url, {}) +                self.assertEqual(response.status_code, 405) diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py index 4222f0c0..241af08c 100644 --- a/pydis_site/apps/api/tests/test_validators.py +++ b/pydis_site/apps/api/tests/test_validators.py @@ -1,7 +1,10 @@ +from datetime import datetime, timezone +  from django.core.exceptions import ValidationError  from django.test import TestCase  from ..models.bot.bot_setting import validate_bot_setting_name +from ..models.bot.offensive_message import future_date_validator  from ..models.bot.tag import validate_tag_embed @@ -245,3 +248,12 @@ class TagEmbedValidatorTests(TestCase):                  'name': "Bob"              }          }) + + +class OffensiveMessageValidatorsTests(TestCase): +    def test_accepts_future_date(self): +        future_date_validator(datetime(3000, 1, 1, tzinfo=timezone.utc)) + +    def test_rejects_non_future_date(self): +        with self.assertRaises(ValidationError): +            future_date_validator(datetime(1000, 1, 1, tzinfo=timezone.utc)) diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index ac6704c8..4a0281b4 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -6,7 +6,8 @@ from .viewsets import (      BotSettingViewSet, DeletedMessageViewSet,      DocumentationLinkViewSet, InfractionViewSet,      LogEntryViewSet, NominationViewSet, -    OffTopicChannelNameViewSet, ReminderViewSet, +    OffTopicChannelNameViewSet, +    OffensiveMessageViewSet, ReminderViewSet,      RoleViewSet, TagViewSet, UserViewSet  ) @@ -34,6 +35,10 @@ bot_router.register(      NominationViewSet  )  bot_router.register( +    'offensive-messages', +    OffensiveMessageViewSet +) +bot_router.register(      'off-topic-channel-names',      OffTopicChannelNameViewSet,      base_name='offtopicchannelname' diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index f9a186d9..3cf9f641 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -5,6 +5,7 @@ from .bot import (      DocumentationLinkViewSet,      InfractionViewSet,      NominationViewSet, +    OffensiveMessageViewSet,      OffTopicChannelNameViewSet,      ReminderViewSet,      RoleViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index f1851e32..b3e0fa4d 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -5,6 +5,7 @@ from .documentation_link import DocumentationLinkViewSet  from .infraction import InfractionViewSet  from .nomination import NominationViewSet  from .off_topic_channel_name import OffTopicChannelNameViewSet +from .offensive_message import OffensiveMessageViewSet  from .reminder import ReminderViewSet  from .role import RoleViewSet  from .tag import TagViewSet diff --git a/pydis_site/apps/api/viewsets/bot/offensive_message.py b/pydis_site/apps/api/viewsets/bot/offensive_message.py new file mode 100644 index 00000000..54cb3a38 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/offensive_message.py @@ -0,0 +1,61 @@ +from rest_framework.mixins import ( +    CreateModelMixin, +    DestroyModelMixin, +    ListModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot.offensive_message import OffensiveMessage +from pydis_site.apps.api.serializers import OffensiveMessageSerializer + + +class OffensiveMessageViewSet( +    CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet +): +    """ +    View providing CRUD access to offensive messages. + +    ## Routes +    ### GET /bot/offensive-messages +    Returns all offensive messages in the database. + +    #### Response format +    >>> [ +    ...     { +    ...         'id': '631953598091100200', +    ...         'channel_id': '291284109232308226', +    ...         'delete_date': '2019-11-01T21:51:15.545000Z' +    ...     }, +    ...     ... +    ... ] + +    #### Status codes +    - 200: returned on success + +    ### POST /bot/offensive-messages +    Create a new offensive message object. + +    #### Request body +    >>> { +    ...     'id': int, +    ...     'channel_id': int, +    ...     'delete_date': datetime.datetime  # ISO-8601-formatted date +    ... } + +    #### Status codes +    - 201: returned on success +    - 400: if the body format is invalid + +    ### DELETE /bot/offensive-messages/<id:int> +    Delete the offensive message object with the given `id`. + +    #### Status codes +    - 204: returned on success +    - 404: if a offensive message object with the given `id` does not exist + +    ## Authentication +    Requires an API token. +    """ + +    serializer_class = OffensiveMessageSerializer +    queryset = OffensiveMessage.objects.all() | 
