diff options
| author | 2020-08-03 10:25:11 +0200 | |
|---|---|---|
| committer | 2020-08-03 10:25:11 +0200 | |
| commit | 685214c5495e0364a57f50c48a8c83d96bd53054 (patch) | |
| tree | e0c473c2081a06282b549c2cfb81907e087b9e5d | |
| parent | Merge pull request #370 from python-discord/role-reminders (diff) | |
| parent | Delete FilterList objects for tests. (diff) | |
Merge pull request #371 from python-discord/whitelist_system
FilterList model and endpoints
42 files changed, 600 insertions, 90 deletions
| diff --git a/Pipfile.lock b/Pipfile.lock index 097c4f81..02d81d76 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -610,11 +610,11 @@          },          "identify": {              "hashes": [ -                "sha256:882c4b08b4569517b5f2257ecca180e01f38400a17f429f5d0edff55530c41c7", -                "sha256:f89add935982d5bc62913ceee16c9297d8ff14b226e9d3072383a4e38136b656" +                "sha256:06b4373546ae55eaaefdac54f006951dbd968fe2912846c00e565b09cfaed101", +                "sha256:5519601b70c831011fb425ffd214101df7639ba3980f24dc283f7675b19127b3"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==1.4.23" +            "version": "==1.4.24"          },          "importlib-metadata": {              "hashes": [ @@ -727,11 +727,11 @@          },          "stevedore": {              "hashes": [ -                "sha256:79270bd5fb4a052e76932e9fef6e19afa77090c4000f2680eb8c2e887d2e6e36", -                "sha256:9fb12884b510fdc25f8a883bb390b8ff82f67863fb360891a33135bcb2ce8c54" +                "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5", +                "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"              ],              "markers": "python_version >= '3.6'", -            "version": "==3.1.0" +            "version": "==3.2.0"          },          "toml": {              "hashes": [ @@ -777,11 +777,11 @@          },          "virtualenv": {              "hashes": [ -                "sha256:c11a475400e98450403c0364eb3a2d25d42f71cf1493da64390487b666de4324", -                "sha256:e10cc66f40cbda459720dfe1d334c4dc15add0d80f09108224f171006a97a172" +                "sha256:26cdd725a57fef4c7c22060dba4647ebd8ca377e30d1c1cf547b30a0b79c43b4", +                "sha256:c51f1ba727d1614ce8fd62457748b469fbedfdab2c7e5dd480c9ae3fbe1233f1"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==20.0.26" +            "version": "==20.0.27"          },          "zipp": {              "hashes": [ 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/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/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 0b279580..78dcbf1d 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -7,7 +7,7 @@ from django.utils import timezone  from pydis_site.apps.api.models.bot.tag import validate_tag_embed  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Message(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 44a0c8ae..04ae8d34 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,7 +1,7 @@  from django.db import models  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class MessageDeletionContext(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index cd9951aa..21e34e87 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -1,7 +1,7 @@  from django.db import models  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Nomination(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 29280c27..20e77b9f 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -1,7 +1,7 @@  from django.core.validators import RegexValidator  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class OffTopicChannelName(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py index b466d9c2..6c0e5ffb 100644 --- a/pydis_site/apps/api/models/bot/offensive_message.py +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError  from django.core.validators import MinValueValidator  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  def future_date_validator(date: datetime.date) -> None: diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index 4b5d15ca..7d968a0e 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -3,7 +3,7 @@ from django.core.validators import MinValueValidator  from django.db import models  from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Reminder(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index 58bbf8b4..721e4815 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -3,7 +3,7 @@ from __future__ import annotations  from django.core.validators import MaxValueValidator, MinValueValidator  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class Role(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py index 5d4cc393..5e53582f 100644 --- a/pydis_site/apps/api/models/bot/tag.py +++ b/pydis_site/apps/api/models/bot/tag.py @@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError  from django.core.validators import MaxLengthValidator, MinLengthValidator  from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  def is_bool_validator(value: Any) -> None: diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index 0d8c574a..cd2d58b9 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -4,7 +4,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator  from django.db import models  from pydis_site.apps.api.models.bot.role import Role -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  def _validate_existing_role(value: int) -> None: diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py index 488af48e..752cd2ca 100644 --- a/pydis_site/apps/api/models/log_entry.py +++ b/pydis_site/apps/api/models/log_entry.py @@ -1,7 +1,7 @@  from django.db import models  from django.utils import timezone -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin  class LogEntry(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/mixins.py index 0540c4de..5d75b78b 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,5 +1,7 @@  from operator import itemgetter +from django.db import models +  class ModelReprMixin:      """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" @@ -15,3 +17,15 @@ class ModelReprMixin:              if not attribute.startswith('_')          )          return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): +    """Mixin providing created_at and updated_at fields.""" + +    created_at = models.DateTimeField(auto_now_add=True) +    updated_at = models.DateTimeField(auto_now=True) + +    class Meta: +        """Metaconfig for the mixin.""" + +        abstract = True diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 80e552a6..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.""" 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_models.py b/pydis_site/apps/api/tests/test_models.py index b4754484..e0e347bb 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -3,13 +3,12 @@ from datetime import datetime as dt  from django.test import SimpleTestCase  from django.utils import timezone -from ..models import ( +from pydis_site.apps.api.models import (      DeletedMessage,      DocumentationLink,      Infraction,      Message,      MessageDeletionContext, -    ModelReprMixin,      Nomination,      OffTopicChannelName,      OffensiveMessage, @@ -18,6 +17,7 @@ from ..models import (      Tag,      User  ) +from pydis_site.apps.api.models.mixins import ModelReprMixin  class SimpleClass(ModelReprMixin): diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 3bb5198e..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  ) 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/tests/__init__.py b/pydis_site/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/tests/__init__.py diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py index e8db7b64..6f8338b4 100644 --- a/pydis_site/tests/test_utils_account.py +++ b/pydis_site/tests/test_utils_account.py @@ -5,7 +5,7 @@ from allauth.socialaccount.models import SocialAccount, SocialLogin  from django.contrib.auth.models import User  from django.contrib.messages.storage.base import BaseStorage  from django.http import HttpRequest -from django.test import TestCase +from django.test import RequestFactory, TestCase  from pydis_site.apps.api.models import Role, User as DiscordUser  from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter @@ -13,28 +13,43 @@ from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter  class AccountUtilsTests(TestCase):      def setUp(self): +        # Create the user          self.django_user = User.objects.create(username="user") +        # Create the roles +        developers_role = Role.objects.create( +            id=1, +            name="Developers", +            colour=0, +            permissions=0, +            position=1 +        ) +        everyone_role = Role.objects.create( +            id=0, +            name="@everyone", +            colour=0, +            permissions=0, +            position=0 +        ) + +        # Create the social accounts          self.discord_account = SocialAccount.objects.create(              user=self.django_user, provider="discord", uid=0          ) - -        self.discord_account_role = SocialAccount.objects.create( +        self.discord_account_one_role = SocialAccount.objects.create(              user=self.django_user, provider="discord", uid=1          ) -          self.discord_account_two_roles = SocialAccount.objects.create(              user=self.django_user, provider="discord", uid=2          ) -          self.discord_account_not_present = SocialAccount.objects.create(              user=self.django_user, provider="discord", uid=3          ) -          self.github_account = SocialAccount.objects.create(              user=self.django_user, provider="github", uid=0          ) +        # Create DiscordUsers          self.discord_user = DiscordUser.objects.create(              id=0,              name="user", @@ -44,35 +59,18 @@ class AccountUtilsTests(TestCase):          self.discord_user_role = DiscordUser.objects.create(              id=1,              name="user present", -            discriminator=0 +            discriminator=0, +            roles=[everyone_role.id]          )          self.discord_user_two_roles = DiscordUser.objects.create(              id=2,              name="user with both roles", -            discriminator=0 +            discriminator=0, +            roles=[everyone_role.id, developers_role.id]          ) -        everyone_role = Role.objects.create( -            id=0, -            name="@everyone", -            colour=0, -            permissions=0, -            position=0 -        ) - -        self.discord_user_role.roles.append(everyone_role.id) -        self.discord_user_two_roles.roles.append(everyone_role.id) - -        developers_role = Role.objects.create( -            id=1, -            name="Developers", -            colour=0, -            permissions=0, -            position=1 -        ) - -        self.discord_user_two_roles.roles.append(developers_role.id) +        self.request_factory = RequestFactory()      def test_account_adapter(self):          """Test that our Allauth account adapter functions correctly.""" @@ -85,13 +83,13 @@ class AccountUtilsTests(TestCase):          adapter = SocialAccountAdapter()          discord_login = SocialLogin(account=self.discord_account) -        discord_login_role = SocialLogin(account=self.discord_account_role) -        discord_login_two_roles = SocialLogin(account=self.discord_account_two_roles) +        discord_login_role = SocialLogin(account=self.discord_account_one_role)          discord_login_not_present = SocialLogin(account=self.discord_account_not_present) +        discord_login_two_roles = SocialLogin(account=self.discord_account_two_roles)          github_login = SocialLogin(account=self.github_account) -        messages_request = HttpRequest() +        messages_request = self.request_factory.get("/")          messages_request._messages = BaseStorage(messages_request)          with patch("pydis_site.utils.account.reverse") as mock_reverse: @@ -108,12 +106,14 @@ class AccountUtilsTests(TestCase):                  with self.assertRaises(ImmediateHttpResponse):                      adapter.is_open_for_signup(messages_request, discord_login_not_present) +                self.assertTrue( +                    adapter.is_open_for_signup(messages_request, discord_login_two_roles) +                ) +                  self.assertEqual(len(messages_request._messages._queued_messages), 4)                  self.assertEqual(mock_redirect.call_count, 4)              self.assertEqual(mock_reverse.call_count, 4) -        self.assertTrue(adapter.is_open_for_signup(HttpRequest(), discord_login_two_roles)) -      def test_social_account_adapter_populate(self):          """Test that our Allauth social account adapter correctly handles data population."""          adapter = SocialAccountAdapter() @@ -122,13 +122,18 @@ class AccountUtilsTests(TestCase):              account=self.discord_account,              user=self.django_user          ) -          discord_login.account.extra_data["discriminator"] = "0000" -        user = adapter.populate_user( -            HttpRequest(), discord_login, +        discord_user = adapter.populate_user( +            self.request_factory.get("/"), discord_login,              {"username": "user"}          ) +        self.assertEqual(discord_user.username, "user#0000") +        self.assertEqual(discord_user.first_name, "user#0000") -        self.assertEqual(user.username, "user#0000") -        self.assertEqual(user.first_name, "user#0000") +        discord_login.account.provider = "not_discord" +        not_discord_user = adapter.populate_user( +            self.request_factory.get("/"), discord_login, +            {"username": "user"} +        ) +        self.assertEqual(not_discord_user.username, "user") | 
