diff options
Diffstat (limited to 'pydis_site')
40 files changed, 375 insertions, 80 deletions
diff --git a/pydis_site/apps/api/migrations/0007_tag.py b/pydis_site/apps/api/migrations/0007_tag.py index c22715f9..b6d146fe 100644 --- a/pydis_site/apps/api/migrations/0007_tag.py +++ b/pydis_site/apps/api/migrations/0007_tag.py @@ -18,6 +18,6 @@ class Migration(migrations.Migration): ('title', models.CharField(help_text='The title of this tag, shown in searches and providing a quick overview over what this embed contains.', max_length=100, primary_key=True, serialize=False)), ('embed', django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0009_snakefact.py b/pydis_site/apps/api/migrations/0009_snakefact.py index 4fc63bc9..fd583846 100644 --- a/pydis_site/apps/api/migrations/0009_snakefact.py +++ b/pydis_site/apps/api/migrations/0009_snakefact.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration): fields=[ ('fact', models.CharField(help_text='A fact about snakes.', max_length=200, primary_key=True, serialize=False)), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0010_snakeidiom.py b/pydis_site/apps/api/migrations/0010_snakeidiom.py index be089cf4..7d06ce5f 100644 --- a/pydis_site/apps/api/migrations/0010_snakeidiom.py +++ b/pydis_site/apps/api/migrations/0010_snakeidiom.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration): fields=[ ('idiom', models.CharField(help_text='A snake idiom', max_length=140, primary_key=True, serialize=False)), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0012_specialsnake.py b/pydis_site/apps/api/migrations/0012_specialsnake.py index 77072526..ed0c1563 100644 --- a/pydis_site/apps/api/migrations/0012_specialsnake.py +++ b/pydis_site/apps/api/migrations/0012_specialsnake.py @@ -17,6 +17,6 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=140, primary_key=True, serialize=False)), ('info', models.TextField()), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py b/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py index dced1288..7e372d04 100644 --- a/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py +++ b/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py @@ -19,6 +19,6 @@ class Migration(migrations.Migration): ('creation', models.DateTimeField(help_text='When this deletion took place.')), ('actor', models.ForeignKey(help_text='The original actor causing this deletion. Could be the author of a manual clean command invocation, the bot when executing automatic actions, or nothing to indicate that the bulk deletion was not issued by us.', null=True, on_delete=django.db.models.deletion.CASCADE, to='api.User')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py index 4b028f0c..33746253 100644 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py @@ -25,6 +25,6 @@ class Migration(migrations.Migration): options={ 'abstract': False, }, - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0020_infraction.py b/pydis_site/apps/api/migrations/0020_infraction.py index 6bef6b77..96c71687 100644 --- a/pydis_site/apps/api/migrations/0020_infraction.py +++ b/pydis_site/apps/api/migrations/0020_infraction.py @@ -25,6 +25,6 @@ class Migration(migrations.Migration): ('actor', models.ForeignKey(help_text='The user which applied the infraction.', on_delete=django.db.models.deletion.CASCADE, related_name='infractions_given', to='api.User')), ('user', models.ForeignKey(help_text='The user to which the infraction was applied.', on_delete=django.db.models.deletion.CASCADE, related_name='infractions_received', to='api.User')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py index 0c02cb91..9589346d 100644 --- a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py +++ b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.4 on 2019-01-06 16:01 -import datetime from django.db import migrations, models +from django.utils import timezone class Migration(migrations.Migration): @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='infraction', name='inserted_at', - field=models.DateTimeField(default=datetime.datetime.utcnow, help_text='The date and time of the creation of this infraction.'), + field=models.DateTimeField(default=timezone.now(), help_text='The date and time of the creation of this infraction.'), ), ] diff --git a/pydis_site/apps/api/migrations/0030_reminder.py b/pydis_site/apps/api/migrations/0030_reminder.py index 8c42f6dc..e1f1afc3 100644 --- a/pydis_site/apps/api/migrations/0030_reminder.py +++ b/pydis_site/apps/api/migrations/0030_reminder.py @@ -22,6 +22,6 @@ class Migration(migrations.Migration): ('expiration', models.DateTimeField(help_text='When this reminder should be sent.')), ('author', models.ForeignKey(help_text='The creator of this reminder.', on_delete=django.db.models.deletion.CASCADE, to='api.User')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0031_nomination.py b/pydis_site/apps/api/migrations/0031_nomination.py index 75e69701..f39436c1 100644 --- a/pydis_site/apps/api/migrations/0031_nomination.py +++ b/pydis_site/apps/api/migrations/0031_nomination.py @@ -21,6 +21,6 @@ class Migration(migrations.Migration): ('inserted_at', models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination.')), ('author', 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')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0032_botsetting.py b/pydis_site/apps/api/migrations/0032_botsetting.py index 25186a2b..3304edef 100644 --- a/pydis_site/apps/api/migrations/0032_botsetting.py +++ b/pydis_site/apps/api/migrations/0032_botsetting.py @@ -18,6 +18,6 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=50, primary_key=True, serialize=False)), ('data', django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual settings of this setting.')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py index a8256a0e..c9a1ad19 100644 --- a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py +++ b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py @@ -24,6 +24,6 @@ class Migration(migrations.Migration): ('line', models.PositiveSmallIntegerField(help_text='The line at which the log line was emitted.')), ('message', models.TextField(help_text='The textual content of the log line.')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0049_offensivemessage.py b/pydis_site/apps/api/migrations/0049_offensivemessage.py index fe4a1961..f342cec3 100644 --- a/pydis_site/apps/api/migrations/0049_offensivemessage.py +++ b/pydis_site/apps/api/migrations/0049_offensivemessage.py @@ -3,7 +3,7 @@ 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 +import pydis_site.apps.api.models.mixins class Migration(migrations.Migration): @@ -20,6 +20,6 @@ class Migration(migrations.Migration): ('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), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py new file mode 100644 index 00000000..c450344b --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py @@ -0,0 +1,32 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='AllowDenyList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE_ID', 'Guild Invite Id'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('WORD_WATCHLIST', 'Word Watchlist')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='allowdenylist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_allow_deny_list'), + ) + ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 450d18cd..34973a8d 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa from .bot import ( + AllowDenyList, BotSetting, DocumentationLink, DeletedMessage, @@ -15,4 +16,3 @@ from .bot import ( User ) from .log_entry import LogEntry -from .utils import ModelReprMixin diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 8ae47746..1234b35a 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa +from .allow_deny_list import AllowDenyList from .bot_setting import BotSetting from .deleted_message import DeletedMessage from .documentation_link import DocumentationLink diff --git a/pydis_site/apps/api/models/bot/allow_deny_list.py b/pydis_site/apps/api/models/bot/allow_deny_list.py new file mode 100644 index 00000000..1eef47ba --- /dev/null +++ b/pydis_site/apps/api/models/bot/allow_deny_list.py @@ -0,0 +1,37 @@ +from django.db import models + +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin + + +class AllowDenyList(ModelTimestampMixin, ModelReprMixin, models.Model): + """An item that is either allowed or denied.""" + + AllowDenyListType = models.TextChoices( + 'AllowDenyListType', + 'GUILD_INVITE_ID ' + 'FILE_FORMAT ' + 'DOMAIN_NAME ' + 'WORD_WATCHLIST ' + ) + type = models.CharField( + max_length=50, + help_text="The type of allowlist this is on.", + choices=AllowDenyListType.choices, + ) + allowed = models.BooleanField( + help_text="Whether this item is on the allowlist or the denylist." + ) + content = models.TextField( + help_text="The data to add to the allow or denylist." + ) + + class Meta: + """Metaconfig for this model.""" + + # This constraint ensures only one allow or denylist with the + # same content can exist. This means that we cannot have both an allow + # and a deny for the same item, and we cannot have duplicates of the + # same item. + constraints = [ + models.UniqueConstraint(fields=['content', 'type'], name='unique_allow_deny_list'), + ] diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py index 8d48eac7..2a3944f8 100644 --- a/pydis_site/apps/api/models/bot/bot_setting.py +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -2,7 +2,7 @@ from django.contrib.postgres import fields as pgfields from django.core.exceptions import ValidationError from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin def validate_bot_setting_name(name: str) -> None: diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py index f844ae04..5a46460b 100644 --- a/pydis_site/apps/api/models/bot/documentation_link.py +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class DocumentationLink(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index f58e89a3..7660cbba 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -2,7 +2,7 @@ from django.db import models from django.utils import timezone from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Infraction(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 0b279580..78dcbf1d 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -7,7 +7,7 @@ from django.utils import timezone from pydis_site.apps.api.models.bot.tag import validate_tag_embed from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Message(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 44a0c8ae..04ae8d34 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,7 +1,7 @@ from django.db import models from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class MessageDeletionContext(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index cd9951aa..21e34e87 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -1,7 +1,7 @@ from django.db import models from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Nomination(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 29280c27..20e77b9f 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -1,7 +1,7 @@ from django.core.validators import RegexValidator from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class OffTopicChannelName(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py index b466d9c2..6c0e5ffb 100644 --- a/pydis_site/apps/api/models/bot/offensive_message.py +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -4,7 +4,7 @@ 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 +from pydis_site.apps.api.models.mixins import ModelReprMixin def future_date_validator(date: datetime.date) -> None: diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index d53fedb5..28722435 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -2,7 +2,7 @@ from django.core.validators import MinValueValidator from django.db import models from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Reminder(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index 58bbf8b4..721e4815 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -3,7 +3,7 @@ from __future__ import annotations from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Role(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py index 5d4cc393..5e53582f 100644 --- a/pydis_site/apps/api/models/bot/tag.py +++ b/pydis_site/apps/api/models/bot/tag.py @@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin def is_bool_validator(value: Any) -> None: diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index 0d8c574a..cd2d58b9 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -4,7 +4,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from pydis_site.apps.api.models.bot.role import Role -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin def _validate_existing_role(value: int) -> None: diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py index 488af48e..752cd2ca 100644 --- a/pydis_site/apps/api/models/log_entry.py +++ b/pydis_site/apps/api/models/log_entry.py @@ -1,7 +1,7 @@ from django.db import models from django.utils import timezone -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class LogEntry(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/mixins.py index 0540c4de..5d75b78b 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,5 +1,7 @@ from operator import itemgetter +from django.db import models + class ModelReprMixin: """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" @@ -15,3 +17,15 @@ class ModelReprMixin: if not attribute.startswith('_') ) return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index f2d5144c..d532dd69 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -4,13 +4,20 @@ from rest_framework.validators import UniqueTogetherValidator from rest_framework_bulk import BulkSerializerMixin from .models import ( - BotSetting, DeletedMessage, - DocumentationLink, Infraction, - LogEntry, MessageDeletionContext, - Nomination, OffTopicChannelName, + AllowDenyList, + BotSetting, + DeletedMessage, + DocumentationLink, + Infraction, + LogEntry, + MessageDeletionContext, + Nomination, + OffTopicChannelName, OffensiveMessage, - Reminder, Role, - Tag, User + Reminder, + Role, + Tag, + User, ) @@ -97,6 +104,16 @@ class DocumentationLinkSerializer(ModelSerializer): fields = ('package', 'base_url', 'inventory_url') +class AllowDenyListSerializer(ModelSerializer): + """A class providing (de-)serialization of `AllowDenyList` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = AllowDenyList + fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content') + + class InfractionSerializer(ModelSerializer): """A class providing (de-)serialization of `Infraction` instances.""" diff --git a/pydis_site/apps/api/tests/test_allowlists.py b/pydis_site/apps/api/tests/test_allowlists.py new file mode 100644 index 00000000..5aa50670 --- /dev/null +++ b/pydis_site/apps/api/tests/test_allowlists.py @@ -0,0 +1,104 @@ +from django_hosts.resolvers import reverse + +from pydis_site.apps.api.models import AllowDenyList +from pydis_site.apps.api.tests.base import APISubdomainTestCase + +URL = reverse('bot:allowdenylist-list', host='api') +JPEG_ALLOWLIST = { + "type": 'FILE_FORMAT', + "allowed": True, + "content": ".jpeg", +} +PNG_ALLOWLIST = { + "type": 'FILE_FORMAT', + "allowed": True, + "content": ".png", +} + + +class UnauthenticatedTests(APISubdomainTestCase): + def setUp(self): + super().setUp() + self.client.force_authenticate(user=None) + + def test_cannot_read_allowedlist_list(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 401) + + +class EmptyDatabaseTests(APISubdomainTestCase): + def test_returns_empty_object(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + +class FetchTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.jpeg_format = AllowDenyList.objects.create(**JPEG_ALLOWLIST) + cls.png_format = AllowDenyList.objects.create(**PNG_ALLOWLIST) + + def test_returns_name_in_list(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()[0]["content"], self.jpeg_format.content) + self.assertEqual(response.json()[1]["content"], self.png_format.content) + + def test_returns_single_item_by_id(self): + response = self.client.get(f'{URL}/{self.jpeg_format.id}') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("content"), self.jpeg_format.content) + + +class CreationTests(APISubdomainTestCase): + def test_returns_400_for_missing_params(self): + no_type_json = { + "allowed": True, + "content": ".jpeg" + } + no_allowed_json = { + "type": "FILE_FORMAT", + "content": ".jpeg" + } + no_content_json = { + "allowed": True, + "type": "FILE_FORMAT" + } + cases = [{}, no_type_json, no_allowed_json, no_content_json] + + for case in cases: + response = self.client.post(URL, data=case) + self.assertEqual(response.status_code, 400) + + def test_returns_201_for_successful_creation(self): + response = self.client.post(URL, data=JPEG_ALLOWLIST) + self.assertEqual(response.status_code, 201) + + +class DeletionTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.jpeg_format = AllowDenyList.objects.create(**JPEG_ALLOWLIST) + cls.png_format = AllowDenyList.objects.create(**PNG_ALLOWLIST) + + def test_deleting_unknown_id_returns_404(self): + response = self.client.delete(f"{URL}/200") + + self.assertEqual(response.status_code, 404) + + def test_deleting_known_id_returns_204(self): + response = self.client.delete(f"{URL}/{self.jpeg_format.id}") + + self.assertEqual(response.status_code, 204) + + def test_name_gets_deleted(self): + response = self.client.delete(f"{URL}/{self.png_format.id}") + self.assertEqual(response.status_code, 204) + + response = self.client.get(f"{URL}/{self.png_format.id}") + self.assertNotIn(self.png_format.content, response.json()) diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index b4754484..e0e347bb 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -3,13 +3,12 @@ from datetime import datetime as dt from django.test import SimpleTestCase from django.utils import timezone -from ..models import ( +from pydis_site.apps.api.models import ( DeletedMessage, DocumentationLink, Infraction, Message, MessageDeletionContext, - ModelReprMixin, Nomination, OffTopicChannelName, OffensiveMessage, @@ -18,6 +17,7 @@ from ..models import ( Tag, User ) +from pydis_site.apps.api.models.mixins import ModelReprMixin class SimpleClass(ModelReprMixin): diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 3bb5198e..b6ed2914 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -3,18 +3,29 @@ from rest_framework.routers import DefaultRouter from .views import HealthcheckView, RulesView from .viewsets import ( - BotSettingViewSet, DeletedMessageViewSet, - DocumentationLinkViewSet, InfractionViewSet, - LogEntryViewSet, NominationViewSet, + AllowDenyListViewSet, + BotSettingViewSet, + DeletedMessageViewSet, + DocumentationLinkViewSet, + InfractionViewSet, + LogEntryViewSet, + NominationViewSet, OffTopicChannelNameViewSet, - OffensiveMessageViewSet, ReminderViewSet, - RoleViewSet, TagViewSet, UserViewSet + OffensiveMessageViewSet, + ReminderViewSet, + RoleViewSet, + TagViewSet, + UserViewSet ) # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter bot_router = DefaultRouter(trailing_slash=False) bot_router.register( + 'allow_deny_lists', + AllowDenyListViewSet +) +bot_router.register( 'bot-settings', BotSettingViewSet ) diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index 3cf9f641..eb7d5098 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa from .bot import ( + AllowDenyListViewSet, BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index b3e0fa4d..11638dd8 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa +from .allow_deny_list import AllowDenyListViewSet from .bot_setting import BotSettingViewSet from .deleted_message import DeletedMessageViewSet from .documentation_link import DocumentationLinkViewSet diff --git a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py new file mode 100644 index 00000000..a2499d89 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py @@ -0,0 +1,72 @@ +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot.allow_deny_list import AllowDenyList +from pydis_site.apps.api.serializers import AllowDenyListSerializer + + +class AllowDenyListViewSet(ModelViewSet): + """ + View providing CRUD operations on items allowed or denied by our bot. + + ## Routes + ### GET /bot/allow_deny_lists + Returns all allow and denylist items in the database. + + #### Response format + >>> [ + ... { + ... 'id': "2309268224", + ... 'created_at': "01-01-2020 ...", + ... 'updated_at': "01-01-2020 ...", + ... 'type': "file_format", + ... 'allowed': 'true', + ... 'content': ".jpeg", + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/allow_deny_lists/<id:int> + Returns a specific AllowDenyList item from the database. + + #### Response format + >>> { + ... 'id': "2309268224", + ... 'created_at': "01-01-2020 ...", + ... 'updated_at': "01-01-2020 ...", + ... 'type': "file_format", + ... 'allowed': 'true', + ... 'content': ".jpeg", + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### POST /bot/allow_deny_lists + Adds a single AllowDenyList item to the database. + + #### Request body + >>> { + ... 'type': str, + ... 'allowed': bool, + ... 'content': str, + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/allow_deny_lists/<id:int> + Deletes the AllowDenyList item with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `id` does not exist + """ + + serializer_class = AllowDenyListSerializer + queryset = AllowDenyList.objects.all() diff --git a/pydis_site/tests/__init__.py b/pydis_site/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/tests/__init__.py diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py index e8db7b64..6f8338b4 100644 --- a/pydis_site/tests/test_utils_account.py +++ b/pydis_site/tests/test_utils_account.py @@ -5,7 +5,7 @@ from allauth.socialaccount.models import SocialAccount, SocialLogin from django.contrib.auth.models import User from django.contrib.messages.storage.base import BaseStorage from django.http import HttpRequest -from django.test import TestCase +from django.test import RequestFactory, TestCase from pydis_site.apps.api.models import Role, User as DiscordUser from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter @@ -13,28 +13,43 @@ from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter class AccountUtilsTests(TestCase): def setUp(self): + # Create the user self.django_user = User.objects.create(username="user") + # Create the roles + developers_role = Role.objects.create( + id=1, + name="Developers", + colour=0, + permissions=0, + position=1 + ) + everyone_role = Role.objects.create( + id=0, + name="@everyone", + colour=0, + permissions=0, + position=0 + ) + + # Create the social accounts self.discord_account = SocialAccount.objects.create( user=self.django_user, provider="discord", uid=0 ) - - self.discord_account_role = SocialAccount.objects.create( + self.discord_account_one_role = SocialAccount.objects.create( user=self.django_user, provider="discord", uid=1 ) - self.discord_account_two_roles = SocialAccount.objects.create( user=self.django_user, provider="discord", uid=2 ) - self.discord_account_not_present = SocialAccount.objects.create( user=self.django_user, provider="discord", uid=3 ) - self.github_account = SocialAccount.objects.create( user=self.django_user, provider="github", uid=0 ) + # Create DiscordUsers self.discord_user = DiscordUser.objects.create( id=0, name="user", @@ -44,35 +59,18 @@ class AccountUtilsTests(TestCase): self.discord_user_role = DiscordUser.objects.create( id=1, name="user present", - discriminator=0 + discriminator=0, + roles=[everyone_role.id] ) self.discord_user_two_roles = DiscordUser.objects.create( id=2, name="user with both roles", - discriminator=0 + discriminator=0, + roles=[everyone_role.id, developers_role.id] ) - everyone_role = Role.objects.create( - id=0, - name="@everyone", - colour=0, - permissions=0, - position=0 - ) - - self.discord_user_role.roles.append(everyone_role.id) - self.discord_user_two_roles.roles.append(everyone_role.id) - - developers_role = Role.objects.create( - id=1, - name="Developers", - colour=0, - permissions=0, - position=1 - ) - - self.discord_user_two_roles.roles.append(developers_role.id) + self.request_factory = RequestFactory() def test_account_adapter(self): """Test that our Allauth account adapter functions correctly.""" @@ -85,13 +83,13 @@ class AccountUtilsTests(TestCase): adapter = SocialAccountAdapter() discord_login = SocialLogin(account=self.discord_account) - discord_login_role = SocialLogin(account=self.discord_account_role) - discord_login_two_roles = SocialLogin(account=self.discord_account_two_roles) + discord_login_role = SocialLogin(account=self.discord_account_one_role) discord_login_not_present = SocialLogin(account=self.discord_account_not_present) + discord_login_two_roles = SocialLogin(account=self.discord_account_two_roles) github_login = SocialLogin(account=self.github_account) - messages_request = HttpRequest() + messages_request = self.request_factory.get("/") messages_request._messages = BaseStorage(messages_request) with patch("pydis_site.utils.account.reverse") as mock_reverse: @@ -108,12 +106,14 @@ class AccountUtilsTests(TestCase): with self.assertRaises(ImmediateHttpResponse): adapter.is_open_for_signup(messages_request, discord_login_not_present) + self.assertTrue( + adapter.is_open_for_signup(messages_request, discord_login_two_roles) + ) + self.assertEqual(len(messages_request._messages._queued_messages), 4) self.assertEqual(mock_redirect.call_count, 4) self.assertEqual(mock_reverse.call_count, 4) - self.assertTrue(adapter.is_open_for_signup(HttpRequest(), discord_login_two_roles)) - def test_social_account_adapter_populate(self): """Test that our Allauth social account adapter correctly handles data population.""" adapter = SocialAccountAdapter() @@ -122,13 +122,18 @@ class AccountUtilsTests(TestCase): account=self.discord_account, user=self.django_user ) - discord_login.account.extra_data["discriminator"] = "0000" - user = adapter.populate_user( - HttpRequest(), discord_login, + discord_user = adapter.populate_user( + self.request_factory.get("/"), discord_login, {"username": "user"} ) + self.assertEqual(discord_user.username, "user#0000") + self.assertEqual(discord_user.first_name, "user#0000") - self.assertEqual(user.username, "user#0000") - self.assertEqual(user.first_name, "user#0000") + discord_login.account.provider = "not_discord" + not_discord_user = adapter.populate_user( + self.request_factory.get("/"), discord_login, + {"username": "user"} + ) + self.assertEqual(not_discord_user.username, "user") |