aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Mark <[email protected]>2019-12-14 21:11:50 -0800
committerGravatar GitHub <[email protected]>2019-12-14 21:11:50 -0800
commit8d79d8962cf221e6859a32daf2079161e2e680b1 (patch)
treeb97f1e90ca46aa3f2ed4351c5a36bd892bd49d0b
parentAdd 1em bottom margin to wiki markdown codeblocks. (diff)
parentRename route /bot/offensive-message to /bot/offensive-messages (diff)
Merge pull request #300 from python-discord/#222-offensive-msg-autodeletion
Create offensive message model for python-discord/bot#617
-rw-r--r--pydis_site/apps/api/admin.py2
-rw-r--r--pydis_site/apps/api/migrations/0049_offensivemessage.py25
-rw-r--r--pydis_site/apps/api/models/__init__.py1
-rw-r--r--pydis_site/apps/api/models/bot/__init__.py1
-rw-r--r--pydis_site/apps/api/models/bot/offensive_message.py48
-rw-r--r--pydis_site/apps/api/serializers.py11
-rw-r--r--pydis_site/apps/api/tests/test_models.py6
-rw-r--r--pydis_site/apps/api/tests/test_offensive_message.py155
-rw-r--r--pydis_site/apps/api/tests/test_validators.py12
-rw-r--r--pydis_site/apps/api/urls.py7
-rw-r--r--pydis_site/apps/api/viewsets/__init__.py1
-rw-r--r--pydis_site/apps/api/viewsets/bot/__init__.py1
-rw-r--r--pydis_site/apps/api/viewsets/bot/offensive_message.py61
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()