diff options
| author | 2020-08-27 00:59:39 +0200 | |
|---|---|---|
| committer | 2020-08-27 00:59:39 +0200 | |
| commit | 05a0575e28abfebb54a7f3996c182ae6ae091ab6 (patch) | |
| tree | 61fa0951297cd0745bda4b95a3c40f04e042f4ad /pydis_site/apps | |
| parent | OT: Rename variable `ext` to `other_names` (diff) | |
| parent | Merge pull request #374 from Numerlor/reminder-direct-retrieve (diff) | |
Merge branch 'master' into off-topic-non-random
Diffstat (limited to 'pydis_site/apps')
62 files changed, 969 insertions, 147 deletions
| diff --git a/pydis_site/apps/api/migrations/0007_tag.py b/pydis_site/apps/api/migrations/0007_tag.py index c22715f9..b6d146fe 100644 --- a/pydis_site/apps/api/migrations/0007_tag.py +++ b/pydis_site/apps/api/migrations/0007_tag.py @@ -18,6 +18,6 @@ class Migration(migrations.Migration):                  ('title', models.CharField(help_text='The title of this tag, shown in searches and providing a quick overview over what this embed contains.', max_length=100, primary_key=True, serialize=False)),                  ('embed', django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.')),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0009_snakefact.py b/pydis_site/apps/api/migrations/0009_snakefact.py index 4fc63bc9..fd583846 100644 --- a/pydis_site/apps/api/migrations/0009_snakefact.py +++ b/pydis_site/apps/api/migrations/0009_snakefact.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration):              fields=[                  ('fact', models.CharField(help_text='A fact about snakes.', max_length=200, primary_key=True, serialize=False)),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0010_snakeidiom.py b/pydis_site/apps/api/migrations/0010_snakeidiom.py index be089cf4..7d06ce5f 100644 --- a/pydis_site/apps/api/migrations/0010_snakeidiom.py +++ b/pydis_site/apps/api/migrations/0010_snakeidiom.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration):              fields=[                  ('idiom', models.CharField(help_text='A snake idiom', max_length=140, primary_key=True, serialize=False)),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0012_specialsnake.py b/pydis_site/apps/api/migrations/0012_specialsnake.py index 77072526..ed0c1563 100644 --- a/pydis_site/apps/api/migrations/0012_specialsnake.py +++ b/pydis_site/apps/api/migrations/0012_specialsnake.py @@ -17,6 +17,6 @@ class Migration(migrations.Migration):                  ('name', models.CharField(max_length=140, primary_key=True, serialize=False)),                  ('info', models.TextField()),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py b/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py index dced1288..7e372d04 100644 --- a/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py +++ b/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py @@ -19,6 +19,6 @@ class Migration(migrations.Migration):                  ('creation', models.DateTimeField(help_text='When this deletion took place.')),                  ('actor', models.ForeignKey(help_text='The original actor causing this deletion. Could be the author of a manual clean command invocation, the bot when executing automatic actions, or nothing to indicate that the bulk deletion was not issued by us.', null=True, on_delete=django.db.models.deletion.CASCADE, to='api.User')),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py index 4b028f0c..33746253 100644 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py @@ -25,6 +25,6 @@ class Migration(migrations.Migration):              options={                  'abstract': False,              }, -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0020_infraction.py b/pydis_site/apps/api/migrations/0020_infraction.py index 6bef6b77..96c71687 100644 --- a/pydis_site/apps/api/migrations/0020_infraction.py +++ b/pydis_site/apps/api/migrations/0020_infraction.py @@ -25,6 +25,6 @@ class Migration(migrations.Migration):                  ('actor', models.ForeignKey(help_text='The user which applied the infraction.', on_delete=django.db.models.deletion.CASCADE, related_name='infractions_given', to='api.User')),                  ('user', models.ForeignKey(help_text='The user to which the infraction was applied.', on_delete=django.db.models.deletion.CASCADE, related_name='infractions_received', to='api.User')),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py index 0c02cb91..c7fac012 100644 --- a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py +++ b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py @@ -1,7 +1,7 @@  # Generated by Django 2.1.4 on 2019-01-06 16:01 -import datetime  from django.db import migrations, models +from django.utils import timezone  class Migration(migrations.Migration): @@ -14,6 +14,6 @@ class Migration(migrations.Migration):          migrations.AlterField(              model_name='infraction',              name='inserted_at', -            field=models.DateTimeField(default=datetime.datetime.utcnow, help_text='The date and time of the creation of this infraction.'), +            field=models.DateTimeField(default=timezone.now, help_text='The date and time of the creation of this infraction.'),          ),      ] diff --git a/pydis_site/apps/api/migrations/0030_reminder.py b/pydis_site/apps/api/migrations/0030_reminder.py index 8c42f6dc..e1f1afc3 100644 --- a/pydis_site/apps/api/migrations/0030_reminder.py +++ b/pydis_site/apps/api/migrations/0030_reminder.py @@ -22,6 +22,6 @@ class Migration(migrations.Migration):                  ('expiration', models.DateTimeField(help_text='When this reminder should be sent.')),                  ('author', models.ForeignKey(help_text='The creator of this reminder.', on_delete=django.db.models.deletion.CASCADE, to='api.User')),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0031_nomination.py b/pydis_site/apps/api/migrations/0031_nomination.py index 75e69701..f39436c1 100644 --- a/pydis_site/apps/api/migrations/0031_nomination.py +++ b/pydis_site/apps/api/migrations/0031_nomination.py @@ -21,6 +21,6 @@ class Migration(migrations.Migration):                  ('inserted_at', models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination.')),                  ('author', models.ForeignKey(help_text='The staff member that nominated this user.', on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', to='api.User')),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0032_botsetting.py b/pydis_site/apps/api/migrations/0032_botsetting.py index 25186a2b..3304edef 100644 --- a/pydis_site/apps/api/migrations/0032_botsetting.py +++ b/pydis_site/apps/api/migrations/0032_botsetting.py @@ -18,6 +18,6 @@ class Migration(migrations.Migration):                  ('name', models.CharField(max_length=50, primary_key=True, serialize=False)),                  ('data', django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual settings of this setting.')),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py index a8256a0e..c9a1ad19 100644 --- a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py +++ b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py @@ -24,6 +24,6 @@ class Migration(migrations.Migration):                  ('line', models.PositiveSmallIntegerField(help_text='The line at which the log line was emitted.')),                  ('message', models.TextField(help_text='The textual content of the log line.')),              ], -            bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0049_offensivemessage.py b/pydis_site/apps/api/migrations/0049_offensivemessage.py index fe4a1961..f342cec3 100644 --- a/pydis_site/apps/api/migrations/0049_offensivemessage.py +++ b/pydis_site/apps/api/migrations/0049_offensivemessage.py @@ -3,7 +3,7 @@  import django.core.validators  from django.db import migrations, models  import pydis_site.apps.api.models.bot.offensive_message -import pydis_site.apps.api.models.utils +import pydis_site.apps.api.models.mixins  class Migration(migrations.Migration): @@ -20,6 +20,6 @@ class Migration(migrations.Migration):                  ('channel_id', models.BigIntegerField(help_text='The channel ID that the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])),                  ('delete_date', models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator])),              ], -            bases=(pydis_site.apps.api.models.utils.ModelReprMixin, models.Model), +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),          ),      ] diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py new file mode 100644 index 00000000..e617e1c9 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.4 on 2020-03-21 17:05 + +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations +import pydis_site.apps.api.models.bot.tag + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0050_remove_infractions_active_default_value'), +    ] + +    operations = [ +        migrations.AlterField( +            model_name='deletedmessage', +            name='embeds', +            field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), blank=True, help_text='Embeds attached to this message.', size=None), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py new file mode 100644 index 00000000..26b3b954 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0051_create_news_setting'), +    ] + +    operations = [ +        migrations.RemoveField( +            model_name='user', +            name='avatar_hash', +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py new file mode 100644 index 00000000..7ff3a548 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0052_remove_user_avatar_hash'), +    ] + +    operations = [ +        migrations.RemoveField( +            model_name='user', +            name='roles', +        ), +        migrations.AddField( +            model_name='user', +            name='roles', +            field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py new file mode 100644 index 00000000..96230015 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0053_user_roles_to_array'), +    ] + +    operations = [ +        migrations.AlterField( +            model_name='user', +            name='roles', +            field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py new file mode 100644 index 00000000..f2a0e638 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0051_allow_blank_message_embeds'), +        ('api', '0054_user_invalidate_unknown_role'), +    ] + +    operations = [ +    ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py new file mode 100644 index 00000000..d73b450d --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0054_user_invalidate_unknown_role'), +    ] + +    operations = [ +        migrations.AddField( +            model_name='reminder', +            name='mentions', +            field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py new file mode 100644 index 00000000..489941c7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0055_merge_20200714_2027'), +    ] + +    operations = [ +        migrations.AlterField( +            model_name='user', +            name='roles', +            field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py new file mode 100644 index 00000000..47a6d2d4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.14 on 2020-07-16 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0055_reminder_mentions'), +        ('api', '0056_allow_blank_user_roles'), +    ] + +    operations = [ +    ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py new file mode 100644 index 00000000..aecfdad7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): +    dependencies = [ +        ('api', '0057_merge_20200716_0751'), +    ] + +    operations = [ +        migrations.CreateModel( +            name='FilterList', +            fields=[ +                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), +                ('created_at', models.DateTimeField(auto_now_add=True)), +                ('updated_at', models.DateTimeField(auto_now=True)), +                ('type', models.CharField( +                    choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), +                             ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], +                    help_text='The type of allowlist this is on.', max_length=50)), +                ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), +                ('content', models.TextField(help_text='The data to add to the allow or denylist.')), +                ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), +            ], +            bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), +        ), +        migrations.AddConstraint( +            model_name='filterlist', +            constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') +        ) +    ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py new file mode 100644 index 00000000..8c550191 --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ +    ("discord.gg/python", "Python Discord", True), +    ("discord.gg/4JJdJKb", "RLBot", True), +    ("discord.gg/djPtTRJ", "Kivy", True), +    ("discord.gg/QXyegWe", "Pyglet", True), +    ("discord.gg/9XsucTT", "Panda3D", True), +    ("discord.gg/AP3rq2k", "PyWeek", True), +    ("discord.gg/vSPsP9t", "Microsoft Python", True), +    ("discord.gg/bRCvFy9", "Discord.js Official", True), +    ("discord.gg/9zT7NHP", "Programming Discussions", True), +    ("discord.gg/ysd6M4r", "JetBrains Community", True), +    ("discord.gg/4xJeCgy", "Raspberry Pie", True), +    ("discord.gg/AStb3kZ", "Ren'Py", True), +    ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), +    ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), +    ("discord.gg/jTtgWuy", "Django", True), +    ("discord.gg/W9BypZF", "STEM", True), +    ("discord.gg/dpy", "discord.py", True), +    ("discord.gg/programming", "Programmers Hangout", True), +    ("discord.gg/qhGUjGD", "SpeakJS", True), +    ("discord.gg/eTbWSZj", "Functional Programming", True), +    ("discord.gg/r8yreB6", "PyGame", True), +    ("discord.gg/5UBnR3P", "Python Atlanta", True), +    ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ +    ("pornhub.com", None, False), +    ("liveleak.com", None, False), +    ("grabify.link", None, False), +    ("bmwforum.co", None, False), +    ("leancoding.co", None, False), +    ("spottyfly.com", None, False), +    ("stopify.co", None, False), +    ("yoütu.be", None, False), +    ("discörd.com", None, False), +    ("minecräft.com", None, False), +    ("freegiftcards.co", None, False), +    ("disçordapp.com", None, False), +    ("fortnight.space", None, False), +    ("fortnitechat.site", None, False), +    ("joinmy.site", None, False), +    ("curiouscat.club", None, False), +    ("catsnthings.fun", None, False), +    ("yourtube.site", None, False), +    ("youtubeshort.watch", None, False), +    ("catsnthing.com", None, False), +    ("youtubeshort.pro", None, False), +    ("canadianlumberjacks.online", None, False), +    ("poweredbydialup.club", None, False), +    ("poweredbydialup.online", None, False), +    ("poweredbysecurity.org", None, False), +    ("poweredbysecurity.online", None, False), +    ("ssteam.site", None, False), +    ("steamwalletgift.com", None, False), +    ("discord.gift", None, False), +    ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ +    ("\bgoo+ks*\b", None, False), +    ("\bky+s+\b", None, False), +    ("\bki+ke+s*\b", None, False), +    ("\bbeaner+s?\b", None, False), +    ("\bcoo+ns*\b", None, False), +    ("\bnig+lets*\b", None, False), +    ("\bslant-eyes*\b", None, False), +    ("\btowe?l-?head+s*\b", None, False), +    ("\bchi*n+k+s*\b", None, False), +    ("\bspick*s*\b", None, False), +    ("\bkill* +(?:yo)?urself+\b", None, False), +    ("\bjew+s*\b", None, False), +    ("\bsuicide\b", None, False), +    ("\brape\b", None, False), +    ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), +    ("\bta+r+d+\b", None, False), +    ("\bcunts*\b", None, False), +    ("\btrann*y\b", None, False), +    ("\bshemale\b", None, False), +    ("fa+g+s*", None, False), +    ("卐", None, False), +    ("卍", None, False), +    ("࿖", None, False), +    ("࿕", None, False), +    ("࿘", None, False), +    ("࿗", None, False), +    ("cuck(?!oo+)", None, False), +    ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), +    ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ +    (".3gp", None, True), +    (".3g2", None, True), +    (".avi", None, True), +    (".bmp", None, True), +    (".gif", None, True), +    (".h264", None, True), +    (".jpg", None, True), +    (".jpeg", None, True), +    (".m4v", None, True), +    (".mkv", None, True), +    (".mov", None, True), +    (".mp4", None, True), +    (".mpeg", None, True), +    (".mpg", None, True), +    (".png", None, True), +    (".tiff", None, True), +    (".wmv", None, True), +    (".svg", None, True), +    (".psd", "Photoshop", True), +    (".ai", "Illustrator", True), +    (".aep", "After Effects", True), +    (".xcf", "GIMP", True), +    (".mp3", None, True), +    (".wav", None, True), +    (".ogg", None, True), +    (".webm", None, True), +    (".webp", None, True), +] + +populate_data = { +    "FILTER_TOKEN": filter_token_blacklist, +    "DOMAIN_NAME": domain_name_blacklist, +    "FILE_FORMAT": file_format_whitelist, +    "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): +    dependencies = [("api", "0058_create_new_filterlist_model")] + +    def populate_filterlists(app, _): +        FilterList = app.get_model("api", "FilterList") + +        for filterlist_type, metadata in populate_data.items(): +            for content, comment, allowed in metadata: +                FilterList.objects.create( +                    type=filterlist_type, +                    allowed=allowed, +                    content=content, +                    comment=comment, +                ) + +    def clear_filterlists(app, _): +        FilterList = app.get_model("api", "FilterList") +        FilterList.objects.all().delete() + +    operations = [ +        migrations.RunPython(populate_filterlists, clear_filterlists) +    ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py new file mode 100644 index 00000000..53846f02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ +    ("discord.gg/python", "Python Discord", True), +    ("discord.gg/4JJdJKb", "RLBot", True), +    ("discord.gg/djPtTRJ", "Kivy", True), +    ("discord.gg/QXyegWe", "Pyglet", True), +    ("discord.gg/9XsucTT", "Panda3D", True), +    ("discord.gg/AP3rq2k", "PyWeek", True), +    ("discord.gg/vSPsP9t", "Microsoft Python", True), +    ("discord.gg/bRCvFy9", "Discord.js Official", True), +    ("discord.gg/9zT7NHP", "Programming Discussions", True), +    ("discord.gg/ysd6M4r", "JetBrains Community", True), +    ("discord.gg/4xJeCgy", "Raspberry Pie", True), +    ("discord.gg/AStb3kZ", "Ren'Py", True), +    ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), +    ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), +    ("discord.gg/jTtgWuy", "Django", True), +    ("discord.gg/W9BypZF", "STEM", True), +    ("discord.gg/dpy", "discord.py", True), +    ("discord.gg/programming", "Programmers Hangout", True), +    ("discord.gg/qhGUjGD", "SpeakJS", True), +    ("discord.gg/eTbWSZj", "Functional Programming", True), +    ("discord.gg/r8yreB6", "PyGame", True), +    ("discord.gg/5UBnR3P", "Python Atlanta", True), +    ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ +    ("267624335836053506", "Python Discord", True), +    ("348658686962696195", "RLBot", True), +    ("423249981340778496", "Kivy", True), +    ("438622377094414346", "Pyglet", True), +    ("524691714909274162", "Panda3D", True), +    ("666560367173828639", "PyWeek", True), +    ("702724176489873509", "Microsoft Python", True), +    ("222078108977594368", "Discord.js Official", True), +    ("238666723824238602", "Programming Discussions", True), +    ("433980600391696384", "JetBrains Community", True), +    ("204621105720328193", "Raspberry Pie", True), +    ("286633898581164032", "Ren'Py", True), +    ("440186186024222721", "Python Discord: Emojis 1", True), +    ("578587418123304970", "Python Discord: Emojis 2", True), +    ("159039020565790721", "Django", True), +    ("273944235143593984", "STEM", True), +    ("336642139381301249", "discord.py", True), +    ("244230771232079873", "Programmers Hangout", True), +    ("239433591950540801", "SpeakJS", True), +    ("280033776820813825", "Functional Programming", True), +    ("349505959032389632", "PyGame", True), +    ("488751051629920277", "Python Atlanta", True), +    ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): +    dependencies = [("api", "0059_populate_filterlists")] + +    def fix_filterlist(app, _): +        FilterList = app.get_model("api", "FilterList") +        FilterList.objects.filter(type="GUILD_INVITE").delete()  # Clear out the stuff added in 0059. + +        for content, comment, allowed in guild_invite_whitelist: +            FilterList.objects.create( +                type="GUILD_INVITE", +                allowed=allowed, +                content=content, +                comment=comment, +            ) + +    def restore_bad_filterlist(app, _): +        FilterList = app.get_model("api", "FilterList") +        FilterList.objects.filter(type="GUILD_INVITE").delete() + +        for content, comment, allowed in bad_guild_invite_whitelist: +            FilterList.objects.create( +                type="GUILD_INVITE", +                allowed=allowed, +                content=content, +                comment=comment, +            ) + +    operations = [ +        migrations.RunPython(fix_filterlist, restore_bad_filterlist) +    ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 450d18cd..1d0ab7ea 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -1,5 +1,6 @@  # flake8: noqa  from .bot import ( +    FilterList,      BotSetting,      DocumentationLink,      DeletedMessage, @@ -15,4 +16,3 @@ from .bot import (      User  )  from .log_entry import LogEntry -from .utils import ModelReprMixin diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 8ae47746..efd98184 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -1,4 +1,5 @@  # flake8: noqa +from .filter_list import FilterList  from .bot_setting import BotSetting  from .deleted_message import DeletedMessage  from .documentation_link import DocumentationLink diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py index 8d48eac7..2a3944f8 100644 --- a/pydis_site/apps/api/models/bot/bot_setting.py +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -2,7 +2,7 @@ from django.contrib.postgres import fields as pgfields  from django.core.exceptions import ValidationError  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  def validate_bot_setting_name(name: str) -> None: diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py index f844ae04..5a46460b 100644 --- a/pydis_site/apps/api/models/bot/documentation_link.py +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -1,6 +1,6 @@  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class DocumentationLink(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py new file mode 100644 index 00000000..d279e137 --- /dev/null +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -0,0 +1,41 @@ +from django.db import models + +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin + + +class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): +    """An item that is either allowed or denied.""" + +    FilterListType = models.TextChoices( +        'FilterListType', +        'GUILD_INVITE ' +        'FILE_FORMAT ' +        'DOMAIN_NAME ' +        'FILTER_TOKEN ' +    ) +    type = models.CharField( +        max_length=50, +        help_text="The type of allowlist this is on.", +        choices=FilterListType.choices, +    ) +    allowed = models.BooleanField( +        help_text="Whether this item is on the allowlist or the denylist." +    ) +    content = models.TextField( +        help_text="The data to add to the allow or denylist." +    ) +    comment = models.TextField( +        help_text="Optional comment on this entry.", +        null=True +    ) + +    class Meta: +        """Metaconfig for this model.""" + +        # This constraint ensures only one filterlist with the +        # same content can exist. This means that we cannot have both an allow +        # and a deny for the same item, and we cannot have duplicates of the +        # same item. +        constraints = [ +            models.UniqueConstraint(fields=['content', 'type'], name='unique_filter_list'), +        ] diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index f58e89a3..7660cbba 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -2,7 +2,7 @@ from django.db import models  from django.utils import timezone  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Infraction(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 8b18fc9f..78dcbf1d 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -7,7 +7,7 @@ from django.utils import timezone  from pydis_site.apps.api.models.bot.tag import validate_tag_embed  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Message(ModelReprMixin, models.Model): @@ -49,6 +49,7 @@ class Message(ModelReprMixin, models.Model):          pgfields.JSONField(              validators=(validate_tag_embed,)          ), +        blank=True,          help_text="Embeds attached to this message."      )      attachments = pgfields.ArrayField( diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 44a0c8ae..04ae8d34 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,7 +1,7 @@  from django.db import models  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class MessageDeletionContext(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index cd9951aa..21e34e87 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -1,7 +1,7 @@  from django.db import models  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Nomination(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 413cbfae..403c7465 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -1,7 +1,7 @@  from django.core.validators import RegexValidator  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class OffTopicChannelName(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py index b466d9c2..6c0e5ffb 100644 --- a/pydis_site/apps/api/models/bot/offensive_message.py +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError  from django.core.validators import MinValueValidator  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  def future_date_validator(date: datetime.date) -> None: diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index d53fedb5..7d968a0e 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -1,8 +1,9 @@ +from django.contrib.postgres.fields import ArrayField  from django.core.validators import MinValueValidator  from django.db import models  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Reminder(ModelReprMixin, models.Model): @@ -45,6 +46,19 @@ class Reminder(ModelReprMixin, models.Model):      expiration = models.DateTimeField(          help_text="When this reminder should be sent."      ) +    mentions = ArrayField( +        models.BigIntegerField( +            validators=( +                MinValueValidator( +                    limit_value=0, +                    message="Mention IDs cannot be negative." +                ), +            ) +        ), +        default=list, +        blank=True, +        help_text="IDs of roles or users to ping with the reminder." +    )      def __str__(self):          """Returns some info on the current reminder, for display purposes.""" diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index 58bbf8b4..721e4815 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -3,7 +3,7 @@ from __future__ import annotations  from django.core.validators import MaxValueValidator, MinValueValidator  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Role(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py index 5d4cc393..5e53582f 100644 --- a/pydis_site/apps/api/models/bot/tag.py +++ b/pydis_site/apps/api/models/bot/tag.py @@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError  from django.core.validators import MaxLengthValidator, MinLengthValidator  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  def is_bool_validator(value: Any) -> None: diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index 5140d2bf..cd2d58b9 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -1,8 +1,18 @@ +from django.contrib.postgres.fields import ArrayField +from django.core.exceptions import ValidationError  from django.core.validators import MaxValueValidator, MinValueValidator  from django.db import models  from pydis_site.apps.api.models.bot.role import Role -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin + + +def _validate_existing_role(value: int) -> None: +    """Validate that a role exists when given in to the user model.""" +    role = Role.objects.filter(id=value) + +    if not role: +        raise ValidationError(f"Role with ID {value} does not exist")  class User(ModelReprMixin, models.Model): @@ -31,17 +41,19 @@ class User(ModelReprMixin, models.Model):          ),          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." +    roles = ArrayField( +        models.BigIntegerField( +            validators=( +                MinValueValidator( +                    limit_value=0, +                    message="Role IDs cannot be negative." +                ), +                _validate_existing_role +            )          ), -        null=True -    ) -    roles = models.ManyToManyField( -        Role, -        help_text="Any roles this user has on our server." +        default=list, +        blank=True, +        help_text="IDs of roles the user has on the server"      )      in_guild = models.BooleanField(          default=True, @@ -59,7 +71,7 @@ class User(ModelReprMixin, models.Model):          This will fall back to the Developers role if the user does not have any roles.          """ -        roles = self.roles.all() +        roles = Role.objects.filter(id__in=self.roles)          if not roles:              return Role.objects.get(name="Developers") -        return max(self.roles.all()) +        return max(roles) diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py index 488af48e..752cd2ca 100644 --- a/pydis_site/apps/api/models/log_entry.py +++ b/pydis_site/apps/api/models/log_entry.py @@ -1,7 +1,7 @@  from django.db import models  from django.utils import timezone -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class LogEntry(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/mixins.py index 0540c4de..5d75b78b 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,5 +1,7 @@  from operator import itemgetter +from django.db import models +  class ModelReprMixin:      """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" @@ -15,3 +17,15 @@ class ModelReprMixin:              if not attribute.startswith('_')          )          return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): +    """Mixin providing created_at and updated_at fields.""" + +    created_at = models.DateTimeField(auto_now_add=True) +    updated_at = models.DateTimeField(auto_now=True) + +    class Meta: +        """Metaconfig for the mixin.""" + +        abstract = True diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index e11c4af2..52e0d972 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -4,13 +4,20 @@ from rest_framework.validators import UniqueTogetherValidator  from rest_framework_bulk import BulkSerializerMixin  from .models import ( -    BotSetting, DeletedMessage, -    DocumentationLink, Infraction, -    LogEntry, MessageDeletionContext, -    Nomination, OffTopicChannelName, +    BotSetting, +    DeletedMessage, +    DocumentationLink, +    FilterList, +    Infraction, +    LogEntry, +    MessageDeletionContext, +    Nomination, +    OffTopicChannelName,      OffensiveMessage, -    Reminder, Role, -    Tag, User +    Reminder, +    Role, +    Tag, +    User  ) @@ -97,6 +104,31 @@ class DocumentationLinkSerializer(ModelSerializer):          fields = ('package', 'base_url', 'inventory_url') +class FilterListSerializer(ModelSerializer): +    """A class providing (de-)serialization of `FilterList` instances.""" + +    class Meta: +        """Metadata defined for the Django REST Framework.""" + +        model = FilterList +        fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content', 'comment') + +        # This validator ensures only one filterlist with the +        # same content can exist. This means that we cannot have both an allow +        # and a deny for the same item, and we cannot have duplicates of the +        # same item. +        validators = [ +            UniqueTogetherValidator( +                queryset=FilterList.objects.all(), +                fields=['content', 'type'], +                message=( +                    "A filterlist for this item already exists. " +                    "Please note that you cannot add the same item to both allow and deny." +                ) +            ), +        ] + +  class InfractionSerializer(ModelSerializer):      """A class providing (de-)serialization of `Infraction` instances.""" @@ -203,7 +235,9 @@ class ReminderSerializer(ModelSerializer):          """Metadata defined for the Django REST Framework."""          model = Reminder -        fields = ('active', 'author', 'jump_url', 'channel_id', 'content', 'expiration', 'id') +        fields = ( +            'active', 'author', 'jump_url', 'channel_id', 'content', 'expiration', 'id', 'mentions' +        )  class RoleSerializer(ModelSerializer): @@ -229,13 +263,11 @@ class TagSerializer(ModelSerializer):  class UserSerializer(BulkSerializerMixin, ModelSerializer):      """A class providing (de-)serialization of `User` instances.""" -    roles = PrimaryKeyRelatedField(many=True, queryset=Role.objects.all(), required=False) -      class Meta:          """Metadata defined for the Django REST Framework."""          model = User -        fields = ('id', 'avatar_hash', 'name', 'discriminator', 'roles', 'in_guild') +        fields = ('id', 'name', 'discriminator', 'roles', 'in_guild')          depth = 1 diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index fb93cae6..f079a8dd 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -13,7 +13,6 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase):              id=55,              name='Robbie Rotten',              discriminator=55, -            avatar_hash=None          )          cls.data = { @@ -54,7 +53,6 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase):              id=12904,              name='Joe Armstrong',              discriminator=1245, -            avatar_hash=None          )          cls.data = { diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py new file mode 100644 index 00000000..188c0fff --- /dev/null +++ b/pydis_site/apps/api/tests/test_filterlists.py @@ -0,0 +1,122 @@ +from django_hosts.resolvers import reverse + +from pydis_site.apps.api.models import FilterList +from pydis_site.apps.api.tests.base import APISubdomainTestCase + +URL = reverse('bot:filterlist-list', host='api') +JPEG_ALLOWLIST = { +    "type": 'FILE_FORMAT', +    "allowed": True, +    "content": ".jpeg", +} +PNG_ALLOWLIST = { +    "type": 'FILE_FORMAT', +    "allowed": True, +    "content": ".png", +} + + +class UnauthenticatedTests(APISubdomainTestCase): +    def setUp(self): +        super().setUp() +        self.client.force_authenticate(user=None) + +    def test_cannot_read_allowedlist_list(self): +        response = self.client.get(URL) + +        self.assertEqual(response.status_code, 401) + + +class EmptyDatabaseTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        FilterList.objects.all().delete() + +    def test_returns_empty_object(self): +        response = self.client.get(URL) + +        self.assertEqual(response.status_code, 200) +        self.assertEqual(response.json(), []) + + +class FetchTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        FilterList.objects.all().delete() +        cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST) +        cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST) + +    def test_returns_name_in_list(self): +        response = self.client.get(URL) + +        self.assertEqual(response.status_code, 200) +        self.assertEqual(response.json()[0]["content"], self.jpeg_format.content) +        self.assertEqual(response.json()[1]["content"], self.png_format.content) + +    def test_returns_single_item_by_id(self): +        response = self.client.get(f'{URL}/{self.jpeg_format.id}') + +        self.assertEqual(response.status_code, 200) +        self.assertEqual(response.json().get("content"), self.jpeg_format.content) + +    def test_returns_filter_list_types(self): +        response = self.client.get(f'{URL}/get-types') + +        self.assertEqual(response.status_code, 200) +        for api_type, model_type in zip(response.json(), FilterList.FilterListType.choices): +            self.assertEquals(api_type[0], model_type[0]) +            self.assertEquals(api_type[1], model_type[1]) + + +class CreationTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        FilterList.objects.all().delete() + +    def test_returns_400_for_missing_params(self): +        no_type_json = { +            "allowed": True, +            "content": ".jpeg" +        } +        no_allowed_json = { +            "type": "FILE_FORMAT", +            "content": ".jpeg" +        } +        no_content_json = { +            "allowed": True, +            "type": "FILE_FORMAT" +        } +        cases = [{}, no_type_json, no_allowed_json, no_content_json] + +        for case in cases: +            with self.subTest(case=case): +                response = self.client.post(URL, data=case) +                self.assertEqual(response.status_code, 400) + +    def test_returns_201_for_successful_creation(self): +        response = self.client.post(URL, data=JPEG_ALLOWLIST) +        self.assertEqual(response.status_code, 201) + +    def test_returns_400_for_duplicate_creation(self): +        self.client.post(URL, data=JPEG_ALLOWLIST) +        response = self.client.post(URL, data=JPEG_ALLOWLIST) +        self.assertEqual(response.status_code, 400) + + +class DeletionTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        FilterList.objects.all().delete() +        cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST) +        cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST) + +    def test_deleting_unknown_id_returns_404(self): +        response = self.client.delete(f"{URL}/200") +        self.assertEqual(response.status_code, 404) + +    def test_deleting_known_id_returns_204(self): +        response = self.client.delete(f"{URL}/{self.jpeg_format.id}") +        self.assertEqual(response.status_code, 204) + +        response = self.client.get(f"{URL}/{self.jpeg_format.id}") +        self.assertNotIn(self.png_format.content, response.json()) diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index bc258b77..93ef8171 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -47,7 +47,6 @@ class InfractionTests(APISubdomainTestCase):              id=5,              name='james',              discriminator=1, -            avatar_hash=None          )          cls.ban_hidden = Infraction.objects.create(              user_id=cls.user.id, @@ -169,13 +168,11 @@ class CreationTests(APISubdomainTestCase):              id=5,              name='james',              discriminator=1, -            avatar_hash=None          )          cls.second_user = User.objects.create(              id=6,              name='carl',              discriminator=2, -            avatar_hash=None          )      def test_accepts_valid_data(self): @@ -522,7 +519,6 @@ class ExpandedTests(APISubdomainTestCase):              id=5,              name='james',              discriminator=1, -            avatar_hash=None          )          cls.kick = Infraction.objects.create(              user_id=cls.user.id, @@ -540,7 +536,7 @@ class ExpandedTests(APISubdomainTestCase):      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'): +            for field in ('id', 'name', 'discriminator', 'roles', 'in_guild'):                  self.assertTrue(field in obj, msg=f'field "{field}" missing from {key}')      def test_list_expanded(self): @@ -599,7 +595,6 @@ class SerializerTests(APISubdomainTestCase):              id=5,              name='james',              discriminator=1, -            avatar_hash=None          )      def create_infraction(self, _type: str, active: bool): diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index a97d3251..e0e347bb 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -3,13 +3,12 @@ from datetime import datetime as dt  from django.test import SimpleTestCase  from django.utils import timezone -from ..models import ( +from pydis_site.apps.api.models import (      DeletedMessage,      DocumentationLink,      Infraction,      Message,      MessageDeletionContext, -    ModelReprMixin,      Nomination,      OffTopicChannelName,      OffensiveMessage, @@ -18,6 +17,7 @@ from ..models import (      Tag,      User  ) +from pydis_site.apps.api.models.mixins import ModelReprMixin  class SimpleClass(ModelReprMixin): @@ -39,12 +39,14 @@ class StringDunderMethodTests(SimpleTestCase):          self.nomination = Nomination(              id=123,              actor=User( -                id=9876, name='Mr. Hemlock', -                discriminator=6666, avatar_hash=None +                id=9876, +                name='Mr. Hemlock', +                discriminator=6666,              ),              user=User( -                id=9876, name="Hemlock's Cat", -                discriminator=7777, avatar_hash=None +                id=9876, +                name="Hemlock's Cat", +                discriminator=7777,              ),              reason="He purrrrs like the best!",          ) @@ -53,15 +55,17 @@ class StringDunderMethodTests(SimpleTestCase):              DeletedMessage(                  id=45,                  author=User( -                    id=444, name='bill', -                    discriminator=5, avatar_hash=None +                    id=444, +                    name='bill', +                    discriminator=5,                  ),                  channel_id=666,                  content="wooey",                  deletion_context=MessageDeletionContext(                      actor=User( -                        id=5555, name='shawn', -                        discriminator=555, avatar_hash=None +                        id=5555, +                        name='shawn', +                        discriminator=555,                      ),                      creation=dt.utcnow()                  ), @@ -84,8 +88,9 @@ class StringDunderMethodTests(SimpleTestCase):              Message(                  id=45,                  author=User( -                    id=444, name='bill', -                    discriminator=5, avatar_hash=None +                    id=444, +                    name='bill', +                    discriminator=5,                  ),                  channel_id=666,                  content="wooey", @@ -93,8 +98,9 @@ class StringDunderMethodTests(SimpleTestCase):              ),              MessageDeletionContext(                  actor=User( -                    id=5555, name='shawn', -                    discriminator=555, avatar_hash=None +                    id=5555, +                    name='shawn', +                    discriminator=555,                  ),                  creation=dt.utcnow()              ), @@ -103,22 +109,29 @@ class StringDunderMethodTests(SimpleTestCase):                  embed={'content': "the builder"}              ),              User( -                id=5, name='bob', -                discriminator=1, avatar_hash=None +                id=5, +                name='bob', +                discriminator=1,              ),              Infraction( -                user_id=5, actor_id=5, -                type='kick', reason='He terk my jerb!' +                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!', +                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 +                    id=452, +                    name='billy', +                    discriminator=5,                  ),                  channel_id=555,                  jump_url=( diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 76cb4112..92c62c87 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -13,7 +13,6 @@ class CreationTests(APISubdomainTestCase):              id=1234,              name='joe dart',              discriminator=1111, -            avatar_hash=None          )      def test_accepts_valid_data(self): @@ -190,7 +189,6 @@ class NominationTests(APISubdomainTestCase):              id=1234,              name='joe dart',              discriminator=1111, -            avatar_hash=None          )          cls.active_nomination = Nomination.objects.create( diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index 3441e0cc..9dffb668 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -53,7 +53,6 @@ class ReminderCreationTests(APISubdomainTestCase):              id=1234,              name='Mermaid Man',              discriminator=1234, -            avatar_hash=None,          )      def test_accepts_valid_data(self): @@ -63,6 +62,7 @@ class ReminderCreationTests(APISubdomainTestCase):              'expiration': datetime.utcnow().isoformat(),              'jump_url': "https://www.google.com",              'channel_id': 123, +            'mentions': [8888, 9999],          }          url = reverse('bot:reminder-list', host='api')          response = self.client.post(url, data=data) @@ -86,7 +86,6 @@ class ReminderDeletionTests(APISubdomainTestCase):              id=6789,              name='Barnacle Boy',              discriminator=6789, -            avatar_hash=None,          )          cls.reminder = Reminder.objects.create( @@ -118,7 +117,6 @@ class ReminderListTests(APISubdomainTestCase):              id=6789,              name='Patrick Star',              discriminator=6789, -            avatar_hash=None,          )          cls.reminder_one = Reminder.objects.create( @@ -165,6 +163,34 @@ class ReminderListTests(APISubdomainTestCase):          self.assertEqual(response.json(), [self.rem_dict_one]) +class ReminderRetrieveTests(APISubdomainTestCase): +    @classmethod +    def setUpTestData(cls): +        cls.author = User.objects.create( +            id=6789, +            name='Reminder author', +            discriminator=6789, +        ) + +        cls.reminder = Reminder.objects.create( +            author=cls.author, +            content="Reminder content", +            expiration=datetime.utcnow().isoformat(), +            jump_url="http://example.com/", +            channel_id=123 +        ) + +    def test_retrieve_unknown_returns_404(self): +        url = reverse('bot:reminder-detail', args=("not_an_id",), host='api') +        response = self.client.get(url) +        self.assertEqual(response.status_code, 404) + +    def test_retrieve_known_returns_200(self): +        url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') +        response = self.client.get(url) +        self.assertEqual(response.status_code, 200) + +  class ReminderUpdateTests(APISubdomainTestCase):      @classmethod      def setUpTestData(cls): @@ -172,7 +198,6 @@ class ReminderUpdateTests(APISubdomainTestCase):              id=666,              name='Man Ray',              discriminator=666, -            avatar_hash=None,          )          cls.reminder = Reminder.objects.create( diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 86799f19..4c0f6e27 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -49,7 +49,6 @@ class CreationTests(APISubdomainTestCase):          url = reverse('bot:user-list', host='api')          data = {              'id': 42, -            'avatar_hash': "validavatarhashiswear",              'name': "Test",              'discriminator': 42,              'roles': [ @@ -63,7 +62,6 @@ class CreationTests(APISubdomainTestCase):          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']) @@ -73,7 +71,6 @@ class CreationTests(APISubdomainTestCase):          data = [              {                  'id': 5, -                'avatar_hash': "hahayes",                  'name': "test man",                  'discriminator': 42,                  'roles': [ @@ -83,7 +80,6 @@ class CreationTests(APISubdomainTestCase):              },              {                  'id': 8, -                'avatar_hash': "maybenot",                  'name': "another test man",                  'discriminator': 555,                  'roles': [], @@ -99,7 +95,6 @@ class CreationTests(APISubdomainTestCase):          url = reverse('bot:user-list', host='api')          data = {              'id': 5, -            'avatar_hash': "hahayes",              'name': "test man",              'discriminator': 42,              'roles': [ @@ -114,7 +109,6 @@ class CreationTests(APISubdomainTestCase):          url = reverse('bot:user-list', host='api')          data = {              'id': True, -            'avatar_hash': 1902831,              'discriminator': "totally!"          } @@ -148,16 +142,14 @@ class UserModelTests(APISubdomainTestCase):          )          cls.user_with_roles = User.objects.create(              id=1, -            avatar_hash="coolavatarhash",              name="Test User with two roles",              discriminator=1111,              in_guild=True,          ) -        cls.user_with_roles.roles.add(cls.role_bottom, cls.role_top) +        cls.user_with_roles.roles.extend([cls.role_bottom.id, cls.role_top.id])          cls.user_without_roles = User.objects.create(              id=2, -            avatar_hash="coolavatarhash",              name="Test User without roles",              discriminator=2222,              in_guild=True, diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 4a0281b4..a4fd5b2e 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -3,18 +3,28 @@ from rest_framework.routers import DefaultRouter  from .views import HealthcheckView, RulesView  from .viewsets import ( -    BotSettingViewSet, DeletedMessageViewSet, -    DocumentationLinkViewSet, InfractionViewSet, -    LogEntryViewSet, NominationViewSet, +    BotSettingViewSet, +    DeletedMessageViewSet, +    DocumentationLinkViewSet, +    FilterListViewSet, +    InfractionViewSet, +    LogEntryViewSet, +    NominationViewSet,      OffTopicChannelNameViewSet, -    OffensiveMessageViewSet, ReminderViewSet, -    RoleViewSet, TagViewSet, UserViewSet +    OffensiveMessageViewSet, +    ReminderViewSet, +    RoleViewSet, +    TagViewSet, +    UserViewSet  ) -  # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter  bot_router = DefaultRouter(trailing_slash=False)  bot_router.register( +    'filter-lists', +    FilterListViewSet +) +bot_router.register(      'bot-settings',      BotSettingViewSet  ) @@ -41,7 +51,7 @@ bot_router.register(  bot_router.register(      'off-topic-channel-names',      OffTopicChannelNameViewSet, -    base_name='offtopicchannelname' +    basename='offtopicchannelname'  )  bot_router.register(      'reminders', diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py index a73d4718..7ac56641 100644 --- a/pydis_site/apps/api/views.py +++ b/pydis_site/apps/api/views.py @@ -140,7 +140,8 @@ class RulesView(APIView):              ),              (                  "No spamming or unapproved advertising, including requests for paid work. " -                "Open-source projects can be showcased in #show-your-projects." +                "Open-source projects can be shared with others in #python-general and " +                "code reviews can be asked for in a help channel."              ),              (                  "Keep discussions relevant to channel topics and guidelines." diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index 3cf9f641..8699517e 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -1,5 +1,6 @@  # flake8: noqa  from .bot import ( +    FilterListViewSet,      BotSettingViewSet,      DeletedMessageViewSet,      DocumentationLinkViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index b3e0fa4d..e64e3988 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,4 +1,5 @@  # flake8: noqa +from .filter_list import FilterListViewSet  from .bot_setting import BotSettingViewSet  from .deleted_message import DeletedMessageViewSet  from .documentation_link import DocumentationLinkViewSet diff --git a/pydis_site/apps/api/viewsets/bot/filter_list.py b/pydis_site/apps/api/viewsets/bot/filter_list.py new file mode 100644 index 00000000..2cb21ab9 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/filter_list.py @@ -0,0 +1,97 @@ +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot.filter_list import FilterList +from pydis_site.apps.api.serializers import FilterListSerializer + + +class FilterListViewSet(ModelViewSet): +    """ +    View providing CRUD operations on items allowed or denied by our bot. + +    ## Routes +    ### GET /bot/filter-lists +    Returns all filterlist items in the database. + +    #### Response format +    >>> [ +    ...     { +    ...         'id': "2309268224", +    ...         'created_at': "01-01-2020 ...", +    ...         'updated_at': "01-01-2020 ...", +    ...         'type': "file_format", +    ...         'allowed': 'true', +    ...         'content': ".jpeg", +    ...         'comment': "Popular image format.", +    ...     }, +    ...     ... +    ... ] + +    #### Status codes +    - 200: returned on success +    - 401: returned if unauthenticated + +    ### GET /bot/filter-lists/<id:int> +    Returns a specific FilterList item from the database. + +    #### Response format +    >>> { +    ...     'id': "2309268224", +    ...     'created_at': "01-01-2020 ...", +    ...     'updated_at': "01-01-2020 ...", +    ...     'type': "file_format", +    ...     'allowed': 'true', +    ...     'content': ".jpeg", +    ...     'comment': "Popular image format.", +    ... } + +    #### Status codes +    - 200: returned on success +    - 404: returned if the id was not found. + +    ### GET /bot/filter-lists/get-types +    Returns a list of valid list types that can be used in POST requests. + +    #### Response format +    >>> [ +    ...     ["GUILD_INVITE","Guild Invite"], +    ...     ["FILE_FORMAT","File Format"], +    ...     ["DOMAIN_NAME","Domain Name"], +    ...     ["FILTER_TOKEN","Filter Token"] +    ... ] + +    #### Status codes +    - 200: returned on success + +    ### POST /bot/filter-lists +    Adds a single FilterList item to the database. + +    #### Request body +    >>> { +    ...    'type': str, +    ...    'allowed': bool, +    ...    'content': str, +    ...    'comment': Optional[str], +    ... } + +    #### Status codes +    - 201: returned on success +    - 400: if one of the given fields is invalid + +    ### DELETE /bot/filter-lists/<id:int> +    Deletes the FilterList item with the given `id`. + +    #### Status codes +    - 204: returned on success +    - 404: if a tag with the given `id` does not exist +    """ + +    serializer_class = FilterListSerializer +    queryset = FilterList.objects.all() + +    @action(detail=False, url_path='get-types', methods=["get"]) +    def get_types(self, _: Request) -> Response: +        """Get a list of all the types of FilterLists we support.""" +        return Response(FilterList.FilterListType.choices) diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 147f6dbc..111660d9 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -4,6 +4,7 @@ from rest_framework.mixins import (      CreateModelMixin,      DestroyModelMixin,      ListModelMixin, +    RetrieveModelMixin,      UpdateModelMixin  )  from rest_framework.viewsets import GenericViewSet @@ -13,7 +14,12 @@ from pydis_site.apps.api.serializers import ReminderSerializer  class ReminderViewSet( -    CreateModelMixin, ListModelMixin, DestroyModelMixin, UpdateModelMixin, GenericViewSet +    CreateModelMixin, +    RetrieveModelMixin, +    ListModelMixin, +    DestroyModelMixin, +    UpdateModelMixin, +    GenericViewSet,  ):      """      View providing CRUD access to reminders. @@ -27,9 +33,16 @@ class ReminderViewSet(      ...     {      ...         'active': True,      ...         'author': 1020103901030, +    ...         'mentions': [ +    ...             336843820513755157, +    ...             165023948638126080, +    ...             267628507062992896 +    ...         ],      ...         'content': "Make dinner",      ...         'expiration': '5018-11-20T15:52:00Z', -    ...         'id': 11 +    ...         'id': 11, +    ...         'channel_id': 634547009956872193, +    ...         'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>"      ...     },      ...     ...      ... ] @@ -37,14 +50,41 @@ class ReminderViewSet(      #### Status codes      - 200: returned on success +    ### GET /bot/reminders/<id:int> +    Fetches the reminder with the given id. + +    #### Response format +    >>> +    ... { +    ...     'active': True, +    ...     'author': 1020103901030, +    ...     'mentions': [ +    ...         336843820513755157, +    ...         165023948638126080, +    ...         267628507062992896 +    ...     ], +    ...     'content': "Make dinner", +    ...     'expiration': '5018-11-20T15:52:00Z', +    ...     'id': 11, +    ...     'channel_id': 634547009956872193, +    ...     'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>" +    ... } + +    #### Status codes +    - 200: returned on success +    - 404: returned when the reminder doesn't exist +      ### POST /bot/reminders      Create a new reminder.      #### Request body      >>> {      ...     'author': int, +    ...     'mentions': List[int],      ...     'content': str, -    ...     'expiration': str  # ISO-formatted datetime +    ...     'expiration': str,  # ISO-formatted datetime +    ...     'channel_id': int, +    ...     'jump_url': str      ... }      #### Status codes @@ -52,6 +92,22 @@ class ReminderViewSet(      - 400: if the body format is invalid      - 404: if no user with the given ID could be found +    ### PATCH /bot/reminders/<id:int> +    Update the user with the given `id`. +    All fields in the request body are optional. + +    #### Request body +    >>> { +    ...     'mentions': List[int], +    ...     'content': str, +    ...     'expiration': str  # ISO-formatted datetime +    ... } + +    #### Status codes +    - 200: 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`. diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index a407787e..9571b3d7 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -17,7 +17,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      >>> [      ...     {      ...         'id': 409107086526644234, -    ...         'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb",      ...         'name': "Python",      ...         'discriminator': 4329,      ...         'roles': [ @@ -39,7 +38,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      #### Response format      >>> {      ...     'id': 409107086526644234, -    ...     'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb",      ...     'name': "Python",      ...     'discriminator': 4329,      ...     'roles': [ @@ -62,7 +60,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      #### Request body      >>> {      ...     'id': int, -    ...     'avatar': str,      ...     'name': str,      ...     'discriminator': int,      ...     'roles': List[int], @@ -83,7 +80,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      #### Request body      >>> {      ...     'id': int, -    ...     'avatar': str,      ...     'name': str,      ...     'discriminator': int,      ...     'roles': List[int], @@ -102,7 +98,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      #### Request body      >>> {      ...     'id': int, -    ...     'avatar': str,      ...     'name': str,      ...     'discriminator': int,      ...     'roles': List[int], @@ -123,4 +118,4 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):      """      serializer_class = UserSerializer -    queryset = User.objects.prefetch_related('roles') +    queryset = User.objects diff --git a/pydis_site/apps/home/forms/account_deletion.py b/pydis_site/apps/home/forms/account_deletion.py index 17ffe5c1..eec70bea 100644 --- a/pydis_site/apps/home/forms/account_deletion.py +++ b/pydis_site/apps/home/forms/account_deletion.py @@ -1,23 +1,9 @@ -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout  from django.forms import CharField, Form -from django_crispy_bulma.layout import IconField, Submit  class AccountDeletionForm(Form):      """Account deletion form, to collect username for confirmation of removal.""" -    def __init__(self, *args, **kwargs): -        super().__init__(*args, **kwargs) -        self.helper = FormHelper() - -        self.helper.form_method = "post" -        self.helper.add_input(Submit("submit", "I understand, delete my account")) - -        self.helper.layout = Layout( -            IconField("username", icon_prepend="user") -        ) -      username = CharField(          label="Username",          required=True diff --git a/pydis_site/apps/home/signals.py b/pydis_site/apps/home/signals.py index 4cb4564b..8af48c15 100644 --- a/pydis_site/apps/home/signals.py +++ b/pydis_site/apps/home/signals.py @@ -150,7 +150,7 @@ class AllauthSignalListener:                  social_account = SocialAccount.objects.get(user=user, provider=DiscordProvider.id)                  discord_user = DiscordUser.objects.get(id=int(social_account.uid)) -                mappings = RoleMapping.objects.filter(role__in=discord_user.roles.all()).all() +                mappings = RoleMapping.objects.filter(role__id__in=discord_user.roles).all()                  is_staff = any(m.is_staff for m in mappings)                  if user.is_staff != is_staff: @@ -185,7 +185,7 @@ class AllauthSignalListener:              self.mapping_model_deleted(RoleMapping, instance=old_instance)          accounts = SocialAccount.objects.filter( -            uid__in=(u.id for u in instance.role.user_set.all()) +            uid__in=(u.id for u in DiscordUser.objects.filter(roles__contains=[instance.role.id]))          )          for account in accounts: @@ -198,7 +198,7 @@ class AllauthSignalListener:                  discord_user = DiscordUser.objects.get(id=int(account.uid))                  mappings = RoleMapping.objects.filter( -                    role__in=discord_user.roles.all() +                    role__id__in=discord_user.roles                  ).exclude(id=instance.id).all()                  is_staff = any(m.is_staff for m in mappings) @@ -289,9 +289,9 @@ class AllauthSignalListener:              new_groups = []              is_staff = False -            for role in user.roles.all(): +            for role in user.roles:                  try: -                    mapping = mappings.get(role=role) +                    mapping = mappings.get(role__id=role)                  except RoleMapping.DoesNotExist:                      continue  # No mapping exists diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json index 37dc672e..35604a85 100644 --- a/pydis_site/apps/home/tests/mock_github_api_response.json +++ b/pydis_site/apps/home/tests/mock_github_api_response.json @@ -28,7 +28,7 @@      "forks_count": 31    },    { -    "full_name": "python-discord/django-crispy-bulma", +    "full_name": "python-discord/flake8-annotations",      "description": "test",      "stargazers_count": 97,      "language": "Python", diff --git a/pydis_site/apps/home/tests/test_signal_listener.py b/pydis_site/apps/home/tests/test_signal_listener.py index 66a67252..d99d81a5 100644 --- a/pydis_site/apps/home/tests/test_signal_listener.py +++ b/pydis_site/apps/home/tests/test_signal_listener.py @@ -81,24 +81,21 @@ class SignalListenerTests(TestCase):              id=0,              name="user",              discriminator=0, -            avatar_hash=None          )          cls.discord_unmapped = DiscordUser.objects.create(              id=2,              name="unmapped",              discriminator=0, -            avatar_hash=None          ) -        cls.discord_unmapped.roles.add(cls.unmapped_role) +        cls.discord_unmapped.roles.append(cls.unmapped_role.id)          cls.discord_unmapped.save()          cls.discord_not_in_guild = DiscordUser.objects.create(              id=3,              name="not-in-guild",              discriminator=0, -            avatar_hash=None,              in_guild=False          ) @@ -106,20 +103,18 @@ class SignalListenerTests(TestCase):              id=1,              name="admin",              discriminator=0, -            avatar_hash=None          ) -        cls.discord_admin.roles.set([cls.admin_role]) +        cls.discord_admin.roles = [cls.admin_role.id]          cls.discord_admin.save()          cls.discord_moderator = DiscordUser.objects.create(              id=4,              name="admin",              discriminator=0, -            avatar_hash=None          ) -        cls.discord_moderator.roles.set([cls.moderator_role]) +        cls.discord_moderator.roles = [cls.moderator_role.id]          cls.discord_moderator.save()          cls.django_user_discordless = DjangoUser.objects.create(username="no-discord") @@ -338,7 +333,7 @@ class SignalListenerTests(TestCase):          handler._apply_groups(self.discord_admin, self.social_admin)          self.assertEqual(self.django_user_discordless.groups.all().count(), 0) -        self.discord_admin.roles.add(self.admin_role) +        self.discord_admin.roles.append(self.admin_role.id)          self.discord_admin.save()      def test_apply_groups_moderator(self): @@ -365,7 +360,7 @@ class SignalListenerTests(TestCase):          handler._apply_groups(self.discord_moderator, self.social_moderator)          self.assertEqual(self.django_user_discordless.groups.all().count(), 0) -        self.discord_moderator.roles.add(self.moderator_role) +        self.discord_moderator.roles.append(self.moderator_role.id)          self.discord_moderator.save()      def test_apply_groups_other(self): diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 4cf22594..20e38ab0 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -23,8 +23,8 @@ class HomeView(View):          "python-discord/bot",          "python-discord/snekbox",          "python-discord/seasonalbot", +        "python-discord/flake8-annotations",          "python-discord/django-simple-bulma", -        "python-discord/django-crispy-bulma",      ]      def _get_api_data(self) -> Dict[str, Dict[str, str]]: @@ -61,7 +61,7 @@ class HomeView(View):                  # Try to get new data from the API. If it fails, return the cached data.                  try:                      api_repositories = self._get_api_data() -                except TypeError: +                except (TypeError, ConnectionError):                      return RepositoryMetadata.objects.all()                  database_repositories = [] diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 1415c558..17910bb6 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -21,10 +21,9 @@ class TestLogsView(TestCase):              id=12345678901,              name='Alan Turing',              discriminator=1912, -            avatar_hash=None          ) -        cls.author.roles.add(cls.developers_role) +        cls.author.roles.append(cls.developers_role.id)          cls.deletion_context = MessageDeletionContext.objects.create(              actor=cls.actor, | 
