diff options
Diffstat (limited to 'api')
62 files changed, 0 insertions, 4202 deletions
diff --git a/api/__init__.py b/api/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/api/__init__.py +++ /dev/null diff --git a/api/admin.py b/api/admin.py deleted file mode 100644 index 3ae7f3c5..00000000 --- a/api/admin.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.contrib import admin - -from .models import ( -    BotSetting, DeletedMessage, -    DocumentationLink, Infraction, -    MessageDeletionContext, OffTopicChannelName, -    Role, SnakeFact, -    SnakeIdiom, SnakeName, -    SpecialSnake, Tag, -    User -) - - -admin.site.register(BotSetting) -admin.site.register(DeletedMessage) -admin.site.register(DocumentationLink) -admin.site.register(Infraction) -admin.site.register(MessageDeletionContext) -admin.site.register(OffTopicChannelName) -admin.site.register(Role) -admin.site.register(SnakeFact) -admin.site.register(SnakeIdiom) -admin.site.register(SnakeName) -admin.site.register(SpecialSnake) -admin.site.register(Tag) -admin.site.register(User) diff --git a/api/apps.py b/api/apps.py deleted file mode 100644 index d87006dd..00000000 --- a/api/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ApiConfig(AppConfig): -    name = 'api' diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py deleted file mode 100644 index dca6d17f..00000000 --- a/api/migrations/0001_initial.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1 on 2018-08-15 17:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    initial = True - -    dependencies = [ -    ] - -    operations = [ -        migrations.CreateModel( -            name='SnakeName', -            fields=[ -                ('name', models.CharField(max_length=100, primary_key=True, serialize=False)), -                ('scientific', models.CharField(max_length=150)), -            ], -        ), -    ] diff --git a/api/migrations/0002_documentationlink.py b/api/migrations/0002_documentationlink.py deleted file mode 100644 index 5dee679a..00000000 --- a/api/migrations/0002_documentationlink.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1 on 2018-08-16 19:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0001_initial'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='DocumentationLink', -            fields=[ -                ('package', models.CharField(max_length=50, primary_key=True, serialize=False)), -                ('base_url', models.URLField()), -                ('inventory_url', models.URLField()), -            ], -        ), -    ] diff --git a/api/migrations/0003_offtopicchannelname.py b/api/migrations/0003_offtopicchannelname.py deleted file mode 100644 index 2f19bfd8..00000000 --- a/api/migrations/0003_offtopicchannelname.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.1 on 2018-08-31 22:21 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0002_documentationlink'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='OffTopicChannelName', -            fields=[ -                ('name', models.CharField(max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^[a-z0-9-]+$')])), -            ], -        ), -    ] diff --git a/api/migrations/0004_role.py b/api/migrations/0004_role.py deleted file mode 100644 index 0a6b6c43..00000000 --- a/api/migrations/0004_role.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1 on 2018-09-01 19:54 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0003_offtopicchannelname'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='Role', -            fields=[ -                ('id', models.BigIntegerField(help_text="The role's ID, taken from Discord.", primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')])), -                ('name', models.CharField(help_text="The role's name, taken from Discord.", max_length=100)), -                ('colour', models.IntegerField(help_text='The integer value of the colour of this role from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Colour hex cannot be negative.')])), -                ('permissions', models.IntegerField(help_text='The integer value of the permission bitset of this role from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role permissions cannot be negative.'), django.core.validators.MaxValueValidator(limit_value=8589934592, message='Role permission bitset exceeds value of having all permissions')])), -            ], -        ), -    ] diff --git a/api/migrations/0005_user.py b/api/migrations/0005_user.py deleted file mode 100644 index a771119c..00000000 --- a/api/migrations/0005_user.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 2.1 on 2018-09-01 20:02 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0004_role'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='Member', -            fields=[ -                ('id', models.BigIntegerField(help_text='The ID of this user, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='User IDs cannot be negative.')])), -                ('name', models.CharField(help_text='The username, taken from Discord.', max_length=32)), -                ('discriminator', models.PositiveSmallIntegerField(help_text='The discriminator of this user, taken from Discord.', validators=[django.core.validators.MaxValueValidator(limit_value=9999, message='Discriminators may not exceed `9999`.')])), -                ('avatar_hash', models.CharField(help_text="The user's avatar hash, taken from Discord. Null if the user does not have any custom avatar.", max_length=100, null=True)), -            ], -        ), -        migrations.AlterField( -            model_name='role', -            name='id', -            field=models.BigIntegerField(help_text='The role ID, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), -        ), -        migrations.AlterField( -            model_name='role', -            name='name', -            field=models.CharField(help_text='The role name, taken from Discord.', max_length=100), -        ), -        migrations.AddField( -            model_name='member', -            name='roles', -            field=models.ManyToManyField(help_text='Any roles this user has on our server.', to='api.Role'), -        ), -    ] diff --git a/api/migrations/0006_add_help_texts.py b/api/migrations/0006_add_help_texts.py deleted file mode 100644 index a57d2289..00000000 --- a/api/migrations/0006_add_help_texts.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 2.1.1 on 2018-09-21 20:26 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0005_user'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='documentationlink', -            name='base_url', -            field=models.URLField(help_text='The base URL from which documentation will be available for this project. Used to generate links to various symbols within this package.'), -        ), -        migrations.AlterField( -            model_name='documentationlink', -            name='inventory_url', -            field=models.URLField(help_text='The URL at which the Sphinx inventory is available for this package.'), -        ), -        migrations.AlterField( -            model_name='documentationlink', -            name='package', -            field=models.CharField(help_text='The Python package name that this documentation link belongs to.', max_length=50, primary_key=True, serialize=False), -        ), -        migrations.AlterField( -            model_name='offtopicchannelname', -            name='name', -            field=models.CharField(help_text='The actual channel name that will be used on our Discord server.', max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^[a-z0-9-]+$')]), -        ), -        migrations.AlterField( -            model_name='snakename', -            name='name', -            field=models.CharField(help_text="The regular name for this snake, e.g. 'Python'.", max_length=100, primary_key=True, serialize=False), -        ), -        migrations.AlterField( -            model_name='snakename', -            name='scientific', -            field=models.CharField(help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", max_length=150), -        ), -    ] diff --git a/api/migrations/0007_tag.py b/api/migrations/0007_tag.py deleted file mode 100644 index fdb3b9cc..00000000 --- a/api/migrations/0007_tag.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1.1 on 2018-09-21 22:05 - -import api.models -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0006_add_help_texts'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='Tag', -            fields=[ -                ('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=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0008_tag_embed_validator.py b/api/migrations/0008_tag_embed_validator.py deleted file mode 100644 index 4c580294..00000000 --- a/api/migrations/0008_tag_embed_validator.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.1.1 on 2018-09-23 10:07 - -import api.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0007_tag'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='tag', -            name='embed', -            field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[api.validators.validate_tag_embed]), -        ), -    ] diff --git a/api/migrations/0009_snakefact.py b/api/migrations/0009_snakefact.py deleted file mode 100644 index 966478a9..00000000 --- a/api/migrations/0009_snakefact.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-11 14:25 - -import api.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0008_tag_embed_validator'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='SnakeFact', -            fields=[ -                ('fact', models.CharField(help_text='A fact about snakes.', max_length=200, primary_key=True, serialize=False)), -            ], -            bases=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0010_snakeidiom.py b/api/migrations/0010_snakeidiom.py deleted file mode 100644 index 982a9466..00000000 --- a/api/migrations/0010_snakeidiom.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-19 16:27 - -import api.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0009_snakefact'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='SnakeIdiom', -            fields=[ -                ('idiom', models.CharField(help_text='A snake idiom', max_length=140, primary_key=True, serialize=False)), -            ], -            bases=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0011_auto_20181020_1904.py b/api/migrations/0011_auto_20181020_1904.py deleted file mode 100644 index bb5a6325..00000000 --- a/api/migrations/0011_auto_20181020_1904.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-20 19:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0010_snakeidiom'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='snakeidiom', -            name='idiom', -            field=models.CharField(help_text='A saying about a snake.', max_length=140, primary_key=True, serialize=False), -        ), -    ] diff --git a/api/migrations/0012_specialsnake.py b/api/migrations/0012_specialsnake.py deleted file mode 100644 index 9c6e8305..00000000 --- a/api/migrations/0012_specialsnake.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-22 09:53 - -import api.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0011_auto_20181020_1904'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='SpecialSnake', -            fields=[ -                ('name', models.CharField(max_length=140, primary_key=True, serialize=False)), -                ('info', models.TextField()), -            ], -            bases=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0013_specialsnake_image.py b/api/migrations/0013_specialsnake_image.py deleted file mode 100644 index a0d0d318..00000000 --- a/api/migrations/0013_specialsnake_image.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-23 11:51 - -import datetime -from django.db import migrations, models -from django.utils.timezone import utc - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0012_specialsnake'), -    ] - -    operations = [ -        migrations.AddField( -            model_name='specialsnake', -            name='image', -            field=models.URLField(default=datetime.datetime(2018, 10, 23, 11, 51, 23, 703868, tzinfo=utc)), -            preserve_default=False, -        ), -    ] diff --git a/api/migrations/0014_auto_20181025_1959.py b/api/migrations/0014_auto_20181025_1959.py deleted file mode 100644 index 3599d2cd..00000000 --- a/api/migrations/0014_auto_20181025_1959.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-25 19:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0013_specialsnake_image'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='specialsnake', -            name='info', -            field=models.TextField(help_text='Info about a special snake.'), -        ), -        migrations.AlterField( -            model_name='specialsnake', -            name='name', -            field=models.CharField(help_text='A special snake name.', max_length=140, primary_key=True, serialize=False), -        ), -    ] diff --git a/api/migrations/0015_auto_20181027_1617.py b/api/migrations/0015_auto_20181027_1617.py deleted file mode 100644 index 8973ff6d..00000000 --- a/api/migrations/0015_auto_20181027_1617.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-27 16:17 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0014_auto_20181025_1959'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='specialsnake', -            name='image', -            field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), size=None), -        ), -    ] diff --git a/api/migrations/0016_auto_20181027_1619.py b/api/migrations/0016_auto_20181027_1619.py deleted file mode 100644 index b8bdfb16..00000000 --- a/api/migrations/0016_auto_20181027_1619.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-27 16:19 - -from django.db import migrations - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0015_auto_20181027_1617'), -    ] - -    operations = [ -        migrations.RenameField( -            model_name='specialsnake', -            old_name='image', -            new_name='images', -        ), -    ] diff --git a/api/migrations/0017_auto_20181029_1921.py b/api/migrations/0017_auto_20181029_1921.py deleted file mode 100644 index 012bda61..00000000 --- a/api/migrations/0017_auto_20181029_1921.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-29 19:21 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0016_auto_20181027_1619'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='specialsnake', -            name='images', -            field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), help_text='Images displaying this special snake.', size=None), -        ), -    ] diff --git a/api/migrations/0018_messagedeletioncontext.py b/api/migrations/0018_messagedeletioncontext.py deleted file mode 100644 index 88cbab28..00000000 --- a/api/migrations/0018_messagedeletioncontext.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.1.1 on 2018-11-18 20:12 - -import api.models -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0017_auto_20181029_1921'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='MessageDeletionContext', -            fields=[ -                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), -                ('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=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0018_user_rename.py b/api/migrations/0018_user_rename.py deleted file mode 100644 index f88eb5bc..00000000 --- a/api/migrations/0018_user_rename.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-19 20:09 - -from django.db import migrations - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0017_auto_20181029_1921'), -    ] - -    operations = [ -        migrations.RenameModel( -            old_name='Member', -            new_name='User', -        ), -    ] diff --git a/api/migrations/0019_deletedmessage.py b/api/migrations/0019_deletedmessage.py deleted file mode 100644 index fbd94949..00000000 --- a/api/migrations/0019_deletedmessage.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 2.1.1 on 2018-11-18 20:26 - -import api.models -import api.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0018_messagedeletioncontext'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='DeletedMessage', -            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 this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), -                ('content', models.CharField(help_text='The content of this message, taken from Discord.', max_length=2000)), -                ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[api.validators.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)), -                ('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')), -                ('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')), -            ], -            options={ -                'abstract': False, -            }, -            bases=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0019_user_in_guild.py b/api/migrations/0019_user_in_guild.py deleted file mode 100644 index fda008c4..00000000 --- a/api/migrations/0019_user_in_guild.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-19 20:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0018_user_rename'), -    ] - -    operations = [ -        migrations.AddField( -            model_name='user', -            name='in_guild', -            field=models.BooleanField(default=True, help_text='Whether this user is in our server.'), -        ), -    ] diff --git a/api/migrations/0020_add_snake_field_validators.py b/api/migrations/0020_add_snake_field_validators.py deleted file mode 100644 index 3b625f9b..00000000 --- a/api/migrations/0020_add_snake_field_validators.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.1.2 on 2018-11-24 17:11 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0019_user_in_guild'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='snakename', -            name='name', -            field=models.CharField(help_text="The regular name for this snake, e.g. 'Python'.", max_length=100, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^([^0-9])+$')]), -        ), -        migrations.AlterField( -            model_name='snakename', -            name='scientific', -            field=models.CharField(help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", max_length=150, validators=[django.core.validators.RegexValidator(regex='^([^0-9])+$')]), -        ), -    ] diff --git a/api/migrations/0020_infraction.py b/api/migrations/0020_infraction.py deleted file mode 100644 index 2844a7f7..00000000 --- a/api/migrations/0020_infraction.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-19 22:02 - -import api.models -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0019_user_in_guild'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='Infraction', -            fields=[ -                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), -                ('inserted_at', models.DateTimeField(auto_now_add=True, help_text='The date and time of the creation of this infraction.')), -                ('expires_at', models.DateTimeField(help_text="The date and time of the expiration of this infraction. Null if the infraction is permanent or it can't expire.", null=True)), -                ('active', models.BooleanField(default=True, help_text='Whether the infraction is still active.')), -                ('type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('mute', 'Mute'), ('ban', 'Ban'), ('kick', 'Kick'), ('superstar', 'Superstar')], help_text='The type of the infraction.', max_length=9)), -                ('reason', models.TextField(help_text='The reason for the infraction.')), -                ('hidden', models.BooleanField(default=False, help_text='Whether the infraction is a shadow infraction.')), -                ('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=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0021_add_special_snake_validator.py b/api/migrations/0021_add_special_snake_validator.py deleted file mode 100644 index d41b96e5..00000000 --- a/api/migrations/0021_add_special_snake_validator.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.2 on 2018-11-25 14:59 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0020_add_snake_field_validators'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='specialsnake', -            name='name', -            field=models.CharField(help_text='A special snake name.', max_length=140, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^([^0-9])+$')]), -        ), -    ] diff --git a/api/migrations/0021_infraction_reason_null.py b/api/migrations/0021_infraction_reason_null.py deleted file mode 100644 index 6600f230..00000000 --- a/api/migrations/0021_infraction_reason_null.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-21 00:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0020_infraction'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='infraction', -            name='reason', -            field=models.TextField(help_text='The reason for the infraction.', null=True), -        ), -    ] diff --git a/api/migrations/0021_merge_20181125_1015.py b/api/migrations/0021_merge_20181125_1015.py deleted file mode 100644 index d8eaa510..00000000 --- a/api/migrations/0021_merge_20181125_1015.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.1.1 on 2018-11-25 10:15 - -from django.db import migrations - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0020_add_snake_field_validators'), -        ('api', '0019_deletedmessage'), -    ] - -    operations = [ -    ] diff --git a/api/migrations/0022_infraction_remove_note.py b/api/migrations/0022_infraction_remove_note.py deleted file mode 100644 index eba84610..00000000 --- a/api/migrations/0022_infraction_remove_note.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-21 21:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0021_infraction_reason_null'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='infraction', -            name='type', -            field=models.CharField(choices=[('warning', 'Warning'), ('mute', 'Mute'), ('ban', 'Ban'), ('kick', 'Kick'), ('superstar', 'Superstar')], help_text='The type of the infraction.', max_length=9), -        ), -    ] diff --git a/api/migrations/0023_merge_infractions_snake_validators.py b/api/migrations/0023_merge_infractions_snake_validators.py deleted file mode 100644 index 916f78f2..00000000 --- a/api/migrations/0023_merge_infractions_snake_validators.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-29 19:37 - -from django.db import migrations - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0022_infraction_remove_note'), -        ('api', '0021_add_special_snake_validator'), -    ] - -    operations = [ -    ] diff --git a/api/migrations/0024_add_note_infraction_type.py b/api/migrations/0024_add_note_infraction_type.py deleted file mode 100644 index 4adb53b8..00000000 --- a/api/migrations/0024_add_note_infraction_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.4 on 2019-01-05 14:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0023_merge_infractions_snake_validators'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='infraction', -            name='type', -            field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar')], help_text='The type of the infraction.', max_length=9), -        ), -    ] diff --git a/api/migrations/0025_allow_custom_inserted_at_infraction_field.py b/api/migrations/0025_allow_custom_inserted_at_infraction_field.py deleted file mode 100644 index 0c02cb91..00000000 --- a/api/migrations/0025_allow_custom_inserted_at_infraction_field.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.4 on 2019-01-06 16:01 - -import datetime -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0024_add_note_infraction_type'), -    ] - -    operations = [ -        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.'), -        ), -    ] diff --git a/api/migrations/0026_use_proper_default_for_infraction_insertion_date.py b/api/migrations/0026_use_proper_default_for_infraction_insertion_date.py deleted file mode 100644 index 56f3b2b8..00000000 --- a/api/migrations/0026_use_proper_default_for_infraction_insertion_date.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-09 19:50 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0025_allow_custom_inserted_at_infraction_field'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='infraction', -            name='inserted_at', -            field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time of the creation of this infraction.'), -        ), -    ] diff --git a/api/migrations/0027_merge_20190120_0852.py b/api/migrations/0027_merge_20190120_0852.py deleted file mode 100644 index 6fab4fd0..00000000 --- a/api/migrations/0027_merge_20190120_0852.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-20 08:52 - -from django.db import migrations - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0026_use_proper_default_for_infraction_insertion_date'), -        ('api', '0021_merge_20181125_1015'), -    ] - -    operations = [ -    ] diff --git a/api/migrations/0028_allow_message_content_blank.py b/api/migrations/0028_allow_message_content_blank.py deleted file mode 100644 index 6d57db27..00000000 --- a/api/migrations/0028_allow_message_content_blank.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-20 09:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0027_merge_20190120_0852'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='deletedmessage', -            name='content', -            field=models.CharField(blank=True, help_text='The content of this message, taken from Discord.', max_length=2000), -        ), -    ] diff --git a/api/migrations/0029_add_infraction_type_watch.py b/api/migrations/0029_add_infraction_type_watch.py deleted file mode 100644 index c6f88a11..00000000 --- a/api/migrations/0029_add_infraction_type_watch.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-20 11:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0028_allow_message_content_blank'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='infraction', -            name='type', -            field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar')], help_text='The type of the infraction.', max_length=9), -        ), -    ] diff --git a/api/migrations/0030_reminder.py b/api/migrations/0030_reminder.py deleted file mode 100644 index f0c14ccb..00000000 --- a/api/migrations/0030_reminder.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-22 22:17 - -import api.models -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0029_add_infraction_type_watch'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='Reminder', -            fields=[ -                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), -                ('active', models.BooleanField(default=True, help_text='Whether this reminder is still active. If not, it has been sent out to the user.')), -                ('channel_id', models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), -                ('content', models.CharField(help_text='The content that the user wants to be reminded of.', max_length=1500)), -                ('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=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0031_nomination.py b/api/migrations/0031_nomination.py deleted file mode 100644 index b739d5cb..00000000 --- a/api/migrations/0031_nomination.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-27 11:01 - -import api.models -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0030_reminder'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='Nomination', -            fields=[ -                ('active', models.BooleanField(default=True, help_text='Whether this nomination is still relevant.')), -                ('reason', models.TextField(help_text='Why this user was nominated.')), -                ('user', models.OneToOneField(help_text='The nominated user.', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='nomination', serialize=False, to='api.User')), -                ('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=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0032_botsetting.py b/api/migrations/0032_botsetting.py deleted file mode 100644 index 4ad1323e..00000000 --- a/api/migrations/0032_botsetting.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1.5 on 2019-02-07 19:03 - -import api.models -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0031_nomination'), -    ] - -    operations = [ -        migrations.CreateModel( -            name='BotSetting', -            fields=[ -                ('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=(api.models.ModelReprMixin, models.Model), -        ), -    ] diff --git a/api/migrations/0033_create_defcon_settings.py b/api/migrations/0033_create_defcon_settings.py deleted file mode 100644 index 830f3fb0..00000000 --- a/api/migrations/0033_create_defcon_settings.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.1.5 on 2019-02-18 19:30 - -from django.db import migrations - - -def up(apps, schema_editor): -    BotSetting = apps.get_model('api', 'BotSetting') -    setting = BotSetting( -        name='defcon', -        data={ -            'enabled': False, -            'days': 0 -        } -    ).save() - - -def down(apps, schema_editor):  # pragma: no cover - not necessary to test -    BotSetting = apps.get_model('api', 'BotSetting') -    BotSetting.get(name='defcon').delete() - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0032_botsetting'), -    ] - -    operations = [ -        migrations.RunPython(up, down) -    ] diff --git a/api/migrations/0034_add_botsetting_name_validator.py b/api/migrations/0034_add_botsetting_name_validator.py deleted file mode 100644 index c5681ef3..00000000 --- a/api/migrations/0034_add_botsetting_name_validator.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.5 on 2019-02-18 19:41 - -import api.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - -    dependencies = [ -        ('api', '0033_create_defcon_settings'), -    ] - -    operations = [ -        migrations.AlterField( -            model_name='botsetting', -            name='name', -            field=models.CharField(max_length=50, primary_key=True, serialize=False, validators=[api.validators.validate_bot_setting_name]), -        ), -    ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/api/migrations/__init__.py +++ /dev/null diff --git a/api/models.py b/api/models.py deleted file mode 100644 index 86c99f86..00000000 --- a/api/models.py +++ /dev/null @@ -1,452 +0,0 @@ -from operator import itemgetter - -from django.contrib.postgres import fields as pgfields -from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator -from django.db import models -from django.utils import timezone - -from .validators import validate_bot_setting_name, validate_tag_embed - - -class ModelReprMixin: -    """ -    Adds a `__repr__` method to the model subclassing this -    mixin which will display the model's class name along -    with all parameters used to construct the object. -    """ - -    def __repr__(self): -        attributes = ' '.join( -            f'{attribute}={value!r}' -            for attribute, value in sorted( -                self.__dict__.items(), -                key=itemgetter(0) -            ) -            if not attribute.startswith('_') -        ) -        return f'<{self.__class__.__name__}({attributes})>' - - -class BotSetting(ModelReprMixin, models.Model): -    """A configuration entry for the bot.""" - -    name = models.CharField( -        primary_key=True, -        max_length=50, -        validators=(validate_bot_setting_name,) -    ) -    data = pgfields.JSONField( -        help_text="The actual settings of this setting." -    ) - - -class DocumentationLink(ModelReprMixin, models.Model): -    """A documentation link used by the `!docs` command of the bot.""" - -    package = models.CharField( -        primary_key=True, -        max_length=50, -        help_text="The Python package name that this documentation link belongs to." -    ) -    base_url = models.URLField( -        help_text=( -            "The base URL from which documentation will be available for this project. " -            "Used to generate links to various symbols within this package." -        ) -    ) -    inventory_url = models.URLField( -        help_text="The URL at which the Sphinx inventory is available for this package." -    ) - -    def __str__(self): -        return f"{self.package} - {self.base_url}" - - -class OffTopicChannelName(ModelReprMixin, models.Model): -    name = models.CharField( -        primary_key=True, -        max_length=96, -        validators=(RegexValidator(regex=r'^[a-z0-9-]+$'),), -        help_text="The actual channel name that will be used on our Discord server." -    ) - -    def __str__(self): -        return self.name - - -class Role(ModelReprMixin, models.Model): -    """A role on our Discord server.""" - -    id = models.BigIntegerField(  # noqa -        primary_key=True, -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Role IDs cannot be negative." -            ), -        ), -        help_text="The role ID, taken from Discord." -    ) -    name = models.CharField( -        max_length=100, -        help_text="The role name, taken from Discord." -    ) -    colour = models.IntegerField( -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Colour hex cannot be negative." -            ), -        ), -        help_text="The integer value of the colour of this role from Discord." -    ) -    permissions = models.IntegerField( -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Role permissions cannot be negative." -            ), -            MaxValueValidator( -                limit_value=2 << 32, -                message="Role permission bitset exceeds value of having all permissions" -            ) -        ), -        help_text="The integer value of the permission bitset of this role from Discord." -    ) - -    def __str__(self): -        return self.name - - -class SnakeFact(ModelReprMixin, models.Model): -    """A snake fact used by the bot's snake cog.""" - -    fact = models.CharField( -        primary_key=True, -        max_length=200, -        help_text="A fact about snakes." -    ) - -    def __str__(self): -        return self.fact - - -class SnakeIdiom(ModelReprMixin, models.Model): -    """A snake idiom used by the snake cog.""" - -    idiom = models.CharField( -        primary_key=True, -        max_length=140, -        help_text="A saying about a snake." -    ) - -    def __str__(self): -        return self.idiom - - -class SnakeName(ModelReprMixin, models.Model): -    """A snake name used by the bot's snake cog.""" - -    name = models.CharField( -        primary_key=True, -        max_length=100, -        help_text="The regular name for this snake, e.g. 'Python'.", -        validators=[RegexValidator(regex=r'^([^0-9])+$')] -    ) -    scientific = models.CharField( -        max_length=150, -        help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", -        validators=[RegexValidator(regex=r'^([^0-9])+$')] -    ) - -    def __str__(self): -        return f"{self.name} ({self.scientific})" - - -class SpecialSnake(ModelReprMixin, models.Model): -    """A special snake's name, info and image from our database used by the bot's snake cog.""" - -    name = models.CharField( -        max_length=140, -        primary_key=True, -        help_text='A special snake name.', -        validators=[RegexValidator(regex=r'^([^0-9])+$')] -    ) -    info = models.TextField( -        help_text='Info about a special snake.' -    ) -    images = pgfields.ArrayField( -        models.URLField(), -        help_text='Images displaying this special snake.' -    ) - -    def __str__(self): -        return self.name - - -class Tag(ModelReprMixin, models.Model): -    """A tag providing (hopefully) useful information.""" - -    title = models.CharField( -        max_length=100, -        help_text=( -            "The title of this tag, shown in searches and providing " -            "a quick overview over what this embed contains." -        ), -        primary_key=True -    ) -    embed = pgfields.JSONField( -        help_text="The actual embed shown by this tag.", -        validators=(validate_tag_embed,) -    ) - -    def __str__(self): -        return self.title - - -class User(ModelReprMixin, models.Model): -    """A Discord user.""" - -    id = models.BigIntegerField(  # noqa -        primary_key=True, -        validators=( -            MinValueValidator( -                limit_value=0, -                message="User IDs cannot be negative." -            ), -        ), -        help_text="The ID of this user, taken from Discord." -    ) -    name = models.CharField( -        max_length=32, -        help_text="The username, taken from Discord." -    ) -    discriminator = models.PositiveSmallIntegerField( -        validators=( -            MaxValueValidator( -                limit_value=9999, -                message="Discriminators may not exceed `9999`." -            ), -        ), -        help_text="The discriminator of this user, taken from Discord." -    ) -    avatar_hash = models.CharField( -        max_length=100, -        help_text=( -            "The user's avatar hash, taken from Discord. " -            "Null if the user does not have any custom avatar." -        ), -        null=True -    ) -    roles = models.ManyToManyField( -        Role, -        help_text="Any roles this user has on our server." -    ) -    in_guild = models.BooleanField( -        default=True, -        help_text="Whether this user is in our server." -    ) - -    def __str__(self): -        return f"{self.name}#{self.discriminator}" - - -class Message(ModelReprMixin, models.Model): -    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." -            ), -        ) -    ) -    author = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        help_text="The author of this message." -    ) -    channel_id = models.BigIntegerField( -        help_text=( -            "The channel ID that this message was " -            "sent in, taken from Discord." -        ), -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Channel IDs cannot be negative." -            ), -        ) -    ) -    content = models.CharField( -        max_length=2_000, -        help_text="The content of this message, taken from Discord.", -        blank=True -    ) -    embeds = pgfields.ArrayField( -        pgfields.JSONField( -            validators=(validate_tag_embed,) -        ), -        help_text="Embeds attached to this message." -    ) - -    class Meta: -        abstract = True - - -class MessageDeletionContext(ModelReprMixin, models.Model): -    actor = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        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 -    ) -    creation = models.DateTimeField( -        # Consider whether we want to add a validator here that ensures -        # the deletion context does not take place in the future. -        help_text="When this deletion took place." -    ) - - -class DeletedMessage(Message): -    deletion_context = models.ForeignKey( -        MessageDeletionContext, -        help_text="The deletion context this message is part of.", -        on_delete=models.CASCADE -    ) - - -class Infraction(ModelReprMixin, models.Model): -    """An infraction for a Discord user.""" - -    TYPE_CHOICES = ( -        ("note", "Note"), -        ("warning", "Warning"), -        ("watch", "Watch"), -        ("mute", "Mute"), -        ("kick", "Kick"), -        ("ban", "Ban"), -        ("superstar", "Superstar") -    ) -    inserted_at = models.DateTimeField( -        default=timezone.now, -        help_text="The date and time of the creation of this infraction." -    ) -    expires_at = models.DateTimeField( -        null=True, -        help_text=( -            "The date and time of the expiration of this infraction. " -            "Null if the infraction is permanent or it can't expire." -        ) -    ) -    active = models.BooleanField( -        default=True, -        help_text="Whether the infraction is still active." -    ) -    user = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        related_name='infractions_received', -        help_text="The user to which the infraction was applied." -    ) -    actor = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        related_name='infractions_given', -        help_text="The user which applied the infraction." -    ) -    type = models.CharField( -        max_length=9, -        choices=TYPE_CHOICES, -        help_text="The type of the infraction." -    ) -    reason = models.TextField( -        null=True, -        help_text="The reason for the infraction." -    ) -    hidden = models.BooleanField( -        default=False, -        help_text="Whether the infraction is a shadow infraction." -    ) - -    def __str__(self): -        s = f"#{self.id}: {self.type} on {self.user_id}" -        if self.expires_at: -            s += f" until {self.expires_at}" -        if self.hidden: -            s += " (hidden)" -        return s - - -class Reminder(ModelReprMixin, models.Model): -    """A reminder created by a user.""" - -    active = models.BooleanField( -        default=True, -        help_text=( -            "Whether this reminder is still active. " -            "If not, it has been sent out to the user." -        ) -    ) -    author = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        help_text="The creator of this reminder." -    ) -    channel_id = models.BigIntegerField( -        help_text=( -            "The channel ID that this message was " -            "sent in, taken from Discord." -        ), -        validators=( -            MinValueValidator( -                limit_value=0, -                message="Channel IDs cannot be negative." -            ), -        ) -    ) -    content = models.CharField( -        max_length=1500, -        help_text="The content that the user wants to be reminded of." -    ) -    expiration = models.DateTimeField( -        help_text="When this reminder should be sent." -    ) - -    def __str__(self): -        return f"{self.content} on {self.expiration} by {self.author}" - - -class Nomination(ModelReprMixin, models.Model): -    """A helper nomination created by staff.""" - -    active = models.BooleanField( -        default=True, -        help_text="Whether this nomination is still relevant." -    ) -    author = models.ForeignKey( -        User, -        on_delete=models.CASCADE, -        help_text="The staff member that nominated this user.", -        related_name='nomination_set' -    ) -    reason = models.TextField( -        help_text="Why this user was nominated." -    ) -    user = models.OneToOneField( -        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." -    ) diff --git a/api/serializers.py b/api/serializers.py deleted file mode 100644 index 9a92313a..00000000 --- a/api/serializers.py +++ /dev/null @@ -1,174 +0,0 @@ -from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError -from rest_framework.validators import UniqueValidator -from rest_framework_bulk import BulkSerializerMixin - -from .models import ( -    BotSetting, DeletedMessage, -    DocumentationLink, Infraction, -    MessageDeletionContext, Nomination, -    OffTopicChannelName, Reminder, -    Role, SnakeFact, -    SnakeIdiom, SnakeName, -    SpecialSnake, Tag, -    User -) - - -class BotSettingSerializer(ModelSerializer): -    class Meta: -        model = BotSetting -        fields = ('name', 'data') - - -class DeletedMessageSerializer(ModelSerializer): -    author = PrimaryKeyRelatedField( -        queryset=User.objects.all() -    ) -    deletion_context = PrimaryKeyRelatedField( -        queryset=MessageDeletionContext.objects.all(), -        # This will be overriden in the `create` function -        # of the deletion context serializer. -        required=False -    ) - -    class Meta: -        model = DeletedMessage -        fields = ( -            'id', 'author', -            'channel_id', 'content', -            'embeds', 'deletion_context' -        ) - - -class MessageDeletionContextSerializer(ModelSerializer): -    deletedmessage_set = DeletedMessageSerializer(many=True) - -    class Meta: -        model = MessageDeletionContext -        fields = ('actor', 'creation', 'id', 'deletedmessage_set') -        depth = 1 - -    def create(self, validated_data): -        messages = validated_data.pop('deletedmessage_set') -        deletion_context = MessageDeletionContext.objects.create(**validated_data) -        for message in messages: -            DeletedMessage.objects.create( -                deletion_context=deletion_context, -                **message -            ) - -        return deletion_context - - -class DocumentationLinkSerializer(ModelSerializer): -    class Meta: -        model = DocumentationLink -        fields = ('package', 'base_url', 'inventory_url') - - -class InfractionSerializer(ModelSerializer): -    class Meta: -        model = Infraction -        fields = ( -            'id', 'inserted_at', 'expires_at', 'active', 'user', 'actor', 'type', 'reason', 'hidden' -        ) - -    def validate(self, attrs): -        infr_type = attrs.get('type') - -        expires_at = attrs.get('expires_at') -        if expires_at and infr_type in ('kick', 'warning'): -            raise ValidationError({'expires_at': [f'{infr_type} infractions cannot expire.']}) - -        hidden = attrs.get('hidden') -        if hidden and infr_type in ('superstar',): -            raise ValidationError({'hidden': [f'{infr_type} infractions cannot be hidden.']}) - -        return attrs - - -class ExpandedInfractionSerializer(InfractionSerializer): -    def to_representation(self, instance): -        ret = super().to_representation(instance) - -        user = User.objects.get(id=ret['user']) -        user_data = UserSerializer(user).data -        ret['user'] = user_data - -        actor = User.objects.get(id=ret['actor']) -        actor_data = UserSerializer(actor).data -        ret['actor'] = actor_data - -        return ret - - -class OffTopicChannelNameSerializer(ModelSerializer): -    class Meta: -        model = OffTopicChannelName -        fields = ('name',) - -    def to_representation(self, obj): -        return obj.name - - -class SnakeFactSerializer(ModelSerializer): -    class Meta: -        model = SnakeFact -        fields = ('fact',) - - -class SnakeIdiomSerializer(ModelSerializer): -    class Meta: -        model = SnakeIdiom -        fields = ('idiom',) - - -class SnakeNameSerializer(ModelSerializer): -    class Meta: -        model = SnakeName -        fields = ('name', 'scientific') - - -class SpecialSnakeSerializer(ModelSerializer): -    class Meta: -        model = SpecialSnake -        fields = ('name', 'images', 'info') - - -class ReminderSerializer(ModelSerializer): -    author = PrimaryKeyRelatedField(queryset=User.objects.all()) - -    class Meta: -        model = Reminder -        fields = ('active', 'author', 'channel_id', 'content', 'expiration', 'id') - - -class RoleSerializer(ModelSerializer): -    class Meta: -        model = Role -        fields = ('id', 'name', 'colour', 'permissions') - - -class TagSerializer(ModelSerializer): -    class Meta: -        model = Tag -        fields = ('title', 'embed') - - -class UserSerializer(BulkSerializerMixin, ModelSerializer): -    roles = PrimaryKeyRelatedField(many=True, queryset=Role.objects.all(), required=False) - -    class Meta: -        model = User -        fields = ('id', 'avatar_hash', 'name', 'discriminator', 'roles', 'in_guild') -        depth = 1 - - -class NominationSerializer(ModelSerializer): -    author = PrimaryKeyRelatedField(queryset=User.objects.all()) -    user = PrimaryKeyRelatedField(queryset=User.objects.all()) - -    class Meta: -        model = Nomination -        fields = ('active', 'author', 'reason', 'user', 'inserted_at') -        depth = 1 diff --git a/api/tests/__init__.py b/api/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/api/tests/__init__.py +++ /dev/null diff --git a/api/tests/base.py b/api/tests/base.py deleted file mode 100644 index 5d8ccf8c..00000000 --- a/api/tests/base.py +++ /dev/null @@ -1,69 +0,0 @@ -from django.contrib.auth.models import User -from rest_framework.test import APITestCase - - -test_user, _created = User.objects.get_or_create( -    username='test', -    email='[email protected]', -    password='testpass',  # noqa: S106 -    is_superuser=True, -    is_staff=True -) - - -class APISubdomainTestCase(APITestCase): -    """ -    Configures the test client to use the proper subdomain -    for requests and forces authentication for the test user. - -    The test user is considered staff and superuser. -    If you want to test for a custom user (for example, to test model permissions), -    create the user, assign the relevant permissions, and use -    `self.client.force_authenticate(user=created_user)` to force authentication -    through the created user. - -    Using this performs the following niceties for you which ease writing tests: -    - setting the `HTTP_HOST` request header to `api.pythondiscord.local:8000`, and -    - forcing authentication for the test user. -    If you don't want to force authentication (for example, to test a route's response -    for an unauthenticated user), un-force authentication by using the following: - -    >>> from api.test.base import APISubdomainTestCase -    >>> class UnauthedUserTestCase(APISubdomainTestCase): -    ...     def setUp(self): -    ...         super().setUp() -    ...         self.client.force_authentication(user=None) -    ...     def test_can_read_objects_at_my_endpoint(self): -    ...         resp = self.client.get('/my-publicly-readable-endpoint') -    ...         self.assertEqual(resp.status_code, 200) -    ...     def test_cannot_delete_objects_at_my_endpoint(self): -    ...         resp = self.client.delete('/my-publicly-readable-endpoint/42') -    ...         self.assertEqual(resp.status_code, 401) - -    Make sure to include the `super().setUp(self)` call, otherwise, you may get -    status code 404 for some URLs due to the missing `HTTP_HOST` header. - -    ## Example -    Using this in a test case is rather straightforward: - -    >>> from api.tests.base import APISubdomainTestCase -    >>> class MyAPITestCase(APISubdomainTestCase): -    ...     def test_that_it_works(self): -    ...         response = self.client.get('/my-endpoint') -    ...         self.assertEqual(response.status_code, 200) - -    To reverse URLs of the API host, you need to use `django_hosts`: - -    >>> from django_hosts.resolvers import reverse -    >>> from api.test.base import APISubdomainTestCase -    >>> class MyReversedTestCase(APISubdomainTestCase): -    ...     def test_my_endpoint(self): -    ...         url = reverse('user-detail', host='api') -    ...         response = self.client.get(url) -    ...         self.assertEqual(response.status_code, 200) -    """ - -    def setUp(self): -        super().setUp() -        self.client.defaults['HTTP_HOST'] = 'api.pythondiscord.local:8000' -        self.client.force_authenticate(test_user) diff --git a/api/tests/test_deleted_messages.py b/api/tests/test_deleted_messages.py deleted file mode 100644 index cd5acab0..00000000 --- a/api/tests/test_deleted_messages.py +++ /dev/null @@ -1,43 +0,0 @@ -from datetime import datetime - -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase -from ..models import User - - -class DeletedMessagesTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.author = User.objects.create( -            id=55, -            name='Robbie Rotten', -            discriminator=55, -            avatar_hash=None -        ) - -        cls.data = { -            'actor': None, -            'creation': datetime.utcnow().isoformat(), -            'deletedmessage_set': [ -                { -                    'author': cls.author.id, -                    'id': 55, -                    'channel_id': 5555, -                    'content': "Terror Billy is a meanie", -                    'embeds': [] -                }, -                { -                    'author': cls.author.id, -                    'id': 56, -                    'channel_id': 5555, -                    'content': "If you purge this, you're evil", -                    'embeds': [] -                } -            ] -        } - -    def test_accepts_valid_data(self): -        url = reverse('bot:messagedeletioncontext-list', host='api') -        response = self.client.post(url, data=self.data) -        self.assertEqual(response.status_code, 201) diff --git a/api/tests/test_documentation_links.py b/api/tests/test_documentation_links.py deleted file mode 100644 index f6c78391..00000000 --- a/api/tests/test_documentation_links.py +++ /dev/null @@ -1,161 +0,0 @@ -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase -from ..models import DocumentationLink - - -class UnauthedDocumentationLinkAPITests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() -        self.client.force_authenticate(user=None) - -    def test_detail_lookup_returns_401(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 401) - -    def test_list_returns_401(self): -        url = reverse('bot:documentationlink-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 401) - -    def test_create_returns_401(self): -        url = reverse('bot:documentationlink-list', host='api') -        response = self.client.post(url, data={'hi': 'there'}) - -        self.assertEqual(response.status_code, 401) - -    def test_delete_returns_401(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') -        response = self.client.delete(url) - -        self.assertEqual(response.status_code, 401) - - -class EmptyDatabaseDocumentationLinkAPITests(APISubdomainTestCase): -    def test_detail_lookup_returns_404(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 404) - -    def test_list_all_returns_empty_list(self): -        url = reverse('bot:documentationlink-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), []) - -    def test_delete_returns_404(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') -        response = self.client.delete(url) - -        self.assertEqual(response.status_code, 404) - - -class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.doc_link = DocumentationLink.objects.create( -            package='testpackage', -            base_url='https://example.com', -            inventory_url='https://example.com' -        ) - -        cls.doc_json = { -            'package': cls.doc_link.package, -            'base_url': cls.doc_link.base_url, -            'inventory_url': cls.doc_link.inventory_url -        } - -    def test_detail_lookup_unknown_package_returns_404(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 404) - -    def test_detail_lookup_created_package_returns_package(self): -        url = reverse('bot:documentationlink-detail', args=(self.doc_link.package,), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), self.doc_json) - -    def test_list_all_packages_shows_created_package(self): -        url = reverse('bot:documentationlink-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), [self.doc_json]) - -    def test_create_invalid_body_returns_400(self): -        url = reverse('bot:documentationlink-list', host='api') -        response = self.client.post(url, data={'i': 'am', 'totally': 'valid'}) - -        self.assertEqual(response.status_code, 400) - -    def test_create_invalid_url_returns_400(self): -        body = { -            'package': 'example', -            'base_url': 'https://example.com', -            'inventory_url': 'totally an url' -        } - -        url = reverse('bot:documentationlink-list', host='api') -        response = self.client.post(url, data=body) - -        self.assertEqual(response.status_code, 400) - - -class DocumentationLinkCreationTests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() - -        self.body = { -            'package': 'example', -            'base_url': 'https://example.com', -            'inventory_url': 'https://docs.example.com' -        } - -        url = reverse('bot:documentationlink-list', host='api') -        response = self.client.post(url, data=self.body) - -        self.assertEqual(response.status_code, 201) - -    def test_package_in_full_list(self): -        url = reverse('bot:documentationlink-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), [self.body]) - -    def test_detail_lookup_works_with_package(self): -        url = reverse('bot:documentationlink-detail', args=(self.body['package'],), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), self.body) - - -class DocumentationLinkDeletionTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.doc_link = DocumentationLink.objects.create( -            package='example', -            base_url='https://example.com', -            inventory_url='https://docs.example.com' -        ) - -    def test_unknown_package_returns_404(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') -        response = self.client.delete(url) - -        self.assertEqual(response.status_code, 404) - -    def test_delete_known_package_returns_204(self): -        url = reverse('bot:documentationlink-detail', args=(self.doc_link.package,), host='api') -        response = self.client.delete(url) - -        self.assertEqual(response.status_code, 204) diff --git a/api/tests/test_healthcheck.py b/api/tests/test_healthcheck.py deleted file mode 100644 index b0fd71bf..00000000 --- a/api/tests/test_healthcheck.py +++ /dev/null @@ -1,16 +0,0 @@ -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase - - -class UnauthedHealthcheckAPITests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() -        self.client.force_authenticate(user=None) - -    def test_can_access_healthcheck_view(self): -        url = reverse('healthcheck', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), {'status': 'ok'}) diff --git a/api/tests/test_infractions.py b/api/tests/test_infractions.py deleted file mode 100644 index 7c370c17..00000000 --- a/api/tests/test_infractions.py +++ /dev/null @@ -1,359 +0,0 @@ -from datetime import datetime as dt, timedelta, timezone -from urllib.parse import quote - -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase -from ..models import Infraction, User - - -class UnauthenticatedTests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() -        self.client.force_authenticate(user=None) - -    def test_detail_lookup_returns_401(self): -        url = reverse('bot:infraction-detail', args=(5,), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 401) - -    def test_list_returns_401(self): -        url = reverse('bot:infraction-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 401) - -    def test_create_returns_401(self): -        url = reverse('bot:infraction-list', host='api') -        response = self.client.post(url, data={'reason': 'Have a nice day.'}) - -        self.assertEqual(response.status_code, 401) - -    def test_partial_update_returns_401(self): -        url = reverse('bot:infraction-detail', args=(5,), host='api') -        response = self.client.patch(url, data={'reason': 'Have a nice day.'}) - -        self.assertEqual(response.status_code, 401) - - -class InfractionTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.user = User.objects.create( -            id=5, -            name='james', -            discriminator=1, -            avatar_hash=None -        ) -        cls.ban_hidden = Infraction.objects.create( -            user_id=cls.user.id, -            actor_id=cls.user.id, -            type='ban', -            reason='He terk my jerb!', -            hidden=True, -            expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) -        ) -        cls.ban_inactive = Infraction.objects.create( -            user_id=cls.user.id, -            actor_id=cls.user.id, -            type='ban', -            reason='James is an ass, and we won\'t be working with him again.', -            active=False -        ) - -    def test_list_all(self): -        url = reverse('bot:infraction-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        infractions = response.json() - -        self.assertEqual(len(infractions), 2) -        self.assertEqual(infractions[0]['id'], self.ban_hidden.id) -        self.assertEqual(infractions[1]['id'], self.ban_inactive.id) - -    def test_filter_search(self): -        url = reverse('bot:infraction-list', host='api') -        pattern = quote(r'^James(\s\w+){3},') -        response = self.client.get(f'{url}?search={pattern}') - -        self.assertEqual(response.status_code, 200) -        infractions = response.json() - -        self.assertEqual(len(infractions), 1) -        self.assertEqual(infractions[0]['id'], self.ban_inactive.id) - -    def test_filter_field(self): -        url = reverse('bot:infraction-list', host='api') -        response = self.client.get(f'{url}?type=ban&hidden=true') - -        self.assertEqual(response.status_code, 200) -        infractions = response.json() - -        self.assertEqual(len(infractions), 1) -        self.assertEqual(infractions[0]['id'], self.ban_hidden.id) - -    def test_returns_empty_for_no_match(self): -        url = reverse('bot:infraction-list', host='api') -        response = self.client.get(f'{url}?type=ban&search=poop') - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(len(response.json()), 0) - -    def test_ignores_bad_filters(self): -        url = reverse('bot:infraction-list', host='api') -        response = self.client.get(f'{url}?type=ban&hidden=maybe&foo=bar') - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(len(response.json()), 2) - -    def test_retrieve_single_from_id(self): -        url = reverse('bot:infraction-detail', args=(self.ban_inactive.id,), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json()['id'], self.ban_inactive.id) - -    def test_retrieve_returns_404_for_absent_id(self): -        url = reverse('bot:infraction-detail', args=(1337,), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 404) - -    def test_partial_update(self): -        url = reverse('bot:infraction-detail', args=(self.ban_hidden.id,), host='api') -        data = { -            'expires_at': '4143-02-15T21:04:31+00:00', -            'active': False, -            'reason': 'durka derr' -        } - -        response = self.client.patch(url, data=data) -        self.assertEqual(response.status_code, 200) -        infraction = Infraction.objects.get(id=self.ban_hidden.id) - -        # These fields were updated. -        self.assertEqual(infraction.expires_at.isoformat(), data['expires_at']) -        self.assertEqual(infraction.active, data['active']) -        self.assertEqual(infraction.reason, data['reason']) - -        # These fields are still the same. -        self.assertEqual(infraction.id, self.ban_hidden.id) -        self.assertEqual(infraction.inserted_at, self.ban_hidden.inserted_at) -        self.assertEqual(infraction.user.id, self.ban_hidden.user.id) -        self.assertEqual(infraction.actor.id, self.ban_hidden.actor.id) -        self.assertEqual(infraction.type, self.ban_hidden.type) -        self.assertEqual(infraction.hidden, self.ban_hidden.hidden) - -    def test_partial_update_returns_400_for_frozen_field(self): -        url = reverse('bot:infraction-detail', args=(self.ban_hidden.id,), host='api') -        data = {'user': 6} - -        response = self.client.patch(url, data=data) -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'user': ['This field cannot be updated.'] -        }) - - -class CreationTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.user = User.objects.create( -            id=5, -            name='james', -            discriminator=1, -            avatar_hash=None -        ) - -    def test_accepts_valid_data(self): -        url = reverse('bot:infraction-list', host='api') -        data = { -            'user': self.user.id, -            'actor': self.user.id, -            'type': 'ban', -            'reason': 'He terk my jerb!', -            'hidden': True, -            'expires_at': '5018-11-20T15:52:00+00:00' -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 201) - -        infraction = Infraction.objects.get(id=response.json()['id']) -        self.assertAlmostEqual( -            infraction.inserted_at, -            dt.now(timezone.utc), -            delta=timedelta(seconds=2) -        ) -        self.assertEqual(infraction.expires_at.isoformat(), data['expires_at']) -        self.assertEqual(infraction.user.id, data['user']) -        self.assertEqual(infraction.actor.id, data['actor']) -        self.assertEqual(infraction.type, data['type']) -        self.assertEqual(infraction.reason, data['reason']) -        self.assertEqual(infraction.hidden, data['hidden']) -        self.assertEqual(infraction.active, True) - -    def test_returns_400_for_missing_user(self): -        url = reverse('bot:infraction-list', host='api') -        data = { -            'actor': self.user.id, -            'type': 'kick' -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'user': ['This field is required.'] -        }) - -    def test_returns_400_for_bad_user(self): -        url = reverse('bot:infraction-list', host='api') -        data = { -            'user': 1337, -            'actor': self.user.id, -            'type': 'kick' -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'user': ['Invalid pk "1337" - object does not exist.'] -        }) - -    def test_returns_400_for_bad_type(self): -        url = reverse('bot:infraction-list', host='api') -        data = { -            'user': self.user.id, -            'actor': self.user.id, -            'type': 'hug' -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'type': ['"hug" is not a valid choice.'] -        }) - -    def test_returns_400_for_bad_expired_at_format(self): -        url = reverse('bot:infraction-list', host='api') -        data = { -            'user': self.user.id, -            'actor': self.user.id, -            'type': 'ban', -            'expires_at': '20/11/5018 15:52:00' -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'expires_at': [ -                'Datetime has wrong format. Use one of these formats instead: ' -                'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].' -            ] -        }) - -    def test_returns_400_for_expiring_non_expirable_type(self): -        url = reverse('bot:infraction-list', host='api') -        data = { -            'user': self.user.id, -            'actor': self.user.id, -            'type': 'kick', -            'expires_at': '5018-11-20T15:52:00+00:00' -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'expires_at': [f'{data["type"]} infractions cannot expire.'] -        }) - -    def test_returns_400_for_hidden_non_hideable_type(self): -        url = reverse('bot:infraction-list', host='api') -        data = { -            'user': self.user.id, -            'actor': self.user.id, -            'type': 'superstar', -            'hidden': True -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'hidden': [f'{data["type"]} infractions cannot be hidden.'] -        }) - - -class ExpandedTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.user = User.objects.create( -            id=5, -            name='james', -            discriminator=1, -            avatar_hash=None -        ) -        cls.kick = Infraction.objects.create( -            user_id=cls.user.id, -            actor_id=cls.user.id, -            type='kick' -        ) -        cls.warning = Infraction.objects.create( -            user_id=cls.user.id, -            actor_id=cls.user.id, -            type='warning' -        ) - -    def check_expanded_fields(self, infraction): -        for key in ('user', 'actor'): -            obj = infraction[key] -            for field in ('id', 'name', 'discriminator', 'avatar_hash', 'roles', 'in_guild'): -                self.assertTrue(field in obj, msg=f'field "{field}" missing from {key}') - -    def test_list_expanded(self): -        url = reverse('bot:infraction-list-expanded', host='api') - -        response = self.client.get(url) -        self.assertEqual(response.status_code, 200) - -        response_data = response.json() -        self.assertEqual(len(response_data), 2) - -        for infraction in response_data: -            self.check_expanded_fields(infraction) - -    def test_create_expanded(self): -        url = reverse('bot:infraction-list-expanded', host='api') -        data = { -            'user': self.user.id, -            'actor': self.user.id, -            'type': 'warning' -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 201) - -        self.assertEqual(len(Infraction.objects.all()), 3) -        self.check_expanded_fields(response.json()) - -    def test_retrieve_expanded(self): -        url = reverse('bot:infraction-detail-expanded', args=(self.warning.id,), host='api') - -        response = self.client.get(url) -        self.assertEqual(response.status_code, 200) - -        infraction = response.json() -        self.assertEqual(infraction['id'], self.warning.id) -        self.check_expanded_fields(infraction) - -    def test_partial_update_expanded(self): -        url = reverse('bot:infraction-detail-expanded', args=(self.kick.id,), host='api') -        data = {'active': False} - -        response = self.client.patch(url, data=data) -        self.assertEqual(response.status_code, 200) - -        infraction = Infraction.objects.get(id=self.kick.id) -        self.assertEqual(infraction.active, data['active']) -        self.check_expanded_fields(response.json()) diff --git a/api/tests/test_models.py b/api/tests/test_models.py deleted file mode 100644 index 43d1eb41..00000000 --- a/api/tests/test_models.py +++ /dev/null @@ -1,113 +0,0 @@ -from datetime import datetime as dt, timezone - -from django.test import SimpleTestCase - -from ..models import ( -    BotSetting, DeletedMessage, -    DocumentationLink, Infraction, -    Message, MessageDeletionContext, -    ModelReprMixin, OffTopicChannelName, -    Reminder, Role, -    SnakeFact, SnakeIdiom, -    SnakeName, SpecialSnake, -    Tag, User -) - - -class SimpleClass(ModelReprMixin): -    def __init__(self, is_what): -        self.the_cake = is_what - - -class ReprMixinTests(SimpleTestCase): -    def setUp(self): -        self.klass = SimpleClass('is a lie') - -    def test_shows_attributes(self): -        expected = "<SimpleClass(the_cake='is a lie')>" -        self.assertEqual(repr(self.klass), expected) - - -class StringDunderMethodTests(SimpleTestCase): -    def setUp(self): -        self.objects = ( -            DeletedMessage( -                id=45, -                author=User( -                    id=444, name='bill', -                    discriminator=5, avatar_hash=None -                ), -                channel_id=666, -                content="wooey", -                deletion_context=MessageDeletionContext( -                    actor=User( -                        id=5555, name='shawn', -                        discriminator=555, avatar_hash=None -                    ), -                    creation=dt.utcnow() -                ), -                embeds=[] -            ), -            DocumentationLink( -                'test', 'http://example.com', 'http://example.com' -            ), -            OffTopicChannelName(name='bob-the-builders-playground'), -            SnakeFact(fact='snakes are cute'), -            SnakeIdiom(idiom='snake snacks'), -            SnakeName(name='python', scientific='3'), -            SpecialSnake( -                name='Pythagoras Pythonista', -                info='The only python snake that is born a triangle' -            ), -            Role( -                id=5, name='test role', -                colour=0x5, permissions=0 -            ), -            Message( -                id=45, -                author=User( -                    id=444, name='bill', -                    discriminator=5, avatar_hash=None -                ), -                channel_id=666, -                content="wooey", -                embeds=[] -            ), -            MessageDeletionContext( -                actor=User( -                    id=5555, name='shawn', -                    discriminator=555, avatar_hash=None -                ), -                creation=dt.utcnow() -            ), -            Tag( -                title='bob', -                embed={'content': "the builder"} -            ), -            User( -                id=5, name='bob', -                discriminator=1, avatar_hash=None -            ), -            Infraction( -                user_id=5, actor_id=5, -                type='kick', reason='He terk my jerb!' -            ), -            Infraction( -                user_id=5, actor_id=5, hidden=True, -                type='kick', reason='He terk my jerb!', -                expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) -            ), -            Reminder( -                author=User( -                    id=452, name='billy', -                    discriminator=5, avatar_hash=None -                ), -                channel_id=555, -                content="oh no", -                expiration=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) -            ) -        ) - -    def test_returns_string(self): -        for instance in self.objects: -            self.assertIsInstance(str(instance), str) diff --git a/api/tests/test_nominations.py b/api/tests/test_nominations.py deleted file mode 100644 index 1f03d1b0..00000000 --- a/api/tests/test_nominations.py +++ /dev/null @@ -1,41 +0,0 @@ -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase -from ..models import Nomination, User - - -class NominationTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.author = User.objects.create( -            id=5152, -            name='Ro Bert', -            discriminator=256, -            avatar_hash=None -        ) -        cls.user = cls.author - -        cls.nomination = Nomination.objects.create( -            author=cls.author, -            reason="he's good", -            user=cls.author -        ) - -    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) diff --git a/api/tests/test_off_topic_channel_names.py b/api/tests/test_off_topic_channel_names.py deleted file mode 100644 index 60af1f62..00000000 --- a/api/tests/test_off_topic_channel_names.py +++ /dev/null @@ -1,152 +0,0 @@ -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase -from ..models import OffTopicChannelName - - -class UnauthenticatedTests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() -        self.client.force_authenticate(user=None) - -    def test_cannot_read_off_topic_channel_name_list(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 401) - -    def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(f'{url}?random_items=no') - -        self.assertEqual(response.status_code, 401) - - -class EmptyDatabaseTests(APISubdomainTestCase): -    def test_returns_empty_object(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), []) - -    def test_returns_empty_list_with_get_all_param(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(f'{url}?random_items=5') - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), []) - -    def test_returns_400_for_bad_random_items_param(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(f'{url}?random_items=totally-a-valid-integer') - -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'random_items': ["Must be a valid integer."] -        }) - -    def test_returns_400_for_negative_random_items_param(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(f'{url}?random_items=-5') - -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'random_items': ["Must be a positive integer."] -        }) - - -class ListTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') -        cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') - -    def test_returns_name_in_list(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual( -            response.json(), -            [ -                self.test_name.name, -                self.test_name_2.name -            ] -        ) - -    def test_returns_single_item_with_random_items_param_set_to_1(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(f'{url}?random_items=1') - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(len(response.json()), 1) - - -class CreationTests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() - -        url = reverse('bot:offtopicchannelname-list', host='api') -        self.name = "lemonade-shop" -        response = self.client.post(f'{url}?name={self.name}') -        self.assertEqual(response.status_code, 201) - -    def test_name_in_full_list(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), [self.name]) - -    def test_returns_400_for_missing_name_param(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.post(url) -        self.assertEqual(response.status_code, 400) -        self.assertEqual(response.json(), { -            'name': ["This query parameter is required."] -        }) - -    def test_returns_400_for_bad_name_param(self): -        url = reverse('bot:offtopicchannelname-list', host='api') -        invalid_names = ( -            'space between words', -            'UPPERCASE', -            '$$$$$$$$' -        ) - -        for name in invalid_names: -            response = self.client.post(f'{url}?name={name}') -            self.assertEqual(response.status_code, 400) -            self.assertEqual(response.json(), { -                'name': ["Enter a valid value."] -            }) - - -class DeletionTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') -        cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') - -    def test_deleting_unknown_name_returns_404(self): -        url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') -        response = self.client.delete(url) - -        self.assertEqual(response.status_code, 404) - -    def test_deleting_known_name_returns_204(self): -        url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') -        response = self.client.delete(url) - -        self.assertEqual(response.status_code, 204) - -    def test_name_gets_deleted(self): -        url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') -        response = self.client.delete(url) - -        self.assertEqual(response.status_code, 204) - -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(url) -        self.assertNotIn(self.test_name_2.name, response.json()) diff --git a/api/tests/test_rules.py b/api/tests/test_rules.py deleted file mode 100644 index c94f89cc..00000000 --- a/api/tests/test_rules.py +++ /dev/null @@ -1,35 +0,0 @@ -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase -from ..views import RulesView - - -class RuleAPITests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() -        self.client.force_authenticate(user=None) - -    def test_can_access_rules_view(self): -        url = reverse('rules', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertIsInstance(response.json(), list) - -    def test_link_format_query_param_produces_different_results(self): -        url = reverse('rules', host='api') -        markdown_links_response = self.client.get(url + '?link_format=md') -        html_links_response = self.client.get(url + '?link_format=html') -        self.assertNotEqual( -            markdown_links_response.json(), -            html_links_response.json() -        ) - -    def test_format_link_raises_value_error_for_invalid_target(self): -        with self.assertRaises(ValueError): -            RulesView._format_link("a", "b", "c") - -    def test_get_returns_400_for_wrong_link_format(self): -        url = reverse('rules', host='api') -        response = self.client.get(url + '?link_format=unknown') -        self.assertEqual(response.status_code, 400) diff --git a/api/tests/test_snake_names.py b/api/tests/test_snake_names.py deleted file mode 100644 index 41dfae63..00000000 --- a/api/tests/test_snake_names.py +++ /dev/null @@ -1,67 +0,0 @@ -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase -from ..models import SnakeName - - -class StatusTests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() -        self.client.force_authenticate(user=None) - -    def test_cannot_read_snake_name_list(self): -        url = reverse('bot:snakename-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 401) - -    def test_cannot_read_snake_names_with_get_all_param(self): -        url = reverse('bot:snakename-list', host='api') -        response = self.client.get(f'{url}?get_all=True') - -        self.assertEqual(response.status_code, 401) - - -class EmptyDatabaseSnakeNameTests(APISubdomainTestCase): -    def test_endpoint_returns_empty_object(self): -        url = reverse('bot:snakename-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), {}) - -    def test_endpoint_returns_empty_list_with_get_all_param(self): -        url = reverse('bot:snakename-list', host='api') -        response = self.client.get(f'{url}?get_all=True') - -        self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), []) - - -class SnakeNameListTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.snake_python = SnakeName.objects.create(name='Python', scientific='Totally.') - -    def test_endpoint_returns_all_snakes_with_get_all_param(self): -        url = reverse('bot:snakename-list', host='api') -        response = self.client.get(f'{url}?get_all=True') - -        self.assertEqual(response.status_code, 200) -        self.assertEqual( -            response.json(), -            [ -                { -                    'name': self.snake_python.name, -                    'scientific': self.snake_python.scientific -                } -            ] -        ) - -    def test_endpoint_returns_single_snake_without_get_all_param(self): -        url = reverse('bot:snakename-list', host='api') -        response = self.client.get(url) -        self.assertEqual(response.json(), { -            'name': self.snake_python.name, -            'scientific': self.snake_python.scientific -        }) diff --git a/api/tests/test_users.py b/api/tests/test_users.py deleted file mode 100644 index 90bc3d30..00000000 --- a/api/tests/test_users.py +++ /dev/null @@ -1,121 +0,0 @@ -from django_hosts.resolvers import reverse - -from .base import APISubdomainTestCase -from ..models import Role, User - - -class UnauthedUserAPITests(APISubdomainTestCase): -    def setUp(self): -        super().setUp() -        self.client.force_authenticate(user=None) - -    def test_detail_lookup_returns_401(self): -        url = reverse('bot:user-detail', args=('whatever',), host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 401) - -    def test_list_returns_401(self): -        url = reverse('bot:user-list', host='api') -        response = self.client.get(url) - -        self.assertEqual(response.status_code, 401) - -    def test_create_returns_401(self): -        url = reverse('bot:user-list', host='api') -        response = self.client.post(url, data={'hi': 'there'}) - -        self.assertEqual(response.status_code, 401) - -    def test_delete_returns_401(self): -        url = reverse('bot:user-detail', args=('whatever',), host='api') -        response = self.client.delete(url) - -        self.assertEqual(response.status_code, 401) - - -class CreationTests(APISubdomainTestCase): -    @classmethod -    def setUpTestData(cls):  # noqa -        cls.role = Role.objects.create( -            id=5, -            name="Test role pls ignore", -            colour=2, -            permissions=0b01010010101 -        ) - -    def test_accepts_valid_data(self): -        url = reverse('bot:user-list', host='api') -        data = { -            'id': 42, -            'avatar_hash': "validavatarhashiswear", -            'name': "Test", -            'discriminator': 42, -            'roles': [ -                self.role.id -            ], -            'in_guild': True -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 201) -        self.assertEqual(response.json(), data) - -        user = User.objects.get(id=42) -        self.assertEqual(user.avatar_hash, data['avatar_hash']) -        self.assertEqual(user.name, data['name']) -        self.assertEqual(user.discriminator, data['discriminator']) -        self.assertEqual(user.in_guild, data['in_guild']) - -    def test_supports_multi_creation(self): -        url = reverse('bot:user-list', host='api') -        data = [ -            { -                'id': 5, -                'avatar_hash': "hahayes", -                'name': "test man", -                'discriminator': 42, -                'roles': [ -                    self.role.id -                ], -                'in_guild': True -            }, -            { -                'id': 8, -                'avatar_hash': "maybenot", -                'name': "another test man", -                'discriminator': 555, -                'roles': [], -                'in_guild': False -            } -        ] - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 201) -        self.assertEqual(response.json(), data) - -    def test_returns_400_for_unknown_role_id(self): -        url = reverse('bot:user-list', host='api') -        data = { -            'id': 5, -            'avatar_hash': "hahayes", -            'name': "test man", -            'discriminator': 42, -            'roles': [ -                190810291 -            ] -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 400) - -    def test_returns_400_for_bad_data(self): -        url = reverse('bot:user-list', host='api') -        data = { -            'id': True, -            'avatar_hash': 1902831, -            'discriminator': "totally!" -        } - -        response = self.client.post(url, data=data) -        self.assertEqual(response.status_code, 400) diff --git a/api/tests/test_validators.py b/api/tests/test_validators.py deleted file mode 100644 index d2c0a136..00000000 --- a/api/tests/test_validators.py +++ /dev/null @@ -1,213 +0,0 @@ -from django.core.exceptions import ValidationError -from django.test import TestCase - -from ..validators import ( -    validate_bot_setting_name, -    validate_tag_embed -) - - -REQUIRED_KEYS = ( -    'content', 'fields', 'image', 'title', 'video' -) - - -class BotSettingValidatorTests(TestCase): -    def test_accepts_valid_names(self): -        validate_bot_setting_name('defcon') - -    def test_rejects_bad_names(self): -        with self.assertRaises(ValidationError): -            validate_bot_setting_name('bad name') - -class TagEmbedValidatorTests(TestCase): -    def test_rejects_non_mapping(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed('non-empty non-mapping') - -    def test_rejects_missing_required_keys(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'unknown': "key" -            }) - -    def test_rejects_one_correct_one_incorrect(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'provider': "??", -                'title': "" -            }) - -    def test_rejects_empty_required_key(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': '' -            }) - -    def test_rejects_list_as_embed(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed([]) - -    def test_rejects_required_keys_and_unknown_keys(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': "the duck walked up to the lemonade stand", -                'and': "he said to the man running the stand" -            }) - -    def test_rejects_too_long_title(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': 'a' * 257 -            }) - -    def test_rejects_too_many_fields(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'fields': [{} for _ in range(26)] -            }) - -    def test_rejects_too_long_description(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'description': 'd' * 2049 -            }) - -    def test_allows_valid_embed(self): -        validate_tag_embed({ -            'title': "My embed", -            'description': "look at my embed, my embed is amazing" -        }) - -    def test_allows_unvalidated_fields(self): -        validate_tag_embed({ -            'title': "My embed", -            'provider': "what am I??" -        }) - -    def test_rejects_fields_as_list_of_non_mappings(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'fields': ['abc'] -            }) - -    def test_rejects_fields_with_unknown_fields(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'fields': [ -                    { -                        'what': "is this field" -                    } -                ] -            }) - -    def test_rejects_fields_with_too_long_name(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'fields': [ -                    { -                        'name': "a" * 257 -                    } -                ] -            }) - -    def test_rejects_one_correct_one_incorrect_field(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'fields': [ -                    { -                        'name': "Totally valid", -                        'value': "LOOK AT ME" -                    }, -                    { -                        'oh': "what is this key?" -                    } -                ] -            }) - -    def test_allows_valid_fields(self): -        validate_tag_embed({ -            'fields': [ -                { -                    'name': "valid", -                    'value': "field" -                } -            ] -        }) - -    def test_rejects_footer_as_non_mapping(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': "whatever", -                'footer': [] -            }) - -    def test_rejects_footer_with_unknown_fields(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': "whatever", -                'footer': { -                    'duck': "quack" -                } -            }) - -    def test_rejects_footer_with_empty_text(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': "whatever", -                'footer': { -                    'text': "" -                } -            }) - -    def test_allows_footer_with_proper_values(self): -        validate_tag_embed({ -            'title': "whatever", -            'footer': { -                'text': "django good" -            } -        }) - -    def test_rejects_author_as_non_mapping(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': "whatever", -                'author': [] -            }) - -    def test_rejects_author_with_unknown_field(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': "whatever", -                'author': { -                    'field': "that is unknown" -                } -            }) - -    def test_rejects_author_with_empty_name(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': "whatever", -                'author': { -                    'name': "" -                } -            }) - -    def test_rejects_author_with_one_correct_one_incorrect(self): -        with self.assertRaises(ValidationError): -            validate_tag_embed({ -                'title': "whatever", -                'author': { -                    # Relies on "dictionary insertion order remembering" (D.I.O.R.) behaviour -                    'url': "bobswebsite.com", -                    'name': "" -                } -            }) - -    def test_allows_author_with_proper_values(self): -        validate_tag_embed({ -            'title': "whatever", -            'author': { -                'name': "Bob" -            } -        }) diff --git a/api/urls.py b/api/urls.py deleted file mode 100644 index 6c89a52e..00000000 --- a/api/urls.py +++ /dev/null @@ -1,86 +0,0 @@ -from django.urls import include, path -from rest_framework.routers import DefaultRouter - -from .views import HealthcheckView, RulesView -from .viewsets import ( -    BotSettingViewSet, DeletedMessageViewSet, -    DocumentationLinkViewSet, InfractionViewSet, -    NominationViewSet, OffTopicChannelNameViewSet, -    ReminderViewSet, RoleViewSet, -    SnakeFactViewSet, SnakeIdiomViewSet, -    SnakeNameViewSet, SpecialSnakeViewSet, -    TagViewSet, UserViewSet -) - - -# http://www.django-rest-framework.org/api-guide/routers/#defaultrouter -bot_router = DefaultRouter(trailing_slash=False) -bot_router.register( -    'bot-settings', -    BotSettingViewSet -) -bot_router.register( -    'deleted-messages', -    DeletedMessageViewSet -) -bot_router.register( -    'documentation-links', -    DocumentationLinkViewSet -) -bot_router.register( -    'infractions', -    InfractionViewSet -) -bot_router.register( -    'nominations', -    NominationViewSet -) -bot_router.register( -    'off-topic-channel-names', -    OffTopicChannelNameViewSet, -    base_name='offtopicchannelname' -) -bot_router.register( -    'reminders', -    ReminderViewSet -) -bot_router.register( -    'roles', -    RoleViewSet -) -bot_router.register( -    'snake-facts', -    SnakeFactViewSet -) -bot_router.register( -    'snake-idioms', -    SnakeIdiomViewSet -) -bot_router.register( -    'snake-names', -    SnakeNameViewSet, -    base_name='snakename' -) -bot_router.register( -    'special-snakes', -    SpecialSnakeViewSet -) -bot_router.register( -    'tags', -    TagViewSet -) -bot_router.register( -    'users', -    UserViewSet -) - -app_name = 'api' -urlpatterns = ( -    # Build URLs using something like... -    # -    # from django_hosts.resolvers import reverse -    # snake_name_endpoint = reverse('bot:snakename-list', host='api')  # `bot/` endpoints -    path('bot/', include((bot_router.urls, 'api'), namespace='bot')), -    path('healthcheck', HealthcheckView.as_view(), name='healthcheck'), -    path('rules', RulesView.as_view(), name='rules') -) diff --git a/api/validators.py b/api/validators.py deleted file mode 100644 index 35bbfab4..00000000 --- a/api/validators.py +++ /dev/null @@ -1,164 +0,0 @@ -from collections.abc import Mapping - -from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator - - -def validate_tag_embed_fields(fields): -    field_validators = { -        'name': (MaxLengthValidator(limit_value=256),), -        'value': (MaxLengthValidator(limit_value=1024),) -    } - -    for field in fields: -        if not isinstance(field, Mapping): -            raise ValidationError("Embed fields must be a mapping.") - -        for field_name, value in field.items(): -            if field_name not in field_validators: -                raise ValidationError(f"Unknown embed field field: {field_name!r}.") - -            for validator in field_validators[field_name]: -                validator(value) - - -def validate_tag_embed_footer(footer): -    field_validators = { -        'text': ( -            MinLengthValidator( -                limit_value=1, -                message="Footer text must not be empty." -            ), -            MaxLengthValidator(limit_value=2048) -        ), -        'icon_url': (), -        'proxy_icon_url': () -    } - -    if not isinstance(footer, Mapping): -        raise ValidationError("Embed footer must be a mapping.") - -    for field_name, value in footer.items(): -        if field_name not in field_validators: -            raise ValidationError(f"Unknown embed footer field: {field_name!r}.") - -        for validator in field_validators[field_name]: -            validator(value) - - -def validate_tag_embed_author(author): -    field_validators = { -        'name': ( -            MinLengthValidator( -                limit_value=1, -                message="Embed author name must not be empty." -            ), -            MaxLengthValidator(limit_value=256) -        ), -        'url': (), -        'icon_url': (), -        'proxy_icon_url': () -    } - -    if not isinstance(author, Mapping): -        raise ValidationError("Embed author must be a mapping.") - -    for field_name, value in author.items(): -        if field_name not in field_validators: -            raise ValidationError(f"Unknown embed author field: {field_name!r}.") - -        for validator in field_validators[field_name]: -            validator(value) - - -def validate_tag_embed(embed): -    """ -    Validate a JSON document containing an embed as possible to send -    on Discord. This attempts to rebuild the validation used by Discord -    as well as possible by checking for various embed limits so we can -    ensure that any embed we store here will also be accepted as a -    valid embed by the Discord API. - -    Using this directly is possible, although not intended - you usually -    stick this onto the `validators` keyword argument of model fields. - -    Example: - -        >>> from django.contrib.postgres import fields as pgfields -        >>> from django.db import models -        >>> from api.validators import validate_tag_embed -        >>> class MyMessage(models.Model): -        ...     embed = pgfields.JSONField( -        ...         validators=( -        ...             validate_tag_embed, -        ...         ) -        ...     ) -        ...     # ... -        ... - -    Args: -        embed (Dict[str, Union[str, List[dict], dict]]): -            A dictionary describing the contents of this embed. -            See the official documentation for a full reference -            of accepted keys by this dictionary: -                https://discordapp.com/developers/docs/resources/channel#embed-object - -    Raises: -        ValidationError: -            In case the given embed is deemed invalid, a `ValidationError` -            is raised which in turn will allow Django to display errors -            as appropriate. -    """ - -    all_keys = { -        'title', 'type', 'description', 'url', 'timestamp', -        'color', 'footer', 'image', 'thumbnail', 'video', -        'provider', 'author', 'fields' -    } -    one_required_of = {'description', 'fields', 'image', 'title', 'video'} -    field_validators = { -        'title': ( -            MinLengthValidator( -                limit_value=1, -                message="Embed title must not be empty." -            ), -            MaxLengthValidator(limit_value=256) -        ), -        'description': (MaxLengthValidator(limit_value=2048),), -        'fields': ( -            MaxLengthValidator(limit_value=25), -            validate_tag_embed_fields -        ), -        'footer': (validate_tag_embed_footer,), -        'author': (validate_tag_embed_author,) -    } - -    if not embed: -        raise ValidationError("Tag embed must not be empty.") - -    elif not isinstance(embed, Mapping): -        raise ValidationError("Tag embed must be a mapping.") - -    elif not any(field in embed for field in one_required_of): -        raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") - -    for required_key in one_required_of: -        if required_key in embed and not embed[required_key]: -            raise ValidationError(f"Key {required_key!r} must not be empty.") - -    for field_name, value in embed.items(): -        if field_name not in all_keys: -            raise ValidationError(f"Unknown field name: {field_name!r}") - -        if field_name in field_validators: -            for validator in field_validators[field_name]: -                validator(value) - - -def validate_bot_setting_name(name): -    KNOWN_SETTINGS = ( -        'defcon', -    ) - -    if name not in KNOWN_SETTINGS: -        raise ValidationError(f"`{name}` is not a known setting name.") diff --git a/api/views.py b/api/views.py deleted file mode 100644 index c529da0f..00000000 --- a/api/views.py +++ /dev/null @@ -1,161 +0,0 @@ -from rest_framework.exceptions import ParseError -from rest_framework.response import Response -from rest_framework.views import APIView - - -class HealthcheckView(APIView): -    """ -    Provides a simple view to check that the website is alive and well. - -    ## Routes -    ### GET /healthcheck -    Returns a simple JSON document showcasing whether the system is working: - -    >>> { -    ...     'status': 'ok' -    ... } - -    Seems to be. - -    ## Authentication -    Does not require any authentication nor permissions. -    """ - -    authentication_classes = () -    permission_classes = () - -    def get(self, request, format=None):  # noqa -        return Response({'status': 'ok'}) - - -class RulesView(APIView): -    """ -    Return a list of the server's rules. - -    ## Routes -    ### GET /rules -    Returns a JSON array containing the server's rules: - -    >>> [ -    ...     "Eat candy.", -    ...     "Wake up at 4 AM.", -    ...     "Take your medicine." -    ... ] - -    Since some of the the rules require links, this view -    gives you the option to return rules in either Markdown -    or HTML format by specifying the `link_format` query parameter -    as either `md` or `html`. Specifying a different value than -    `md` or `html` will return 400. - -    ## Authentication -    Does not require any authentication nor permissions. -    """ - -    authentication_classes = () -    permission_classes = () - -    @staticmethod -    def _format_link(description, link, target): -        """ -        Build the markup necessary to render `link` with `description` -        as its description in the given `target` language. - -        Arguments: -            description (str): -                A textual description of the string. Represents the content -                between the `<a>` tags in HTML, or the content between the -                array brackets in Markdown. - -            link (str): -                The resulting link that a user should be redirected to -                upon clicking the generated element. - -            target (str): -                One of `{'md', 'html'}`, denoting the target format that the -                link should be rendered in. - -        Returns: -            str: -                The link, rendered appropriately for the given `target` format -                using `description` as its textual description. - -        Raises: -            ValueError: -                If `target` is not `'md'` or `'html'`. -        """ - -        if target == 'html': -            return f'<a href="{link}">{description}</a>' -        elif target == 'md': -            return f'[{description}]({link})' -        else: -            raise ValueError( -                f"Can only template links to `html` or `md`, got `{target}`" -            ) - -    # `format` here is the result format, we have a link format here instead. -    def get(self, request, format=None):  # noqa -        link_format = request.query_params.get('link_format', 'md') -        if link_format not in ('html', 'md'): -            raise ParseError( -                f"`format` must be `html` or `md`, got `{format}`." -            ) - -        discord_community_guidelines_link = self._format_link( -            'Discord Community Guidelines', -            'https://discordapp.com/guidelines', -            link_format -        ) -        channels_page_link = self._format_link( -            'channels page', -            'https://pythondiscord.com/about/channels', -            link_format -        ) -        google_translate_link = self._format_link( -            'Google Translate', -            'https://translate.google.com/', -            link_format -        ) - -        return Response([ -            "Be polite, and do not spam.", -            f"Follow the {discord_community_guidelines_link}.", -            ( -                "Don't intentionally make other people uncomfortable - if " -                "someone asks you to stop discussing something, you should stop." -            ), -            ( -                "Be patient both with users asking " -                "questions, and the users answering them." -            ), -            ( -                "We will not help you with anything that might break a law or the " -                "terms of service of any other community, site, service, or " -                "otherwise - No piracy, brute-forcing, captcha circumvention, " -                "sneaker bots, or anything else of that nature." -            ), -            ( -                "Listen to and respect the staff members - we're " -                "here to help, but we're all human beings." -            ), -            ( -                "All discussion should be kept within the relevant " -                "channels for the subject - See the " -                f"{channels_page_link} for more information." -            ), -            ( -                "This is an English-speaking server, so please speak English " -                f"to the best of your ability - {google_translate_link} " -                "should be fine if you're not sure." -            ), -            ( -                "Keep all discussions safe for work - No gore, nudity, sexual " -                "soliciting, references to suicide, or anything else of that nature" -            ), -            ( -                "We do not allow advertisements for communities (including " -                "other Discord servers) or commercial projects - Contact " -                "us directly if you want to discuss a partnership!" -            ) -        ]) diff --git a/api/viewsets.py b/api/viewsets.py deleted file mode 100644 index 17024fe8..00000000 --- a/api/viewsets.py +++ /dev/null @@ -1,890 +0,0 @@ -from django.shortcuts import get_object_or_404 -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.decorators import action -from rest_framework.exceptions import ParseError, ValidationError -from rest_framework.filters import SearchFilter -from rest_framework.mixins import ( -    CreateModelMixin, DestroyModelMixin, -    ListModelMixin, RetrieveModelMixin, -    UpdateModelMixin -) -from rest_framework.response import Response -from rest_framework.status import HTTP_201_CREATED -from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet -from rest_framework_bulk import BulkCreateModelMixin - -from .models import ( -    BotSetting, DocumentationLink, -    Infraction, MessageDeletionContext, -    Nomination, OffTopicChannelName, -    Reminder, Role, -    SnakeFact, SnakeIdiom, -    SnakeName, SpecialSnake, -    Tag, User -) -from .serializers import ( -    BotSettingSerializer, DocumentationLinkSerializer, -    ExpandedInfractionSerializer, InfractionSerializer, -    MessageDeletionContextSerializer, NominationSerializer, -    OffTopicChannelNameSerializer, ReminderSerializer, -    RoleSerializer, SnakeFactSerializer, -    SnakeIdiomSerializer, SnakeNameSerializer, -    SpecialSnakeSerializer, TagSerializer, -    UserSerializer -) - - -class BotSettingViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet): -    """ -    View providing update operations on bot setting routes. -    """ - -    serializer_class = BotSettingSerializer -    queryset = BotSetting.objects.all() - - -class DeletedMessageViewSet(CreateModelMixin, GenericViewSet): -    """ -    View providing support for posting bulk deletion logs generated by the bot. - -    ## Routes -    ### POST /bot/deleted-messages -    Post messages from bulk deletion logs. - -    #### Body schema -    >>> { -    ...     # The member ID of the original actor, if applicable. -    ...     # If a member ID is given, it must be present on the site. -    ...     'actor': Optional[int] -    ...     'creation': datetime, -    ...     'messages': [ -    ...         { -    ...             'id': int, -    ...             'author': int, -    ...             'channel_id': int, -    ...             'content': str, -    ...             'embeds': [ -    ...                 # Discord embed objects -    ...             ] -    ...         } -    ...     ] -    ... } - -    #### Status codes -    - 204: returned on success -    """ - -    queryset = MessageDeletionContext.objects.all() -    serializer_class = MessageDeletionContextSerializer - - -class DocumentationLinkViewSet( -    CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet -): -    """ -    View providing management of documentation links used in the bot's `Doc` cog. - -    ## Routes -    ### GET /bot/documentation-links -    Retrieve all currently stored entries from the database. - -    #### Response format -    >>> [ -    ...     { -    ...         'package': 'flask', -    ...         'base_url': 'https://flask.pocoo.org/docs/dev', -    ...         'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' -    ...     }, -    ...     # ... -    ... ] - -    #### Status codes -    - 200: returned on success - -    ### GET /bot/documentation-links/<package:str> -    Look up the documentation object for the given `package`. - -    #### Response format -    >>> { -    ...     'package': 'flask', -    ...     'base_url': 'https://flask.pocoo.org/docs/dev', -    ...     'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' -    ... } - -    #### Status codes -    - 200: returned on success -    - 404: if no entry for the given `package` exists - -    ### POST /bot/documentation-links -    Create a new documentation link object. - -    #### Body schema -    >>> { -    ...     'package': str, -    ...     'base_url': URL, -    ...     'inventory_url': URL -    ... } - -    #### Status codes -    - 201: returned on success -    - 400: if the request body has invalid fields, see the response for details - -    ### DELETE /bot/documentation-links/<package:str> -    Delete the entry for the given `package`. - -    #### Status codes -    - 204: returned on success -    - 404: if the given `package` could not be found -    """ - -    queryset = DocumentationLink.objects.all() -    serializer_class = DocumentationLinkSerializer -    lookup_field = 'package' - - -class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): -    """ -    View providing CRUD operations on infractions for Discord users. - -    ## Routes -    ### GET /bot/infractions -    Retrieve all infractions. -    May be filtered by the query parameters. - -    #### Query parameters -    - **active** `bool`: whether the infraction is still active -    - **actor** `int`: snowflake of the user which applied the infraction -    - **hidden** `bool`: whether the infraction is a shadow infraction -    - **search** `str`: regular expression applied to the infraction's reason -    - **type** `str`: the type of the infraction -    - **user** `int`: snowflake of the user to which the infraction was applied - -    Invalid query parameters are ignored. - -    #### Response format -    >>> [ -    ...     { -    ...         'id': 5, -    ...         'inserted_at': '2018-11-22T07:24:06.132307Z', -    ...         'expires_at': '5018-11-20T15:52:00Z', -    ...         'active': False, -    ...         'user': 172395097705414656, -    ...         'actor': 125435062127820800, -    ...         'type': 'ban', -    ...         'reason': 'He terk my jerb!', -    ...         'hidden': True -    ...     } -    ... ] - -    #### Status codes -    - 200: returned on success - -    ### GET /bot/infractions/<id:int> -    Retrieve a single infraction by ID. - -    #### Response format -    See `GET /bot/infractions`. - -    #### Status codes -    - 200: returned on success -    - 404: if an infraction with the given `id` could not be found - -    ### POST /bot/infractions -    Create a new infraction and return the created infraction. -    Only `actor`, `type`, and `user` are required. -    The `actor` and `user` must be users known by the site. - -    #### Request body -    >>> { -    ...     'active': False, -    ...     'actor': 125435062127820800, -    ...     'expires_at': '5018-11-20T15:52:00+00:00', -    ...     'hidden': True, -    ...     'type': 'ban', -    ...     'reason': 'He terk my jerb!', -    ...     'user': 172395097705414656 -    ... } - -    #### Response format -    See `GET /bot/infractions`. - -    #### Status codes -    - 201: returned on success -    - 400: if a given user is unknown or a field in the request body is invalid - -    ### PATCH /bot/infractions/<id:int> -    Update the infraction with the given `id` and return the updated infraction. -    Only `active`, `reason`, and `expires_at` may be updated. - -    #### Request body -    >>> { -    ...     'active': True, -    ...     'expires_at': '4143-02-15T21:04:31+00:00', -    ...     'reason': 'durka derr' -    ... } - -    #### Response format -    See `GET /bot/infractions`. - -    #### Status codes -    - 200: returned on success -    - 400: if a field in the request body is invalid or disallowed -    - 404: if an infraction with the given `id` could not be found - -    ### Expanded routes -    All routes support expansion of `user` and `actor` in responses. To use an expanded route, -    append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`. - -    #### Response format -    See `GET /bot/users/<snowflake:int>` for the expanded formats of `user` and `actor`. Responses -    are otherwise identical to their non-expanded counterparts. -    """ - -    serializer_class = InfractionSerializer -    queryset = Infraction.objects.all() -    filter_backends = (DjangoFilterBackend, SearchFilter) -    filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type') -    search_fields = ('$reason',) -    frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden') - -    def partial_update(self, request, *args, **kwargs): -        for field in request.data: -            if field in self.frozen_fields: -                raise ValidationError({field: ['This field cannot be updated.']}) - -        instance = self.get_object() -        serializer = self.get_serializer(instance, data=request.data, partial=True) -        serializer.is_valid(raise_exception=True) -        serializer.save() - -        return Response(serializer.data) - -    @action(url_path='expanded', detail=False) -    def list_expanded(self, *args, **kwargs): -        self.serializer_class = ExpandedInfractionSerializer -        return self.list(*args, **kwargs) - -    @list_expanded.mapping.post -    def create_expanded(self, *args, **kwargs): -        self.serializer_class = ExpandedInfractionSerializer -        return self.create(*args, **kwargs) - -    @action(url_path='expanded', url_name='detail-expanded', detail=True) -    def retrieve_expanded(self, *args, **kwargs): -        self.serializer_class = ExpandedInfractionSerializer -        return self.retrieve(*args, **kwargs) - -    @retrieve_expanded.mapping.patch -    def partial_update_expanded(self, *args, **kwargs): -        self.serializer_class = ExpandedInfractionSerializer -        return self.partial_update(*args, **kwargs) - - -class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): -    """ -    View of off-topic channel names used by the bot -    to rotate our off-topic names on a daily basis. - -    ## Routes -    ### GET /bot/off-topic-channel-names -    Return all known off-topic channel names from the database. -    If the `random_items` query parameter is given, for example using... -        $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 -    ... then the API will return `5` random items from the database. - -    #### Response format -    Return a list of off-topic-channel names: -    >>> [ -    ...     "lemons-lemonade-stand", -    ...     "bbq-with-bisk" -    ... ] - -    #### Status codes -    - 200: returned on success -    - 400: returned when `random_items` is not a positive integer - -    ### POST /bot/off-topic-channel-names -    Create a new off-topic-channel name in the database. -    The name must be given as a query parameter, for example: -        $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?name=lemons-lemonade-shop - -    #### Status codes -    - 201: returned on success -    - 400: if the request body has invalid fields, see the response for details - -    ### DELETE /bot/off-topic-channel-names/<name:str> -    Delete the off-topic-channel name with the given `name`. - -    #### Status codes -    - 204: returned on success -    - 404: returned when the given `name` was not found - -    ## Authentication -    Requires a API token. -    """ - -    lookup_field = 'name' -    serializer_class = OffTopicChannelNameSerializer - -    def get_object(self): -        queryset = self.get_queryset() -        name = self.kwargs[self.lookup_field] -        return get_object_or_404(queryset, name=name) - -    def get_queryset(self): -        return OffTopicChannelName.objects.all() - -    def create(self, request): -        if 'name' in request.query_params: -            create_data = {'name': request.query_params['name']} -            serializer = OffTopicChannelNameSerializer(data=create_data) -            serializer.is_valid(raise_exception=True) -            serializer.save() -            return Response(create_data, status=HTTP_201_CREATED) - -        else: -            raise ParseError(detail={ -                'name': ["This query parameter is required."] -            }) - -    def list(self, request):  # noqa -        if 'random_items' in request.query_params: -            param = request.query_params['random_items'] -            try: -                random_count = int(param) -            except ValueError: -                raise ParseError(detail={'random_items': ["Must be a valid integer."]}) - -            if random_count <= 0: -                raise ParseError(detail={ -                    'random_items': ["Must be a positive integer."] -                }) - -            queryset = self.get_queryset().order_by('?')[:random_count] -            serialized = self.serializer_class(queryset, many=True) -            return Response(serialized.data) - -        queryset = self.get_queryset() -        serialized = self.serializer_class(queryset, many=True) -        return Response(serialized.data) - - -class ReminderViewSet(CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet): -    """ -    View providing CRUD access to reminders. - -    ## Routes -    ### GET /bot/reminders -    Returns all reminders in the database. - -    #### Response format -    >>> [ -    ...     { -    ...         'active': True, -    ...         'author': 1020103901030, -    ...         'content': "Make dinner", -    ...         'expiration': '5018-11-20T15:52:00Z' -    ...         'id': 11 -    ...     }, -    ...     ... -    ... ] - -    #### Status codes -    - 200: returned on success - -    ### POST /bot/reminders -    Create a new reminder. - -    #### Request body -    >>> { -    ...     'author': int, -    ...     'content': str, -    ...     'expiration': str  # ISO-formatted datetime -    ... } - -    #### Status codes -    - 201: returned on success -    - 400: if the body format is invalid -    - 404: if no user with the given ID could be found - -    ### DELETE /bot/reminders/<id:int> -    Delete the reminder with the given `id`. - -    #### Status codes -    - 204: returned on success -    - 404: if a reminder with the given `id` does not exist - -    ## Authentication -    Requires an API token. -    """ - -    serializer_class = ReminderSerializer -    queryset = Reminder.objects.prefetch_related('author') -    filter_backends = (DjangoFilterBackend, SearchFilter) -    filter_fields = ('active', 'author__id') - - -class RoleViewSet(ModelViewSet): -    """ -    View providing CRUD access to the roles on our server, used -    by the bot to keep a mirror of our server's roles on the site. - -    ## Routes -    ### GET /bot/roles -    Returns all roles in the database. - -    #### Response format -    >>> [ -    ...     { -    ...         'id': 267628507062992896, -    ...         'name': "Admins", -    ...         'colour': 1337, -    ...         'permissions': 8 -    ...     } -    ... ] - -    #### Status codes -    - 200: returned on success - -    ### GET /bot/roles/<snowflake:int> -    Gets a single role by ID. - -    #### Response format -    >>> { -    ...     'id': 267628507062992896, -    ...     'name': "Admins", -    ...     'colour': 1337, -    ...     'permissions': 8 -    ... } - -    #### Status codes -    - 200: returned on success -    - 404: if a role with the given `snowflake` could not be found - -    ### POST /bot/roles -    Adds a single new role. - -    #### Request body -    >>> { -    ...     'id': int, -    ...     'name': str, -    ...     'colour': int, -    ...     'permissions': int, -    ... } - -    #### Status codes -    - 201: returned on success -    - 400: if the body format is invalid - -    ### PUT /bot/roles/<snowflake:int> -    Update the role with the given `snowflake`. -    All fields in the request body are required. - -    #### Request body -    >>> { -    ...     'id': int, -    ...     'name': str, -    ...     'colour': int, -    ...     'permissions': int -    ... } - -    #### Status codes -    - 200: returned on success -    - 400: if the request body was invalid - -    ### PATCH /bot/roles/<snowflake:int> -    Update the role with the given `snowflake`. -    All fields in the request body are required. - -    >>> { -    ...     'id': int, -    ...     'name': str, -    ...     'colour': int, -    ...     'permissions': int -    ... } - -    ### DELETE /bot/roles/<snowflake:int> -    Deletes the role with the given `snowflake`. - -    #### Status codes -    - 204: returned on success -    - 404: if a role with the given `snowflake` does not exist -    """ - -    queryset = Role.objects.all() -    serializer_class = RoleSerializer - - -class SnakeFactViewSet(ListModelMixin, GenericViewSet): -    """ -    View providing snake facts created by the Pydis community in the first code jam. - -    ## Routes -    ### GET /bot/snake-facts -    Returns snake facts from the database. - -    #### Response format -    >>> [ -    ...     {'fact': 'Snakes are dangerous'}, -    ...     {'fact': 'Except for Python, we all love it'} -    ... ] - -    #### Status codes -    - 200: returned on success - -    ## Authentication -    Requires an API token. -    """ - -    serializer_class = SnakeFactSerializer -    queryset = SnakeFact.objects.all() - - -class SnakeIdiomViewSet(ListModelMixin, GenericViewSet): -    """ -    View providing snake idioms for the snake cog. - -    ## Routes -    ### GET /bot/snake-idioms -    Returns snake idioms from the database. - -    #### Response format -    >>> [ -    ...    {'idiom': 'Sneky snek'}, -    ...    {'idiom': 'Snooky Snake'} -    ... ] - -    #### Status codes -    - 200: returned on success - -    ## Authentication -    Requires an API token -    """ - -    serializer_class = SnakeIdiomSerializer -    queryset = SnakeIdiom.objects.all() - - -class SnakeNameViewSet(ViewSet): -    """ -    View providing snake names for the bot's snake cog from our first code jam's winners. - -    ## Routes -    ### GET /bot/snake-names -    By default, return a single random snake name along with its name and scientific name. -    If the `get_all` query parameter is given, for example using... -        $ curl api.pythondiscord.local:8000/bot/snake-names?get_all=yes -    ... then the API will return all snake names and scientific names in the database. - -    #### Response format -    Without `get_all` query parameter: -    >>> { -    ...     'name': "Python", -    ...     'scientific': "Langus greatus" -    ... } - -    If the database is empty for whatever reason, this will return an empty dictionary. - -    With `get_all` query parameter: -    >>> [ -    ...     {'name': "Python 3", 'scientific': "Langus greatus"}, -    ...     {'name': "Python 2", 'scientific': "Langus decentus"} -    ... ] - -    #### Status codes -    - 200: returned on success - -    ## Authentication -    Requires a API token. -    """ - -    serializer_class = SnakeNameSerializer - -    def get_queryset(self): -        return SnakeName.objects.all() - -    def list(self, request):  # noqa -        if request.query_params.get('get_all'): -            queryset = self.get_queryset() -            serialized = self.serializer_class(queryset, many=True) -            return Response(serialized.data) - -        single_snake = SnakeName.objects.order_by('?').first() -        if single_snake is not None: -            body = { -                'name': single_snake.name, -                'scientific': single_snake.scientific -            } - -            return Response(body) - -        return Response({}) - - -class SpecialSnakeViewSet(ListModelMixin, GenericViewSet): -    """ -    View providing special snake names for our bot's snake cog. - -    ## Routes -    ### GET /bot/special-snakes -    Returns a list of special snake names. - -    #### Response Format -    >>> [ -    ...   { -    ...     'name': 'Snakky sneakatus', -    ...     'info': 'Scary snek', -    ...     'image': 'https://discordapp.com/assets/53ef346458017da2062aca5c7955946b.svg' -    ...   } -    ... ] - -    #### Status codes -    - 200: returned on success - -    ## Authentication -    Requires an API token. -    """ - -    serializer_class = SpecialSnakeSerializer -    queryset = SpecialSnake.objects.all() - - -class TagViewSet(ModelViewSet): -    """ -    View providing CRUD operations on tags shown by our bot. - -    ## Routes -    ### GET /bot/tags -    Returns all tags in the database. - -    #### Response format -    >>> [ -    ...     { -    ...         'title': "resources", -    ...         'embed': { -    ...             'content': "Did you really think I'd put something useful here?" -    ...         } -    ...     } -    ... ] - -    #### Status codes -    - 200: returned on success - -    ### GET /bot/tags/<title:str> -    Gets a single tag by its title. - -    #### Response format -    >>> { -    ...     'title': "My awesome tag", -    ...     'embed': { -    ...         'content': "totally not filler words" -    ...     } -    ... } - -    #### Status codes -    - 200: returned on success -    - 404: if a tag with the given `title` could not be found - -    ### POST /bot/tags -    Adds a single tag to the database. - -    #### Request body -    >>> { -    ...     'title': str, -    ...     'embed': dict -    ... } - -    The embed structure is the same as the embed structure that the Discord API -    expects. You can view the documentation for it here: -        https://discordapp.com/developers/docs/resources/channel#embed-object - -    #### Status codes -    - 201: returned on success -    - 400: if one of the given fields is invalid - -    ### PUT /bot/tags/<title:str> -    Update the tag with the given `title`. - -    #### Request body -    >>> { -    ...     'title': str, -    ...     'embed': dict -    ... } - -    The embed structure is the same as the embed structure that the Discord API -    expects. You can view the documentation for it here: -        https://discordapp.com/developers/docs/resources/channel#embed-object - -    #### Status codes -    - 200: returned on success -    - 400: if the request body was invalid, see response body for details -    - 404: if the tag with the given `title` could not be found - -    ### PATCH /bot/tags/<title:str> -    Update the tag with the given `title`. - -    #### Request body -    >>> { -    ...     'title': str, -    ...     'embed': dict -    ... } - -    The embed structure is the same as the embed structure that the Discord API -    expects. You can view the documentation for it here: -        https://discordapp.com/developers/docs/resources/channel#embed-object - -    #### Status codes -    - 200: returned on success -    - 400: if the request body was invalid, see response body for details -    - 404: if the tag with the given `title` could not be found - -    ### DELETE /bot/tags/<title:str> -    Deletes the tag with the given `title`. - -    #### Status codes -    - 204: returned on success -    - 404: if a tag with the given `title` does not exist -    """ - -    serializer_class = TagSerializer -    queryset = Tag.objects.all() - - -class UserViewSet(BulkCreateModelMixin, ModelViewSet): -    """ -    View providing CRUD operations on Discord users through the bot. - -    ## Routes -    ### GET /bot/users -    Returns all users currently known. - -    #### Response format -    >>> [ -    ...     { -    ...         'id': 409107086526644234, -    ...         'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", -    ...         'name': "Python", -    ...         'discriminator': 4329, -    ...         'roles': [ -    ...             352427296948486144, -    ...             270988689419665409, -    ...             277546923144249364, -    ...             458226699344019457 -    ...         ], -    ...         'in_guild': True -    ...     } -    ... ] - -    #### Status codes -    - 200: returned on success - -    ### GET /bot/users/<snowflake:int> -    Gets a single user by ID. - -    #### Response format -    >>> { -    ...     'id': 409107086526644234, -    ...     'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", -    ...     'name': "Python", -    ...     'discriminator': 4329, -    ...     'roles': [ -    ...         352427296948486144, -    ...         270988689419665409, -    ...         277546923144249364, -    ...         458226699344019457 -    ...     ], -    ...     'in_guild': True -    ... } - -    #### Status codes -    - 200: returned on success -    - 404: if a user with the given `snowflake` could not be found - -    ### POST /bot/users -    Adds a single or multiple new users. -    The roles attached to the user(s) must be roles known by the site. - -    #### Request body -    >>> { -    ...     'id': int, -    ...     'avatar': str, -    ...     'name': str, -    ...     'discriminator': int, -    ...     'roles': List[int], -    ...     'in_guild': bool -    ... } - -    Alternatively, request users can be POSTed as a list of above objects, -    in which case multiple users will be created at once. - -    #### Status codes -    - 201: returned on success -    - 400: if one of the given roles does not exist, or one of the given fields is invalid - -    ### PUT /bot/users/<snowflake:int> -    Update the user with the given `snowflake`. -    All fields in the request body are required. - -    #### Request body -    >>> { -    ...     'id': int, -    ...     'avatar': str, -    ...     'name': str, -    ...     'discriminator': int, -    ...     'roles': List[int], -    ...     'in_guild': bool -    ... } - -    #### Status codes -    - 200: returned on success -    - 400: if the request body was invalid, see response body for details -    - 404: if the user with the given `snowflake` could not be found - -    ### PATCH /bot/users/<snowflake:int> -    Update the user with the given `snowflake`. -    All fields in the request body are optional. - -    #### Request body -    >>> { -    ...     'id': int, -    ...     'avatar': str, -    ...     'name': str, -    ...     'discriminator': int, -    ...     'roles': List[int], -    ...     'in_guild': bool -    ... } - -    #### Status codes -    - 200: returned on success -    - 400: if the request body was invalid, see response body for details -    - 404: if the user with the given `snowflake` could not be found - -    ### DELETE /bot/users/<snowflake:int> -    Deletes the user with the given `snowflake`. - -    #### Status codes -    - 204: returned on success -    - 404: if a user with the given `snowflake` does not exist -    """ - -    serializer_class = UserSerializer -    queryset = User.objects.prefetch_related('roles') - - -class NominationViewSet(ModelViewSet): -    # TODO: doc me -    serializer_class = NominationSerializer -    queryset = Nomination.objects.prefetch_related('author', 'user') -    frozen_fields = ('author', 'inserted_at', 'user') - -    def update(self, request, *args, **kwargs): -        for field in request.data: -            if field in self.frozen_fields: -                raise ValidationError({field: ['This field cannot be updated.']}) - -        instance = self.get_object() -        serializer = self.get_serializer(instance, data=request.data, partial=True) -        serializer.is_valid(raise_exception=True) -        serializer.save() - -        return Response(serializer.data)  |