From 2f5effe075287dab4965f3278031bcd433a83f7c Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 26 Apr 2021 17:17:06 +0200 Subject: Filter: new schema This commit adds new filter schema as described in #479 --- pydis_site/apps/api/models/__init__.py | 5 + pydis_site/apps/api/models/bot/__init__.py | 2 +- pydis_site/apps/api/models/bot/filter_list.py | 42 ------ pydis_site/apps/api/models/bot/filters.py | 187 ++++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/filter_list.py | 2 +- 5 files changed, 194 insertions(+), 44 deletions(-) delete mode 100644 pydis_site/apps/api/models/bot/filter_list.py create mode 100644 pydis_site/apps/api/models/bot/filters.py diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index fd5bf220..72f59b57 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -1,6 +1,11 @@ # flake8: noqa from .bot import ( FilterList, + FilterSettings, + FilterAction, + ChannelRange, + Filter, + FilterOverride, BotSetting, DocumentationLink, DeletedMessage, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index ac864de3..1bfe0063 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa -from .filter_list import FilterList +from .filters import FilterList, FilterSettings, FilterAction, ChannelRange, Filter, FilterOverride from .bot_setting import BotSetting from .deleted_message import DeletedMessage from .documentation_link import DocumentationLink diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py deleted file mode 100644 index d30f7213..00000000 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ /dev/null @@ -1,42 +0,0 @@ -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 ' - 'REDIRECT ' - ) - 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/filters.py b/pydis_site/apps/api/models/bot/filters.py new file mode 100644 index 00000000..dfc38e82 --- /dev/null +++ b/pydis_site/apps/api/models/bot/filters.py @@ -0,0 +1,187 @@ +from typing import List + +from django.contrib.postgres.fields import ArrayField +from django.core.exceptions import ValidationError +from django.db import models +from django.db.models import UniqueConstraint + + +class FilterListType(models.IntegerChoices): + """Choice between allow or deny for a list type.""" + + ALLOW: 1 + DENY: 0 + + +class InfractionType(models.TextChoices): + """Possible type of infractions.""" + + NOTE = "Note" + WARN = "Warn" + MUTE = "Mute" + KICK = "Kick" + BAN = "Ban" + + +# Valid special values in ping related fields +VALID_PINGS = ("everyone", "here", "moderators", "onduty", "admins") + + +def validate_ping_field(value_list: List[str]) -> None: + """Validate that the values are either a special value or a UID.""" + for value in value_list: + # Check if it is a special value + if value in VALID_PINGS: + continue + # Check if it is a UID + if value.isnumeric(): + continue + + raise ValidationError(f"{value!r} isn't a valid ping type.") + + +class FilterList(models.Model): + """Represent a list in its allow or deny form.""" + + name = models.CharField(max_length=50, help_text="The unique name of this list.") + list_type = models.IntegerField( + choices=FilterListType.choices, + help_text="Whenever this list is an allowlist or denylist" + ) + + filters = models.ManyToManyField("Filter", help_text="The content of this list.") + default_settings = models.ForeignKey( + "FilterSettings", + models.CASCADE, + help_text="Default parameters of this list." + ) + + class Meta: + """Constrain name and list_type unique.""" + + constraints = ( + UniqueConstraint(fields=("name", "list_type"), name="unique_name_type"), + ) + + def __str__(self) -> str: + return f"Filter {'allow' if self.list_type == 1 else 'deny'}list {self.name!r}" + + +class FilterSettings(models.Model): + """Persistent settings of a filter list.""" + + ping_type = ArrayField( + models.CharField(max_length=20), + validators=(validate_ping_field,), + help_text="Who to ping when this filter triggers." + ) + filter_dm = models.BooleanField(help_text="Whenever DMs should be filtered.") + dm_ping_type = ArrayField( + models.CharField(max_length=20), + validators=(validate_ping_field,), + help_text="Who to ping when this filter triggers on a DM." + ) + delete_messages = models.BooleanField( + help_text="Whenever this filter should delete messages triggering it." + ) + bypass_roles = ArrayField( + models.BigIntegerField(), + help_text="Roles and users who can bypass this filter." + ) + enabled = models.BooleanField( + help_text="Whenever ths filter is currently enabled." + ) + default_action = models.ForeignKey( + "FilterAction", + models.CASCADE, + help_text="The default action to perform." + ) + default_range = models.ForeignKey( + "ChannelRange", + models.CASCADE, + help_text="Where does this filter apply." + ) + + +class FilterAction(models.Model): + """The action to take when a filter is triggered.""" + + user_dm = models.CharField( + max_length=1000, + null=True, + help_text="The DM to send to a user triggering this filter." + ) + infraction_type = models.CharField( + choices=InfractionType.choices, + max_length=4, + null=True, + help_text="The infraction to apply to this user." + ) + infraction_reason = models.CharField( + max_length=1000, + help_text="The reason to give for the infraction." + ) + infraction_duration = models.DurationField( + null=True, + help_text="The duration of the infraction. Null if permanent." + ) + + +class ChannelRange(models.Model): + """ + Where a filter should apply. + + The resolution is done in the following order: + - disallowed channels + - disallowed categories + - allowed categories + - allowed channels + - default + """ + + disallowed_channels = ArrayField(models.IntegerField()) + disallowed_categories = ArrayField(models.IntegerField()) + allowed_channels = ArrayField(models.IntegerField()) + allowed_category = ArrayField(models.IntegerField()) + default = models.BooleanField() + + +class Filter(models.Model): + """One specific trigger of a list.""" + + content = models.CharField(max_length=100, help_text="The definition of this filter.") + description = models.CharField(max_length=200, help_text="Why this filter has been added.") + additional_field = models.BooleanField(null=True, help_text="Implementation specific field.") + override = models.ForeignKey( + "FilterOverride", + models.SET_NULL, + null=True, + help_text="Override the default settings." + ) + + def __str__(self) -> str: + return f"Filter {self.content!r}" + + +class FilterOverride(models.Model): + """ + Setting overrides of a specific filter. + + Any non-null value will override the default ones. + """ + + ping_type = ArrayField( + models.CharField(max_length=20), + validators=(validate_ping_field,), null=True + ) + filter_dm = models.BooleanField(null=True) + dm_ping_type = ArrayField( + models.CharField(max_length=20), + validators=(validate_ping_field,), + null=True + ) + delete_messages = models.BooleanField(null=True) + bypass_roles = ArrayField(models.IntegerField(), null=True) + enabled = models.BooleanField(null=True) + default_action = models.ForeignKey("FilterAction", models.CASCADE, null=True) + default_range = models.ForeignKey("ChannelRange", models.CASCADE, null=True) diff --git a/pydis_site/apps/api/viewsets/bot/filter_list.py b/pydis_site/apps/api/viewsets/bot/filter_list.py index 4b05acee..3eacdaaa 100644 --- a/pydis_site/apps/api/viewsets/bot/filter_list.py +++ b/pydis_site/apps/api/viewsets/bot/filter_list.py @@ -3,7 +3,7 @@ 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.models.bot.filters import FilterList from pydis_site.apps.api.serializers import FilterListSerializer -- cgit v1.2.3 From c6bcca08e58855cf3c3f87602f752dd40b10efad Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 26 Apr 2021 17:29:01 +0200 Subject: Filters: Add new models to Django Admin --- pydis_site/apps/api/admin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 2aca38a1..e123d150 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -13,6 +13,8 @@ from .models import ( BotSetting, DeletedMessage, DocumentationLink, + Filter, + FilterList, Infraction, MessageDeletionContext, Nomination, @@ -194,6 +196,16 @@ class DeletedMessageInline(admin.TabularInline): model = DeletedMessage +@admin.register(FilterList) +class FilterListAdmin(admin.ModelAdmin): + """Admin formatting for the FilterList model.""" + + +@admin.register(Filter) +class FilterAdmin(admin.ModelAdmin): + """Admin formatting for the Filter model.""" + + @admin.register(MessageDeletionContext) class MessageDeletionContextAdmin(admin.ModelAdmin): """Admin formatting for the MessageDeletionContext model.""" -- cgit v1.2.3 From 87c78ceb49f6a2a0ab268fa2dde1850df5506eee Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 26 Apr 2021 17:30:10 +0200 Subject: Filters: Add migration to the new model This will take the currently defined filter list and put them inside the new schema while trying to keep defaults similar to our current setup. --- .../apps/api/migrations/0070_new_filter_schema.py | 165 +++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0070_new_filter_schema.py diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py new file mode 100644 index 00000000..e6d7ffe7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -0,0 +1,165 @@ +# Modified migration file to migrate existing filters to the new one + +import django.contrib.postgres.fields +from django.apps.registry import Apps +from django.db import migrations, models +import django.db.models.deletion +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + +import pydis_site.apps.api.models.bot.filters + +OLD_LIST_NAMES = (('GUILD_INVITE', 'ALLOW'), ('FILE_FORMAT', 'DENY'), ('DOMAIN_NAME', 'DENY'), ('FILTER_TOKEN', 'DENY')) + + +def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + filter_: pydis_site.apps.api.models.Filter = apps.get_model("api", "Filter") + filter_list: pydis_site.apps.api.models.FilterList = apps.get_model("api", "FilterList") + filter_settings: pydis_site.apps.api.models.FilterSettings = apps.get_model("api", "FilterSettings") + channel_range: pydis_site.apps.api.models.ChannelRange = apps.get_model("api", "ChannelRange") + filter_action: pydis_site.apps.api.models.FilterAction = apps.get_model("api", "FilterAction") + filter_list_old = apps.get_model("api", "FilterListOld") + + for name, type_ in OLD_LIST_NAMES: + objects = filter_list_old.objects.filter(type=name) + + default_action = filter_action.objects.create( + user_dm=None, + infraction_type=None, + infraction_reason="", + infraction_duration=None + ) + default_action.save() + default_range = channel_range.objects.create( + disallowed_channels=[], + disallowed_categories=[], + allowed_channels=[], + allowed_category=[], + default=True + ) + default_range.save() + default_settings = filter_settings.objects.create( + ping_type=["onduty"], + filter_dm=True, + dm_ping_type=["onduty"], + delete_messages=True, + bypass_roles=[267630620367257601], + enabled=False, + default_action=default_action, + default_range=default_range + ) + default_settings.save() + list_ = filter_list.objects.create( + name=name.lower(), + default_settings=default_settings, + list_type=1 if type_ == "ALLOW" else 0 + ) + + new_objects = [] + for object_ in objects: + new_object = filter_.objects.create( + content=object_.content, + description=object_.comment or "", + additional_field=None, override=None + ) + new_object.save() + new_objects.append(new_object) + + list_.filters.add(*new_objects) + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0069_documentationlink_validators'), + ] + + operations = [ + migrations.RenameModel( + old_name='FilterList', + new_name='FilterListOld' + ), + migrations.CreateModel( + name='ChannelRange', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('allowed_category', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('default', models.BooleanField()), + ], + ), + migrations.CreateModel( + name='Filter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), + ('description', models.CharField(help_text='Why this filter has been added.', max_length=200)), + ('additional_field', models.BooleanField(help_text='Implementation specific field.', null=True)), + ], + ), + migrations.CreateModel( + name='FilterAction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user_dm', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), + ('infraction_type', models.CharField(choices=[('Note', 'Note'), ('Warn', 'Warn'), ('Mute', 'Mute'), ('Kick', 'Kick'), ('Ban', 'Ban')], help_text='The infraction to apply to this user.', max_length=4, null=True)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), + ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ], + ), + migrations.CreateModel( + name='FilterSettings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('filter_dm', models.BooleanField(help_text='Whenever DMs should be filtered.')), + ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('delete_messages', models.BooleanField(help_text='Whenever this filter should delete messages triggering it.')), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None)), + ('enabled', models.BooleanField(help_text='Whenever ths filter is currently enabled.')), + ('default_action', models.ForeignKey(help_text='The default action to perform.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterAction')), + ('default_range', models.ForeignKey(help_text='Where does this filter apply.', on_delete=django.db.models.deletion.CASCADE, to='api.ChannelRange')), + ], + ), + migrations.CreateModel( + name='FilterOverride', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), null=True, size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('filter_dm', models.BooleanField(null=True)), + ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), null=True, size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('delete_messages', models.BooleanField(null=True)), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), + ('enabled', models.BooleanField(null=True)), + ('default_action', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.FilterAction')), + ('default_range', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.ChannelRange')), + ], + ), + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), + ('list_type', models.IntegerField(choices=[], help_text='Whenever this list is an allowlist or denylist')), + ('default_settings', models.ForeignKey(help_text='Default parameters of this list.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterSettings')), + ('filters', models.ManyToManyField(help_text='The content of this list.', to='api.Filter')), + ], + ), + migrations.AddField( + model_name='filter', + name='override', + field=models.ForeignKey(help_text='Override the default settings.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.FilterOverride'), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('name', 'list_type'), name='unique_name_type'), + ), + migrations.RunPython( + code=forward, # Core of the migration + reverse_code=lambda *_: None + ), + migrations.DeleteModel( + name='FilterListOld' + ) + ] -- cgit v1.2.3 From 64c5c617df68cc7b54fffbe8d76bb5c67d641c98 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 27 Apr 2021 10:54:21 +0200 Subject: Filters: hook the new models into the REST API --- .../apps/api/migrations/0070_new_filter_schema.py | 4 +- pydis_site/apps/api/models/bot/filters.py | 4 +- pydis_site/apps/api/serializers.py | 98 +++- pydis_site/apps/api/urls.py | 29 +- pydis_site/apps/api/viewsets/__init__.py | 7 +- pydis_site/apps/api/viewsets/bot/__init__.py | 9 +- pydis_site/apps/api/viewsets/bot/filter_list.py | 98 ---- pydis_site/apps/api/viewsets/bot/filters.py | 640 +++++++++++++++++++++ 8 files changed, 773 insertions(+), 116 deletions(-) delete mode 100644 pydis_site/apps/api/viewsets/bot/filter_list.py create mode 100644 pydis_site/apps/api/viewsets/bot/filters.py diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index e6d7ffe7..f4fc9494 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -132,8 +132,8 @@ class Migration(migrations.Migration): ('delete_messages', models.BooleanField(null=True)), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), ('enabled', models.BooleanField(null=True)), - ('default_action', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.FilterAction')), - ('default_range', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.ChannelRange')), + ('filter_action', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.FilterAction')), + ('filter_range', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.ChannelRange')), ], ), migrations.CreateModel( diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index dfc38e82..16ac206e 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -183,5 +183,5 @@ class FilterOverride(models.Model): delete_messages = models.BooleanField(null=True) bypass_roles = ArrayField(models.IntegerField(), null=True) enabled = models.BooleanField(null=True) - default_action = models.ForeignKey("FilterAction", models.CASCADE, null=True) - default_range = models.ForeignKey("ChannelRange", models.CASCADE, null=True) + filter_action = models.ForeignKey("FilterAction", models.CASCADE, null=True) + filter_range = models.ForeignKey("ChannelRange", models.CASCADE, null=True) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index de2fccff..306dccb3 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -12,12 +12,17 @@ from rest_framework.serializers import ( from rest_framework.settings import api_settings from rest_framework.validators import UniqueTogetherValidator -from .models import ( +from .models import ( # noqa: I101 - Preserving the filter order BotSetting, DeletedMessage, DocumentationLink, - FilterList, Infraction, + FilterList, + FilterSettings, + FilterAction, + ChannelRange, + Filter, + FilterOverride, MessageDeletionContext, Nomination, NominationEntry, @@ -119,24 +124,97 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content', 'comment') + fields = ('id', 'name', 'list_type', 'filters', 'default_settings') - # 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. + # Ensure that we can only have one filter list with the same name and field validators = [ UniqueTogetherValidator( queryset=FilterList.objects.all(), - fields=['content', 'type'], + fields=('name', 'list_type'), message=( - "A filterlist for this item already exists. " - "Please note that you cannot add the same item to both allow and deny." + "A filterlist with the same name and type already exist." ) ), ] +class FilterSettingsSerializer(ModelSerializer): + """A class providing (de-)serialization of `FilterSettings` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = FilterSettings + fields = ( + 'id', + 'ping_type', + 'filter_dm', + 'dm_ping_type', + 'delete_messages', + 'bypass_roles', + 'enabled', + 'default_action', + 'default_range' + ) + + +class FilterActionSerializer(ModelSerializer): + """A class providing (de-)serialization of `FilterAction` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = FilterAction + fields = ('id', 'user_dm', 'infraction_type', 'infraction_reason', 'infraction_duration') + + +class FilterChannelRangeSerializer(ModelSerializer): + """A class providing (de-)serialization of `ChannelRange` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = ChannelRange + fields = ( + 'id', + 'disallowed_channels', + 'disallowed_categories', + 'allowed_channels', + 'allowed_category', + 'default' + ) + + +class FilterSerializer(ModelSerializer): + """A class providing (de-)serialization of `Filter` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = Filter + fields = ('id', 'content', 'description', 'additional_field', 'override') + + +class FilterOverrideSerializer(ModelSerializer): + """A class providing (de-)serialization of `FilterOverride` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = FilterOverride + fields = ( + 'id', + 'ping_type', + 'filter_dm', + 'dm_ping_type', + 'delete_messages', + 'bypass_roles', + 'enabled', + 'filter_action', + 'filter_range' + ) + + class InfractionSerializer(ModelSerializer): """A class providing (de-)serialization of `Infraction` instances.""" diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index b0ab545b..7af2e505 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -2,11 +2,16 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter from .views import HealthcheckView, RulesView -from .viewsets import ( +from .viewsets import ( # noqa: I101 - Preserving the filter order BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, FilterListViewSet, + FilterSettingsViewSet, + FilterActionViewSet, + FilterChannelRangeViewSet, + FilterViewSet, + FilterOverrideViewSet, InfractionViewSet, NominationViewSet, OffTopicChannelNameViewSet, @@ -19,9 +24,29 @@ from .viewsets import ( # https://www.django-rest-framework.org/api-guide/routers/#defaultrouter bot_router = DefaultRouter(trailing_slash=False) bot_router.register( - 'filter-lists', + 'filter/filter_lists', FilterListViewSet ) +bot_router.register( + 'filter/filter_settings', + FilterSettingsViewSet +) +bot_router.register( + 'filter/filter_action', + FilterActionViewSet +) +bot_router.register( + 'filter/channel_range', + FilterChannelRangeViewSet +) +bot_router.register( + 'filter/filter_override', + FilterOverrideViewSet +) +bot_router.register( + 'filter/filters', + FilterViewSet +) 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 f133e77f..b3992d66 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -1,10 +1,15 @@ # flake8: noqa from .bot import ( - FilterListViewSet, BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, InfractionViewSet, + FilterListViewSet, + FilterSettingsViewSet, + FilterActionViewSet, + FilterChannelRangeViewSet, + FilterViewSet, + FilterOverrideViewSet, NominationViewSet, OffensiveMessageViewSet, OffTopicChannelNameViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index 84b87eab..781624bd 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,5 +1,12 @@ # flake8: noqa -from .filter_list import FilterListViewSet +from .filters import ( + FilterListViewSet, + FilterSettingsViewSet, + FilterActionViewSet, + FilterChannelRangeViewSet, + FilterViewSet, + FilterOverrideViewSet +) 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 deleted file mode 100644 index 3eacdaaa..00000000 --- a/pydis_site/apps/api/viewsets/bot/filter_list.py +++ /dev/null @@ -1,98 +0,0 @@ -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.filters 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/ - 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"], - ... ["REDIRECT", "Redirect"] - ... ] - - #### 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/ - 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/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py new file mode 100644 index 00000000..fea53265 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -0,0 +1,640 @@ +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot.filters import ( # noqa: I101 - Preserving the filter order + FilterList, + FilterSettings, + FilterAction, + ChannelRange, + Filter, + FilterOverride +) +from pydis_site.apps.api.serializers import ( # noqa: I101 - Preserving the filter order + FilterListSerializer, + FilterSettingsSerializer, + FilterActionSerializer, + FilterChannelRangeSerializer, + FilterSerializer, + FilterOverrideSerializer +) + + +class FilterListViewSet(ModelViewSet): + """ + View providing CRUD operations on lists of items allowed or denied by our bot. + + ## Routes + ### GET /bot/filter/filter_lists + Returns all FilterList items in the database. + + #### Response format + >>> [ + ... { + ... "id": 1, + ... "name": "guild_invite", + ... "list_type": 1, + ... "filters": [ + ... 1, + ... 2, + ... ... + ... ], + ... "default_settings": 1 + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/filter/filter_lists/ + Returns a specific FilterList item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "name": "guild_invite", + ... "list_type": 1, + ... "filters": [ + ... 1, + ... 2, + ... ... + ... ], + ... "default_settings": 1 + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### POST /bot/filter/filter_lists + Adds a single FilterList item to the database. + + #### Request body + >>> { + ... "name": "guild_invite", + ... "list_type": 1, + ... "filters": [ + ... 1, + ... 2, + ... ... + ... ], + ... "default_settings": 1 + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### PATCH /bot/filter/filter_lists/ + Updates a specific FilterList item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "name": "guild_invite", + ... "list_type": 1, + ... "filters": [ + ... 1, + ... 2, + ... ... + ... ], + ... "default_settings": 1 + ... } + + #### Status codes + - 200: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/filter/filter_lists/ + 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() + + +class FilterSettingsViewSet(ModelViewSet): + """ + View providing CRUD operations on settings of items allowed or denied by our bot. + + ## Routes + ### GET /bot/filter/filter_settings + Returns all FilterSettings items in the database. + + #### Response format + >>> [ + ... { + ... "id": 1, + ... "ping_type": [ + ... "onduty", + ... ... + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty", + ... ... + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601, + ... ... + ... ], + ... "enabled": True, + ... "default_action": 1, + ... "default_range": 1 + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/filter/filter_settings/ + Returns a specific FilterSettings item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "ping_type": [ + ... "onduty", + ... ... + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty", + ... ... + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601, + ... ... + ... ], + ... "enabled": True, + ... "default_action": 1, + ... "default_range": 1 + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### POST /bot/filter/filter_settings + Adds a single FilterSettings item to the database. + + #### Request body + >>> { + ... "ping_type": [ + ... "onduty", + ... ... + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty", + ... ... + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601, + ... ... + ... ], + ... "enabled": True, + ... "default_action": 1, + ... "default_range": 1 + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### PATCH /bot/filter/filter_settings/ + Updates a specific FilterSettings item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "ping_type": [ + ... "onduty", + ... ... + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty", + ... ... + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601, + ... ... + ... ], + ... "enabled": True, + ... "default_action": 1, + ... "default_range": 1 + ... } + + #### Status codes + - 200: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/filter/filter_settings/ + Deletes the FilterSettings item with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `id` does not exist + """ + + serializer_class = FilterSettingsSerializer + queryset = FilterSettings.objects.all() + + +class FilterActionViewSet(ModelViewSet): + """ + View providing CRUD operations on actions taken by items allowed or denied by our bot. + + ## Routes + ### GET /bot/filter/filter_action + Returns all FilterAction items in the database. + + #### Response format + >>> [ + ... { + ... "id": 1, + ... "user_dm": "message", + ... "infraction_type": "Warn", + ... "infraction_reason": "", + ... "infraction_duration": "01 12:34:56.123456" + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/filter/filter_action/ + Returns a specific FilterAction item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "user_dm": "message", + ... "infraction_type": "Warn", + ... "infraction_reason": "", + ... "infraction_duration": "01 12:34:56.123456" + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### POST /bot/filter/filter_action + Adds a single FilterAction item to the database. + + #### Request body + >>> { + ... "user_dm": "message", + ... "infraction_type": "Warn", + ... "infraction_reason": "", + ... "infraction_duration": "01 12:34:56.123456" + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### PATCH /bot/filter/filter_action/ + Updates a specific FilterAction item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "user_dm": "message", + ... "infraction_type": "Warn", + ... "infraction_reason": "", + ... "infraction_duration": "01 12:34:56.123456" + ... } + + #### Status codes + - 200: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/filter/filter_action/ + Deletes the FilterAction item with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `id` does not exist + """ + + serializer_class = FilterActionSerializer + queryset = FilterAction.objects.all() + + +class FilterChannelRangeViewSet(ModelViewSet): + """ + View providing CRUD operations on channels targeted by items allowed or denied by our bot. + + ## Routes + ### GET /bot/filter/channel_range + Returns all ChannelRange items in the database. + + #### Response format + >>> [ + ... { + ... "id": 1, + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_category": [], + ... "default": True + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/filter/channel_range/ + Returns a specific ChannelRange item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_category": [], + ... "default": True + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### POST /bot/filter/channel_range + Adds a single ChannelRange item to the database. + + #### Request body + >>> { + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_category": [], + ... "default": True + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### PATCH /bot/filter/channel_range/ + Updates a specific ChannelRange item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_category": [], + ... "default": True + ... } + + #### Status codes + - 200: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/filter/channel_range/ + Deletes the ChannelRange item with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `id` does not exist + """ + + serializer_class = FilterChannelRangeSerializer + queryset = ChannelRange.objects.all() + + +class FilterViewSet(ModelViewSet): + """ + View providing CRUD operations on items allowed or denied by our bot. + + ## Routes + ### GET /bot/filter/filters + Returns all Filter items in the database. + + #### Response format + >>> [ + ... { + ... "id": 1, + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "override": 1 + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/filter/filters/ + Returns a specific Filter item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "override": 1 + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### POST /bot/filter/filters + Adds a single Filter item to the database. + + #### Request body + >>> { + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "override": 1 + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### PATCH /bot/filter/filters/ + Updates a specific Filter item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "override": 1 + ... } + + #### Status codes + - 200: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/filter/filters/ + Deletes the Filter item with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `id` does not exist + """ + + serializer_class = FilterSerializer + queryset = Filter.objects.all() + + +class FilterOverrideViewSet(ModelViewSet): + """ + View providing CRUD operations setting overrides of items allowed or denied by our bot. + + ## Routes + ### GET /bot/filter/filter_override + Returns all FilterOverride items in the database. + + #### Response format + >>> [ + ... { + ... "id": 1, + ... "ping_type": [ + ... "onduty", + ... ... + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty", + ... ... + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601, + ... ... + ... ], + ... "enabled": True, + ... "filter_action": 1, + ... "filter_range": 1 + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/filter/filter_override/ + Returns a specific FilterOverride item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "ping_type": [ + ... "onduty", + ... ... + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty", + ... ... + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601, + ... ... + ... ], + ... "enabled": True, + ... "filter_action": 1, + ... "filter_range": 1 + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### POST /bot/filter/filter_override + Adds a single FilterOverride item to the database. + + #### Request body + >>> { + ... "ping_type": [ + ... "onduty", + ... ... + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty", + ... ... + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601, + ... ... + ... ], + ... "enabled": True, + ... "filter_action": 1, + ... "filter_range": 1 + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### PATCH /bot/filter/filter_override/ + Updates a specific FilterOverride item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "ping_type": [ + ... "onduty", + ... ... + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty", + ... ... + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601, + ... ... + ... ], + ... "enabled": True, + ... "filter_action": 1, + ... "filter_range": 1 + ... } + + #### Status codes + - 200: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/filter/filter_override/ + Deletes the FilterOverride item with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `id` does not exist + """ + + serializer_class = FilterOverrideSerializer + queryset = FilterOverride.objects.all() -- cgit v1.2.3 From c6dfd896304cb4e36c4020f4704d9537fd3e8e9f Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 27 Apr 2021 16:39:00 +0200 Subject: Filters: update tests to the new schema --- .../apps/api/migrations/0070_new_filter_schema.py | 2 +- pydis_site/apps/api/models/bot/filters.py | 2 +- pydis_site/apps/api/tests/test_filterlists.py | 122 --------- pydis_site/apps/api/tests/test_filters.py | 284 +++++++++++++++++++++ pydis_site/apps/api/tests/test_models.py | 14 + 5 files changed, 300 insertions(+), 124 deletions(-) delete mode 100644 pydis_site/apps/api/tests/test_filterlists.py create mode 100644 pydis_site/apps/api/tests/test_filters.py diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index f4fc9494..de75e677 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -143,7 +143,7 @@ class Migration(migrations.Migration): ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), ('list_type', models.IntegerField(choices=[], help_text='Whenever this list is an allowlist or denylist')), ('default_settings', models.ForeignKey(help_text='Default parameters of this list.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterSettings')), - ('filters', models.ManyToManyField(help_text='The content of this list.', to='api.Filter')), + ('filters', models.ManyToManyField(help_text='The content of this list.', to='api.Filter', default=[])), ], ), migrations.AddField( diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 16ac206e..869f947c 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -49,7 +49,7 @@ class FilterList(models.Model): help_text="Whenever this list is an allowlist or denylist" ) - filters = models.ManyToManyField("Filter", help_text="The content of this list.") + filters = models.ManyToManyField("Filter", help_text="The content of this list.", default=[]) default_settings = models.ForeignKey( "FilterSettings", models.CASCADE, diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py deleted file mode 100644 index 5a5bca60..00000000 --- a/pydis_site/apps/api/tests/test_filterlists.py +++ /dev/null @@ -1,122 +0,0 @@ -from django.urls import reverse - -from pydis_site.apps.api.models import FilterList -from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase - -URL = reverse('api:bot:filterlist-list') -JPEG_ALLOWLIST = { - "type": 'FILE_FORMAT', - "allowed": True, - "content": ".jpeg", -} -PNG_ALLOWLIST = { - "type": 'FILE_FORMAT', - "allowed": True, - "content": ".png", -} - - -class UnauthenticatedTests(AuthenticatedAPITestCase): - 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(AuthenticatedAPITestCase): - @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(AuthenticatedAPITestCase): - @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(AuthenticatedAPITestCase): - @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(AuthenticatedAPITestCase): - @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_filters.py b/pydis_site/apps/api/tests/test_filters.py new file mode 100644 index 00000000..de78ecfd --- /dev/null +++ b/pydis_site/apps/api/tests/test_filters.py @@ -0,0 +1,284 @@ +import contextlib +from dataclasses import dataclass +from typing import Any, Dict, Tuple, Type + +from django.db.models import Model +from django_hosts import reverse + +from pydis_site.apps.api.models.bot.filters import ( # noqa: I101 - Preserving the filter order + FilterList, + FilterSettings, + FilterAction, + ChannelRange, + Filter, + FilterOverride +) +from pydis_site.apps.api.tests.base import APISubdomainTestCase + + +@dataclass() +class TestSequence: + model: Type[Model] + route: str + object: Dict[str, Any] + ignored_fields: Tuple[str] = () + + def url(self, detail: bool = False) -> str: + return reverse(f'bot:{self.route}-{"detail" if detail else "list"}', host='api') + + +FK_FIELDS: Dict[Type[Model], Tuple[str]] = { + FilterList: ("default_settings",), + FilterSettings: ("default_action", "default_range"), + FilterAction: (), + ChannelRange: (), + Filter: (), + FilterOverride: ("filter_action", "filter_range") +} + + +def get_test_sequences() -> Dict[str, TestSequence]: + return { + "filter_list": TestSequence( + FilterList, + "filterlist", + { + "name": "testname", + "list_type": 0, + "default_settings": FilterSettings( + ping_type=[], + filter_dm=False, + dm_ping_type=[], + delete_messages=False, + bypass_roles=[], + enabled=False, + default_action=FilterAction( + user_dm=None, + infraction_type=None, + infraction_reason="", + infraction_duration=None + ), + default_range=ChannelRange( + disallowed_channels=[], + disallowed_categories=[], + allowed_channels=[], + allowed_category=[], + default=False + ) + ) + }, + ignored_fields=("filters",) + ), + "filter_settings": TestSequence( + FilterSettings, + "filtersettings", + { + "ping_type": ["onduty"], + "filter_dm": True, + "dm_ping_type": ["123456"], + "delete_messages": True, + "bypass_roles": [123456], + "enabled": True, + "default_action": FilterAction( + user_dm=None, + infraction_type=None, + infraction_reason="", + infraction_duration=None + ), + "default_range": ChannelRange( + disallowed_channels=[], + disallowed_categories=[], + allowed_channels=[], + allowed_category=[], + default=False + ) + } + ), + "filter_action": TestSequence( + FilterAction, + "filteraction", + { + "user_dm": "This is a DM message.", + "infraction_type": "Mute", + "infraction_reason": "Too long beard", + "infraction_duration": "1 02:03:00" + } + ), + "channel_range": TestSequence( + ChannelRange, + "channelrange", + { + "disallowed_channels": [1234], + "disallowed_categories": [5678], + "allowed_channels": [9101], + "allowed_category": [1121], + "default": True + } + ), + "filter": TestSequence( + Filter, + "filter", + { + "content": "bad word", + "description": "This is a really bad word.", + "additional_field": None, + "override": None + } + ), + "filter_override": TestSequence( + FilterOverride, + "filteroverride", + { + "ping_type": ["everyone"], + "filter_dm": False, + "dm_ping_type": ["here"], + "delete_messages": False, + "bypass_roles": [9876], + "enabled": True, + "filter_action": None, + "filter_range": None + } + ) + } + + +def save_nested_objects(object_: Model, save_root: bool = True) -> None: + for field in FK_FIELDS[object_.__class__]: + value = getattr(object_, field) + + if value is not None: + save_nested_objects(value) + + if save_root: + object_.save() + + +def clean_test_json(json: dict) -> dict: + for key, value in json.items(): + if isinstance(value, Model): + json[key] = value.id + + return json + + +def clean_api_json(json: dict, sequence: TestSequence) -> dict: + for field in sequence.ignored_fields + ("id",): + with contextlib.suppress(KeyError): + del json[field] + + return json + + +class GenericFilterTest(APISubdomainTestCase): + def test_cannot_read_unauthenticated(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + self.client.force_authenticate(user=None) + + response = self.client.get(sequence.url()) + self.assertEqual(response.status_code, 401) + + def test_empty_database(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + sequence.model.objects.all().delete() + + response = self.client.get(sequence.url()) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + def test_fetch(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + sequence.model.objects.all().delete() + + save_nested_objects(sequence.model(**sequence.object)) + + response = self.client.get(sequence.url()) + self.assertDictEqual( + clean_test_json(sequence.object), + clean_api_json(response.json()[0], sequence) + ) + + def test_fetch_by_id(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + sequence.model.objects.all().delete() + + saved = sequence.model(**sequence.object) + save_nested_objects(saved) + + response = self.client.get(f"{sequence.url()}/{saved.id}") + self.assertDictEqual( + clean_test_json(sequence.object), + clean_api_json(response.json(), sequence) + ) + + def test_fetch_non_existing(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + sequence.model.objects.all().delete() + + response = self.client.get(f"{sequence.url()}/42") + self.assertEqual(response.status_code, 404) + self.assertDictEqual(response.json(), {'detail': 'Not found.'}) + + def test_creation(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + sequence.model.objects.all().delete() + + save_nested_objects(sequence.model(**sequence.object), False) + data = clean_test_json(sequence.object.copy()) + response = self.client.post(sequence.url(), data=data) + + self.assertEqual(response.status_code, 201) + self.assertDictEqual( + clean_api_json(response.json(), sequence), + clean_test_json(sequence.object) + ) + + def test_creation_missing_field(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + save_nested_objects(sequence.model(**sequence.object), False) + data = clean_test_json(sequence.object.copy()) + + for field in sequence.model._meta.get_fields(): + with self.subTest(field=field): + if field.null or field.name in sequence.ignored_fields + ("id",): + continue + + test_data = data.copy() + del test_data[field.name] + + response = self.client.post(sequence.url(), data=test_data) + self.assertEqual(response.status_code, 400) + + def test_deletion(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + saved = sequence.model(**sequence.object) + save_nested_objects(saved) + + response = self.client.delete(f"{sequence.url()}/{saved.id}") + self.assertEqual(response.status_code, 204) + + def test_deletion_non_existing(self) -> None: + for name, sequence in get_test_sequences().items(): + with self.subTest(name=name): + sequence.model.objects.all().delete() + + response = self.client.delete(f"{sequence.url()}/42") + self.assertEqual(response.status_code, 404) + + def test_reject_invalid_ping(self) -> None: + url = reverse('bot:filteroverride-list', host='api') + data = { + "ping_type": ["invalid"] + } + + response = self.client.post(url, data=data) + + self.assertEqual(response.status_code, 400) + self.assertDictEqual(response.json(), {'ping_type': ["'invalid' isn't a valid ping type."]}) diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index 5c9ddea4..c8f4e1b1 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -7,6 +7,9 @@ from django.utils import timezone from pydis_site.apps.api.models import ( DeletedMessage, DocumentationLink, + Filter, + FilterList, + FilterSettings, Infraction, Message, MessageDeletionContext, @@ -106,6 +109,17 @@ class StringDunderMethodTests(SimpleTestCase): DocumentationLink( 'test', 'http://example.com', 'http://example.com' ), + FilterList( + name="forbidden_duckies", + list_type=0, + default_settings=FilterSettings() + ), + Filter( + content="ducky_nsfw", + description="This ducky is totally inappropriate!", + additional_field=None, + override=None + ), OffensiveMessage( id=602951077675139072, channel_id=291284109232308226, -- cgit v1.2.3 From 71a5e0d854c587ca2ae70aaec80f1110ea8800e5 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 27 Apr 2021 16:41:18 +0200 Subject: Filters: allowed_category -> allowed_categories --- pydis_site/apps/api/migrations/0070_new_filter_schema.py | 4 ++-- pydis_site/apps/api/models/bot/filters.py | 2 +- pydis_site/apps/api/serializers.py | 2 +- pydis_site/apps/api/tests/test_filters.py | 6 +++--- pydis_site/apps/api/viewsets/bot/filters.py | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index de75e677..eb55e329 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -33,7 +33,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: disallowed_channels=[], disallowed_categories=[], allowed_channels=[], - allowed_category=[], + allowed_categories=[], default=True ) default_range.save() @@ -85,7 +85,7 @@ class Migration(migrations.Migration): ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('allowed_category', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), ('default', models.BooleanField()), ], ), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 869f947c..c2f776d3 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -142,7 +142,7 @@ class ChannelRange(models.Model): disallowed_channels = ArrayField(models.IntegerField()) disallowed_categories = ArrayField(models.IntegerField()) allowed_channels = ArrayField(models.IntegerField()) - allowed_category = ArrayField(models.IntegerField()) + allowed_categories = ArrayField(models.IntegerField()) default = models.BooleanField() diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 306dccb3..54acf366 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -180,7 +180,7 @@ class FilterChannelRangeSerializer(ModelSerializer): 'disallowed_channels', 'disallowed_categories', 'allowed_channels', - 'allowed_category', + 'allowed_categories', 'default' ) diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index de78ecfd..f38f3659 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -62,7 +62,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: disallowed_channels=[], disallowed_categories=[], allowed_channels=[], - allowed_category=[], + allowed_categories=[], default=False ) ) @@ -89,7 +89,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: disallowed_channels=[], disallowed_categories=[], allowed_channels=[], - allowed_category=[], + allowed_categories=[], default=False ) } @@ -111,7 +111,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: "disallowed_channels": [1234], "disallowed_categories": [5678], "allowed_channels": [9101], - "allowed_category": [1121], + "allowed_categories": [1121], "default": True } ), diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index fea53265..e290fc65 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -350,7 +350,7 @@ class FilterChannelRangeViewSet(ModelViewSet): ... "disallowed_channels": [], ... "disallowed_categories": [], ... "allowed_channels": [], - ... "allowed_category": [], + ... "allowed_categories": [], ... "default": True ... }, ... ... @@ -369,7 +369,7 @@ class FilterChannelRangeViewSet(ModelViewSet): ... "disallowed_channels": [], ... "disallowed_categories": [], ... "allowed_channels": [], - ... "allowed_category": [], + ... "allowed_categories": [], ... "default": True ... } @@ -385,7 +385,7 @@ class FilterChannelRangeViewSet(ModelViewSet): ... "disallowed_channels": [], ... "disallowed_categories": [], ... "allowed_channels": [], - ... "allowed_category": [], + ... "allowed_categories": [], ... "default": True ... } @@ -402,7 +402,7 @@ class FilterChannelRangeViewSet(ModelViewSet): ... "disallowed_channels": [], ... "disallowed_categories": [], ... "allowed_channels": [], - ... "allowed_category": [], + ... "allowed_categories": [], ... "default": True ... } -- cgit v1.2.3 From d48d5ddbaa6b068d3a24f55ee7c8f3760006f04b Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sun, 11 Jul 2021 14:46:16 +0800 Subject: Improve help text message. --- pydis_site/apps/api/models/bot/filters.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index c2f776d3..a2d3af6a 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -46,7 +46,7 @@ class FilterList(models.Model): name = models.CharField(max_length=50, help_text="The unique name of this list.") list_type = models.IntegerField( choices=FilterListType.choices, - help_text="Whenever this list is an allowlist or denylist" + help_text="Whether this list is an allowlist or denylist" ) filters = models.ManyToManyField("Filter", help_text="The content of this list.", default=[]) @@ -75,31 +75,31 @@ class FilterSettings(models.Model): validators=(validate_ping_field,), help_text="Who to ping when this filter triggers." ) - filter_dm = models.BooleanField(help_text="Whenever DMs should be filtered.") + filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.") dm_ping_type = ArrayField( models.CharField(max_length=20), validators=(validate_ping_field,), help_text="Who to ping when this filter triggers on a DM." ) delete_messages = models.BooleanField( - help_text="Whenever this filter should delete messages triggering it." + help_text="Whether this filter should delete messages triggering it." ) bypass_roles = ArrayField( models.BigIntegerField(), help_text="Roles and users who can bypass this filter." ) enabled = models.BooleanField( - help_text="Whenever ths filter is currently enabled." + help_text="Whether this filter is currently enabled." ) default_action = models.ForeignKey( "FilterAction", models.CASCADE, - help_text="The default action to perform." + help_text="What action to perform on the triggering user." ) default_range = models.ForeignKey( "ChannelRange", models.CASCADE, - help_text="Where does this filter apply." + help_text="The channels and categories in which this filter applies." ) -- cgit v1.2.3 From 6694ac4159c6d0f17451997df7f20b1363952ef3 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sun, 11 Jul 2021 15:58:32 +0800 Subject: Fix faulty model enumeration. This also allows us to simplify the str dunder for a FilterList. --- pydis_site/apps/api/models/bot/filters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index a2d3af6a..6f35bfb0 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -9,8 +9,8 @@ from django.db.models import UniqueConstraint class FilterListType(models.IntegerChoices): """Choice between allow or deny for a list type.""" - ALLOW: 1 - DENY: 0 + ALLOW = 1 + DENY = 0 class InfractionType(models.TextChoices): @@ -64,7 +64,7 @@ class FilterList(models.Model): ) def __str__(self) -> str: - return f"Filter {'allow' if self.list_type == 1 else 'deny'}list {self.name!r}" + return f"Filter {FilterListType(self.list_type).label}list {self.name!r}" class FilterSettings(models.Model): -- cgit v1.2.3 From 1095346a1f86e43d5d5c39045a54354d1290fe0e Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sun, 11 Jul 2021 16:35:41 +0800 Subject: Improve name of dm sent to triggered user. --- pydis_site/apps/api/migrations/0070_new_filter_schema.py | 4 ++-- pydis_site/apps/api/models/bot/filters.py | 2 +- pydis_site/apps/api/serializers.py | 2 +- pydis_site/apps/api/tests/test_filters.py | 6 +++--- pydis_site/apps/api/viewsets/bot/filters.py | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index eb55e329..8580033a 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -23,7 +23,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: objects = filter_list_old.objects.filter(type=name) default_action = filter_action.objects.create( - user_dm=None, + dm_content=None, infraction_type=None, infraction_reason="", infraction_duration=None @@ -102,7 +102,7 @@ class Migration(migrations.Migration): name='FilterAction', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('user_dm', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('infraction_type', models.CharField(choices=[('Note', 'Note'), ('Warn', 'Warn'), ('Mute', 'Mute'), ('Kick', 'Kick'), ('Ban', 'Ban')], help_text='The infraction to apply to this user.', max_length=4, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 6f35bfb0..b5c80bda 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -106,7 +106,7 @@ class FilterSettings(models.Model): class FilterAction(models.Model): """The action to take when a filter is triggered.""" - user_dm = models.CharField( + dm_content = models.CharField( max_length=1000, null=True, help_text="The DM to send to a user triggering this filter." diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 54acf366..584d1f22 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -165,7 +165,7 @@ class FilterActionSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterAction - fields = ('id', 'user_dm', 'infraction_type', 'infraction_reason', 'infraction_duration') + fields = ('id', 'dm_content', 'infraction_type', 'infraction_reason', 'infraction_duration') class FilterChannelRangeSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index f38f3659..2df671e0 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -53,7 +53,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: bypass_roles=[], enabled=False, default_action=FilterAction( - user_dm=None, + dm_content=None, infraction_type=None, infraction_reason="", infraction_duration=None @@ -80,7 +80,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: "bypass_roles": [123456], "enabled": True, "default_action": FilterAction( - user_dm=None, + dm_content=None, infraction_type=None, infraction_reason="", infraction_duration=None @@ -98,7 +98,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: FilterAction, "filteraction", { - "user_dm": "This is a DM message.", + "dm_content": "This is a DM message.", "infraction_type": "Mute", "infraction_reason": "Too long beard", "infraction_duration": "1 02:03:00" diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index e290fc65..9553fcac 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -264,7 +264,7 @@ class FilterActionViewSet(ModelViewSet): >>> [ ... { ... "id": 1, - ... "user_dm": "message", + ... "dm_content": "message", ... "infraction_type": "Warn", ... "infraction_reason": "", ... "infraction_duration": "01 12:34:56.123456" @@ -282,7 +282,7 @@ class FilterActionViewSet(ModelViewSet): #### Response format >>> { ... "id": 1, - ... "user_dm": "message", + ... "dm_content": "message", ... "infraction_type": "Warn", ... "infraction_reason": "", ... "infraction_duration": "01 12:34:56.123456" @@ -297,7 +297,7 @@ class FilterActionViewSet(ModelViewSet): #### Request body >>> { - ... "user_dm": "message", + ... "dm_content": "message", ... "infraction_type": "Warn", ... "infraction_reason": "", ... "infraction_duration": "01 12:34:56.123456" @@ -313,7 +313,7 @@ class FilterActionViewSet(ModelViewSet): #### Response format >>> { ... "id": 1, - ... "user_dm": "message", + ... "dm_content": "message", ... "infraction_type": "Warn", ... "infraction_reason": "", ... "infraction_duration": "01 12:34:56.123456" -- cgit v1.2.3 From 9ec355955895d5b26ce99aade3c0c6ccf913e6a4 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sun, 11 Jul 2021 16:40:34 +0800 Subject: Migrate misc field names and help text changes. --- .../apps/api/migrations/0071_auto_20210711_0839.py | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0071_auto_20210711_0839.py diff --git a/pydis_site/apps/api/migrations/0071_auto_20210711_0839.py b/pydis_site/apps/api/migrations/0071_auto_20210711_0839.py new file mode 100644 index 00000000..e1c45fb6 --- /dev/null +++ b/pydis_site/apps/api/migrations/0071_auto_20210711_0839.py @@ -0,0 +1,44 @@ +# Generated by Django 3.0.14 on 2021-07-11 08:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0070_new_filter_schema'), + ] + + operations = [ + migrations.AlterField( + model_name='filterlist', + name='list_type', + field=models.IntegerField(choices=[(1, 'Allow'), (0, 'Deny')], help_text='Whether this list is an allowlist or denylist'), + ), + migrations.AlterField( + model_name='filtersettings', + name='default_action', + field=models.ForeignKey(help_text='What action to perform on the triggering user.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterAction'), + ), + migrations.AlterField( + model_name='filtersettings', + name='default_range', + field=models.ForeignKey(help_text='The channels and categories in which this filter applies.', on_delete=django.db.models.deletion.CASCADE, to='api.ChannelRange'), + ), + migrations.AlterField( + model_name='filtersettings', + name='delete_messages', + field=models.BooleanField(help_text='Whether this filter should delete messages triggering it.'), + ), + migrations.AlterField( + model_name='filtersettings', + name='enabled', + field=models.BooleanField(help_text='Whether this filter is currently enabled.'), + ), + migrations.AlterField( + model_name='filtersettings', + name='filter_dm', + field=models.BooleanField(help_text='Whether DMs should be filtered.'), + ), + ] -- cgit v1.2.3 From b082de6662e1b57f6831d219b44d95f93ed8a884 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Fri, 23 Jul 2021 18:58:35 +0800 Subject: Correct Filter-FilterList relationship. Instead of a many-many relationship, one filterlist has multiple filters. Nested serialization is read-only by default, so not all CRUD methods are implemented yet for the FilterList viewset. --- .../apps/api/migrations/0070_new_filter_schema.py | 11 ++-- pydis_site/apps/api/models/bot/filters.py | 6 +- pydis_site/apps/api/serializers.py | 22 ++++---- pydis_site/apps/api/tests/test_filters.py | 29 +++++++++- pydis_site/apps/api/viewsets/bot/filters.py | 64 +++++++--------------- 5 files changed, 68 insertions(+), 64 deletions(-) diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 8580033a..237ce7d7 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -54,17 +54,14 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: list_type=1 if type_ == "ALLOW" else 0 ) - new_objects = [] for object_ in objects: new_object = filter_.objects.create( content=object_.content, + filter_list = list_, description=object_.comment or "", additional_field=None, override=None ) new_object.save() - new_objects.append(new_object) - - list_.filters.add(*new_objects) class Migration(migrations.Migration): @@ -143,7 +140,6 @@ class Migration(migrations.Migration): ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), ('list_type', models.IntegerField(choices=[], help_text='Whenever this list is an allowlist or denylist')), ('default_settings', models.ForeignKey(help_text='Default parameters of this list.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterSettings')), - ('filters', models.ManyToManyField(help_text='The content of this list.', to='api.Filter', default=[])), ], ), migrations.AddField( @@ -151,6 +147,11 @@ class Migration(migrations.Migration): name='override', field=models.ForeignKey(help_text='Override the default settings.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.FilterOverride'), ), + migrations.AddField( + model_name='filter', + name='filter_list', + field=models.ForeignKey(help_text='The filter list containing this filter.', on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='api.FilterList'), + ), migrations.AddConstraint( model_name='filterlist', constraint=models.UniqueConstraint(fields=('name', 'list_type'), name='unique_name_type'), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index b5c80bda..99d6d5e4 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -48,8 +48,6 @@ class FilterList(models.Model): choices=FilterListType.choices, help_text="Whether this list is an allowlist or denylist" ) - - filters = models.ManyToManyField("Filter", help_text="The content of this list.", default=[]) default_settings = models.ForeignKey( "FilterSettings", models.CASCADE, @@ -152,6 +150,10 @@ class Filter(models.Model): content = models.CharField(max_length=100, help_text="The definition of this filter.") description = models.CharField(max_length=200, help_text="Why this filter has been added.") additional_field = models.BooleanField(null=True, help_text="Implementation specific field.") + filter_list = models.ForeignKey( + FilterList, models.CASCADE, related_name="filters", + help_text="The filter list containing this filter." + ) override = models.ForeignKey( "FilterOverride", models.SET_NULL, diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 584d1f22..afcf4d55 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -117,9 +117,21 @@ class DocumentationLinkSerializer(ModelSerializer): fields = ('package', 'base_url', 'inventory_url') +class FilterSerializer(ModelSerializer): + """A class providing (de-)serialization of `Filter` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = Filter + fields = ('id', 'content', 'description', 'additional_field', 'filter_list', 'override') + + class FilterListSerializer(ModelSerializer): """A class providing (de-)serialization of `FilterList` instances.""" + filters = FilterSerializer(many=True, read_only=True) + class Meta: """Metadata defined for the Django REST Framework.""" @@ -185,16 +197,6 @@ class FilterChannelRangeSerializer(ModelSerializer): ) -class FilterSerializer(ModelSerializer): - """A class providing (de-)serialization of `Filter` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = Filter - fields = ('id', 'content', 'description', 'additional_field', 'override') - - class FilterOverrideSerializer(ModelSerializer): """A class providing (de-)serialization of `FilterOverride` instances.""" diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index 2df671e0..f694053d 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -32,7 +32,7 @@ FK_FIELDS: Dict[Type[Model], Tuple[str]] = { FilterSettings: ("default_action", "default_range"), FilterAction: (), ChannelRange: (), - Filter: (), + Filter: ("filter_list",), FilterOverride: ("filter_action", "filter_range") } @@ -122,7 +122,32 @@ def get_test_sequences() -> Dict[str, TestSequence]: "content": "bad word", "description": "This is a really bad word.", "additional_field": None, - "override": None + "override": None, + "filter_list": FilterList( + name="testname", + list_type=0, + default_settings=FilterSettings( + ping_type=[], + filter_dm=False, + dm_ping_type=[], + delete_messages=False, + bypass_roles=[], + enabled=False, + default_action=FilterAction( + dm_content=None, + infraction_type=None, + infraction_reason="", + infraction_duration=None + ), + default_range=ChannelRange( + disallowed_channels=[], + disallowed_categories=[], + allowed_channels=[], + allowed_categories=[], + default=False + ) + ) + ) } ), "filter_override": TestSequence( diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 9553fcac..1b893f8c 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -20,7 +20,7 @@ from pydis_site.apps.api.serializers import ( # noqa: I101 - Preserving the fil class FilterListViewSet(ModelViewSet): """ - View providing CRUD operations on lists of items allowed or denied by our bot. + View providing GET/DELETE on lists of items allowed or denied by our bot. ## Routes ### GET /bot/filter/filter_lists @@ -33,8 +33,14 @@ class FilterListViewSet(ModelViewSet): ... "name": "guild_invite", ... "list_type": 1, ... "filters": [ - ... 1, - ... 2, + ... { + ... "id": 1, + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "override": 1, + ... "filter_list": 1 + ... }, ... ... ... ], ... "default_settings": 1 @@ -55,8 +61,14 @@ class FilterListViewSet(ModelViewSet): ... "name": "guild_invite", ... "list_type": 1, ... "filters": [ - ... 1, - ... 2, + ... { + ... "id": 1, + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "override": 1, + ... "filter_list": 1 + ... }, ... ... ... ], ... "default_settings": 1 @@ -66,45 +78,6 @@ class FilterListViewSet(ModelViewSet): - 200: returned on success - 404: returned if the id was not found. - ### POST /bot/filter/filter_lists - Adds a single FilterList item to the database. - - #### Request body - >>> { - ... "name": "guild_invite", - ... "list_type": 1, - ... "filters": [ - ... 1, - ... 2, - ... ... - ... ], - ... "default_settings": 1 - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### PATCH /bot/filter/filter_lists/ - Updates a specific FilterList item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "name": "guild_invite", - ... "list_type": 1, - ... "filters": [ - ... 1, - ... 2, - ... ... - ... ], - ... "default_settings": 1 - ... } - - #### Status codes - - 200: returned on success - - 400: if one of the given fields is invalid - ### DELETE /bot/filter/filter_lists/ Deletes the FilterList item with the given `id`. @@ -437,7 +410,8 @@ class FilterViewSet(ModelViewSet): ... "content": "267624335836053506", ... "description": "Python Discord", ... "additional_field": None, - ... "override": 1 + ... "override": 1, + ... "filter_list": 1 ... }, ... ... ... ] -- cgit v1.2.3 From 98d36f6fce899680fa10177556f06cc5357eb675 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 25 Sep 2021 13:04:39 +0300 Subject: Remove one-to-one relationships from filters tables --- pydis_site/apps/api/models/bot/filters.py | 153 ++++++++++++------------------ 1 file changed, 63 insertions(+), 90 deletions(-) diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 99d6d5e4..68ac191b 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -1,3 +1,4 @@ +from abc import abstractmethod from typing import List from django.contrib.postgres.fields import ArrayField @@ -5,6 +6,8 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models import UniqueConstraint +from pydis_site.apps.api.models.mixins import AbstractModelMeta + class FilterListType(models.IntegerChoices): """Choice between allow or deny for a list type.""" @@ -40,70 +43,40 @@ def validate_ping_field(value_list: List[str]) -> None: raise ValidationError(f"{value!r} isn't a valid ping type.") -class FilterList(models.Model): - """Represent a list in its allow or deny form.""" - - name = models.CharField(max_length=50, help_text="The unique name of this list.") - list_type = models.IntegerField( - choices=FilterListType.choices, - help_text="Whether this list is an allowlist or denylist" - ) - default_settings = models.ForeignKey( - "FilterSettings", - models.CASCADE, - help_text="Default parameters of this list." - ) - - class Meta: - """Constrain name and list_type unique.""" - - constraints = ( - UniqueConstraint(fields=("name", "list_type"), name="unique_name_type"), - ) - - def __str__(self) -> str: - return f"Filter {FilterListType(self.list_type).label}list {self.name!r}" - +class FilterSettingsMixin(models.Model, metaclass=AbstractModelMeta): + """Mixin for settings of a filter list.""" -class FilterSettings(models.Model): - """Persistent settings of a filter list.""" + @staticmethod + @abstractmethod + def allow_null() -> bool: + """Abstract property for allowing null values.""" ping_type = ArrayField( models.CharField(max_length=20), validators=(validate_ping_field,), - help_text="Who to ping when this filter triggers." + help_text="Who to ping when this filter triggers.", + null=allow_null.__func__() ) - filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.") + filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=True) dm_ping_type = ArrayField( models.CharField(max_length=20), validators=(validate_ping_field,), - help_text="Who to ping when this filter triggers on a DM." + help_text="Who to ping when this filter triggers on a DM.", + null=allow_null.__func__() ) delete_messages = models.BooleanField( - help_text="Whether this filter should delete messages triggering it." + help_text="Whether this filter should delete messages triggering it.", + null=allow_null.__func__() ) bypass_roles = ArrayField( models.BigIntegerField(), - help_text="Roles and users who can bypass this filter." + help_text="Roles and users who can bypass this filter.", + null=allow_null.__func__() ) enabled = models.BooleanField( - help_text="Whether this filter is currently enabled." + help_text="Whether this filter is currently enabled.", + null=allow_null.__func__() ) - default_action = models.ForeignKey( - "FilterAction", - models.CASCADE, - help_text="What action to perform on the triggering user." - ) - default_range = models.ForeignKey( - "ChannelRange", - models.CASCADE, - help_text="The channels and categories in which this filter applies." - ) - - -class FilterAction(models.Model): - """The action to take when a filter is triggered.""" - dm_content = models.CharField( max_length=1000, null=True, @@ -124,27 +97,52 @@ class FilterAction(models.Model): help_text="The duration of the infraction. Null if permanent." ) - -class ChannelRange(models.Model): - """ - Where a filter should apply. - - The resolution is done in the following order: - - disallowed channels - - disallowed categories - - allowed categories - - allowed channels - - default - """ - + # Where a filter should apply. + # + # The resolution is done in the following order: + # - disallowed channels + # - disallowed categories + # - allowed categories + # - allowed channels + # - default disallowed_channels = ArrayField(models.IntegerField()) disallowed_categories = ArrayField(models.IntegerField()) allowed_channels = ArrayField(models.IntegerField()) allowed_categories = ArrayField(models.IntegerField()) default = models.BooleanField() + class Meta: + """Metaclass for settings mixin.""" + + abstract = True -class Filter(models.Model): + +class FilterList(FilterSettingsMixin): + """Represent a list in its allow or deny form.""" + + name = models.CharField(max_length=50, help_text="The unique name of this list.") + list_type = models.IntegerField( + choices=FilterListType.choices, + help_text="Whether this list is an allowlist or denylist" + ) + + @staticmethod + def allow_null() -> bool: + """Do not allow null values for default settings.""" + return False + + class Meta: + """Constrain name and list_type unique.""" + + constraints = ( + UniqueConstraint(fields=("name", "list_type"), name="unique_name_type"), + ) + + def __str__(self) -> str: + return f"Filter {FilterListType(self.list_type).label}list {self.name!r}" + + +class Filter(FilterSettingsMixin): """One specific trigger of a list.""" content = models.CharField(max_length=100, help_text="The definition of this filter.") @@ -154,36 +152,11 @@ class Filter(models.Model): FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." ) - override = models.ForeignKey( - "FilterOverride", - models.SET_NULL, - null=True, - help_text="Override the default settings." - ) def __str__(self) -> str: return f"Filter {self.content!r}" - -class FilterOverride(models.Model): - """ - Setting overrides of a specific filter. - - Any non-null value will override the default ones. - """ - - ping_type = ArrayField( - models.CharField(max_length=20), - validators=(validate_ping_field,), null=True - ) - filter_dm = models.BooleanField(null=True) - dm_ping_type = ArrayField( - models.CharField(max_length=20), - validators=(validate_ping_field,), - null=True - ) - delete_messages = models.BooleanField(null=True) - bypass_roles = ArrayField(models.IntegerField(), null=True) - enabled = models.BooleanField(null=True) - filter_action = models.ForeignKey("FilterAction", models.CASCADE, null=True) - filter_range = models.ForeignKey("ChannelRange", models.CASCADE, null=True) + @staticmethod + def allow_null() -> bool: + """Allow null values for overrides.""" + return True -- cgit v1.2.3 From 08a52168dd3b0a9a366f5ca68c10437b83af5cf1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 25 Sep 2021 13:05:52 +0300 Subject: Remove old one-to-one filters relationships serializers, views and URLs --- pydis_site/apps/api/models/__init__.py | 4 - pydis_site/apps/api/models/bot/__init__.py | 2 +- pydis_site/apps/api/serializers.py | 84 +---- pydis_site/apps/api/urls.py | 20 -- pydis_site/apps/api/viewsets/__init__.py | 4 - pydis_site/apps/api/viewsets/bot/__init__.py | 6 +- pydis_site/apps/api/viewsets/bot/filters.py | 450 +-------------------------- 7 files changed, 15 insertions(+), 555 deletions(-) diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 72f59b57..63087990 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -1,11 +1,7 @@ # flake8: noqa from .bot import ( FilterList, - FilterSettings, - FilterAction, - ChannelRange, Filter, - FilterOverride, BotSetting, DocumentationLink, DeletedMessage, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 1bfe0063..9ba763a4 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa -from .filters import FilterList, FilterSettings, FilterAction, ChannelRange, Filter, FilterOverride +from .filters import FilterList, Filter from .bot_setting import BotSetting from .deleted_message import DeletedMessage from .documentation_link import DocumentationLink diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index afcf4d55..ff2bd929 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -18,11 +18,7 @@ from .models import ( # noqa: I101 - Preserving the filter order DocumentationLink, Infraction, FilterList, - FilterSettings, - FilterAction, - ChannelRange, Filter, - FilterOverride, MessageDeletionContext, Nomination, NominationEntry, @@ -136,7 +132,18 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ('id', 'name', 'list_type', 'filters', 'default_settings') + fields = ( + 'id', + 'name', + 'list_type', + 'filters', + 'ping_type', + 'filter_dm', + 'dm_ping_type', + 'delete_messages', + 'bypass_roles', + '' + ) # Ensure that we can only have one filter list with the same name and field validators = [ @@ -150,73 +157,6 @@ class FilterListSerializer(ModelSerializer): ] -class FilterSettingsSerializer(ModelSerializer): - """A class providing (de-)serialization of `FilterSettings` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = FilterSettings - fields = ( - 'id', - 'ping_type', - 'filter_dm', - 'dm_ping_type', - 'delete_messages', - 'bypass_roles', - 'enabled', - 'default_action', - 'default_range' - ) - - -class FilterActionSerializer(ModelSerializer): - """A class providing (de-)serialization of `FilterAction` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = FilterAction - fields = ('id', 'dm_content', 'infraction_type', 'infraction_reason', 'infraction_duration') - - -class FilterChannelRangeSerializer(ModelSerializer): - """A class providing (de-)serialization of `ChannelRange` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = ChannelRange - fields = ( - 'id', - 'disallowed_channels', - 'disallowed_categories', - 'allowed_channels', - 'allowed_categories', - 'default' - ) - - -class FilterOverrideSerializer(ModelSerializer): - """A class providing (de-)serialization of `FilterOverride` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = FilterOverride - fields = ( - 'id', - 'ping_type', - 'filter_dm', - 'dm_ping_type', - 'delete_messages', - 'bypass_roles', - 'enabled', - 'filter_action', - 'filter_range' - ) - - class InfractionSerializer(ModelSerializer): """A class providing (de-)serialization of `Infraction` instances.""" diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 7af2e505..4e8edaf0 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -7,11 +7,7 @@ from .viewsets import ( # noqa: I101 - Preserving the filter order DeletedMessageViewSet, DocumentationLinkViewSet, FilterListViewSet, - FilterSettingsViewSet, - FilterActionViewSet, - FilterChannelRangeViewSet, FilterViewSet, - FilterOverrideViewSet, InfractionViewSet, NominationViewSet, OffTopicChannelNameViewSet, @@ -27,22 +23,6 @@ bot_router.register( 'filter/filter_lists', FilterListViewSet ) -bot_router.register( - 'filter/filter_settings', - FilterSettingsViewSet -) -bot_router.register( - 'filter/filter_action', - FilterActionViewSet -) -bot_router.register( - 'filter/channel_range', - FilterChannelRangeViewSet -) -bot_router.register( - 'filter/filter_override', - FilterOverrideViewSet -) bot_router.register( 'filter/filters', FilterViewSet diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index b3992d66..4cf4c655 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -5,11 +5,7 @@ from .bot import ( DocumentationLinkViewSet, InfractionViewSet, FilterListViewSet, - FilterSettingsViewSet, - FilterActionViewSet, - FilterChannelRangeViewSet, FilterViewSet, - FilterOverrideViewSet, NominationViewSet, OffensiveMessageViewSet, OffTopicChannelNameViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index 781624bd..4649fcde 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,11 +1,7 @@ # flake8: noqa from .filters import ( FilterListViewSet, - FilterSettingsViewSet, - FilterActionViewSet, - FilterChannelRangeViewSet, - FilterViewSet, - FilterOverrideViewSet + FilterViewSet ) from .bot_setting import BotSettingViewSet from .deleted_message import DeletedMessageViewSet diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 1b893f8c..5b21de26 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -2,19 +2,11 @@ from rest_framework.viewsets import ModelViewSet from pydis_site.apps.api.models.bot.filters import ( # noqa: I101 - Preserving the filter order FilterList, - FilterSettings, - FilterAction, - ChannelRange, - Filter, - FilterOverride + Filter ) from pydis_site.apps.api.serializers import ( # noqa: I101 - Preserving the filter order FilterListSerializer, - FilterSettingsSerializer, - FilterActionSerializer, - FilterChannelRangeSerializer, FilterSerializer, - FilterOverrideSerializer ) @@ -90,311 +82,6 @@ class FilterListViewSet(ModelViewSet): queryset = FilterList.objects.all() -class FilterSettingsViewSet(ModelViewSet): - """ - View providing CRUD operations on settings of items allowed or denied by our bot. - - ## Routes - ### GET /bot/filter/filter_settings - Returns all FilterSettings items in the database. - - #### Response format - >>> [ - ... { - ... "id": 1, - ... "ping_type": [ - ... "onduty", - ... ... - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty", - ... ... - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601, - ... ... - ... ], - ... "enabled": True, - ... "default_action": 1, - ... "default_range": 1 - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - 401: returned if unauthenticated - - ### GET /bot/filter/filter_settings/ - Returns a specific FilterSettings item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "ping_type": [ - ... "onduty", - ... ... - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty", - ... ... - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601, - ... ... - ... ], - ... "enabled": True, - ... "default_action": 1, - ... "default_range": 1 - ... } - - #### Status codes - - 200: returned on success - - 404: returned if the id was not found. - - ### POST /bot/filter/filter_settings - Adds a single FilterSettings item to the database. - - #### Request body - >>> { - ... "ping_type": [ - ... "onduty", - ... ... - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty", - ... ... - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601, - ... ... - ... ], - ... "enabled": True, - ... "default_action": 1, - ... "default_range": 1 - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### PATCH /bot/filter/filter_settings/ - Updates a specific FilterSettings item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "ping_type": [ - ... "onduty", - ... ... - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty", - ... ... - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601, - ... ... - ... ], - ... "enabled": True, - ... "default_action": 1, - ... "default_range": 1 - ... } - - #### Status codes - - 200: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/filter/filter_settings/ - Deletes the FilterSettings item with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `id` does not exist - """ - - serializer_class = FilterSettingsSerializer - queryset = FilterSettings.objects.all() - - -class FilterActionViewSet(ModelViewSet): - """ - View providing CRUD operations on actions taken by items allowed or denied by our bot. - - ## Routes - ### GET /bot/filter/filter_action - Returns all FilterAction items in the database. - - #### Response format - >>> [ - ... { - ... "id": 1, - ... "dm_content": "message", - ... "infraction_type": "Warn", - ... "infraction_reason": "", - ... "infraction_duration": "01 12:34:56.123456" - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - 401: returned if unauthenticated - - ### GET /bot/filter/filter_action/ - Returns a specific FilterAction item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "dm_content": "message", - ... "infraction_type": "Warn", - ... "infraction_reason": "", - ... "infraction_duration": "01 12:34:56.123456" - ... } - - #### Status codes - - 200: returned on success - - 404: returned if the id was not found. - - ### POST /bot/filter/filter_action - Adds a single FilterAction item to the database. - - #### Request body - >>> { - ... "dm_content": "message", - ... "infraction_type": "Warn", - ... "infraction_reason": "", - ... "infraction_duration": "01 12:34:56.123456" - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### PATCH /bot/filter/filter_action/ - Updates a specific FilterAction item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "dm_content": "message", - ... "infraction_type": "Warn", - ... "infraction_reason": "", - ... "infraction_duration": "01 12:34:56.123456" - ... } - - #### Status codes - - 200: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/filter/filter_action/ - Deletes the FilterAction item with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `id` does not exist - """ - - serializer_class = FilterActionSerializer - queryset = FilterAction.objects.all() - - -class FilterChannelRangeViewSet(ModelViewSet): - """ - View providing CRUD operations on channels targeted by items allowed or denied by our bot. - - ## Routes - ### GET /bot/filter/channel_range - Returns all ChannelRange items in the database. - - #### Response format - >>> [ - ... { - ... "id": 1, - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [], - ... "default": True - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - 401: returned if unauthenticated - - ### GET /bot/filter/channel_range/ - Returns a specific ChannelRange item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [], - ... "default": True - ... } - - #### Status codes - - 200: returned on success - - 404: returned if the id was not found. - - ### POST /bot/filter/channel_range - Adds a single ChannelRange item to the database. - - #### Request body - >>> { - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [], - ... "default": True - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### PATCH /bot/filter/channel_range/ - Updates a specific ChannelRange item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [], - ... "default": True - ... } - - #### Status codes - - 200: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/filter/channel_range/ - Deletes the ChannelRange item with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `id` does not exist - """ - - serializer_class = FilterChannelRangeSerializer - queryset = ChannelRange.objects.all() - - class FilterViewSet(ModelViewSet): """ View providing CRUD operations on items allowed or denied by our bot. @@ -477,138 +164,3 @@ class FilterViewSet(ModelViewSet): serializer_class = FilterSerializer queryset = Filter.objects.all() - - -class FilterOverrideViewSet(ModelViewSet): - """ - View providing CRUD operations setting overrides of items allowed or denied by our bot. - - ## Routes - ### GET /bot/filter/filter_override - Returns all FilterOverride items in the database. - - #### Response format - >>> [ - ... { - ... "id": 1, - ... "ping_type": [ - ... "onduty", - ... ... - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty", - ... ... - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601, - ... ... - ... ], - ... "enabled": True, - ... "filter_action": 1, - ... "filter_range": 1 - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - 401: returned if unauthenticated - - ### GET /bot/filter/filter_override/ - Returns a specific FilterOverride item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "ping_type": [ - ... "onduty", - ... ... - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty", - ... ... - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601, - ... ... - ... ], - ... "enabled": True, - ... "filter_action": 1, - ... "filter_range": 1 - ... } - - #### Status codes - - 200: returned on success - - 404: returned if the id was not found. - - ### POST /bot/filter/filter_override - Adds a single FilterOverride item to the database. - - #### Request body - >>> { - ... "ping_type": [ - ... "onduty", - ... ... - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty", - ... ... - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601, - ... ... - ... ], - ... "enabled": True, - ... "filter_action": 1, - ... "filter_range": 1 - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### PATCH /bot/filter/filter_override/ - Updates a specific FilterOverride item from the database. - - #### Response format - >>> { - ... "id": 1, - ... "ping_type": [ - ... "onduty", - ... ... - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty", - ... ... - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601, - ... ... - ... ], - ... "enabled": True, - ... "filter_action": 1, - ... "filter_range": 1 - ... } - - #### Status codes - - 200: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/filter/filter_override/ - Deletes the FilterOverride item with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `id` does not exist - """ - - serializer_class = FilterOverrideSerializer - queryset = FilterOverride.objects.all() -- cgit v1.2.3 From faf1948eb39f0389633a6f86f2d4e406f6e83b74 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 25 Sep 2021 13:06:20 +0300 Subject: Add AbstractModelMeta mixin --- pydis_site/apps/api/models/mixins.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 5d75b78b..d32e6e72 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,3 +1,4 @@ +from abc import ABCMeta from operator import itemgetter from django.db import models @@ -29,3 +30,7 @@ class ModelTimestampMixin(models.Model): """Metaconfig for the mixin.""" abstract = True + + +class AbstractModelMeta(ABCMeta, type(models.Model)): + """Metaclass for ABCModel class.""" -- cgit v1.2.3 From 679472436bbb6250fab91d333c3e6fe3a20dea90 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 25 Sep 2021 13:06:38 +0300 Subject: Update filters migrations --- .../apps/api/migrations/0070_new_filter_schema.py | 129 +++++++++------------ .../apps/api/migrations/0071_auto_20210711_0839.py | 44 ------- 2 files changed, 53 insertions(+), 120 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0071_auto_20210711_0839.py diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 237ce7d7..7925f5ff 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -14,7 +14,6 @@ OLD_LIST_NAMES = (('GUILD_INVITE', 'ALLOW'), ('FILE_FORMAT', 'DENY'), ('DOMAIN_N def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: filter_: pydis_site.apps.api.models.Filter = apps.get_model("api", "Filter") filter_list: pydis_site.apps.api.models.FilterList = apps.get_model("api", "FilterList") - filter_settings: pydis_site.apps.api.models.FilterSettings = apps.get_model("api", "FilterSettings") channel_range: pydis_site.apps.api.models.ChannelRange = apps.get_model("api", "ChannelRange") filter_action: pydis_site.apps.api.models.FilterAction = apps.get_model("api", "FilterAction") filter_list_old = apps.get_model("api", "FilterListOld") @@ -22,44 +21,47 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: for name, type_ in OLD_LIST_NAMES: objects = filter_list_old.objects.filter(type=name) - default_action = filter_action.objects.create( + list_ = filter_list.objects.create( + name=name.lower(), + list_type=1 if type_ == "ALLOW" else 0, + ping_type=["onduty"], + filter_dm=True, + dm_ping_type=["onduty"], + delete_messages=True, + bypass_roles=[267630620367257601], + enabled=False, dm_content=None, infraction_type=None, infraction_reason="", - infraction_duration=None - ) - default_action.save() - default_range = channel_range.objects.create( + infraction_duration=None, disallowed_channels=[], disallowed_categories=[], allowed_channels=[], allowed_categories=[], default=True ) - default_range.save() - default_settings = filter_settings.objects.create( - ping_type=["onduty"], - filter_dm=True, - dm_ping_type=["onduty"], - delete_messages=True, - bypass_roles=[267630620367257601], - enabled=False, - default_action=default_action, - default_range=default_range - ) - default_settings.save() - list_ = filter_list.objects.create( - name=name.lower(), - default_settings=default_settings, - list_type=1 if type_ == "ALLOW" else 0 - ) for object_ in objects: new_object = filter_.objects.create( content=object_.content, - filter_list = list_, + filter_list=list_, description=object_.comment or "", - additional_field=None, override=None + additional_field=None, + ping_type=None, + filter_dm=None, + dm_ping_type=None, + delete_messages=None, + bypass_roles=None, + enabled=None, + dm_content=None, + infraction_type=None, + infraction_reason="", + infraction_duration=None, + disallowed_channels=[], + disallowed_categories=[], + allowed_channels=[], + allowed_categories=[], + default=False ) new_object.save() @@ -75,17 +77,6 @@ class Migration(migrations.Migration): old_name='FilterList', new_name='FilterListOld' ), - migrations.CreateModel( - name='ChannelRange', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('default', models.BooleanField()), - ], - ), migrations.CreateModel( name='Filter', fields=[ @@ -93,60 +84,46 @@ class Migration(migrations.Migration): ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200)), ('additional_field', models.BooleanField(help_text='Implementation specific field.', null=True)), - ], - ), - migrations.CreateModel( - name='FilterAction', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), + ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), + ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), + ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None, null=True)), + ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('infraction_type', models.CharField(choices=[('Note', 'Note'), ('Warn', 'Warn'), ('Mute', 'Mute'), ('Kick', 'Kick'), ('Ban', 'Ban')], help_text='The infraction to apply to this user.', max_length=4, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('default', models.BooleanField()), ], ), migrations.CreateModel( - name='FilterSettings', + name='FilterList', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), + ('list_type', models.IntegerField(choices=[(1, 'Allow'), (0, 'Deny')], help_text='Whether this list is an allowlist or denylist')), ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), - ('filter_dm', models.BooleanField(help_text='Whenever DMs should be filtered.')), + ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), - ('delete_messages', models.BooleanField(help_text='Whenever this filter should delete messages triggering it.')), + ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None)), - ('enabled', models.BooleanField(help_text='Whenever ths filter is currently enabled.')), - ('default_action', models.ForeignKey(help_text='The default action to perform.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterAction')), - ('default_range', models.ForeignKey(help_text='Where does this filter apply.', on_delete=django.db.models.deletion.CASCADE, to='api.ChannelRange')), - ], - ), - migrations.CreateModel( - name='FilterOverride', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), null=True, size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), - ('filter_dm', models.BooleanField(null=True)), - ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), null=True, size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), - ('delete_messages', models.BooleanField(null=True)), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), - ('enabled', models.BooleanField(null=True)), - ('filter_action', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.FilterAction')), - ('filter_range', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.ChannelRange')), - ], - ), - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), - ('list_type', models.IntegerField(choices=[], help_text='Whenever this list is an allowlist or denylist')), - ('default_settings', models.ForeignKey(help_text='Default parameters of this list.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterSettings')), + ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), + ('infraction_type', models.CharField(choices=[('Note', 'Note'), ('Warn', 'Warn'), ('Mute', 'Mute'), ('Kick', 'Kick'), ('Ban', 'Ban')], help_text='The infraction to apply to this user.', max_length=4, null=True)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), + ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('default', models.BooleanField()), ], ), - migrations.AddField( - model_name='filter', - name='override', - field=models.ForeignKey(help_text='Override the default settings.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.FilterOverride'), - ), migrations.AddField( model_name='filter', name='filter_list', diff --git a/pydis_site/apps/api/migrations/0071_auto_20210711_0839.py b/pydis_site/apps/api/migrations/0071_auto_20210711_0839.py deleted file mode 100644 index e1c45fb6..00000000 --- a/pydis_site/apps/api/migrations/0071_auto_20210711_0839.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 3.0.14 on 2021-07-11 08:39 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0070_new_filter_schema'), - ] - - operations = [ - migrations.AlterField( - model_name='filterlist', - name='list_type', - field=models.IntegerField(choices=[(1, 'Allow'), (0, 'Deny')], help_text='Whether this list is an allowlist or denylist'), - ), - migrations.AlterField( - model_name='filtersettings', - name='default_action', - field=models.ForeignKey(help_text='What action to perform on the triggering user.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterAction'), - ), - migrations.AlterField( - model_name='filtersettings', - name='default_range', - field=models.ForeignKey(help_text='The channels and categories in which this filter applies.', on_delete=django.db.models.deletion.CASCADE, to='api.ChannelRange'), - ), - migrations.AlterField( - model_name='filtersettings', - name='delete_messages', - field=models.BooleanField(help_text='Whether this filter should delete messages triggering it.'), - ), - migrations.AlterField( - model_name='filtersettings', - name='enabled', - field=models.BooleanField(help_text='Whether this filter is currently enabled.'), - ), - migrations.AlterField( - model_name='filtersettings', - name='filter_dm', - field=models.BooleanField(help_text='Whether DMs should be filtered.'), - ), - ] -- cgit v1.2.3 From 75a4b0eb57520b247ecaa228440b1abbd6c65845 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 25 Sep 2021 13:08:01 +0300 Subject: Remove default field from FilterSettingsMixin and migration --- pydis_site/apps/api/migrations/0070_new_filter_schema.py | 8 ++------ pydis_site/apps/api/models/bot/filters.py | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 7925f5ff..c1db2a07 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -37,8 +37,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: disallowed_channels=[], disallowed_categories=[], allowed_channels=[], - allowed_categories=[], - default=True + allowed_categories=[] ) for object_ in objects: @@ -60,8 +59,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: disallowed_channels=[], disallowed_categories=[], allowed_channels=[], - allowed_categories=[], - default=False + allowed_categories=[] ) new_object.save() @@ -98,7 +96,6 @@ class Migration(migrations.Migration): ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('default', models.BooleanField()), ], ), migrations.CreateModel( @@ -121,7 +118,6 @@ class Migration(migrations.Migration): ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('default', models.BooleanField()), ], ), migrations.AddField( diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 68ac191b..365259e7 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -104,12 +104,10 @@ class FilterSettingsMixin(models.Model, metaclass=AbstractModelMeta): # - disallowed categories # - allowed categories # - allowed channels - # - default disallowed_channels = ArrayField(models.IntegerField()) disallowed_categories = ArrayField(models.IntegerField()) allowed_channels = ArrayField(models.IntegerField()) allowed_categories = ArrayField(models.IntegerField()) - default = models.BooleanField() class Meta: """Metaclass for settings mixin.""" -- cgit v1.2.3 From c5092f2895447b672dd9101a32997ce8a1c737e3 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 8 Oct 2021 19:21:36 +0300 Subject: Remove old models from migration --- pydis_site/apps/api/migrations/0070_new_filter_schema.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index c1db2a07..aa114ca1 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -14,8 +14,6 @@ OLD_LIST_NAMES = (('GUILD_INVITE', 'ALLOW'), ('FILE_FORMAT', 'DENY'), ('DOMAIN_N def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: filter_: pydis_site.apps.api.models.Filter = apps.get_model("api", "Filter") filter_list: pydis_site.apps.api.models.FilterList = apps.get_model("api", "FilterList") - channel_range: pydis_site.apps.api.models.ChannelRange = apps.get_model("api", "ChannelRange") - filter_action: pydis_site.apps.api.models.FilterAction = apps.get_model("api", "FilterAction") filter_list_old = apps.get_model("api", "FilterListOld") for name, type_ in OLD_LIST_NAMES: -- cgit v1.2.3 From 25da18321e82f0a3cd18923d59d86b59acec160d Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 Oct 2021 19:46:07 +0300 Subject: Update filters API to actually work --- .../apps/api/migrations/0070_new_filter_schema.py | 16 +-- pydis_site/apps/api/models/bot/filters.py | 129 ++++++++++++--------- pydis_site/apps/api/models/mixins.py | 5 - pydis_site/apps/api/serializers.py | 58 ++++++--- 4 files changed, 123 insertions(+), 85 deletions(-) diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index aa114ca1..a595bda2 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -54,10 +54,10 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: infraction_type=None, infraction_reason="", infraction_duration=None, - disallowed_channels=[], - disallowed_categories=[], - allowed_channels=[], - allowed_categories=[] + disallowed_channels=None, + disallowed_categories=None, + allowed_channels=None, + allowed_categories=None ) new_object.save() @@ -90,10 +90,10 @@ class Migration(migrations.Migration): ('infraction_type', models.CharField(choices=[('Note', 'Note'), ('Warn', 'Warn'), ('Mute', 'Mute'), ('Kick', 'Kick'), ('Ban', 'Ban')], help_text='The infraction to apply to this user.', max_length=4, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), - ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), + ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), + ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), + ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), + ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), ], ), migrations.CreateModel( diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 365259e7..b9a081e6 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -1,4 +1,3 @@ -from abc import abstractmethod from typing import List from django.contrib.postgres.fields import ArrayField @@ -6,8 +5,6 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models import UniqueConstraint -from pydis_site.apps.api.models.mixins import AbstractModelMeta - class FilterListType(models.IntegerChoices): """Choice between allow or deny for a list type.""" @@ -43,40 +40,9 @@ def validate_ping_field(value_list: List[str]) -> None: raise ValidationError(f"{value!r} isn't a valid ping type.") -class FilterSettingsMixin(models.Model, metaclass=AbstractModelMeta): - """Mixin for settings of a filter list.""" - - @staticmethod - @abstractmethod - def allow_null() -> bool: - """Abstract property for allowing null values.""" +class FilterSettingsMixin(models.Model): + """Mixin for common settings of a filters and filter lists.""" - ping_type = ArrayField( - models.CharField(max_length=20), - validators=(validate_ping_field,), - help_text="Who to ping when this filter triggers.", - null=allow_null.__func__() - ) - filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=True) - dm_ping_type = ArrayField( - models.CharField(max_length=20), - validators=(validate_ping_field,), - help_text="Who to ping when this filter triggers on a DM.", - null=allow_null.__func__() - ) - delete_messages = models.BooleanField( - help_text="Whether this filter should delete messages triggering it.", - null=allow_null.__func__() - ) - bypass_roles = ArrayField( - models.BigIntegerField(), - help_text="Roles and users who can bypass this filter.", - null=allow_null.__func__() - ) - enabled = models.BooleanField( - help_text="Whether this filter is currently enabled.", - null=allow_null.__func__() - ) dm_content = models.CharField( max_length=1000, null=True, @@ -97,18 +63,6 @@ class FilterSettingsMixin(models.Model, metaclass=AbstractModelMeta): help_text="The duration of the infraction. Null if permanent." ) - # Where a filter should apply. - # - # The resolution is done in the following order: - # - disallowed channels - # - disallowed categories - # - allowed categories - # - allowed channels - disallowed_channels = ArrayField(models.IntegerField()) - disallowed_categories = ArrayField(models.IntegerField()) - allowed_channels = ArrayField(models.IntegerField()) - allowed_categories = ArrayField(models.IntegerField()) - class Meta: """Metaclass for settings mixin.""" @@ -123,11 +77,43 @@ class FilterList(FilterSettingsMixin): choices=FilterListType.choices, help_text="Whether this list is an allowlist or denylist" ) - - @staticmethod - def allow_null() -> bool: - """Do not allow null values for default settings.""" - return False + ping_type = ArrayField( + models.CharField(max_length=20), + validators=(validate_ping_field,), + help_text="Who to ping when this filter triggers.", + null=False + ) + filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=False) + dm_ping_type = ArrayField( + models.CharField(max_length=20), + validators=(validate_ping_field,), + help_text="Who to ping when this filter triggers on a DM.", + null=False + ) + delete_messages = models.BooleanField( + help_text="Whether this filter should delete messages triggering it.", + null=False + ) + bypass_roles = ArrayField( + models.BigIntegerField(), + help_text="Roles and users who can bypass this filter.", + null=False + ) + enabled = models.BooleanField( + help_text="Whether this filter is currently enabled.", + null=False + ) + # Where a filter should apply. + # + # The resolution is done in the following order: + # - disallowed channels + # - disallowed categories + # - allowed categories + # - allowed channels + disallowed_channels = ArrayField(models.IntegerField()) + disallowed_categories = ArrayField(models.IntegerField()) + allowed_channels = ArrayField(models.IntegerField()) + allowed_categories = ArrayField(models.IntegerField()) class Meta: """Constrain name and list_type unique.""" @@ -150,11 +136,38 @@ class Filter(FilterSettingsMixin): FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." ) + ping_type = ArrayField( + models.CharField(max_length=20), + validators=(validate_ping_field,), + help_text="Who to ping when this filter triggers.", + null=True + ) + filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=True) + dm_ping_type = ArrayField( + models.CharField(max_length=20), + validators=(validate_ping_field,), + help_text="Who to ping when this filter triggers on a DM.", + null=True + ) + delete_messages = models.BooleanField( + help_text="Whether this filter should delete messages triggering it.", + null=True + ) + bypass_roles = ArrayField( + models.BigIntegerField(), + help_text="Roles and users who can bypass this filter.", + null=True + ) + enabled = models.BooleanField( + help_text="Whether this filter is currently enabled.", + null=True + ) + + # Check FilterList model for information about these properties. + disallowed_channels = ArrayField(models.IntegerField(), null=True) + disallowed_categories = ArrayField(models.IntegerField(), null=True) + allowed_channels = ArrayField(models.IntegerField(), null=True) + allowed_categories = ArrayField(models.IntegerField(), null=True) def __str__(self) -> str: return f"Filter {self.content!r}" - - @staticmethod - def allow_null() -> bool: - """Allow null values for overrides.""" - return True diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index d32e6e72..5d75b78b 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,4 +1,3 @@ -from abc import ABCMeta from operator import itemgetter from django.db import models @@ -30,7 +29,3 @@ class ModelTimestampMixin(models.Model): """Metaconfig for the mixin.""" abstract = True - - -class AbstractModelMeta(ABCMeta, type(models.Model)): - """Metaclass for ABCModel class.""" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index ff2bd929..4e92b3a0 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -113,6 +113,29 @@ class DocumentationLinkSerializer(ModelSerializer): fields = ('package', 'base_url', 'inventory_url') +ALWAYS_OPTIONAL_SETTINGS = ( + 'dm_content', + 'infraction_type', + 'infraction_reason', + 'infraction_duration', +) + +REQUIRED_FOR_FILTER_LIST_SETTINGS = ( + 'ping_type', + 'filter_dm', + 'dm_ping_type', + 'delete_messages', + 'bypass_roles', + 'enabled', + 'disallowed_channels', + 'disallowed_categories', + 'allowed_channels', + 'allowed_categories', +) + +SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS + + class FilterSerializer(ModelSerializer): """A class providing (de-)serialization of `Filter` instances.""" @@ -120,7 +143,16 @@ class FilterSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = Filter - fields = ('id', 'content', 'description', 'additional_field', 'filter_list', 'override') + fields = ('id', 'content', 'description', 'additional_field', 'filter_list') + SETTINGS_FIELDS + extra_kwargs = { + field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS + } | { + 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, + 'disallowed_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'disallowed_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'allowed_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'allowed_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, + } class FilterListSerializer(ModelSerializer): @@ -132,18 +164,16 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ( - 'id', - 'name', - 'list_type', - 'filters', - 'ping_type', - 'filter_dm', - 'dm_ping_type', - 'delete_messages', - 'bypass_roles', - '' - ) + fields = ('id', 'name', 'list_type', 'filters') + SETTINGS_FIELDS + extra_kwargs = { + field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS + } | { + 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, + 'disallowed_channels': {'allow_empty': True}, + 'disallowed_categories': {'allow_empty': True}, + 'allowed_channels': {'allow_empty': True}, + 'allowed_categories': {'allow_empty': True}, + } # Ensure that we can only have one filter list with the same name and field validators = [ @@ -200,7 +230,7 @@ class InfractionSerializer(ModelSerializer): if hidden and infr_type in ('superstar', 'warning', 'voice_ban'): raise ValidationError({'hidden': [f'{infr_type} infractions cannot be hidden.']}) - if not hidden and infr_type in ('note', ): + if not hidden and infr_type in ('note',): raise ValidationError({'hidden': [f'{infr_type} infractions must be hidden.']}) return attrs -- cgit v1.2.3 From 4c2eaff72ba9e95e1ef8d7b40396187783d87a50 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 Oct 2021 20:15:36 +0300 Subject: Add basic validation for infraction fields + use common infraction types --- pydis_site/apps/api/models/bot/filters.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index b9a081e6..eebcf703 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -5,6 +5,8 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models import UniqueConstraint +from pydis_site.apps.api.models import Infraction + class FilterListType(models.IntegerChoices): """Choice between allow or deny for a list type.""" @@ -13,16 +15,6 @@ class FilterListType(models.IntegerChoices): DENY = 0 -class InfractionType(models.TextChoices): - """Possible type of infractions.""" - - NOTE = "Note" - WARN = "Warn" - MUTE = "Mute" - KICK = "Kick" - BAN = "Ban" - - # Valid special values in ping related fields VALID_PINGS = ("everyone", "here", "moderators", "onduty", "admins") @@ -49,7 +41,7 @@ class FilterSettingsMixin(models.Model): help_text="The DM to send to a user triggering this filter." ) infraction_type = models.CharField( - choices=InfractionType.choices, + choices=Infraction.TYPE_CHOICES, max_length=4, null=True, help_text="The infraction to apply to this user." @@ -63,6 +55,11 @@ class FilterSettingsMixin(models.Model): help_text="The duration of the infraction. Null if permanent." ) + def clean(self): + """Validate infraction fields as whole.""" + if (self.infraction_duration or self.infraction_reason) and not self.infraction_type: + raise ValidationError("Infraction type is required if setting infraction duration or reason.") + class Meta: """Metaclass for settings mixin.""" -- cgit v1.2.3 From d8ad1bdbcfcc8a0881c0ceb4d7d486455d23e170 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 Oct 2021 20:31:09 +0300 Subject: Add validation to filters to not allow duplicated channels and categories --- pydis_site/apps/api/models/bot/filters.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index eebcf703..45dea2c4 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -112,6 +112,20 @@ class FilterList(FilterSettingsMixin): allowed_channels = ArrayField(models.IntegerField()) allowed_categories = ArrayField(models.IntegerField()) + def clean(self): + """Do not allow duplicates in allowed and disallowed lists.""" + # Still run infraction fields validation + super().clean() + + channels_collection = self.allowed_channels + self.disallowed_channels + categories_collection = self.allowed_categories + self.disallowed_categories + + if len(channels_collection) != len(set(channels_collection)): + raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + + if len(categories_collection) != len(set(categories_collection)): + raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + class Meta: """Constrain name and list_type unique.""" @@ -166,5 +180,20 @@ class Filter(FilterSettingsMixin): allowed_channels = ArrayField(models.IntegerField(), null=True) allowed_categories = ArrayField(models.IntegerField(), null=True) + def clean(self): + """Do not allow duplicates in allowed and disallowed lists.""" + # Still run infraction fields validation + super().clean() + + if self.allowed_channels is not None or self.disallowed_channels is not None: + channels_collection = self.allowed_channels + self.disallowed_channels + if len(channels_collection) != len(set(channels_collection)): + raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + + if self.allowed_categories is not None or self.disallowed_categories is not None: + categories_collection = self.allowed_categories + self.disallowed_categories + if len(categories_collection) != len(set(categories_collection)): + raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + def __str__(self) -> str: return f"Filter {self.content!r}" -- cgit v1.2.3 From f4152448dfa4cd9912c22134af01fe37f0b153f6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 Oct 2021 20:38:59 +0300 Subject: Add validation to filters to not allow duplicates + additional_field -> JSON --- pydis_site/apps/api/migrations/0070_new_filter_schema.py | 6 +++--- pydis_site/apps/api/models/bot/filters.py | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index a595bda2..8716cbad 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -79,7 +79,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200)), - ('additional_field', models.BooleanField(help_text='Implementation specific field.', null=True)), + ('additional_field', django.contrib.postgres.fields.jsonb.JSONField(help_text='Implementation specific field.', null=True)), ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), @@ -87,7 +87,7 @@ class Migration(migrations.Migration): ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None, null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), - ('infraction_type', models.CharField(choices=[('Note', 'Note'), ('Warn', 'Warn'), ('Mute', 'Mute'), ('Kick', 'Kick'), ('Ban', 'Ban')], help_text='The infraction to apply to this user.', max_length=4, null=True)), + ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), @@ -109,7 +109,7 @@ class Migration(migrations.Migration): ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), - ('infraction_type', models.CharField(choices=[('Note', 'Note'), ('Warn', 'Warn'), ('Mute', 'Mute'), ('Kick', 'Kick'), ('Ban', 'Ban')], help_text='The infraction to apply to this user.', max_length=4, null=True)), + ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 45dea2c4..472354f8 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -1,11 +1,12 @@ from typing import List -from django.contrib.postgres.fields import ArrayField +from django.contrib.postgres.fields import ArrayField, JSONField from django.core.exceptions import ValidationError from django.db import models from django.db.models import UniqueConstraint -from pydis_site.apps.api.models import Infraction +# Must be imported that way to avoid circular imports +from .infraction import Infraction class FilterListType(models.IntegerChoices): @@ -42,7 +43,7 @@ class FilterSettingsMixin(models.Model): ) infraction_type = models.CharField( choices=Infraction.TYPE_CHOICES, - max_length=4, + max_length=9, null=True, help_text="The infraction to apply to this user." ) @@ -142,7 +143,7 @@ class Filter(FilterSettingsMixin): content = models.CharField(max_length=100, help_text="The definition of this filter.") description = models.CharField(max_length=200, help_text="Why this filter has been added.") - additional_field = models.BooleanField(null=True, help_text="Implementation specific field.") + additional_field = JSONField(null=True, help_text="Implementation specific field.") filter_list = models.ForeignKey( FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." -- cgit v1.2.3 From b49612db59ca075f64a4c7da11e3c9ce7e7b19eb Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 29 Oct 2021 20:11:27 +0300 Subject: Move filters validations to serializers --- pydis_site/apps/api/models/bot/filters.py | 34 ------------------------------- pydis_site/apps/api/serializers.py | 33 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 472354f8..3a1f3c6a 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -56,11 +56,6 @@ class FilterSettingsMixin(models.Model): help_text="The duration of the infraction. Null if permanent." ) - def clean(self): - """Validate infraction fields as whole.""" - if (self.infraction_duration or self.infraction_reason) and not self.infraction_type: - raise ValidationError("Infraction type is required if setting infraction duration or reason.") - class Meta: """Metaclass for settings mixin.""" @@ -113,20 +108,6 @@ class FilterList(FilterSettingsMixin): allowed_channels = ArrayField(models.IntegerField()) allowed_categories = ArrayField(models.IntegerField()) - def clean(self): - """Do not allow duplicates in allowed and disallowed lists.""" - # Still run infraction fields validation - super().clean() - - channels_collection = self.allowed_channels + self.disallowed_channels - categories_collection = self.allowed_categories + self.disallowed_categories - - if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Allowed and disallowed channels lists contain duplicates.") - - if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Allowed and disallowed categories lists contain duplicates.") - class Meta: """Constrain name and list_type unique.""" @@ -181,20 +162,5 @@ class Filter(FilterSettingsMixin): allowed_channels = ArrayField(models.IntegerField(), null=True) allowed_categories = ArrayField(models.IntegerField(), null=True) - def clean(self): - """Do not allow duplicates in allowed and disallowed lists.""" - # Still run infraction fields validation - super().clean() - - if self.allowed_channels is not None or self.disallowed_channels is not None: - channels_collection = self.allowed_channels + self.disallowed_channels - if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Allowed and disallowed channels lists contain duplicates.") - - if self.allowed_categories is not None or self.disallowed_categories is not None: - categories_collection = self.allowed_categories + self.disallowed_categories - if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Allowed and disallowed categories lists contain duplicates.") - def __str__(self) -> str: return f"Filter {self.content!r}" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 4e92b3a0..b5f083b0 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -139,6 +139,23 @@ SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS class FilterSerializer(ModelSerializer): """A class providing (de-)serialization of `Filter` instances.""" + def validate(self, data): + """Perform infraction data + allow and disallowed lists validation.""" + if (data.get('infraction_reason') or data.get('infraction_duration')) and not data.get('infraction_type'): + raise ValidationError("Infraction type is required with infraction duration or reason") + + if data.get('allowed_channels') is not None and data.get('disallowed_channels') is not None: + channels_collection = data['allowed_channels'] + data['disallowed_channels'] + if len(channels_collection) != len(set(channels_collection)): + raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + + if data.get('allowed_categories') is not None and data.get('disallowed_categories') is not None: + categories_collection = data['allowed_categories'] + data['disallowed_categories'] + if len(categories_collection) != len(set(categories_collection)): + raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + + return data + class Meta: """Metadata defined for the Django REST Framework.""" @@ -160,6 +177,22 @@ class FilterListSerializer(ModelSerializer): filters = FilterSerializer(many=True, read_only=True) + def validate(self, data): + """Perform infraction data + allow and disallowed lists validation.""" + if (data['infraction_reason'] or data['infraction_duration']) and not data['infraction_type']: + raise ValidationError("Infraction type is required with infraction duration or reason") + + channels_collection = data['allowed_channels'] + data['disallowed_channels'] + categories_collection = data['allowed_categories'] + data['disallowed_categories'] + + if len(channels_collection) != len(set(channels_collection)): + raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + + if len(categories_collection) != len(set(categories_collection)): + raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + + return data + class Meta: """Metadata defined for the Django REST Framework.""" -- cgit v1.2.3 From 55d9288f11e2981eb9251f92164a597869c07cf9 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 29 Oct 2021 20:11:38 +0300 Subject: Add merge migration --- pydis_site/apps/api/migrations/0074_merge_20211017_0822.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0074_merge_20211017_0822.py diff --git a/pydis_site/apps/api/migrations/0074_merge_20211017_0822.py b/pydis_site/apps/api/migrations/0074_merge_20211017_0822.py new file mode 100644 index 00000000..ae41ac71 --- /dev/null +++ b/pydis_site/apps/api/migrations/0074_merge_20211017_0822.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.14 on 2021-10-17 08:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0073_otn_allow_GT_and_LT'), + ('api', '0070_new_filter_schema'), + ] + + operations = [ + ] -- cgit v1.2.3 From 7d21797b8bc14b92a48bc782694e226b2562c1b5 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 29 Oct 2021 20:17:03 +0300 Subject: Fix linting --- pydis_site/apps/api/serializers.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index b5f083b0..c82b0797 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -139,17 +139,25 @@ SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS class FilterSerializer(ModelSerializer): """A class providing (de-)serialization of `Filter` instances.""" - def validate(self, data): + def validate(self, data: dict) -> dict: """Perform infraction data + allow and disallowed lists validation.""" - if (data.get('infraction_reason') or data.get('infraction_duration')) and not data.get('infraction_type'): + if ( + data.get('infraction_reason') or data.get('infraction_duration') + ) and not data.get('infraction_type'): raise ValidationError("Infraction type is required with infraction duration or reason") - if data.get('allowed_channels') is not None and data.get('disallowed_channels') is not None: + if ( + data.get('allowed_channels') is not None + and data.get('disallowed_channels') is not None + ): channels_collection = data['allowed_channels'] + data['disallowed_channels'] if len(channels_collection) != len(set(channels_collection)): raise ValidationError("Allowed and disallowed channels lists contain duplicates.") - if data.get('allowed_categories') is not None and data.get('disallowed_categories') is not None: + if ( + data.get('allowed_categories') is not None + and data.get('disallowed_categories') is not None + ): categories_collection = data['allowed_categories'] + data['disallowed_categories'] if len(categories_collection) != len(set(categories_collection)): raise ValidationError("Allowed and disallowed categories lists contain duplicates.") @@ -160,7 +168,9 @@ class FilterSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = Filter - fields = ('id', 'content', 'description', 'additional_field', 'filter_list') + SETTINGS_FIELDS + fields = ( + 'id', 'content', 'description', 'additional_field', 'filter_list' + ) + SETTINGS_FIELDS extra_kwargs = { field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS } | { @@ -177,9 +187,11 @@ class FilterListSerializer(ModelSerializer): filters = FilterSerializer(many=True, read_only=True) - def validate(self, data): + def validate(self, data: dict) -> dict: """Perform infraction data + allow and disallowed lists validation.""" - if (data['infraction_reason'] or data['infraction_duration']) and not data['infraction_type']: + if ( + data['infraction_reason'] or data['infraction_duration'] + ) and not data['infraction_type']: raise ValidationError("Infraction type is required with infraction duration or reason") channels_collection = data['allowed_channels'] + data['disallowed_channels'] -- cgit v1.2.3 From 8ab32d7820b57b9f3edb61d4bd93864b6037502b Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Sun, 5 Dec 2021 16:09:56 +0100 Subject: Adjust Filter JSON Schema From now on the Serializer will have a different JSON representation than the table schema itself, conforming to the format needed on the bot-side. --- pydis_site/apps/api/serializers.py | 30 +++++++ pydis_site/apps/api/viewsets/bot/filters.py | 127 +++++++++++++++++++++------- 2 files changed, 127 insertions(+), 30 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index c82b0797..864ab52e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -133,6 +133,17 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'allowed_categories', ) +# Required fields for custom JSON representation purposes +BASE_FIELDS = ('id', 'content', 'description', 'additional_field') +BASE_SETTINGS_FIELDS = ("ping_type", "dm_ping_type", "bypass_roles", "filter_dm") +INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") +CHANNEL_SCOPE_FIELDS = ( + "allowed_channels", + "allowed_categories", + "disallowed_channels", + "disallowed_categories" +) + SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS @@ -181,6 +192,25 @@ class FilterSerializer(ModelSerializer): 'allowed_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, } + def to_representation(self, instance: Filter) -> dict: + """ + Provides a custom JSON representation to the Filter Serializers + + That does not affect how the Serializer works in general. + """ + item = Filter.objects.get(id=instance.id) + schema_settings = { + "settings": + {name: getattr(item, name) for name in BASE_SETTINGS_FIELDS} + | {"infraction": {name: getattr(item, name) for name in INFRACTION_FIELDS}} + | {"channel_scope": {name: getattr(item, name) for name in CHANNEL_SCOPE_FIELDS}} + } + + schema_base = {name: getattr(item, name) for name in BASE_FIELDS} | \ + {"filter_list": item.filter_list.id} + + return schema_base | schema_settings + class FilterListSerializer(ModelSerializer): """A class providing (de-)serialization of `FilterList` instances.""" diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 5b21de26..64329ebe 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -27,11 +27,28 @@ class FilterListViewSet(ModelViewSet): ... "filters": [ ... { ... "id": 1, + ... "filter_list": 1 ... "content": "267624335836053506", ... "description": "Python Discord", ... "additional_field": None, - ... "override": 1, - ... "filter_list": 1 + ... "settings": { + ... "ping_type": None, + ... "dm_ping_type": None + ... "bypass_roles": None + ... "filter_dm": None, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None + ... }, + ... "channel_scope": { + ... "allowed_channels": None, + ... "allowed_categories": None, + ... "disallowed_channels": None, + ... "disallowed_categories": None + ... } + ... } + ... ... }, ... ... ... ], @@ -48,23 +65,40 @@ class FilterListViewSet(ModelViewSet): Returns a specific FilterList item from the database. #### Response format - >>> { - ... "id": 1, - ... "name": "guild_invite", - ... "list_type": 1, - ... "filters": [ - ... { - ... "id": 1, - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "override": 1, - ... "filter_list": 1 - ... }, - ... ... - ... ], - ... "default_settings": 1 - ... } + ... { + ... "id": 1, + ... "name": "guild_invite", + ... "list_type": 1, + ... "filters": [ + ... { + ... "id": 1, + ... "filter_list": 1 + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "settings": { + ... "ping_type": None, + ... "dm_ping_type": None + ... "bypass_roles": None + ... "filter_dm": None, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None + ... }, + ... "channel_scope": { + ... "allowed_channels": None, + ... "allowed_categories": None, + ... "disallowed_channels": None, + ... "disallowed_categories": None + ... } + ... } + ... + ... }, + ... ... + ... ], + ... "default_settings": 1 + ... } #### Status codes - 200: returned on success @@ -93,12 +127,28 @@ class FilterViewSet(ModelViewSet): #### Response format >>> [ ... { - ... "id": 1, - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "override": 1, - ... "filter_list": 1 + ... "id": 1, + ... "filter_list": 1 + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "settings": { + ... "ping_type": None, + ... "dm_ping_type": None + ... "bypass_roles": None + ... "filter_dm": None, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None + ... }, + ... "channel_scope": { + ... "allowed_channels": None, + ... "allowed_categories": None, + ... "disallowed_channels": None, + ... "disallowed_categories": None + ... } + ... } ... }, ... ... ... ] @@ -112,11 +162,28 @@ class FilterViewSet(ModelViewSet): #### Response format >>> { - ... "id": 1, - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "override": 1 + ... "id": 1, + ... "filter_list": 1 + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "settings": { + ... "ping_type": None, + ... "dm_ping_type": None + ... "bypass_roles": None + ... "filter_dm": None, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None + ... }, + ... "channel_scope": { + ... "allowed_channels": None, + ... "allowed_categories": None, + ... "disallowed_channels": None, + ... "disallowed_categories": None + ... } + ... } ... } #### Status codes -- cgit v1.2.3 From e3a45e09041898ffd0bccd3c730524e8c673e696 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:22:01 +0100 Subject: Adjust FilterList Representation From now on the FilterList Serializer will contain a settings field with all the settings that were listed previously, on the model. --- pydis_site/apps/api/serializers.py | 17 ++++++++-- pydis_site/apps/api/viewsets/bot/filters.py | 49 +++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 864ab52e..267cf761 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -181,7 +181,7 @@ class FilterSerializer(ModelSerializer): model = Filter fields = ( 'id', 'content', 'description', 'additional_field', 'filter_list' - ) + SETTINGS_FIELDS + ) extra_kwargs = { field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS } | { @@ -194,7 +194,8 @@ class FilterSerializer(ModelSerializer): def to_representation(self, instance: Filter) -> dict: """ - Provides a custom JSON representation to the Filter Serializers + + Provides a custom JSON representation to the Filter Serializers. That does not affect how the Serializer works in general. """ @@ -239,7 +240,7 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ('id', 'name', 'list_type', 'filters') + SETTINGS_FIELDS + fields = ('id', 'name', 'list_type', 'filters') extra_kwargs = { field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS } | { @@ -261,6 +262,16 @@ class FilterListSerializer(ModelSerializer): ), ] + def to_representation(self, instance: FilterList) -> dict: + """ + Provides a custom JSON representation to the FilterList Serializers. + + That does not affect how the Serializer works in general. + """ + ret = super().to_representation(instance) + ret["settings"] = {name: getattr(instance, name) for name in SETTINGS_FIELDS} + return ret + class InfractionSerializer(ModelSerializer): """A class providing (de-)serialization of `Infraction` instances.""" diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 64329ebe..cbadcf2b 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -52,7 +52,28 @@ class FilterListViewSet(ModelViewSet): ... }, ... ... ... ], - ... "default_settings": 1 + ... "settings": { + ... "dm_content": None, + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None, + ... "ping_type": [ + ... "onduty" + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty" + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601 + ... ], + ... "enabled": False, + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_categories": [] + ... } ... }, ... ... ... ] @@ -65,6 +86,7 @@ class FilterListViewSet(ModelViewSet): Returns a specific FilterList item from the database. #### Response format + >>> ... { ... "id": 1, ... "name": "guild_invite", @@ -95,9 +117,30 @@ class FilterListViewSet(ModelViewSet): ... } ... ... }, - ... ... + ... ... ], - ... "default_settings": 1 + ... "settings": { + ... "dm_content": None, + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None, + ... "ping_type": [ + ... "onduty" + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty" + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601 + ... ], + ... "enabled": False, + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_categories": [] + ... } ... } #### Status codes -- cgit v1.2.3 From 3e8f164525bdd3a728bb7383da237feb9aacb44e Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Thu, 9 Dec 2021 20:53:49 +0100 Subject: Adjust FilterList Schema to group settings into subcategories - This commit patches the FilterList serializer's schema, and puts the settings into the relevant subcategories. --- pydis_site/apps/api/serializers.py | 9 ++- pydis_site/apps/api/viewsets/bot/filters.py | 93 ++++++++++++++--------------- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 267cf761..89005a9b 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -269,7 +269,14 @@ class FilterListSerializer(ModelSerializer): That does not affect how the Serializer works in general. """ ret = super().to_representation(instance) - ret["settings"] = {name: getattr(instance, name) for name in SETTINGS_FIELDS} + schema_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + schema_settings = { + "infraction": + {name: getattr(instance, name) for name in INFRACTION_FIELDS}} \ + | { + "channel_scope": + {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} + ret["settings"] = schema_base | schema_settings return ret diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index cbadcf2b..2b587696 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -52,31 +52,29 @@ class FilterListViewSet(ModelViewSet): ... }, ... ... ... ], - ... "settings": { - ... "dm_content": None, - ... "infraction_type": None, - ... "infraction_reason": "", - ... "infraction_duration": None, - ... "ping_type": [ - ... "onduty" - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty" - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601 - ... ], - ... "enabled": False, - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [] - ... } - ... }, - ... ... - ... ] + ... "settings": { + ... "ping_type": [ + ... "onduty" + ... ], + ... "dm_ping_type": [ + ... "onduty" + ... ], + ... "bypass_roles": [ + ... 267630620367257601 + ... ], + ... "filter_dm": True, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None, + ... } + ... "channel_scope": { + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_categories": [] + ... } + ... } #### Status codes - 200: returned on success @@ -120,28 +118,29 @@ class FilterListViewSet(ModelViewSet): ... ... ], ... "settings": { - ... "dm_content": None, - ... "infraction_type": None, - ... "infraction_reason": "", - ... "infraction_duration": None, - ... "ping_type": [ - ... "onduty" - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty" - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601 - ... ], - ... "enabled": False, - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [] - ... } - ... } + ... "ping_type": [ + ... "onduty" + ... ], + ... "dm_ping_type": [ + ... "onduty" + ... ], + ... "bypass_roles": [ + ... 267630620367257601 + ... ], + ... "filter_dm": True, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None, + ... } + ... "channel_scope": { + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_categories": [] + ... } + ... } + ... } #### Status codes - 200: returned on success -- cgit v1.2.3 From a24cac8d43893f792d4fa495cf2a9ce65f69051c Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Fri, 10 Dec 2021 22:19:09 +0100 Subject: Patch Filter and FilterList Serializer validation logic and representation - This commit patches an error with the FilterListSerializer validation logic, so that it won't raise an error when an optional field is not present. - It also adds the `enabled` and `delete_messages` fields, to the FilterSerializer's representation - Furthermore the commit introduces minor bug patches, regarding DRF Serializer Fields. --- pydis_site/apps/api/serializers.py | 73 +++++++++++++++++++---------- pydis_site/apps/api/viewsets/bot/filters.py | 12 +++++ 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 89005a9b..cb8313ac 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -134,8 +134,16 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( ) # Required fields for custom JSON representation purposes -BASE_FIELDS = ('id', 'content', 'description', 'additional_field') -BASE_SETTINGS_FIELDS = ("ping_type", "dm_ping_type", "bypass_roles", "filter_dm") +BASE_FILTER_FIELDS = ('id', 'content', 'description', 'additional_field') +BASE_FILTERLIST_FIELDS = ('id', 'name', 'list_type') +BASE_SETTINGS_FIELDS = ( + "ping_type", + "dm_ping_type", + "bypass_roles", + "filter_dm", + "enabled", + "delete_messages" +) INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") CHANNEL_SCOPE_FIELDS = ( "allowed_channels", @@ -181,7 +189,7 @@ class FilterSerializer(ModelSerializer): model = Filter fields = ( 'id', 'content', 'description', 'additional_field', 'filter_list' - ) + ) + SETTINGS_FIELDS extra_kwargs = { field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS } | { @@ -199,16 +207,18 @@ class FilterSerializer(ModelSerializer): That does not affect how the Serializer works in general. """ - item = Filter.objects.get(id=instance.id) schema_settings = { "settings": - {name: getattr(item, name) for name in BASE_SETTINGS_FIELDS} - | {"infraction": {name: getattr(item, name) for name in INFRACTION_FIELDS}} - | {"channel_scope": {name: getattr(item, name) for name in CHANNEL_SCOPE_FIELDS}} + {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + | {"infraction": {name: getattr(instance, name) for name in INFRACTION_FIELDS}} + | { + "channel_scope": + {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} + } } - schema_base = {name: getattr(item, name) for name in BASE_FIELDS} | \ - {"filter_list": item.filter_list.id} + schema_base = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} | \ + {"filter_list": instance.filter_list.id} return schema_base | schema_settings @@ -221,18 +231,25 @@ class FilterListSerializer(ModelSerializer): def validate(self, data: dict) -> dict: """Perform infraction data + allow and disallowed lists validation.""" if ( - data['infraction_reason'] or data['infraction_duration'] - ) and not data['infraction_type']: + data.get('infraction_reason') or data.get('infraction_duration') + ) and not data.get('infraction_type'): raise ValidationError("Infraction type is required with infraction duration or reason") - channels_collection = data['allowed_channels'] + data['disallowed_channels'] - categories_collection = data['allowed_categories'] + data['disallowed_categories'] - - if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + if ( + data.get('allowed_channels') is not None + and data.get('disallowed_channels') is not None + ): + channels_collection = data['allowed_channels'] + data['disallowed_channels'] + if len(channels_collection) != len(set(channels_collection)): + raise ValidationError("Allowed and disallowed channels lists contain duplicates.") - if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + if ( + data.get('allowed_categories') is not None + and data.get('disallowed_categories') is not None + ): + categories_collection = data['allowed_categories'] + data['disallowed_categories'] + if len(categories_collection) != len(set(categories_collection)): + raise ValidationError("Allowed and disallowed categories lists contain duplicates.") return data @@ -240,7 +257,7 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ('id', 'name', 'list_type', 'filters') + fields = ('id', 'name', 'list_type', 'filters') + SETTINGS_FIELDS extra_kwargs = { field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS } | { @@ -268,16 +285,24 @@ class FilterListSerializer(ModelSerializer): That does not affect how the Serializer works in general. """ - ret = super().to_representation(instance) - schema_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - schema_settings = { + # Fetches the relating filters + filters = [ + FilterSerializer(many=False).to_representation( + instance=item + ) for item in Filter.objects.filter( + filter_list=instance.id + ) + ] + schema_base = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} \ + | {"filters": filters} + schema_settings_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + schema_settings_categories = { "infraction": {name: getattr(instance, name) for name in INFRACTION_FIELDS}} \ | { "channel_scope": {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} - ret["settings"] = schema_base | schema_settings - return ret + return schema_base | {"settings": schema_settings_base | schema_settings_categories} class InfractionSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 2b587696..e8f3e3d9 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -36,6 +36,8 @@ class FilterListViewSet(ModelViewSet): ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -63,6 +65,8 @@ class FilterListViewSet(ModelViewSet): ... 267630620367257601 ... ], ... "filter_dm": True, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -101,6 +105,8 @@ class FilterListViewSet(ModelViewSet): ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -128,6 +134,8 @@ class FilterListViewSet(ModelViewSet): ... 267630620367257601 ... ], ... "filter_dm": True, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -179,6 +187,8 @@ class FilterViewSet(ModelViewSet): ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -214,6 +224,8 @@ class FilterViewSet(ModelViewSet): ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", -- cgit v1.2.3 From 4c93b1b9b75cce4e45bdbdae608f4497372c2b56 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Wed, 15 Dec 2021 19:25:55 +0100 Subject: Prepare FilterList and Filter models, serializers for the new filter schema - Rename channel scope fields: - "allowed" -> "disabled" eg.: "allowed_channels" -> "disabled_channels" - Rename FilterLists` names: filter_token -> tokens domain_name -> domains guild_invite -> invites file_format -> formats - Patch the docs and validators accordingly. --- ..._filter_and_filterlist_for_new_filter_schema.py | 80 ++++++++++++++++++ pydis_site/apps/api/models/bot/filters.py | 21 +++-- pydis_site/apps/api/serializers.py | 90 +++++++++++---------- pydis_site/apps/api/viewsets/bot/filters.py | 94 ++++++++++++---------- 4 files changed, 189 insertions(+), 96 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py new file mode 100644 index 00000000..30537e3d --- /dev/null +++ b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py @@ -0,0 +1,80 @@ +# Generated by Django 3.0.14 on 2021-12-11 23:14 +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + change_map = { + "filter_token": "tokens", + "domain_name": "domains", + "guild_invite": "invites", + "file_format": "formats" + } + for filter_list in FilterList.objects.all(): + if change_map.get(filter_list.name): + filter_list.name = change_map.get(filter_list.name) + filter_list.save() + + +def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + change_map = { + "tokens": "filter_token", + "domains": "domain_name", + "invites": "guild_invite", + "formats": "file_format" + } + for filter_list in FilterList.objects.all(): + if change_map.get(filter_list.name): + filter_list.name = change_map.get(filter_list.name) + filter_list.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0074_merge_20211017_0822'), + ] + + operations = [ + migrations.RenameField( + model_name='filter', + old_name='allowed_categories', + new_name='disabled_categories', + ), + migrations.RenameField( + model_name='filter', + old_name='allowed_channels', + new_name='disabled_channels', + ), + migrations.RenameField( + model_name='filter', + old_name='disallowed_channels', + new_name='enabled_channels', + ), + migrations.RenameField( + model_name='filterlist', + old_name='allowed_categories', + new_name='disabled_categories', + ), + migrations.RenameField( + model_name='filterlist', + old_name='allowed_channels', + new_name='disabled_channels', + ), + migrations.RenameField( + model_name='filterlist', + old_name='disallowed_channels', + new_name='enabled_channels', + ), + migrations.RemoveField( + model_name='filterlist', + name='disallowed_categories', + ), + migrations.RemoveField( + model_name='filter', + name='disallowed_categories', + ), + migrations.RunPython(migrate_filterlist, unmigrate_filterlist) + ] diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 3a1f3c6a..ae877685 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -99,14 +99,12 @@ class FilterList(FilterSettingsMixin): # Where a filter should apply. # # The resolution is done in the following order: - # - disallowed channels - # - disallowed categories - # - allowed categories - # - allowed channels - disallowed_channels = ArrayField(models.IntegerField()) - disallowed_categories = ArrayField(models.IntegerField()) - allowed_channels = ArrayField(models.IntegerField()) - allowed_categories = ArrayField(models.IntegerField()) + # - enabled_channels + # - disabled_categories + # - disabled_channels + enabled_channels = ArrayField(models.IntegerField()) + disabled_channels = ArrayField(models.IntegerField()) + disabled_categories = ArrayField(models.IntegerField()) class Meta: """Constrain name and list_type unique.""" @@ -157,10 +155,9 @@ class Filter(FilterSettingsMixin): ) # Check FilterList model for information about these properties. - disallowed_channels = ArrayField(models.IntegerField(), null=True) - disallowed_categories = ArrayField(models.IntegerField(), null=True) - allowed_channels = ArrayField(models.IntegerField(), null=True) - allowed_categories = ArrayField(models.IntegerField(), null=True) + enabled_channels = ArrayField(models.IntegerField(), null=True) + disabled_channels = ArrayField(models.IntegerField(), null=True) + disabled_categories = ArrayField(models.IntegerField(), null=True) def __str__(self) -> str: return f"Filter {self.content!r}" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index cb8313ac..784f8160 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -127,18 +127,15 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'delete_messages', 'bypass_roles', 'enabled', - 'disallowed_channels', - 'disallowed_categories', - 'allowed_channels', - 'allowed_categories', + 'enabled_channels', + 'disabled_channels', + 'disabled_categories', ) # Required fields for custom JSON representation purposes BASE_FILTER_FIELDS = ('id', 'content', 'description', 'additional_field') BASE_FILTERLIST_FIELDS = ('id', 'name', 'list_type') BASE_SETTINGS_FIELDS = ( - "ping_type", - "dm_ping_type", "bypass_roles", "filter_dm", "enabled", @@ -146,11 +143,11 @@ BASE_SETTINGS_FIELDS = ( ) INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") CHANNEL_SCOPE_FIELDS = ( - "allowed_channels", - "allowed_categories", - "disallowed_channels", - "disallowed_categories" + "disabled_channels", + "disabled_categories", + "enabled_channels", ) +MENTIONS_FIELDS = ("ping_type", "dm_ping_type") SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS @@ -166,20 +163,17 @@ class FilterSerializer(ModelSerializer): raise ValidationError("Infraction type is required with infraction duration or reason") if ( - data.get('allowed_channels') is not None - and data.get('disallowed_channels') is not None + data.get('disabled_channels') is not None + and data.get('enabled_channels') is not None ): - channels_collection = data['allowed_channels'] + data['disallowed_channels'] + channels_collection = data['disabled_channels'] + data['enabled_channels'] if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + raise ValidationError("Enabled and Disabled channels lists contain duplicates.") - if ( - data.get('allowed_categories') is not None - and data.get('disallowed_categories') is not None - ): - categories_collection = data['allowed_categories'] + data['disallowed_categories'] + if data.get('disabled_categories') is not None: + categories_collection = data['disabled_categories'] if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + raise ValidationError("Disabled categories lists contain duplicates.") return data @@ -194,18 +188,20 @@ class FilterSerializer(ModelSerializer): field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS } | { 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, - 'disallowed_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'disallowed_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'allowed_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'allowed_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'enabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'disabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'disabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, } def to_representation(self, instance: Filter) -> dict: """ - Provides a custom JSON representation to the Filter Serializers. - That does not affect how the Serializer works in general. + This representation restructures how the Filter is represented. + It groups the Infraction, Channel and Mention related fields into their own separated group. + + Furthermore, it puts the fields that meant to represent Filter settings, + into a sub-field called `settings`. """ schema_settings = { "settings": @@ -214,6 +210,11 @@ class FilterSerializer(ModelSerializer): | { "channel_scope": {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} + } | { + "mentions": + { + schema_field_name: getattr(instance, schema_field_name) + for schema_field_name in MENTIONS_FIELDS} } } @@ -236,20 +237,17 @@ class FilterListSerializer(ModelSerializer): raise ValidationError("Infraction type is required with infraction duration or reason") if ( - data.get('allowed_channels') is not None - and data.get('disallowed_channels') is not None + data.get('disabled_channels') is not None + and data.get('enabled_channels') is not None ): - channels_collection = data['allowed_channels'] + data['disallowed_channels'] + channels_collection = data['disabled_channels'] + data['enabled_channels'] if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + raise ValidationError("Enabled and Disabled channels lists contain duplicates.") - if ( - data.get('allowed_categories') is not None - and data.get('disallowed_categories') is not None - ): - categories_collection = data['allowed_categories'] + data['disallowed_categories'] + if data.get('disabled_categories') is not None: + categories_collection = data['disabled_categories'] if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + raise ValidationError("Disabled categories lists contain duplicates.") return data @@ -262,10 +260,9 @@ class FilterListSerializer(ModelSerializer): field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS } | { 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, - 'disallowed_channels': {'allow_empty': True}, - 'disallowed_categories': {'allow_empty': True}, - 'allowed_channels': {'allow_empty': True}, - 'allowed_categories': {'allow_empty': True}, + 'enabled_channels': {'allow_empty': True}, + 'disabled_channels': {'allow_empty': True}, + 'disabled_categories': {'allow_empty': True}, } # Ensure that we can only have one filter list with the same name and field @@ -283,7 +280,11 @@ class FilterListSerializer(ModelSerializer): """ Provides a custom JSON representation to the FilterList Serializers. - That does not affect how the Serializer works in general. + This representation restructures how the Filter is represented. + It groups the Infraction, Channel and Mention related fields into their own separated group. + + Furthermore, it puts the fields that meant to represent FilterList settings, + into a sub-field called `settings`. """ # Fetches the relating filters filters = [ @@ -301,7 +302,12 @@ class FilterListSerializer(ModelSerializer): {name: getattr(instance, name) for name in INFRACTION_FIELDS}} \ | { "channel_scope": - {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} + {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} | { + "mentions": { + schema_field_name: getattr(instance, schema_field_name) + for schema_field_name in MENTIONS_FIELDS + } + } return schema_base | {"settings": schema_settings_base | schema_settings_categories} diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index e8f3e3d9..20af079d 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -32,8 +32,6 @@ class FilterListViewSet(ModelViewSet): ... "description": "Python Discord", ... "additional_field": None, ... "settings": { - ... "ping_type": None, - ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, ... "enabled": False @@ -44,11 +42,14 @@ class FilterListViewSet(ModelViewSet): ... "infraction_duration": None ... }, ... "channel_scope": { - ... "allowed_channels": None, - ... "allowed_categories": None, - ... "disallowed_channels": None, - ... "disallowed_categories": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } ... } ... ... }, @@ -72,13 +73,18 @@ class FilterListViewSet(ModelViewSet): ... "infraction_reason": "", ... "infraction_duration": None, ... } - ... "channel_scope": { - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [] - ... } - ... } + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None + ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } + ... }, + ... ... + ... ] #### Status codes - 200: returned on success @@ -88,8 +94,7 @@ class FilterListViewSet(ModelViewSet): Returns a specific FilterList item from the database. #### Response format - >>> - ... { + >>> { ... "id": 1, ... "name": "guild_invite", ... "list_type": 1, @@ -101,8 +106,6 @@ class FilterListViewSet(ModelViewSet): ... "description": "Python Discord", ... "additional_field": None, ... "settings": { - ... "ping_type": None, - ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, ... "enabled": False @@ -113,15 +116,18 @@ class FilterListViewSet(ModelViewSet): ... "infraction_duration": None ... }, ... "channel_scope": { - ... "allowed_channels": None, - ... "allowed_categories": None, - ... "disallowed_channels": None, - ... "disallowed_categories": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } ... } ... ... }, - ... + ... ... ... ], ... "settings": { ... "ping_type": [ @@ -141,14 +147,16 @@ class FilterListViewSet(ModelViewSet): ... "infraction_reason": "", ... "infraction_duration": None, ... } - ... "channel_scope": { - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [] - ... } - ... } - ... } + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None + ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } + ... } #### Status codes - 200: returned on success @@ -183,8 +191,6 @@ class FilterViewSet(ModelViewSet): ... "description": "Python Discord", ... "additional_field": None, ... "settings": { - ... "ping_type": None, - ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, ... "enabled": False @@ -195,11 +201,14 @@ class FilterViewSet(ModelViewSet): ... "infraction_duration": None ... }, ... "channel_scope": { - ... "allowed_channels": None, - ... "allowed_categories": None, - ... "disallowed_channels": None, - ... "disallowed_categories": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None ... } + ... "mentions": { + ... "ping_type": None, + ... "dm_ping_type": None + ... } ... } ... }, ... ... @@ -220,8 +229,6 @@ class FilterViewSet(ModelViewSet): ... "description": "Python Discord", ... "additional_field": None, ... "settings": { - ... "ping_type": None, - ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, ... "enabled": False @@ -232,11 +239,14 @@ class FilterViewSet(ModelViewSet): ... "infraction_duration": None ... }, ... "channel_scope": { - ... "allowed_channels": None, - ... "allowed_categories": None, - ... "disallowed_channels": None, - ... "disallowed_categories": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } ... } ... } -- cgit v1.2.3 From af3980fe65b997287ceaf68e53ce3ab7bf4607e5 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Wed, 22 Dec 2021 18:18:22 +0100 Subject: Patch Filter/FilterList's default values and add new fields - Patch default values, so that further implementations can be performed on the bot side - Add three new fields: "send_alert", and in settings under the "server_message" field: "send_message_text", and "server_message_embed" fields. - Patch documentation, and validators accordingly. - Perform further patches, and minor corrections. --- .../apps/api/migrations/0070_new_filter_schema.py | 28 ++++-- ..._filter_and_filterlist_for_new_filter_schema.py | 17 +++- .../api/migrations/0078_merge_20211218_2200.py | 14 +++ .../0079_add_server_message_and_alert_fields.py | 69 ++++++++++++++ pydis_site/apps/api/models/bot/filters.py | 44 ++++++++- pydis_site/apps/api/serializers.py | 19 +++- pydis_site/apps/api/viewsets/bot/filters.py | 104 ++++++++++++--------- 7 files changed, 234 insertions(+), 61 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0078_merge_20211218_2200.py create mode 100644 pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 8716cbad..f56c29f8 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -1,4 +1,5 @@ # Modified migration file to migrate existing filters to the new one +from datetime import timedelta import django.contrib.postgres.fields from django.apps.registry import Apps @@ -18,20 +19,27 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: for name, type_ in OLD_LIST_NAMES: objects = filter_list_old.objects.filter(type=name) + if name == "DOMAIN_NAME": + dm_content = "Your URL has been removed because it matched a blacklisted domain: {match}" + elif name == "GUILD_INVITE": + dm_content = "Per Rule 6, your invite link has been removed. " \ + "Our server rules can be found here: https://pythondiscord.com/pages/rules" + else: + dm_content = "" list_ = filter_list.objects.create( name=name.lower(), list_type=1 if type_ == "ALLOW" else 0, - ping_type=["onduty"], + ping_type=(["onduty"] if name != "FILE_FORMAT" else []), filter_dm=True, - dm_ping_type=["onduty"], - delete_messages=True, - bypass_roles=[267630620367257601], - enabled=False, - dm_content=None, - infraction_type=None, + dm_ping_type=[], + delete_messages=(True if name != "FILTER_TOKEN" else False), + bypass_roles=["staff"], + enabled=True, + dm_content=dm_content, + infraction_type="", infraction_reason="", - infraction_duration=None, + infraction_duration=timedelta(seconds=0), disallowed_channels=[], disallowed_categories=[], allowed_channels=[], @@ -84,7 +92,7 @@ class Migration(migrations.Migration): ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None, null=True)), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field], null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), @@ -106,7 +114,7 @@ class Migration(migrations.Migration): ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None)), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field])), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py index 30537e3d..cc524fcb 100644 --- a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py @@ -10,12 +10,26 @@ def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N "filter_token": "tokens", "domain_name": "domains", "guild_invite": "invites", - "file_format": "formats" + "file_format": "extensions" } for filter_list in FilterList.objects.all(): if change_map.get(filter_list.name): filter_list.name = change_map.get(filter_list.name) filter_list.save() + redirects = FilterList( + name="redirects", + ping_type=[], + dm_ping_type=[], + enabled_channels=[], + disabled_channels=[], + disabled_categories=[], + list_type=0, + filter_dm=True, + delete_messages=False, + bypass_roles=[0], + enabled=True + ) + redirects.save() def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: @@ -30,6 +44,7 @@ def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> if change_map.get(filter_list.name): filter_list.name = change_map.get(filter_list.name) filter_list.save() + FilterList.objects.filter(name="redirects").delete() class Migration(migrations.Migration): diff --git a/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py b/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py new file mode 100644 index 00000000..7fe559f5 --- /dev/null +++ b/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.14 on 2021-12-18 22:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0075_prepare_filter_and_filterlist_for_new_filter_schema'), + ('api', '0077_use_generic_jsonfield'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py b/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py new file mode 100644 index 00000000..f9803bd3 --- /dev/null +++ b/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py @@ -0,0 +1,69 @@ +# Generated by Django 3.1.14 on 2021-12-19 23:05 +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + change_map = { + "tokens": True, + "domains": True, + "invites": True, + "extensions": False, + "redirects": False + } + for filter_list in FilterList.objects.all(): + filter_list.send_alert = change_map.get(filter_list.name) + filter_list.server_message_text = "" + filter_list.server_message_embed = "" + filter_list.save() + + +def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + for filter_list in FilterList.objects.all(): + filter_list.send_alert = True + filter_list.server_message_text = None + filter_list.server_message_embed = None + filter_list.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0078_merge_20211218_2200'), + ] + + operations = [ + migrations.AddField( + model_name='filter', + name='send_alert', + field=models.BooleanField(help_text='Whether alert should be sent.', null=True), + ), + migrations.AddField( + model_name='filter', + name='server_message_embed', + field=models.CharField(help_text='The content of the server message embed', max_length=100, null=True), + ), + migrations.AddField( + model_name='filter', + name='server_message_text', + field=models.CharField(help_text='The message to send on the server', max_length=100, null=True), + ), + migrations.AddField( + model_name='filterlist', + name='send_alert', + field=models.BooleanField(default=True, help_text='Whether alert should be sent.'), + ), + migrations.AddField( + model_name='filterlist', + name='server_message_embed', + field=models.CharField(help_text='The content of the server message embed', max_length=100, null=True), + ), + migrations.AddField( + model_name='filterlist', + name='server_message_text', + field=models.CharField(help_text='The message to send on the server', max_length=100, null=True), + ), + migrations.RunPython(migrate_filterlist, unmigrate_filterlist) + ] diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index ae877685..92251ee4 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -18,6 +18,7 @@ class FilterListType(models.IntegerChoices): # Valid special values in ping related fields VALID_PINGS = ("everyone", "here", "moderators", "onduty", "admins") +VALID_BYPASS_ROLES = ("staff",) def validate_ping_field(value_list: List[str]) -> None: @@ -33,6 +34,14 @@ def validate_ping_field(value_list: List[str]) -> None: raise ValidationError(f"{value!r} isn't a valid ping type.") +def validate_bypass_roles_field(value_list: List[str]) -> None: + """Validate that the vclues are either a special value or a Role ID.""" + for value in value_list: + if value.isnumeric() or value in VALID_BYPASS_ROLES: + continue + raise ValidationError(f"{value!r} isn't a valid (bypass) role.") + + class FilterSettingsMixin(models.Model): """Mixin for common settings of a filters and filter lists.""" @@ -88,14 +97,30 @@ class FilterList(FilterSettingsMixin): null=False ) bypass_roles = ArrayField( - models.BigIntegerField(), + models.CharField(max_length=100), help_text="Roles and users who can bypass this filter.", + validators=(validate_bypass_roles_field,), null=False ) enabled = models.BooleanField( help_text="Whether this filter is currently enabled.", null=False ) + send_alert = models.BooleanField( + help_text="Whether alert should be sent.", + null=False, + default=True + ) + server_message_text = models.CharField( + max_length=100, + help_text="The message to send on the server", + null=True + ) + server_message_embed = models.CharField( + max_length=100, + help_text="The content of the server message embed", + null=True + ) # Where a filter should apply. # # The resolution is done in the following order: @@ -145,14 +170,29 @@ class Filter(FilterSettingsMixin): null=True ) bypass_roles = ArrayField( - models.BigIntegerField(), + models.CharField(max_length=100), help_text="Roles and users who can bypass this filter.", + validators=(validate_bypass_roles_field,), null=True ) enabled = models.BooleanField( help_text="Whether this filter is currently enabled.", null=True ) + send_alert = models.BooleanField( + help_text="Whether alert should be sent.", + null=True + ) + server_message_text = models.CharField( + max_length=100, + help_text="The message to send on the server", + null=True + ) + server_message_embed = models.CharField( + max_length=100, + help_text="The content of the server message embed", + null=True + ) # Check FilterList model for information about these properties. enabled_channels = ArrayField(models.IntegerField(), null=True) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 784f8160..30af9512 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -139,7 +139,8 @@ BASE_SETTINGS_FIELDS = ( "bypass_roles", "filter_dm", "enabled", - "delete_messages" + "delete_messages", + "send_alert" ) INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") CHANNEL_SCOPE_FIELDS = ( @@ -147,6 +148,7 @@ CHANNEL_SCOPE_FIELDS = ( "disabled_categories", "enabled_channels", ) +SERVER_MESSAGE_FIELDS = ("server_message_text", "server_message_embed") MENTIONS_FIELDS = ("ping_type", "dm_ping_type") SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS @@ -214,10 +216,16 @@ class FilterSerializer(ModelSerializer): "mentions": { schema_field_name: getattr(instance, schema_field_name) - for schema_field_name in MENTIONS_FIELDS} + for schema_field_name in MENTIONS_FIELDS + } } + } | { + "server_message": + { + schema_field_name: getattr(instance, schema_field_name) + for schema_field_name in SERVER_MESSAGE_FIELDS + } } - schema_base = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} | \ {"filter_list": instance.filter_list.id} @@ -307,6 +315,11 @@ class FilterListSerializer(ModelSerializer): schema_field_name: getattr(instance, schema_field_name) for schema_field_name in MENTIONS_FIELDS } + } | { + "server_message": { + schema_field_name: getattr(instance, schema_field_name) + for schema_field_name in SERVER_MESSAGE_FIELDS + } } return schema_base | {"settings": schema_settings_base | schema_settings_categories} diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 20af079d..e52cd4e5 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -22,20 +22,21 @@ class FilterListViewSet(ModelViewSet): >>> [ ... { ... "id": 1, - ... "name": "guild_invite", + ... "name": "invites", ... "list_type": 1, ... "filters": [ ... { ... "id": 1, - ... "filter_list": 1 ... "content": "267624335836053506", ... "description": "Python Discord", ... "additional_field": None, + ... "filter_list": 1 ... "settings": { ... "bypass_roles": None ... "filter_dm": None, - ... "enabled": False - ... "delete_messages": True + ... "enabled": None + ... "send_alert": True, + ... "delete_messages": None ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -50,37 +51,42 @@ class FilterListViewSet(ModelViewSet): ... "ping_type": None ... "dm_ping_type": None ... } + ... "server_message": { + ... "server_message_text": None, + ... "server_message_embed": None + ... } ... } ... ... }, ... ... ... ], ... "settings": { - ... "ping_type": [ - ... "onduty" - ... ], - ... "dm_ping_type": [ - ... "onduty" - ... ], ... "bypass_roles": [ - ... 267630620367257601 + ... "staff" ... ], ... "filter_dm": True, - ... "enabled": False - ... "delete_messages": True + ... "enabled": True + ... "delete_messages": True, + ... "send_alert": True ... "infraction": { - ... "infraction_type": None, + ... "infraction_type": "", ... "infraction_reason": "", - ... "infraction_duration": None, + ... "infraction_duration": "0.0", ... } ... "channel_scope": { - ... "disabled_channels": None, - ... "disabled_categories": None, - ... "enabled_channels": None - ... } + ... "disabled_channels": [], + ... "disabled_categories": [], + ... "enabled_channels": [] + ... } ... "mentions": { - ... "ping_type": None - ... "dm_ping_type": None + ... "ping_type": [ + ... "onduty" + ... ] + ... "dm_ping_type": [] + ... } + ... "server_message": { + ... "server_message_text": "", + ... "server_message_embed": "" ... } ... }, ... ... @@ -96,7 +102,7 @@ class FilterListViewSet(ModelViewSet): #### Response format >>> { ... "id": 1, - ... "name": "guild_invite", + ... "name": "invites", ... "list_type": 1, ... "filters": [ ... { @@ -108,8 +114,9 @@ class FilterListViewSet(ModelViewSet): ... "settings": { ... "bypass_roles": None ... "filter_dm": None, - ... "enabled": False - ... "delete_messages": True + ... "enabled": None + ... "delete_messages": None, + ... "send_alert": None ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -124,37 +131,42 @@ class FilterListViewSet(ModelViewSet): ... "ping_type": None ... "dm_ping_type": None ... } + ... "server_message": { + ... "server_message_text": None, + ... "server_message_embed": None + ... } ... } ... ... }, ... ... ... ], ... "settings": { - ... "ping_type": [ - ... "onduty" - ... ], - ... "dm_ping_type": [ - ... "onduty" - ... ], ... "bypass_roles": [ - ... 267630620367257601 + ... "staff" ... ], ... "filter_dm": True, - ... "enabled": False + ... "enabled": True ... "delete_messages": True + ... "send_alert": True ... "infraction": { - ... "infraction_type": None, + ... "infraction_type": "", ... "infraction_reason": "", - ... "infraction_duration": None, + ... "infraction_duration": "0.0", ... } ... "channel_scope": { - ... "disabled_channels": None, - ... "disabled_categories": None, - ... "enabled_channels": None + ... "disabled_channels": [], + ... "disabled_categories": [], + ... "enabled_channels": [] ... } ... "mentions": { - ... "ping_type": None - ... "dm_ping_type": None + ... "ping_type": [ + ... "onduty" + ... ] + ... "dm_ping_type": [] + ... } + ... "server_message": { + ... "server_message_text": "", + ... "server_message_embed": "" ... } ... } @@ -193,11 +205,12 @@ class FilterViewSet(ModelViewSet): ... "settings": { ... "bypass_roles": None ... "filter_dm": None, - ... "enabled": False - ... "delete_messages": True + ... "enabled": None + ... "delete_messages": True, + ... "send_alert": True ... "infraction": { ... "infraction_type": None, - ... "infraction_reason": "", + ... "infraction_reason": None, ... "infraction_duration": None ... }, ... "channel_scope": { @@ -231,11 +244,12 @@ class FilterViewSet(ModelViewSet): ... "settings": { ... "bypass_roles": None ... "filter_dm": None, - ... "enabled": False - ... "delete_messages": True + ... "enabled": None + ... "delete_messages": True, + ... "send_alert": True ... "infraction": { ... "infraction_type": None, - ... "infraction_reason": "", + ... "infraction_reason": None, ... "infraction_duration": None ... }, ... "channel_scope": { -- cgit v1.2.3 From c2aaa8d672484a698b8aec6a65c2f4af3cff18b1 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:15:52 +0100 Subject: Include 'dm_content ' field under Infraction settings in Filters/FilterLists --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 30af9512..66236d92 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -142,7 +142,7 @@ BASE_SETTINGS_FIELDS = ( "delete_messages", "send_alert" ) -INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") +INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration", "dm_content") CHANNEL_SCOPE_FIELDS = ( "disabled_channels", "disabled_categories", -- cgit v1.2.3 From 466fc7cc4297fe4f5d921f6ca950b926ecc2d14d Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:22:29 +0100 Subject: Correct 'Redirect' FilterLists' default values. --- .../0075_prepare_filter_and_filterlist_for_new_filter_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py index cc524fcb..56cbdedb 100644 --- a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py @@ -26,7 +26,7 @@ def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N list_type=0, filter_dm=True, delete_messages=False, - bypass_roles=[0], + bypass_roles=["staff"], enabled=True ) redirects.save() -- cgit v1.2.3 From c082ad818608fd52238e61f9c69d99cfb2aa503b Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Dec 2021 20:18:02 +0200 Subject: Merged infraction and notification settings in JSON The settings for infracting and notifying the user were merged under one field, which is renamed to "infraction_and_notification". The only place which sends a message in the server by default is the antimalware, the rest try to DM the user first, and antimalware can do the same. This avoids complications which may result from the filtering cog trying to send two messages: one for the defined server message, and another for a failed DM. --- .../0079_add_server_message_and_alert_fields.py | 22 ++++----------- pydis_site/apps/api/models/bot/filters.py | 25 ++++------------- pydis_site/apps/api/serializers.py | 28 ++++++++----------- pydis_site/apps/api/viewsets/bot/filters.py | 32 ++++++++-------------- 4 files changed, 34 insertions(+), 73 deletions(-) diff --git a/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py b/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py index f9803bd3..c6299cb9 100644 --- a/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py +++ b/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py @@ -15,8 +15,7 @@ def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N } for filter_list in FilterList.objects.all(): filter_list.send_alert = change_map.get(filter_list.name) - filter_list.server_message_text = "" - filter_list.server_message_embed = "" + filter_list.dm_embed = "" filter_list.save() @@ -24,7 +23,6 @@ def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> FilterList = apps.get_model("api", "FilterList") for filter_list in FilterList.objects.all(): filter_list.send_alert = True - filter_list.server_message_text = None filter_list.server_message_embed = None filter_list.save() @@ -42,13 +40,8 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='filter', - name='server_message_embed', - field=models.CharField(help_text='The content of the server message embed', max_length=100, null=True), - ), - migrations.AddField( - model_name='filter', - name='server_message_text', - field=models.CharField(help_text='The message to send on the server', max_length=100, null=True), + name='dm_embed', + field=models.CharField(help_text='The content of the DM embed', max_length=2000, null=True), ), migrations.AddField( model_name='filterlist', @@ -57,13 +50,8 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='filterlist', - name='server_message_embed', - field=models.CharField(help_text='The content of the server message embed', max_length=100, null=True), - ), - migrations.AddField( - model_name='filterlist', - name='server_message_text', - field=models.CharField(help_text='The message to send on the server', max_length=100, null=True), + name='dm_embed', + field=models.CharField(help_text='The content of the DM embed', max_length=2000, null=True), ), migrations.RunPython(migrate_filterlist, unmigrate_filterlist) ] diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 92251ee4..97af21f8 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -50,6 +50,11 @@ class FilterSettingsMixin(models.Model): null=True, help_text="The DM to send to a user triggering this filter." ) + dm_embed = models.CharField( + max_length=2000, + help_text="The content of the DM embed", + null=True + ) infraction_type = models.CharField( choices=Infraction.TYPE_CHOICES, max_length=9, @@ -111,16 +116,6 @@ class FilterList(FilterSettingsMixin): null=False, default=True ) - server_message_text = models.CharField( - max_length=100, - help_text="The message to send on the server", - null=True - ) - server_message_embed = models.CharField( - max_length=100, - help_text="The content of the server message embed", - null=True - ) # Where a filter should apply. # # The resolution is done in the following order: @@ -183,16 +178,6 @@ class Filter(FilterSettingsMixin): help_text="Whether alert should be sent.", null=True ) - server_message_text = models.CharField( - max_length=100, - help_text="The message to send on the server", - null=True - ) - server_message_embed = models.CharField( - max_length=100, - help_text="The content of the server message embed", - null=True - ) # Check FilterList model for information about these properties. enabled_channels = ArrayField(models.IntegerField(), null=True) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 66236d92..91aac822 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -142,13 +142,18 @@ BASE_SETTINGS_FIELDS = ( "delete_messages", "send_alert" ) -INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration", "dm_content") +INFRACTION_AND_NOTIFICATION_FIELDS = ( + "infraction_type", + "infraction_reason", + "infraction_duration", + "dm_content", + "dm_embed" +) CHANNEL_SCOPE_FIELDS = ( "disabled_channels", "disabled_categories", "enabled_channels", ) -SERVER_MESSAGE_FIELDS = ("server_message_text", "server_message_embed") MENTIONS_FIELDS = ("ping_type", "dm_ping_type") SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS @@ -208,8 +213,10 @@ class FilterSerializer(ModelSerializer): schema_settings = { "settings": {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - | {"infraction": {name: getattr(instance, name) for name in INFRACTION_FIELDS}} | { + "infraction_and_notification": + {name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS} + } | { "channel_scope": {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} } | { @@ -219,12 +226,6 @@ class FilterSerializer(ModelSerializer): for schema_field_name in MENTIONS_FIELDS } } - } | { - "server_message": - { - schema_field_name: getattr(instance, schema_field_name) - for schema_field_name in SERVER_MESSAGE_FIELDS - } } schema_base = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} | \ {"filter_list": instance.filter_list.id} @@ -306,8 +307,8 @@ class FilterListSerializer(ModelSerializer): | {"filters": filters} schema_settings_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} schema_settings_categories = { - "infraction": - {name: getattr(instance, name) for name in INFRACTION_FIELDS}} \ + "infraction_and_notification": + {name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS}} \ | { "channel_scope": {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} | { @@ -315,11 +316,6 @@ class FilterListSerializer(ModelSerializer): schema_field_name: getattr(instance, schema_field_name) for schema_field_name in MENTIONS_FIELDS } - } | { - "server_message": { - schema_field_name: getattr(instance, schema_field_name) - for schema_field_name in SERVER_MESSAGE_FIELDS - } } return schema_base | {"settings": schema_settings_base | schema_settings_categories} diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index e52cd4e5..dd9a7d87 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -37,10 +37,12 @@ class FilterListViewSet(ModelViewSet): ... "enabled": None ... "send_alert": True, ... "delete_messages": None - ... "infraction": { + ... "infraction_and_notification": { ... "infraction_type": None, ... "infraction_reason": "", ... "infraction_duration": None + ... "dm_content": None, + ... "dm_embed": None ... }, ... "channel_scope": { ... "disabled_channels": None, @@ -51,10 +53,6 @@ class FilterListViewSet(ModelViewSet): ... "ping_type": None ... "dm_ping_type": None ... } - ... "server_message": { - ... "server_message_text": None, - ... "server_message_embed": None - ... } ... } ... ... }, @@ -68,10 +66,12 @@ class FilterListViewSet(ModelViewSet): ... "enabled": True ... "delete_messages": True, ... "send_alert": True - ... "infraction": { + ... "infraction_and_notification": { ... "infraction_type": "", ... "infraction_reason": "", ... "infraction_duration": "0.0", + ... "dm_content": "", + ... "dm_embed": "" ... } ... "channel_scope": { ... "disabled_channels": [], @@ -84,10 +84,6 @@ class FilterListViewSet(ModelViewSet): ... ] ... "dm_ping_type": [] ... } - ... "server_message": { - ... "server_message_text": "", - ... "server_message_embed": "" - ... } ... }, ... ... ... ] @@ -117,10 +113,12 @@ class FilterListViewSet(ModelViewSet): ... "enabled": None ... "delete_messages": None, ... "send_alert": None - ... "infraction": { + ... "infraction_and_notification": { ... "infraction_type": None, ... "infraction_reason": "", ... "infraction_duration": None + ... "dm_content": None, + ... "dm_embed": None ... }, ... "channel_scope": { ... "disabled_channels": None, @@ -131,10 +129,6 @@ class FilterListViewSet(ModelViewSet): ... "ping_type": None ... "dm_ping_type": None ... } - ... "server_message": { - ... "server_message_text": None, - ... "server_message_embed": None - ... } ... } ... ... }, @@ -148,10 +142,12 @@ class FilterListViewSet(ModelViewSet): ... "enabled": True ... "delete_messages": True ... "send_alert": True - ... "infraction": { + ... "infraction_and_notification": { ... "infraction_type": "", ... "infraction_reason": "", ... "infraction_duration": "0.0", + ... "dm_content": "", + ... "dm_embed": "" ... } ... "channel_scope": { ... "disabled_channels": [], @@ -164,10 +160,6 @@ class FilterListViewSet(ModelViewSet): ... ] ... "dm_ping_type": [] ... } - ... "server_message": { - ... "server_message_text": "", - ... "server_message_embed": "" - ... } ... } #### Status codes -- cgit v1.2.3 From 78e91c433b193682d82bbeecd6e73c2b01964b3d Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 1 Jan 2022 14:41:36 +0200 Subject: Merge migrations and correct filter defaults to be all null --- .../apps/api/migrations/0070_new_filter_schema.py | 4 +- .../0079_add_server_message_and_alert_fields.py | 57 --------------------- .../migrations/0079_dm_embed_and_alert_fields.py | 58 ++++++++++++++++++++++ 3 files changed, 60 insertions(+), 59 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py create mode 100644 pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index f56c29f8..2c15605c 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -60,7 +60,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: enabled=None, dm_content=None, infraction_type=None, - infraction_reason="", + infraction_reason=None, infraction_duration=None, disallowed_channels=None, disallowed_categories=None, @@ -96,7 +96,7 @@ class Migration(migrations.Migration): ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), diff --git a/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py b/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py deleted file mode 100644 index c6299cb9..00000000 --- a/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 3.1.14 on 2021-12-19 23:05 -from django.apps.registry import Apps -from django.db import migrations, models -from django.db.backends.base.schema import BaseDatabaseSchemaEditor - - -def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - FilterList = apps.get_model("api", "FilterList") - change_map = { - "tokens": True, - "domains": True, - "invites": True, - "extensions": False, - "redirects": False - } - for filter_list in FilterList.objects.all(): - filter_list.send_alert = change_map.get(filter_list.name) - filter_list.dm_embed = "" - filter_list.save() - - -def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - FilterList = apps.get_model("api", "FilterList") - for filter_list in FilterList.objects.all(): - filter_list.send_alert = True - filter_list.server_message_embed = None - filter_list.save() - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0078_merge_20211218_2200'), - ] - - operations = [ - migrations.AddField( - model_name='filter', - name='send_alert', - field=models.BooleanField(help_text='Whether alert should be sent.', null=True), - ), - migrations.AddField( - model_name='filter', - name='dm_embed', - field=models.CharField(help_text='The content of the DM embed', max_length=2000, null=True), - ), - migrations.AddField( - model_name='filterlist', - name='send_alert', - field=models.BooleanField(default=True, help_text='Whether alert should be sent.'), - ), - migrations.AddField( - model_name='filterlist', - name='dm_embed', - field=models.CharField(help_text='The content of the DM embed', max_length=2000, null=True), - ), - migrations.RunPython(migrate_filterlist, unmigrate_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py b/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py new file mode 100644 index 00000000..49da62b6 --- /dev/null +++ b/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py @@ -0,0 +1,58 @@ +# Generated by Django 3.1.14 on 2021-12-19 23:05 +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + change_map = { + "tokens": True, + "domains": True, + "invites": True, + "extensions": False, + "redirects": False + } + for filter_list in FilterList.objects.all(): + filter_list.send_alert = change_map.get(filter_list.name) + filter_list.dm_embed = "" + filter_list.save() + + +def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + for filter_list in FilterList.objects.all(): + filter_list.send_alert = True + filter_list.server_message_embed = None + filter_list.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0078_merge_20211213_0552'), + ('api', '0078_merge_20211218_2200'), + ] + + operations = [ + migrations.AddField( + model_name='filter', + name='send_alert', + field=models.BooleanField(help_text='Whether alert should be sent.', null=True), + ), + migrations.AddField( + model_name='filter', + name='dm_embed', + field=models.CharField(help_text='The content of the DM embed', max_length=2000, null=True), + ), + migrations.AddField( + model_name='filterlist', + name='send_alert', + field=models.BooleanField(default=True, help_text='Whether alert should be sent.'), + ), + migrations.AddField( + model_name='filterlist', + name='dm_embed', + field=models.CharField(help_text='The content of the DM embed', max_length=2000, null=True), + ), + migrations.RunPython(migrate_filterlist, unmigrate_filterlist) + ] -- cgit v1.2.3 From f30e1d9e4fc420085a1187fa12ac23efccd21663 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 15 Feb 2022 22:05:20 +0200 Subject: Allow filter descriptions to be null --- pydis_site/apps/api/migrations/0070_new_filter_schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 2c15605c..f33c112b 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -50,7 +50,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: new_object = filter_.objects.create( content=object_.content, filter_list=list_, - description=object_.comment or "", + description=object_.comment, additional_field=None, ping_type=None, filter_dm=None, @@ -86,7 +86,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), - ('description', models.CharField(help_text='Why this filter has been added.', max_length=200)), + ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), ('additional_field', django.contrib.postgres.fields.jsonb.JSONField(help_text='Implementation specific field.', null=True)), ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), -- cgit v1.2.3 From a28cdded7dabb62d639125dca2320234263809c2 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 15 Feb 2022 22:11:40 +0200 Subject: Use singular nouns for filter list names --- .../0075_prepare_filter_and_filterlist_for_new_filter_schema.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py index 56cbdedb..2a85fa63 100644 --- a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py @@ -7,10 +7,10 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: FilterList = apps.get_model("api", "FilterList") change_map = { - "filter_token": "tokens", - "domain_name": "domains", - "guild_invite": "invites", - "file_format": "extensions" + "filter_token": "token", + "domain_name": "domain", + "guild_invite": "invite", + "file_format": "extension" } for filter_list in FilterList.objects.all(): if change_map.get(filter_list.name): -- cgit v1.2.3 From 7d22d8427fa73e6209ffcea827d9e460b6c1d985 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Tue, 15 Feb 2022 22:59:04 +0100 Subject: Patch a minor issue with FilterList field naming in migrations --- ...75_prepare_filter_and_filterlist_for_new_filter_schema.py | 12 ++++++------ .../apps/api/migrations/0079_dm_embed_and_alert_fields.py | 10 +++++----- pydis_site/apps/api/serializers.py | 3 ++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py index 2a85fa63..1e24b379 100644 --- a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py @@ -17,7 +17,7 @@ def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N filter_list.name = change_map.get(filter_list.name) filter_list.save() redirects = FilterList( - name="redirects", + name="redirect", ping_type=[], dm_ping_type=[], enabled_channels=[], @@ -35,16 +35,16 @@ def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: FilterList = apps.get_model("api", "FilterList") change_map = { - "tokens": "filter_token", - "domains": "domain_name", - "invites": "guild_invite", - "formats": "file_format" + "token": "filter_token", + "domain": "domain_name", + "invite": "guild_invite", + "format": "file_format" } for filter_list in FilterList.objects.all(): if change_map.get(filter_list.name): filter_list.name = change_map.get(filter_list.name) filter_list.save() - FilterList.objects.filter(name="redirects").delete() + FilterList.objects.filter(name="redirect").delete() class Migration(migrations.Migration): diff --git a/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py b/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py index 49da62b6..cae175df 100644 --- a/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py +++ b/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py @@ -7,11 +7,11 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: FilterList = apps.get_model("api", "FilterList") change_map = { - "tokens": True, - "domains": True, - "invites": True, - "extensions": False, - "redirects": False + "token": True, + "domain": True, + "invite": True, + "extension": False, + "redirect": False } for filter_list in FilterList.objects.all(): filter_list.send_alert = change_map.get(filter_list.name) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 88b6e2bd..99f2b630 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -215,7 +215,8 @@ class FilterSerializer(ModelSerializer): {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} | { "infraction_and_notification": - {name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS} + {name: getattr(instance, name) + for name in INFRACTION_AND_NOTIFICATION_FIELDS} } | { "channel_scope": {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} -- cgit v1.2.3 From 01ccc1dac80cc2958849d5be90255294f38878fb Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 22 Feb 2022 20:44:24 +0200 Subject: Changed channeld fields to char arrays, merged migrations - The fields concerning channels were changed to contains strings instead of integers in order to allow specifying channels and categories by name. The migrations were merged into a single migration. --- .../apps/api/migrations/0070_new_filter_schema.py | 145 ------------------- .../api/migrations/0074_merge_20211017_0822.py | 14 -- ..._filter_and_filterlist_for_new_filter_schema.py | 95 ------------- .../api/migrations/0078_merge_20211218_2200.py | 14 -- .../migrations/0079_dm_embed_and_alert_fields.py | 58 -------- .../apps/api/migrations/0079_new_filter_schema.py | 156 +++++++++++++++++++++ 6 files changed, 156 insertions(+), 326 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0070_new_filter_schema.py delete mode 100644 pydis_site/apps/api/migrations/0074_merge_20211017_0822.py delete mode 100644 pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py delete mode 100644 pydis_site/apps/api/migrations/0078_merge_20211218_2200.py delete mode 100644 pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py create mode 100644 pydis_site/apps/api/migrations/0079_new_filter_schema.py diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py deleted file mode 100644 index f33c112b..00000000 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ /dev/null @@ -1,145 +0,0 @@ -# Modified migration file to migrate existing filters to the new one -from datetime import timedelta - -import django.contrib.postgres.fields -from django.apps.registry import Apps -from django.db import migrations, models -import django.db.models.deletion -from django.db.backends.base.schema import BaseDatabaseSchemaEditor - -import pydis_site.apps.api.models.bot.filters - -OLD_LIST_NAMES = (('GUILD_INVITE', 'ALLOW'), ('FILE_FORMAT', 'DENY'), ('DOMAIN_NAME', 'DENY'), ('FILTER_TOKEN', 'DENY')) - - -def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - filter_: pydis_site.apps.api.models.Filter = apps.get_model("api", "Filter") - filter_list: pydis_site.apps.api.models.FilterList = apps.get_model("api", "FilterList") - filter_list_old = apps.get_model("api", "FilterListOld") - - for name, type_ in OLD_LIST_NAMES: - objects = filter_list_old.objects.filter(type=name) - if name == "DOMAIN_NAME": - dm_content = "Your URL has been removed because it matched a blacklisted domain: {match}" - elif name == "GUILD_INVITE": - dm_content = "Per Rule 6, your invite link has been removed. " \ - "Our server rules can be found here: https://pythondiscord.com/pages/rules" - else: - dm_content = "" - - list_ = filter_list.objects.create( - name=name.lower(), - list_type=1 if type_ == "ALLOW" else 0, - ping_type=(["onduty"] if name != "FILE_FORMAT" else []), - filter_dm=True, - dm_ping_type=[], - delete_messages=(True if name != "FILTER_TOKEN" else False), - bypass_roles=["staff"], - enabled=True, - dm_content=dm_content, - infraction_type="", - infraction_reason="", - infraction_duration=timedelta(seconds=0), - disallowed_channels=[], - disallowed_categories=[], - allowed_channels=[], - allowed_categories=[] - ) - - for object_ in objects: - new_object = filter_.objects.create( - content=object_.content, - filter_list=list_, - description=object_.comment, - additional_field=None, - ping_type=None, - filter_dm=None, - dm_ping_type=None, - delete_messages=None, - bypass_roles=None, - enabled=None, - dm_content=None, - infraction_type=None, - infraction_reason=None, - infraction_duration=None, - disallowed_channels=None, - disallowed_categories=None, - allowed_channels=None, - allowed_categories=None - ) - new_object.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0069_documentationlink_validators'), - ] - - operations = [ - migrations.RenameModel( - old_name='FilterList', - new_name='FilterListOld' - ), - migrations.CreateModel( - name='Filter', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), - ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), - ('additional_field', django.contrib.postgres.fields.jsonb.JSONField(help_text='Implementation specific field.', null=True)), - ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), - ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), - ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), - ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field], null=True)), - ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), - ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), - ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), - ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), - ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), - ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), - ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), - ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), null=True, size=None)), - ], - ), - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), - ('list_type', models.IntegerField(choices=[(1, 'Allow'), (0, 'Deny')], help_text='Whether this list is an allowlist or denylist')), - ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), - ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), - ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), - ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field])), - ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), - ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), - ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), - ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), - ('disallowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('disallowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('allowed_channels', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('allowed_categories', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ], - ), - migrations.AddField( - model_name='filter', - name='filter_list', - field=models.ForeignKey(help_text='The filter list containing this filter.', on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='api.FilterList'), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('name', 'list_type'), name='unique_name_type'), - ), - migrations.RunPython( - code=forward, # Core of the migration - reverse_code=lambda *_: None - ), - migrations.DeleteModel( - name='FilterListOld' - ) - ] diff --git a/pydis_site/apps/api/migrations/0074_merge_20211017_0822.py b/pydis_site/apps/api/migrations/0074_merge_20211017_0822.py deleted file mode 100644 index ae41ac71..00000000 --- a/pydis_site/apps/api/migrations/0074_merge_20211017_0822.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.14 on 2021-10-17 08:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0073_otn_allow_GT_and_LT'), - ('api', '0070_new_filter_schema'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py deleted file mode 100644 index 1e24b379..00000000 --- a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py +++ /dev/null @@ -1,95 +0,0 @@ -# Generated by Django 3.0.14 on 2021-12-11 23:14 -from django.apps.registry import Apps -from django.db import migrations, models -from django.db.backends.base.schema import BaseDatabaseSchemaEditor - - -def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - FilterList = apps.get_model("api", "FilterList") - change_map = { - "filter_token": "token", - "domain_name": "domain", - "guild_invite": "invite", - "file_format": "extension" - } - for filter_list in FilterList.objects.all(): - if change_map.get(filter_list.name): - filter_list.name = change_map.get(filter_list.name) - filter_list.save() - redirects = FilterList( - name="redirect", - ping_type=[], - dm_ping_type=[], - enabled_channels=[], - disabled_channels=[], - disabled_categories=[], - list_type=0, - filter_dm=True, - delete_messages=False, - bypass_roles=["staff"], - enabled=True - ) - redirects.save() - - -def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - FilterList = apps.get_model("api", "FilterList") - change_map = { - "token": "filter_token", - "domain": "domain_name", - "invite": "guild_invite", - "format": "file_format" - } - for filter_list in FilterList.objects.all(): - if change_map.get(filter_list.name): - filter_list.name = change_map.get(filter_list.name) - filter_list.save() - FilterList.objects.filter(name="redirect").delete() - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0074_merge_20211017_0822'), - ] - - operations = [ - migrations.RenameField( - model_name='filter', - old_name='allowed_categories', - new_name='disabled_categories', - ), - migrations.RenameField( - model_name='filter', - old_name='allowed_channels', - new_name='disabled_channels', - ), - migrations.RenameField( - model_name='filter', - old_name='disallowed_channels', - new_name='enabled_channels', - ), - migrations.RenameField( - model_name='filterlist', - old_name='allowed_categories', - new_name='disabled_categories', - ), - migrations.RenameField( - model_name='filterlist', - old_name='allowed_channels', - new_name='disabled_channels', - ), - migrations.RenameField( - model_name='filterlist', - old_name='disallowed_channels', - new_name='enabled_channels', - ), - migrations.RemoveField( - model_name='filterlist', - name='disallowed_categories', - ), - migrations.RemoveField( - model_name='filter', - name='disallowed_categories', - ), - migrations.RunPython(migrate_filterlist, unmigrate_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py b/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py deleted file mode 100644 index 7fe559f5..00000000 --- a/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.1.14 on 2021-12-18 22:00 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0075_prepare_filter_and_filterlist_for_new_filter_schema'), - ('api', '0077_use_generic_jsonfield'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py b/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py deleted file mode 100644 index cae175df..00000000 --- a/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 3.1.14 on 2021-12-19 23:05 -from django.apps.registry import Apps -from django.db import migrations, models -from django.db.backends.base.schema import BaseDatabaseSchemaEditor - - -def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - FilterList = apps.get_model("api", "FilterList") - change_map = { - "token": True, - "domain": True, - "invite": True, - "extension": False, - "redirect": False - } - for filter_list in FilterList.objects.all(): - filter_list.send_alert = change_map.get(filter_list.name) - filter_list.dm_embed = "" - filter_list.save() - - -def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - FilterList = apps.get_model("api", "FilterList") - for filter_list in FilterList.objects.all(): - filter_list.send_alert = True - filter_list.server_message_embed = None - filter_list.save() - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0078_merge_20211213_0552'), - ('api', '0078_merge_20211218_2200'), - ] - - operations = [ - migrations.AddField( - model_name='filter', - name='send_alert', - field=models.BooleanField(help_text='Whether alert should be sent.', null=True), - ), - migrations.AddField( - model_name='filter', - name='dm_embed', - field=models.CharField(help_text='The content of the DM embed', max_length=2000, null=True), - ), - migrations.AddField( - model_name='filterlist', - name='send_alert', - field=models.BooleanField(default=True, help_text='Whether alert should be sent.'), - ), - migrations.AddField( - model_name='filterlist', - name='dm_embed', - field=models.CharField(help_text='The content of the DM embed', max_length=2000, null=True), - ), - migrations.RunPython(migrate_filterlist, unmigrate_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0079_new_filter_schema.py b/pydis_site/apps/api/migrations/0079_new_filter_schema.py new file mode 100644 index 00000000..94494186 --- /dev/null +++ b/pydis_site/apps/api/migrations/0079_new_filter_schema.py @@ -0,0 +1,156 @@ +# Modified migration file to migrate existing filters to the new one +from datetime import timedelta + +import django.contrib.postgres.fields +from django.apps.registry import Apps +from django.db import migrations, models +import django.db.models.deletion +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + +import pydis_site.apps.api.models.bot.filters + +OLD_LIST_NAMES = (('GUILD_INVITE', 'ALLOW'), ('FILE_FORMAT', 'DENY'), ('DOMAIN_NAME', 'DENY'), ('FILTER_TOKEN', 'DENY'), ('REDIRECT', 'DENY')) +change_map = { + "FILTER_TOKEN": "token", + "DOMAIN_NAME": "domain", + "GUILD_INVITE": "invite", + "FILE_FORMAT": "extension", + "REDIRECT": "redirect" +} + + +def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + filter_: pydis_site.apps.api.models.Filter = apps.get_model("api", "Filter") + filter_list: pydis_site.apps.api.models.FilterList = apps.get_model("api", "FilterList") + filter_list_old = apps.get_model("api", "FilterListOld") + + for name, type_ in OLD_LIST_NAMES: + objects = filter_list_old.objects.filter(type=name) + if name == "DOMAIN_NAME": + dm_content = "Your URL has been removed because it matched a blacklisted domain: {match}" + elif name == "GUILD_INVITE": + dm_content = "Per Rule 6, your invite link has been removed. " \ + "Our server rules can be found here: https://pythondiscord.com/pages/rules" + else: + dm_content = "" + + list_ = filter_list.objects.create( + name=change_map[name], + list_type=1 if type_ == "ALLOW" else 0, + ping_type=(["Moderators"] if name != "FILE_FORMAT" else []), + filter_dm=True, + dm_ping_type=[], + delete_messages=(True if name != "FILTER_TOKEN" else False), + bypass_roles=["Helpers"], + enabled=True, + dm_content=dm_content, + dm_embed="", + infraction_type="", + infraction_reason="", + infraction_duration=timedelta(seconds=0), + disabled_channels=[], + disabled_categories=(["CODE JAM"] if name in ("FILE_FORMAT", "GUILD_INVITE") else []), + enabled_channels=[], + send_alert=(name in ('GUILD_INVITE', 'DOMAIN_NAME', 'FILTER_TOKEN')) + ) + + for object_ in objects: + new_object = filter_.objects.create( + content=object_.content, + filter_list=list_, + description=object_.comment, + additional_field=None, + ping_type=None, + filter_dm=None, + dm_ping_type=None, + delete_messages=None, + bypass_roles=None, + enabled=None, + dm_content=None, + dm_embed=None, + infraction_type=None, + infraction_reason=None, + infraction_duration=None, + disabled_channels=None, + disabled_categories=None, + enabled_channels=None, + send_alert=None, + ) + new_object.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0078_merge_20211213_0552'), + ] + + operations = [ + migrations.RenameModel( + old_name='FilterList', + new_name='FilterListOld' + ), + migrations.CreateModel( + name='Filter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), + ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), + ('additional_field', django.contrib.postgres.fields.jsonb.JSONField(help_text='Implementation specific field.', null=True)), + ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), + ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), + ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), + ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field], null=True)), + ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), + ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), + ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), + ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), + ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", null=True, size=None)), + ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", null=True, size=None)), + ('send_alert', models.BooleanField(help_text='Whether an alert should be sent.', null=True)), + ], + ), + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), + ('list_type', models.IntegerField(choices=[(1, 'Allow'), (0, 'Deny')], help_text='Whether this list is an allowlist or denylist')), + ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), + ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field])), + ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), + ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), + ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), + ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), + ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), + ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", size=None)), + ('send_alert', models.BooleanField(help_text='Whether an alert should be sent.')), + ], + ), + migrations.AddField( + model_name='filter', + name='filter_list', + field=models.ForeignKey(help_text='The filter list containing this filter.', on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='api.FilterList'), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('name', 'list_type'), name='unique_name_type'), + ), + migrations.RunPython( + code=forward, # Core of the migration + reverse_code=lambda *_: None + ), + migrations.DeleteModel( + name='FilterListOld' + ) + ] -- cgit v1.2.3 From 37f9296322d5aaef6aefc68eb97e6e1d5c0df531 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 23 Feb 2022 23:51:05 +0200 Subject: Extensions list is ALLOW, not DENY --- pydis_site/apps/api/migrations/0079_new_filter_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/migrations/0079_new_filter_schema.py b/pydis_site/apps/api/migrations/0079_new_filter_schema.py index 94494186..4728ea91 100644 --- a/pydis_site/apps/api/migrations/0079_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0079_new_filter_schema.py @@ -9,7 +9,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor import pydis_site.apps.api.models.bot.filters -OLD_LIST_NAMES = (('GUILD_INVITE', 'ALLOW'), ('FILE_FORMAT', 'DENY'), ('DOMAIN_NAME', 'DENY'), ('FILTER_TOKEN', 'DENY'), ('REDIRECT', 'DENY')) +OLD_LIST_NAMES = (('GUILD_INVITE', 'ALLOW'), ('FILE_FORMAT', 'ALLOW'), ('DOMAIN_NAME', 'DENY'), ('FILTER_TOKEN', 'DENY'), ('REDIRECT', 'DENY')) change_map = { "FILTER_TOKEN": "token", "DOMAIN_NAME": "domain", -- cgit v1.2.3 From a2fcfdf8fd80fc4cfd89be19ffb18a3c1799d2cb Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 24 Feb 2022 21:14:35 +0200 Subject: Create placeholder value for dm embed content in ext list Some value is needed to signal the bot a message should be sent for a blocked extension. The value itself will be changed at runtime, but this allows avoiding the bot code delving into the exact API response format. --- pydis_site/apps/api/migrations/0079_new_filter_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/migrations/0079_new_filter_schema.py b/pydis_site/apps/api/migrations/0079_new_filter_schema.py index 4728ea91..58ed0025 100644 --- a/pydis_site/apps/api/migrations/0079_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0079_new_filter_schema.py @@ -44,7 +44,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: bypass_roles=["Helpers"], enabled=True, dm_content=dm_content, - dm_embed="", + dm_embed="" if name != "FILE_FORMAT" else "*Defined at runtime.*", infraction_type="", infraction_reason="", infraction_duration=timedelta(seconds=0), -- cgit v1.2.3 From b0f4b93ee831d0873f134440a6554177cc043feb Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 26 Feb 2022 00:53:28 +0200 Subject: Add invites denylist to the migration --- pydis_site/apps/api/migrations/0079_new_filter_schema.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/migrations/0079_new_filter_schema.py b/pydis_site/apps/api/migrations/0079_new_filter_schema.py index 58ed0025..43915edb 100644 --- a/pydis_site/apps/api/migrations/0079_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0079_new_filter_schema.py @@ -9,7 +9,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor import pydis_site.apps.api.models.bot.filters -OLD_LIST_NAMES = (('GUILD_INVITE', 'ALLOW'), ('FILE_FORMAT', 'ALLOW'), ('DOMAIN_NAME', 'DENY'), ('FILTER_TOKEN', 'DENY'), ('REDIRECT', 'DENY')) +OLD_LIST_NAMES = (('GUILD_INVITE', True), ('GUILD_INVITE', False), ('FILE_FORMAT', True), ('DOMAIN_NAME', False), ('FILTER_TOKEN', False), ('REDIRECT', False)) change_map = { "FILTER_TOKEN": "token", "DOMAIN_NAME": "domain", @@ -25,7 +25,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: filter_list_old = apps.get_model("api", "FilterListOld") for name, type_ in OLD_LIST_NAMES: - objects = filter_list_old.objects.filter(type=name) + objects = filter_list_old.objects.filter(type=name, allowed=type_) if name == "DOMAIN_NAME": dm_content = "Your URL has been removed because it matched a blacklisted domain: {match}" elif name == "GUILD_INVITE": @@ -36,7 +36,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: list_ = filter_list.objects.create( name=change_map[name], - list_type=1 if type_ == "ALLOW" else 0, + list_type=int(type_), ping_type=(["Moderators"] if name != "FILE_FORMAT" else []), filter_dm=True, dm_ping_type=[], -- cgit v1.2.3 From 02ea9e97f68e5388f7c3ade6ec48b11b272018bf Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 1 Mar 2022 23:49:52 +0200 Subject: Refine DM content for domains --- pydis_site/apps/api/migrations/0079_new_filter_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/migrations/0079_new_filter_schema.py b/pydis_site/apps/api/migrations/0079_new_filter_schema.py index 43915edb..89f70799 100644 --- a/pydis_site/apps/api/migrations/0079_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0079_new_filter_schema.py @@ -27,7 +27,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: for name, type_ in OLD_LIST_NAMES: objects = filter_list_old.objects.filter(type=name, allowed=type_) if name == "DOMAIN_NAME": - dm_content = "Your URL has been removed because it matched a blacklisted domain: {match}" + dm_content = "Your message has been removed because it contained a blocked domain: `{domain}`." elif name == "GUILD_INVITE": dm_content = "Per Rule 6, your invite link has been removed. " \ "Our server rules can be found here: https://pythondiscord.com/pages/rules" -- cgit v1.2.3 From 955122d028b81529fffbf73f9298d0f06cb2e412 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 9 Mar 2022 04:02:29 +0200 Subject: Change ping fields names --- pydis_site/apps/api/migrations/0079_new_filter_schema.py | 16 ++++++++-------- pydis_site/apps/api/models/bot/filters.py | 8 ++++---- pydis_site/apps/api/serializers.py | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pydis_site/apps/api/migrations/0079_new_filter_schema.py b/pydis_site/apps/api/migrations/0079_new_filter_schema.py index 89f70799..b67740d2 100644 --- a/pydis_site/apps/api/migrations/0079_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0079_new_filter_schema.py @@ -37,9 +37,9 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: list_ = filter_list.objects.create( name=change_map[name], list_type=int(type_), - ping_type=(["Moderators"] if name != "FILE_FORMAT" else []), + guild_pings=(["Moderators"] if name != "FILE_FORMAT" else []), filter_dm=True, - dm_ping_type=[], + dm_pings=[], delete_messages=(True if name != "FILTER_TOKEN" else False), bypass_roles=["Helpers"], enabled=True, @@ -60,9 +60,9 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: filter_list=list_, description=object_.comment, additional_field=None, - ping_type=None, + guild_pings=None, filter_dm=None, - dm_ping_type=None, + dm_pings=None, delete_messages=None, bypass_roles=None, enabled=None, @@ -97,9 +97,9 @@ class Migration(migrations.Migration): ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), ('additional_field', django.contrib.postgres.fields.jsonb.JSONField(help_text='Implementation specific field.', null=True)), - ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), + ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), - ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), + ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field], null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), @@ -120,9 +120,9 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), ('list_type', models.IntegerField(choices=[(1, 'Allow'), (0, 'Deny')], help_text='Whether this list is an allowlist or denylist')), - ('ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), - ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field])), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 97af21f8..4dbf1875 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -84,14 +84,14 @@ class FilterList(FilterSettingsMixin): choices=FilterListType.choices, help_text="Whether this list is an allowlist or denylist" ) - ping_type = ArrayField( + guild_pings = ArrayField( models.CharField(max_length=20), validators=(validate_ping_field,), help_text="Who to ping when this filter triggers.", null=False ) filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=False) - dm_ping_type = ArrayField( + dm_pings = ArrayField( models.CharField(max_length=20), validators=(validate_ping_field,), help_text="Who to ping when this filter triggers on a DM.", @@ -147,14 +147,14 @@ class Filter(FilterSettingsMixin): FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." ) - ping_type = ArrayField( + guild_pings = ArrayField( models.CharField(max_length=20), validators=(validate_ping_field,), help_text="Who to ping when this filter triggers.", null=True ) filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=True) - dm_ping_type = ArrayField( + dm_pings = ArrayField( models.CharField(max_length=20), validators=(validate_ping_field,), help_text="Who to ping when this filter triggers on a DM.", diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 99f2b630..5a637976 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -121,9 +121,9 @@ ALWAYS_OPTIONAL_SETTINGS = ( ) REQUIRED_FOR_FILTER_LIST_SETTINGS = ( - 'ping_type', + 'guild_pings', 'filter_dm', - 'dm_ping_type', + 'dm_pings', 'delete_messages', 'bypass_roles', 'enabled', @@ -154,7 +154,7 @@ CHANNEL_SCOPE_FIELDS = ( "disabled_categories", "enabled_channels", ) -MENTIONS_FIELDS = ("ping_type", "dm_ping_type") +MENTIONS_FIELDS = ("guild_pings", "dm_pings") SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS -- cgit v1.2.3 From 05e2bce1e82e422755396d1e6e489d6792ec0115 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 10 Mar 2022 20:56:47 +0200 Subject: Remove role validation Roles can be either IDs or names, so the current validation is not relevant anymore. Furthermore the ping fields can accept user IDs or names. --- .../apps/api/migrations/0079_new_filter_schema.py | 12 ++++---- pydis_site/apps/api/models/bot/filters.py | 32 ---------------------- pydis_site/apps/api/tests/test_filters.py | 11 -------- 3 files changed, 6 insertions(+), 49 deletions(-) diff --git a/pydis_site/apps/api/migrations/0079_new_filter_schema.py b/pydis_site/apps/api/migrations/0079_new_filter_schema.py index b67740d2..053f9782 100644 --- a/pydis_site/apps/api/migrations/0079_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0079_new_filter_schema.py @@ -97,11 +97,11 @@ class Migration(migrations.Migration): ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), ('additional_field', django.contrib.postgres.fields.jsonb.JSONField(help_text='Implementation specific field.', null=True)), - ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), + ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), - ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), + ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, null=True)), ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field], null=True)), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), @@ -120,11 +120,11 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), ('list_type', models.IntegerField(choices=[(1, 'Allow'), (0, 'Deny')], help_text='Whether this list is an allowlist or denylist')), - ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), - ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), + ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None)), ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field])), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 4dbf1875..13b332d2 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -16,32 +16,6 @@ class FilterListType(models.IntegerChoices): DENY = 0 -# Valid special values in ping related fields -VALID_PINGS = ("everyone", "here", "moderators", "onduty", "admins") -VALID_BYPASS_ROLES = ("staff",) - - -def validate_ping_field(value_list: List[str]) -> None: - """Validate that the values are either a special value or a UID.""" - for value in value_list: - # Check if it is a special value - if value in VALID_PINGS: - continue - # Check if it is a UID - if value.isnumeric(): - continue - - raise ValidationError(f"{value!r} isn't a valid ping type.") - - -def validate_bypass_roles_field(value_list: List[str]) -> None: - """Validate that the vclues are either a special value or a Role ID.""" - for value in value_list: - if value.isnumeric() or value in VALID_BYPASS_ROLES: - continue - raise ValidationError(f"{value!r} isn't a valid (bypass) role.") - - class FilterSettingsMixin(models.Model): """Mixin for common settings of a filters and filter lists.""" @@ -86,14 +60,12 @@ class FilterList(FilterSettingsMixin): ) guild_pings = ArrayField( models.CharField(max_length=20), - validators=(validate_ping_field,), help_text="Who to ping when this filter triggers.", null=False ) filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=False) dm_pings = ArrayField( models.CharField(max_length=20), - validators=(validate_ping_field,), help_text="Who to ping when this filter triggers on a DM.", null=False ) @@ -104,7 +76,6 @@ class FilterList(FilterSettingsMixin): bypass_roles = ArrayField( models.CharField(max_length=100), help_text="Roles and users who can bypass this filter.", - validators=(validate_bypass_roles_field,), null=False ) enabled = models.BooleanField( @@ -149,14 +120,12 @@ class Filter(FilterSettingsMixin): ) guild_pings = ArrayField( models.CharField(max_length=20), - validators=(validate_ping_field,), help_text="Who to ping when this filter triggers.", null=True ) filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=True) dm_pings = ArrayField( models.CharField(max_length=20), - validators=(validate_ping_field,), help_text="Who to ping when this filter triggers on a DM.", null=True ) @@ -167,7 +136,6 @@ class Filter(FilterSettingsMixin): bypass_roles = ArrayField( models.CharField(max_length=100), help_text="Roles and users who can bypass this filter.", - validators=(validate_bypass_roles_field,), null=True ) enabled = models.BooleanField( diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index f694053d..5f40c6f9 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -296,14 +296,3 @@ class GenericFilterTest(APISubdomainTestCase): response = self.client.delete(f"{sequence.url()}/42") self.assertEqual(response.status_code, 404) - - def test_reject_invalid_ping(self) -> None: - url = reverse('bot:filteroverride-list', host='api') - data = { - "ping_type": ["invalid"] - } - - response = self.client.post(url, data=data) - - self.assertEqual(response.status_code, 400) - self.assertDictEqual(response.json(), {'ping_type': ["'invalid' isn't a valid ping type."]}) -- cgit v1.2.3 From c7300a92c885b01a5663913fa73679fc680bfb74 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Sat, 12 Mar 2022 16:48:54 +0100 Subject: Sync Filter models with relating migrations, adjust code consistency --- .../apps/api/migrations/0079_new_filter_schema.py | 2 +- pydis_site/apps/api/models/bot/filters.py | 57 ++++++++++++++-------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/pydis_site/apps/api/migrations/0079_new_filter_schema.py b/pydis_site/apps/api/migrations/0079_new_filter_schema.py index 053f9782..bd807f02 100644 --- a/pydis_site/apps/api/migrations/0079_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0079_new_filter_schema.py @@ -129,7 +129,7 @@ class Migration(migrations.Migration): ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 13b332d2..f8bbfd14 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -1,7 +1,4 @@ -from typing import List - from django.contrib.postgres.fields import ArrayField, JSONField -from django.core.exceptions import ValidationError from django.db import models from django.db.models import UniqueConstraint @@ -37,7 +34,8 @@ class FilterSettingsMixin(models.Model): ) infraction_reason = models.CharField( max_length=1000, - help_text="The reason to give for the infraction." + help_text="The reason to give for the infraction.", + null=True ) infraction_duration = models.DurationField( null=True, @@ -59,13 +57,13 @@ class FilterList(FilterSettingsMixin): help_text="Whether this list is an allowlist or denylist" ) guild_pings = ArrayField( - models.CharField(max_length=20), + models.CharField(max_length=100), help_text="Who to ping when this filter triggers.", null=False ) filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=False) dm_pings = ArrayField( - models.CharField(max_length=20), + models.CharField(max_length=100), help_text="Who to ping when this filter triggers on a DM.", null=False ) @@ -83,9 +81,7 @@ class FilterList(FilterSettingsMixin): null=False ) send_alert = models.BooleanField( - help_text="Whether alert should be sent.", - null=False, - default=True + help_text="Whether an alert should be sent.", ) # Where a filter should apply. # @@ -93,9 +89,18 @@ class FilterList(FilterSettingsMixin): # - enabled_channels # - disabled_categories # - disabled_channels - enabled_channels = ArrayField(models.IntegerField()) - disabled_channels = ArrayField(models.IntegerField()) - disabled_categories = ArrayField(models.IntegerField()) + enabled_channels = ArrayField( + models.CharField(max_length=100), + help_text="Channels in which to run the filter even if it's disabled in the category." + ) + disabled_channels = ArrayField( + models.CharField(max_length=100), + help_text="Channels in which to not run the filter." + ) + disabled_categories = ArrayField( + models.CharField(max_length=100), + help_text="Categories in which to not run the filter." + ) class Meta: """Constrain name and list_type unique.""" @@ -112,20 +117,23 @@ class Filter(FilterSettingsMixin): """One specific trigger of a list.""" content = models.CharField(max_length=100, help_text="The definition of this filter.") - description = models.CharField(max_length=200, help_text="Why this filter has been added.") + description = models.CharField( + max_length=200, + help_text="Why this filter has been added.", null=True + ) additional_field = JSONField(null=True, help_text="Implementation specific field.") filter_list = models.ForeignKey( FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." ) guild_pings = ArrayField( - models.CharField(max_length=20), + models.CharField(max_length=100), help_text="Who to ping when this filter triggers.", null=True ) filter_dm = models.BooleanField(help_text="Whether DMs should be filtered.", null=True) dm_pings = ArrayField( - models.CharField(max_length=20), + models.CharField(max_length=100), help_text="Who to ping when this filter triggers on a DM.", null=True ) @@ -143,14 +151,25 @@ class Filter(FilterSettingsMixin): null=True ) send_alert = models.BooleanField( - help_text="Whether alert should be sent.", + help_text="Whether an alert should be sent.", null=True ) # Check FilterList model for information about these properties. - enabled_channels = ArrayField(models.IntegerField(), null=True) - disabled_channels = ArrayField(models.IntegerField(), null=True) - disabled_categories = ArrayField(models.IntegerField(), null=True) + enabled_channels = ArrayField( + models.CharField(max_length=100), + help_text="Channels in which to run the filter even if it's disabled in the category.", + null=True + ) + disabled_channels = ArrayField( + models.CharField(max_length=100), + help_text="Channels in which to not run the filter.", null=True + ) + disabled_categories = ArrayField( + models.CharField(max_length=100), + help_text="Categories in which to not run the filter.", + null=True + ) def __str__(self) -> str: return f"Filter {self.content!r}" -- cgit v1.2.3 From 3ff628ad44b80f1c5483832f72ee8b63bcbc4fdb Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Tue, 22 Mar 2022 18:55:02 +0100 Subject: Add UniqueConstraint to the Filter model - The UniqueConstraint includes every field, except for id and description. --- .../migrations/0080_unique_constraint_filters.py | 36 ++++++++++++++++++++++ pydis_site/apps/api/models/bot/filters.py | 28 ++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0080_unique_constraint_filters.py diff --git a/pydis_site/apps/api/migrations/0080_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0080_unique_constraint_filters.py new file mode 100644 index 00000000..0b3b4162 --- /dev/null +++ b/pydis_site/apps/api/migrations/0080_unique_constraint_filters.py @@ -0,0 +1,36 @@ +# Generated by Django 3.1.14 on 2022-03-22 16:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0079_new_filter_schema'), + ] + + operations = [ + migrations.AddConstraint( + model_name='filter', + constraint=models.UniqueConstraint(fields=( + 'dm_content', + 'dm_embed', + 'infraction_type', + 'infraction_reason', + 'infraction_duration', + 'content', + 'additional_field', + 'filter_list', + 'guild_pings', + 'filter_dm', + 'dm_pings', + 'delete_messages', + 'bypass_roles', + 'enabled', + 'send_alert', + 'enabled_channels', + 'disabled_channels', + 'disabled_categories' + ), name='unique_filters'), + ), + ] diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index f8bbfd14..708ceadc 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -113,7 +113,7 @@ class FilterList(FilterSettingsMixin): return f"Filter {FilterListType(self.list_type).label}list {self.name!r}" -class Filter(FilterSettingsMixin): +class FilterBase(FilterSettingsMixin): """One specific trigger of a list.""" content = models.CharField(max_length=100, help_text="The definition of this filter.") @@ -173,3 +173,29 @@ class Filter(FilterSettingsMixin): def __str__(self) -> str: return f"Filter {self.content!r}" + + class Meta: + """Metaclass for FilterBase to make it abstract model.""" + + abstract = True + + +class Filter(FilterBase): + """ + The main Filter models based on `FilterBase`. + + The purpose to have this model is to have access to the Fields of the Filter model + and set the unique constraint based on those fields. + """ + + class Meta: + """Metaclass Filter to set the unique constraint.""" + + constraints = ( + UniqueConstraint( + fields=tuple( + [field.name for field in FilterBase._meta.fields + if field.name != "id" and field.name != "description"] + ), + name="unique_filters"), + ) -- cgit v1.2.3 From 8c07c20c184371552d6811398ed5208ed2213c9b Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 27 Sep 2022 00:57:09 +0300 Subject: Add voice mute to infraction choices --- pydis_site/apps/api/migrations/0084_new_filter_schema.py | 4 ++-- pydis_site/apps/api/models/bot/filters.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/migrations/0084_new_filter_schema.py b/pydis_site/apps/api/migrations/0084_new_filter_schema.py index 10e83b8b..393f4b9f 100644 --- a/pydis_site/apps/api/migrations/0084_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0084_new_filter_schema.py @@ -105,7 +105,7 @@ class Migration(migrations.Migration): ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), - ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), + ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ("voice_mute", "Voice Mute"), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), @@ -128,7 +128,7 @@ class Migration(migrations.Migration): ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), - ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), + ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ("voice_mute", "Voice Mute"), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 708ceadc..f90eb6e6 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -28,7 +28,7 @@ class FilterSettingsMixin(models.Model): ) infraction_type = models.CharField( choices=Infraction.TYPE_CHOICES, - max_length=9, + max_length=10, null=True, help_text="The infraction to apply to this user." ) -- cgit v1.2.3 From e7e55af80b3853b75b86e3fb347af330f9c1d376 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 27 Sep 2022 00:59:39 +0300 Subject: Use the new models.JSONField --- pydis_site/apps/api/migrations/0084_new_filter_schema.py | 2 +- pydis_site/apps/api/models/bot/filters.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/migrations/0084_new_filter_schema.py b/pydis_site/apps/api/migrations/0084_new_filter_schema.py index 393f4b9f..96431b4a 100644 --- a/pydis_site/apps/api/migrations/0084_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0084_new_filter_schema.py @@ -96,7 +96,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), - ('additional_field', django.contrib.postgres.fields.jsonb.JSONField(help_text='Implementation specific field.', null=True)), + ('additional_field', models.JSONField(help_text='Implementation specific field.', null=True)), ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, null=True)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index f90eb6e6..33891890 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -1,4 +1,4 @@ -from django.contrib.postgres.fields import ArrayField, JSONField +from django.contrib.postgres.fields import ArrayField from django.db import models from django.db.models import UniqueConstraint @@ -121,7 +121,7 @@ class FilterBase(FilterSettingsMixin): max_length=200, help_text="Why this filter has been added.", null=True ) - additional_field = JSONField(null=True, help_text="Implementation specific field.") + additional_field = models.JSONField(null=True, help_text="Implementation specific field.") filter_list = models.ForeignKey( FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." -- cgit v1.2.3 From 870238e5ed31ae5dfb0e22fe0bc131f40d855013 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 27 Sep 2022 12:21:32 +0300 Subject: Convert the infraction choices to uppercase This is done to match the values used on the bot. --- pydis_site/apps/api/migrations/0084_new_filter_schema.py | 4 ++-- pydis_site/apps/api/models/bot/filters.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pydis_site/apps/api/migrations/0084_new_filter_schema.py b/pydis_site/apps/api/migrations/0084_new_filter_schema.py index 96431b4a..ba228d70 100644 --- a/pydis_site/apps/api/migrations/0084_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0084_new_filter_schema.py @@ -105,7 +105,7 @@ class Migration(migrations.Migration): ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), - ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ("voice_mute", "Voice Mute"), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=10, null=True)), + ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), @@ -128,7 +128,7 @@ class Migration(migrations.Migration): ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), - ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ("voice_mute", "Voice Mute"), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=10, null=True)), + ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 33891890..1fb9707d 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -27,7 +27,7 @@ class FilterSettingsMixin(models.Model): null=True ) infraction_type = models.CharField( - choices=Infraction.TYPE_CHOICES, + choices=[(choices[0].upper(), choices[1]) for choices in Infraction.TYPE_CHOICES], max_length=10, null=True, help_text="The infraction to apply to this user." -- cgit v1.2.3 From 855ce1e018bbb3a489c28768f60300c297890281 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 30 Sep 2022 21:51:25 +0300 Subject: Fix send_alert not being added correctly in serializers --- pydis_site/apps/api/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0976ed29..50200035 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -154,6 +154,7 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'filter_dm', 'dm_pings', 'delete_messages', + 'send_alert', 'bypass_roles', 'enabled', 'enabled_channels', -- cgit v1.2.3 From 1970a3651db1e1a4f2ef92c85a0a733fa23fa6f0 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 1 Oct 2022 19:48:25 +0300 Subject: Bring back enabled categories There needs to be a way to only enable a filter in a specific category, so this setting now fulfills that role. Disabled channels can be used to disable a filter in a specific channel within the category. --- pydis_site/apps/api/migrations/0084_new_filter_schema.py | 4 ++++ .../apps/api/migrations/0085_unique_constraint_filters.py | 1 + pydis_site/apps/api/models/bot/filters.py | 14 +++++++++----- pydis_site/apps/api/serializers.py | 6 ++++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/pydis_site/apps/api/migrations/0084_new_filter_schema.py b/pydis_site/apps/api/migrations/0084_new_filter_schema.py index ba228d70..74e1f009 100644 --- a/pydis_site/apps/api/migrations/0084_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0084_new_filter_schema.py @@ -51,6 +51,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: disabled_channels=[], disabled_categories=(["CODE JAM"] if name in ("FILE_FORMAT", "GUILD_INVITE") else []), enabled_channels=[], + enabled_categories=[], send_alert=(name in ('GUILD_INVITE', 'DOMAIN_NAME', 'FILTER_TOKEN')) ) @@ -74,6 +75,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: disabled_channels=None, disabled_categories=None, enabled_channels=None, + enabled_categories=None, send_alert=None, ) new_object.save() @@ -111,6 +113,7 @@ class Migration(migrations.Migration): ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", null=True, size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", null=True, size=None)), + ('enabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="The only categories in which to run the filter.", null=True, size=None)), ('send_alert', models.BooleanField(help_text='Whether an alert should be sent.', null=True)), ], ), @@ -134,6 +137,7 @@ class Migration(migrations.Migration): ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", size=None)), + ('enabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="The only categories in which to run the filter.", size=None)), ('send_alert', models.BooleanField(help_text='Whether an alert should be sent.')), ], ), diff --git a/pydis_site/apps/api/migrations/0085_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0085_unique_constraint_filters.py index 418c6e71..55ede901 100644 --- a/pydis_site/apps/api/migrations/0085_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0085_unique_constraint_filters.py @@ -30,6 +30,7 @@ class Migration(migrations.Migration): 'send_alert', 'enabled_channels', 'disabled_channels', + 'enabled_categories', 'disabled_categories' ), name='unique_filters'), ), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 1fb9707d..95a10e42 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -84,11 +84,6 @@ class FilterList(FilterSettingsMixin): help_text="Whether an alert should be sent.", ) # Where a filter should apply. - # - # The resolution is done in the following order: - # - enabled_channels - # - disabled_categories - # - disabled_channels enabled_channels = ArrayField( models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category." @@ -97,6 +92,10 @@ class FilterList(FilterSettingsMixin): models.CharField(max_length=100), help_text="Channels in which to not run the filter." ) + enabled_categories = ArrayField( + models.CharField(max_length=100), + help_text="The only categories in which to run the filter." + ) disabled_categories = ArrayField( models.CharField(max_length=100), help_text="Categories in which to not run the filter." @@ -165,6 +164,11 @@ class FilterBase(FilterSettingsMixin): models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True ) + enabled_categories = ArrayField( + models.CharField(max_length=100), + help_text="The only categories in which to run the filter.", + null=True + ) disabled_categories = ArrayField( models.CharField(max_length=100), help_text="Categories in which to not run the filter.", diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 50200035..26bda035 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -159,6 +159,7 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'enabled', 'enabled_channels', 'disabled_channels', + 'enabled_categories', 'disabled_categories', ) @@ -183,6 +184,7 @@ CHANNEL_SCOPE_FIELDS = ( "disabled_channels", "disabled_categories", "enabled_channels", + "enabled_categories" ) MENTIONS_FIELDS = ("guild_pings", "dm_pings") @@ -208,9 +210,9 @@ class FilterSerializer(ModelSerializer): raise ValidationError("Enabled and Disabled channels lists contain duplicates.") if data.get('disabled_categories') is not None: - categories_collection = data['disabled_categories'] + categories_collection = data['disabled_categories'] + data['enabled_categories'] if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Disabled categories lists contain duplicates.") + raise ValidationError("Enabled and Disabled categories lists contain duplicates.") return data -- cgit v1.2.3 From 862d00162309f4c061508545a377309bbd1871eb Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 7 Oct 2022 16:51:49 +0300 Subject: Properly add dm_embed to serializers --- pydis_site/apps/api/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 7c1c107a..0dcbf2ee 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -144,6 +144,7 @@ class DocumentationLinkSerializer(ModelSerializer): ALWAYS_OPTIONAL_SETTINGS = ( 'dm_content', + 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', -- cgit v1.2.3 From e5d655a81f71c4b5bfb15d567bc11f88023e5879 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 8 Oct 2022 01:18:59 +0300 Subject: Add infraction channel setting --- pydis_site/apps/api/migrations/0085_new_filter_schema.py | 5 +++++ .../apps/api/migrations/0086_unique_constraint_filters.py | 1 + pydis_site/apps/api/models/bot/filters.py | 11 +++++++++++ pydis_site/apps/api/serializers.py | 6 +++++- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/migrations/0085_new_filter_schema.py b/pydis_site/apps/api/migrations/0085_new_filter_schema.py index d16c26ac..2e721df4 100644 --- a/pydis_site/apps/api/migrations/0085_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0085_new_filter_schema.py @@ -3,6 +3,7 @@ from datetime import timedelta import django.contrib.postgres.fields from django.apps.registry import Apps +from django.core.validators import MinValueValidator from django.db import migrations, models import django.db.models.deletion from django.db.backends.base.schema import BaseDatabaseSchemaEditor @@ -48,6 +49,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: infraction_type="", infraction_reason="", infraction_duration=timedelta(seconds=0), + infraction_channel=None, disabled_channels=[], disabled_categories=(["CODE JAM"] if name in ("FILE_FORMAT", "GUILD_INVITE") else []), enabled_channels=[], @@ -72,6 +74,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: infraction_type=None, infraction_reason=None, infraction_duration=None, + infraction_channel=None, disabled_channels=None, disabled_categories=None, enabled_channels=None, @@ -110,6 +113,7 @@ class Migration(migrations.Migration): ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", null=True, size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", null=True, size=None)), @@ -134,6 +138,7 @@ class Migration(migrations.Migration): ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", size=None)), diff --git a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py index 8072ed2e..e7816e19 100644 --- a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py @@ -18,6 +18,7 @@ class Migration(migrations.Migration): 'infraction_type', 'infraction_reason', 'infraction_duration', + 'infraction_channel', 'content', 'additional_field', 'filter_list', diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 95a10e42..22482870 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -1,4 +1,5 @@ from django.contrib.postgres.fields import ArrayField +from django.core.validators import MinValueValidator from django.db import models from django.db.models import UniqueConstraint @@ -41,6 +42,16 @@ class FilterSettingsMixin(models.Model): null=True, help_text="The duration of the infraction. Null if permanent." ) + infraction_channel = models.BigIntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ), + help_text="Channel in which to send the infraction.", + null=True + ) class Meta: """Metaclass for settings mixin.""" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0dcbf2ee..83471ca2 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -148,6 +148,7 @@ ALWAYS_OPTIONAL_SETTINGS = ( 'infraction_type', 'infraction_reason', 'infraction_duration', + 'infraction_channel', ) REQUIRED_FOR_FILTER_LIST_SETTINGS = ( @@ -178,6 +179,7 @@ INFRACTION_AND_NOTIFICATION_FIELDS = ( "infraction_type", "infraction_reason", "infraction_duration", + "infraction_channel", "dm_content", "dm_embed" ) @@ -230,6 +232,7 @@ class FilterSerializer(ModelSerializer): 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, 'enabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, 'disabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'enabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, 'disabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, } @@ -305,6 +308,7 @@ class FilterListSerializer(ModelSerializer): 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, 'enabled_channels': {'allow_empty': True}, 'disabled_channels': {'allow_empty': True}, + 'enabled_categories': {'allow_empty': True}, 'disabled_categories': {'allow_empty': True}, } @@ -314,7 +318,7 @@ class FilterListSerializer(ModelSerializer): queryset=FilterList.objects.all(), fields=('name', 'list_type'), message=( - "A filterlist with the same name and type already exist." + "A filterlist with the same name and type already exists." ) ), ] -- cgit v1.2.3 From e23fcc3f1d8575243bb4acee3b8747d05e21ef22 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 8 Oct 2022 12:02:56 +0300 Subject: Fix categories validation --- pydis_site/apps/api/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 83471ca2..13cd7fea 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -212,7 +212,10 @@ class FilterSerializer(ModelSerializer): if len(channels_collection) != len(set(channels_collection)): raise ValidationError("Enabled and Disabled channels lists contain duplicates.") - if data.get('disabled_categories') is not None: + if ( + data.get('disabled_categories') is not None + and data.get('enabled_categories') is not None + ): categories_collection = data['disabled_categories'] + data['enabled_categories'] if len(categories_collection) != len(set(categories_collection)): raise ValidationError("Enabled and Disabled categories lists contain duplicates.") -- cgit v1.2.3 From 88a2be8ec1dc0bb85d3ac50f3f24b70a8ce12b3e Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 8 Oct 2022 12:07:38 +0300 Subject: Allow ping arrays to be empty --- pydis_site/apps/api/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 13cd7fea..7a5e76b7 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -237,6 +237,8 @@ class FilterSerializer(ModelSerializer): 'disabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, 'enabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, 'disabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'guild_pings': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'dm_pings': {'allow_empty': True, 'allow_null': True, 'required': False}, } def to_representation(self, instance: Filter) -> dict: @@ -313,6 +315,8 @@ class FilterListSerializer(ModelSerializer): 'disabled_channels': {'allow_empty': True}, 'enabled_categories': {'allow_empty': True}, 'disabled_categories': {'allow_empty': True}, + 'guild_pings': {'allow_empty': True}, + 'dm_pings': {'allow_empty': True}, } # Ensure that we can only have one filter list with the same name and field -- cgit v1.2.3 From c3747b6d09ff968858eab698eb5fcffb9c3fbd1f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 8 Oct 2022 15:44:37 +0300 Subject: Allow char fields to be blank This is necessary allow filters to define a blank message when the default is not blank. Additionally allows bypass_roles to be empty like the other array fields --- pydis_site/apps/api/migrations/0085_new_filter_schema.py | 12 ++++++------ pydis_site/apps/api/models/bot/filters.py | 5 ++++- pydis_site/apps/api/serializers.py | 12 +++++++++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/pydis_site/apps/api/migrations/0085_new_filter_schema.py b/pydis_site/apps/api/migrations/0085_new_filter_schema.py index 2e721df4..a38194ef 100644 --- a/pydis_site/apps/api/migrations/0085_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0085_new_filter_schema.py @@ -108,10 +108,10 @@ class Migration(migrations.Migration): ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), - ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), - ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True, blank=True)), + ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True, blank=True)), ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True, blank=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), @@ -133,10 +133,10 @@ class Migration(migrations.Migration): ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), - ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), - ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True, blank=True)), + ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True, blank=True)), ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True, blank=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 22482870..81b72c6e 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -20,12 +20,14 @@ class FilterSettingsMixin(models.Model): dm_content = models.CharField( max_length=1000, null=True, + blank=True, help_text="The DM to send to a user triggering this filter." ) dm_embed = models.CharField( max_length=2000, help_text="The content of the DM embed", - null=True + null=True, + blank=True ) infraction_type = models.CharField( choices=[(choices[0].upper(), choices[1]) for choices in Infraction.TYPE_CHOICES], @@ -36,6 +38,7 @@ class FilterSettingsMixin(models.Model): infraction_reason = models.CharField( max_length=1000, help_text="The reason to give for the infraction.", + blank=True, null=True ) infraction_duration = models.DurationField( diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 7a5e76b7..a42d567b 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -151,6 +151,13 @@ ALWAYS_OPTIONAL_SETTINGS = ( 'infraction_channel', ) +ALWAYS_BLANKABLE_SETTINGS = ( + 'dm_content', + 'dm_embed', + 'infraction_type', + 'infraction_reason', +) + REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'guild_pings', 'filter_dm', @@ -310,7 +317,10 @@ class FilterListSerializer(ModelSerializer): extra_kwargs = { field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS } | { - 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, + field: {'allow_blank': True, 'allow_null': True, 'required': False} + for field in ALWAYS_BLANKABLE_SETTINGS + } | { + 'bypass_roles': {'allow_empty': True}, 'enabled_channels': {'allow_empty': True}, 'disabled_channels': {'allow_empty': True}, 'enabled_categories': {'allow_empty': True}, -- cgit v1.2.3 From 65559eee45f0b17e4db3c80ad8b147d5413fab6f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 18 Oct 2022 18:59:32 +0300 Subject: Refactors filters serialier --- pydis_site/apps/api/serializers.py | 170 +++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 81 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a42d567b..aac8d06e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -142,7 +142,24 @@ class DocumentationLinkSerializer(ModelSerializer): fields = ('package', 'base_url', 'inventory_url') -ALWAYS_OPTIONAL_SETTINGS = ( +# region: filters serializers + + +REQUIRED_FOR_FILTER_LIST_SETTINGS = ( + 'guild_pings', + 'filter_dm', + 'dm_pings', + 'delete_messages', + 'send_alert', + 'bypass_roles', + 'enabled', + 'enabled_channels', + 'disabled_channels', + 'enabled_categories', + 'disabled_categories', +) + +OPTIONAL_FOR_FILTER_LIST_SETTINGS = ( 'dm_content', 'dm_embed', 'infraction_type', @@ -151,25 +168,21 @@ ALWAYS_OPTIONAL_SETTINGS = ( 'infraction_channel', ) -ALWAYS_BLANKABLE_SETTINGS = ( +ALLOW_BLANK_SETTINGS = ( 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', ) -REQUIRED_FOR_FILTER_LIST_SETTINGS = ( - 'guild_pings', - 'filter_dm', - 'dm_pings', - 'delete_messages', - 'send_alert', - 'bypass_roles', - 'enabled', +ALLOW_EMPTY_SETTINGS = ( 'enabled_channels', 'disabled_channels', 'enabled_categories', 'disabled_categories', + 'guild_pings', + 'dm_pings', + 'bypass_roles', ) # Required fields for custom JSON representation purposes @@ -198,7 +211,20 @@ CHANNEL_SCOPE_FIELDS = ( ) MENTIONS_FIELDS = ("guild_pings", "dm_pings") -SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS +SETTINGS_FIELDS = REQUIRED_FOR_FILTER_LIST_SETTINGS + OPTIONAL_FOR_FILTER_LIST_SETTINGS + + +def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: + """Create the extra kwargs of the Filter serializer's Meta class.""" + extra_kwargs = {} + for field in SETTINGS_FIELDS: + field_args = {'required': False, 'allow_null': True} + if field in ALLOW_BLANK_SETTINGS: + field_args['allow_blank'] = True + if field in ALLOW_EMPTY_SETTINGS: + field_args['allow_empty'] = True + extra_kwargs[field] = field_args + return extra_kwargs class FilterSerializer(ModelSerializer): @@ -236,17 +262,7 @@ class FilterSerializer(ModelSerializer): fields = ( 'id', 'content', 'description', 'additional_field', 'filter_list' ) + SETTINGS_FIELDS - extra_kwargs = { - field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS - } | { - 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, - 'enabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'disabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'enabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'disabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'guild_pings': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'dm_pings': {'allow_empty': True, 'allow_null': True, 'required': False}, - } + extra_kwargs = _create_filter_meta_extra_kwargs() def to_representation(self, instance: Filter) -> dict: """ @@ -258,28 +274,36 @@ class FilterSerializer(ModelSerializer): Furthermore, it puts the fields that meant to represent Filter settings, into a sub-field called `settings`. """ - schema_settings = { - "settings": - {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - | { - "infraction_and_notification": - {name: getattr(instance, name) - for name in INFRACTION_AND_NOTIFICATION_FIELDS} - } | { - "channel_scope": - {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} - } | { - "mentions": - { - schema_field_name: getattr(instance, schema_field_name) - for schema_field_name in MENTIONS_FIELDS - } - } + settings = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + settings["infraction_and_notification"] = { + name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS + } + settings["channel_scope"] = { + name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS } - schema_base = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} | \ - {"filter_list": instance.filter_list.id} + settings["mentions"] = { + name: getattr(instance, name) for name in MENTIONS_FIELDS + } + + schema = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} + schema["filter_list"] = instance.filter_list.id + schema["settings"] = settings + return schema - return schema_base | schema_settings + +def _create_filter_list_meta_extra_kwargs() -> dict[str, dict[str, bool]]: + """Create the extra kwargs of the FilterList serializer's Meta class.""" + extra_kwargs = {} + for field in SETTINGS_FIELDS: + field_args = {} + if field in OPTIONAL_FOR_FILTER_LIST_SETTINGS: + field_args = {'required': False, 'allow_null': True} + if field in ALLOW_BLANK_SETTINGS: + field_args['allow_blank'] = True + if field in ALLOW_EMPTY_SETTINGS: + field_args['allow_empty'] = True + extra_kwargs[field] = field_args + return extra_kwargs class FilterListSerializer(ModelSerializer): @@ -302,10 +326,13 @@ class FilterListSerializer(ModelSerializer): if len(channels_collection) != len(set(channels_collection)): raise ValidationError("Enabled and Disabled channels lists contain duplicates.") - if data.get('disabled_categories') is not None: - categories_collection = data['disabled_categories'] + if ( + data.get('disabled_categories') is not None + and data.get('enabled_categories') is not None + ): + categories_collection = data['disabled_categories'] + data['enabled_categories'] if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Disabled categories lists contain duplicates.") + raise ValidationError("Enabled and Disabled categories lists contain duplicates.") return data @@ -314,22 +341,9 @@ class FilterListSerializer(ModelSerializer): model = FilterList fields = ('id', 'name', 'list_type', 'filters') + SETTINGS_FIELDS - extra_kwargs = { - field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS - } | { - field: {'allow_blank': True, 'allow_null': True, 'required': False} - for field in ALWAYS_BLANKABLE_SETTINGS - } | { - 'bypass_roles': {'allow_empty': True}, - 'enabled_channels': {'allow_empty': True}, - 'disabled_channels': {'allow_empty': True}, - 'enabled_categories': {'allow_empty': True}, - 'disabled_categories': {'allow_empty': True}, - 'guild_pings': {'allow_empty': True}, - 'dm_pings': {'allow_empty': True}, - } + extra_kwargs = _create_filter_list_meta_extra_kwargs() - # Ensure that we can only have one filter list with the same name and field + # Ensure there can only be one filter list with the same name and type. validators = [ UniqueTogetherValidator( queryset=FilterList.objects.all(), @@ -350,29 +364,23 @@ class FilterListSerializer(ModelSerializer): Furthermore, it puts the fields that meant to represent FilterList settings, into a sub-field called `settings`. """ - # Fetches the relating filters - filters = [ - FilterSerializer(many=False).to_representation( - instance=item - ) for item in Filter.objects.filter( - filter_list=instance.id - ) + schema = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} + schema["filters"] = [ + FilterSerializer(many=False).to_representation(instance=item) + for item in Filter.objects.filter(filter_list=instance.id) ] - schema_base = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} \ - | {"filters": filters} - schema_settings_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - schema_settings_categories = { - "infraction_and_notification": - {name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS}} \ - | { - "channel_scope": - {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} | { - "mentions": { - schema_field_name: getattr(instance, schema_field_name) - for schema_field_name in MENTIONS_FIELDS - } + + settings = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + settings["infraction_and_notification"] = { + name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS } - return schema_base | {"settings": schema_settings_base | schema_settings_categories} + settings["channel_scope"] = {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} + settings["mentions"] = {name: getattr(instance, name) for name in MENTIONS_FIELDS} + + schema["settings"] = settings + return schema + +# endregion class InfractionSerializer(ModelSerializer): -- cgit v1.2.3 From c248047efd5ea9d0c899f9a2e577735649652fb4 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 27 Oct 2022 00:14:46 +0300 Subject: Add uniques filter list to migrations --- .../apps/api/migrations/0087_unique_filter_list.py | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0087_unique_filter_list.py diff --git a/pydis_site/apps/api/migrations/0087_unique_filter_list.py b/pydis_site/apps/api/migrations/0087_unique_filter_list.py new file mode 100644 index 00000000..843bb00a --- /dev/null +++ b/pydis_site/apps/api/migrations/0087_unique_filter_list.py @@ -0,0 +1,101 @@ +from datetime import timedelta + +from django.apps.registry import Apps +from django.db import migrations + +import pydis_site.apps.api.models.bot.filters + + +def create_unique_list(apps: Apps, _): + """Create the 'unique' FilterList and its related Filters.""" + filter_list: pydis_site.apps.api.models.FilterList = apps.get_model("api", "FilterList") + filter_: pydis_site.apps.api.models.Filter = apps.get_model("api", "Filter") + + list_ = filter_list.objects.create( + name="unique", + list_type=0, + guild_pings=[], + filter_dm=True, + dm_pings=[], + delete_messages=False, + bypass_roles=[], + enabled=True, + dm_content="", + dm_embed="", + infraction_type="", + infraction_reason="", + infraction_duration=timedelta(seconds=0), + infraction_channel=None, + disabled_channels=[], + disabled_categories=[], + enabled_channels=[], + enabled_categories=[], + send_alert=True + ) + + everyone = filter_.objects.create( + content="everyone", + filter_list=list_, + description="", + delete_messages=True, + bypass_roles=["Helpers"], + dm_content=( + "Please don't try to ping `@everyone` or `@here`. Your message has been removed. " + "If you believe this was a mistake, please let staff know!" + ), + ) + everyone.save() + + webhook = filter_.objects.create( + content="webhook", + filter_list=list_, + description="", + delete_messages=True, + dm_content=( + "Looks like you posted a Discord webhook URL. " + "Therefore, your message has been removed, and your webhook has been deleted. " + "You can re-create it if you wish to. " + "If you believe this was a mistake, please let us know." + ), + ) + webhook.save() + + rich_embed = filter_.objects.create( + content="rich_embed", + filter_list=list_, + description="", + guild_pings=["Moderators"], + dm_pings=["Moderators"] + ) + rich_embed.save() + + discord_token = filter_.objects.create( + content="discord_token", + filter_list=list_, + filter_dm=False, + delete_messages=True, + dm_content=( + "I noticed you posted a seemingly valid Discord API " + "token in your message and have removed your message. " + "This means that your token has been **compromised**. " + "Please change your token **immediately** at: " + "\n\n" + "Feel free to re-post it with the token removed. " + "If you believe this was a mistake, please let us know!" + ) + ) + discord_token.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0086_unique_constraint_filters'), + ] + + operations = [ + migrations.RunPython( + code=create_unique_list, + reverse_code=None + ), + ] \ No newline at end of file -- cgit v1.2.3 From d0a21729de55db9681d2e4e6689607c537ac5f79 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 1 Nov 2022 22:54:14 +0200 Subject: Add antispam list in migrations --- .../api/migrations/0088_antispam_filter_list.py | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0088_antispam_filter_list.py diff --git a/pydis_site/apps/api/migrations/0088_antispam_filter_list.py b/pydis_site/apps/api/migrations/0088_antispam_filter_list.py new file mode 100644 index 00000000..d425293f --- /dev/null +++ b/pydis_site/apps/api/migrations/0088_antispam_filter_list.py @@ -0,0 +1,52 @@ +from datetime import timedelta + +from django.apps.registry import Apps +from django.db import migrations + +import pydis_site.apps.api.models.bot.filters + + +def create_antispam_list(apps: Apps, _): + """Create the 'unique' FilterList and its related Filters.""" + filter_list: pydis_site.apps.api.models.FilterList = apps.get_model("api", "FilterList") + filter_: pydis_site.apps.api.models.Filter = apps.get_model("api", "Filter") + + list_ = filter_list.objects.create( + name="antispam", + list_type=0, + guild_pings=["Moderators"], + filter_dm=False, + dm_pings=[], + delete_messages=True, + bypass_roles=["Helpers"], + enabled=True, + dm_content="", + dm_embed="", + infraction_type="mute", + infraction_reason="", + infraction_duration=timedelta(seconds=600), + infraction_channel=None, + disabled_channels=[], + disabled_categories=["CODE JAM"], + enabled_channels=[], + enabled_categories=[], + send_alert=True + ) + + rules = ("duplicates", "attachments", "burst", "chars", "emoji", "links", "mentions", "newlines", "role_mentions") + + filter_.objects.bulk_create([filter_(content=rule, filter_list=list_) for rule in rules]) + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0087_unique_filter_list'), + ] + + operations = [ + migrations.RunPython( + code=create_antispam_list, + reverse_code=None + ), + ] -- cgit v1.2.3 From fee81cf1f4205024d663fc8055f04ed22bec9f32 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 4 Nov 2022 00:30:04 +0200 Subject: Stop using None as a valid setting value See e100ae9b on bot --- .../apps/api/migrations/0085_new_filter_schema.py | 18 ++--- .../migrations/0086_unique_constraint_filters.py | 6 +- .../apps/api/migrations/0087_unique_filter_list.py | 4 +- .../api/migrations/0088_antispam_filter_list.py | 4 +- pydis_site/apps/api/models/bot/filters.py | 84 +++++++++++++++------- pydis_site/apps/api/serializers.py | 23 ++---- 6 files changed, 82 insertions(+), 57 deletions(-) diff --git a/pydis_site/apps/api/migrations/0085_new_filter_schema.py b/pydis_site/apps/api/migrations/0085_new_filter_schema.py index a38194ef..b0665ba5 100644 --- a/pydis_site/apps/api/migrations/0085_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0085_new_filter_schema.py @@ -46,10 +46,10 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: enabled=True, dm_content=dm_content, dm_embed="" if name != "FILE_FORMAT" else "*Defined at runtime.*", - infraction_type="", + infraction_type="NONE", infraction_reason="", infraction_duration=timedelta(seconds=0), - infraction_channel=None, + infraction_channel=0, disabled_channels=[], disabled_categories=(["CODE JAM"] if name in ("FILE_FORMAT", "GUILD_INVITE") else []), enabled_channels=[], @@ -110,7 +110,7 @@ class Migration(migrations.Migration): ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True, blank=True)), ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True, blank=True)), - ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), + ('infraction_type', models.CharField(choices=[('NONE', 'None'), ('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True, blank=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), @@ -133,12 +133,12 @@ class Migration(migrations.Migration): ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), - ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True, blank=True)), - ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True, blank=True)), - ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True, blank=True)), - ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), - ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, blank=True)), + ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, blank=True)), + ('infraction_type', models.CharField(choices=[('NONE', 'None'), ('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, blank=True)), + ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.')), + ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.")), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", size=None)), diff --git a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py index e7816e19..6fa99e9e 100644 --- a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py @@ -13,15 +13,15 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name='filter', constraint=models.UniqueConstraint(fields=( + 'content', + 'additional_field', + 'filter_list', 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', 'infraction_channel', - 'content', - 'additional_field', - 'filter_list', 'guild_pings', 'filter_dm', 'dm_pings', diff --git a/pydis_site/apps/api/migrations/0087_unique_filter_list.py b/pydis_site/apps/api/migrations/0087_unique_filter_list.py index 843bb00a..9db966fb 100644 --- a/pydis_site/apps/api/migrations/0087_unique_filter_list.py +++ b/pydis_site/apps/api/migrations/0087_unique_filter_list.py @@ -22,10 +22,10 @@ def create_unique_list(apps: Apps, _): enabled=True, dm_content="", dm_embed="", - infraction_type="", + infraction_type="NONE", infraction_reason="", infraction_duration=timedelta(seconds=0), - infraction_channel=None, + infraction_channel=0, disabled_channels=[], disabled_categories=[], enabled_channels=[], diff --git a/pydis_site/apps/api/migrations/0088_antispam_filter_list.py b/pydis_site/apps/api/migrations/0088_antispam_filter_list.py index d425293f..354e4520 100644 --- a/pydis_site/apps/api/migrations/0088_antispam_filter_list.py +++ b/pydis_site/apps/api/migrations/0088_antispam_filter_list.py @@ -22,10 +22,10 @@ def create_antispam_list(apps: Apps, _): enabled=True, dm_content="", dm_embed="", - infraction_type="mute", + infraction_type="MUTE", infraction_reason="", infraction_duration=timedelta(seconds=600), - infraction_channel=None, + infraction_channel=0, disabled_channels=[], disabled_categories=["CODE JAM"], enabled_channels=[], diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 81b72c6e..7398f8a0 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -14,35 +14,43 @@ class FilterListType(models.IntegerChoices): DENY = 0 -class FilterSettingsMixin(models.Model): - """Mixin for common settings of a filters and filter lists.""" +class FilterList(models.Model): + """Represent a list in its allow or deny form.""" + name = models.CharField(max_length=50, help_text="The unique name of this list.") + list_type = models.IntegerField( + choices=FilterListType.choices, + help_text="Whether this list is an allowlist or denylist" + ) dm_content = models.CharField( max_length=1000, - null=True, + null=False, blank=True, help_text="The DM to send to a user triggering this filter." ) dm_embed = models.CharField( max_length=2000, help_text="The content of the DM embed", - null=True, + null=False, blank=True ) infraction_type = models.CharField( - choices=[(choices[0].upper(), choices[1]) for choices in Infraction.TYPE_CHOICES], + choices=[ + (choices[0].upper(), choices[1]) + for choices in [("NONE", "None"), *Infraction.TYPE_CHOICES] + ], max_length=10, - null=True, + null=False, help_text="The infraction to apply to this user." ) infraction_reason = models.CharField( max_length=1000, help_text="The reason to give for the infraction.", blank=True, - null=True + null=False ) infraction_duration = models.DurationField( - null=True, + null=False, help_text="The duration of the infraction. Null if permanent." ) infraction_channel = models.BigIntegerField( @@ -53,22 +61,7 @@ class FilterSettingsMixin(models.Model): ), ), help_text="Channel in which to send the infraction.", - null=True - ) - - class Meta: - """Metaclass for settings mixin.""" - - abstract = True - - -class FilterList(FilterSettingsMixin): - """Represent a list in its allow or deny form.""" - - name = models.CharField(max_length=50, help_text="The unique name of this list.") - list_type = models.IntegerField( - choices=FilterListType.choices, - help_text="Whether this list is an allowlist or denylist" + null=False ) guild_pings = ArrayField( models.CharField(max_length=100), @@ -126,7 +119,7 @@ class FilterList(FilterSettingsMixin): return f"Filter {FilterListType(self.list_type).label}list {self.name!r}" -class FilterBase(FilterSettingsMixin): +class FilterBase(models.Model): """One specific trigger of a list.""" content = models.CharField(max_length=100, help_text="The definition of this filter.") @@ -139,6 +132,47 @@ class FilterBase(FilterSettingsMixin): FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." ) + dm_content = models.CharField( + max_length=1000, + null=True, + blank=True, + help_text="The DM to send to a user triggering this filter." + ) + dm_embed = models.CharField( + max_length=2000, + help_text="The content of the DM embed", + null=True, + blank=True + ) + infraction_type = models.CharField( + choices=[ + (choices[0].upper(), choices[1]) + for choices in [("NONE", "None"), *Infraction.TYPE_CHOICES] + ], + max_length=10, + null=True, + help_text="The infraction to apply to this user." + ) + infraction_reason = models.CharField( + max_length=1000, + help_text="The reason to give for the infraction.", + blank=True, + null=True + ) + infraction_duration = models.DurationField( + null=True, + help_text="The duration of the infraction. Null if permanent." + ) + infraction_channel = models.BigIntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ), + help_text="Channel in which to send the infraction.", + null=True + ) guild_pings = ArrayField( models.CharField(max_length=100), help_text="Who to ping when this filter triggers.", diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index aac8d06e..a902523e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -144,8 +144,13 @@ class DocumentationLinkSerializer(ModelSerializer): # region: filters serializers - -REQUIRED_FOR_FILTER_LIST_SETTINGS = ( +SETTINGS_FIELDS = ( + 'dm_content', + 'dm_embed', + 'infraction_type', + 'infraction_reason', + 'infraction_duration', + 'infraction_channel', 'guild_pings', 'filter_dm', 'dm_pings', @@ -159,19 +164,9 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'disabled_categories', ) -OPTIONAL_FOR_FILTER_LIST_SETTINGS = ( - 'dm_content', - 'dm_embed', - 'infraction_type', - 'infraction_reason', - 'infraction_duration', - 'infraction_channel', -) - ALLOW_BLANK_SETTINGS = ( 'dm_content', 'dm_embed', - 'infraction_type', 'infraction_reason', ) @@ -211,8 +206,6 @@ CHANNEL_SCOPE_FIELDS = ( ) MENTIONS_FIELDS = ("guild_pings", "dm_pings") -SETTINGS_FIELDS = REQUIRED_FOR_FILTER_LIST_SETTINGS + OPTIONAL_FOR_FILTER_LIST_SETTINGS - def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: """Create the extra kwargs of the Filter serializer's Meta class.""" @@ -296,8 +289,6 @@ def _create_filter_list_meta_extra_kwargs() -> dict[str, dict[str, bool]]: extra_kwargs = {} for field in SETTINGS_FIELDS: field_args = {} - if field in OPTIONAL_FOR_FILTER_LIST_SETTINGS: - field_args = {'required': False, 'allow_null': True} if field in ALLOW_BLANK_SETTINGS: field_args['allow_blank'] = True if field in ALLOW_EMPTY_SETTINGS: -- cgit v1.2.3 From 649fbc4799082f6ad5d9f986c86ca37ae6fe859d Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 5 Nov 2022 15:20:14 +0200 Subject: Add creation and update timestamps to filtering models This is to support auto-infractions reporting (bot 7fcec400) --- pydis_site/apps/api/migrations/0085_new_filter_schema.py | 6 ++++++ pydis_site/apps/api/models/bot/filters.py | 7 ++++--- pydis_site/apps/api/serializers.py | 8 ++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pydis_site/apps/api/migrations/0085_new_filter_schema.py b/pydis_site/apps/api/migrations/0085_new_filter_schema.py index b0665ba5..d902be7f 100644 --- a/pydis_site/apps/api/migrations/0085_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0085_new_filter_schema.py @@ -60,6 +60,8 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: for object_ in objects: new_object = filter_.objects.create( content=object_.content, + created_at=object_.created_at, + updated_at=object_.updated_at, filter_list=list_, description=object_.comment, additional_field=None, @@ -99,6 +101,8 @@ class Migration(migrations.Migration): name='Filter', 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)), ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), ('additional_field', models.JSONField(help_text='Implementation specific field.', null=True)), @@ -125,6 +129,8 @@ class Migration(migrations.Migration): 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)), ('name', models.CharField(help_text='The unique name of this list.', max_length=50)), ('list_type', models.IntegerField(choices=[(1, 'Allow'), (0, 'Deny')], help_text='Whether this list is an allowlist or denylist')), ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 7398f8a0..1ea21a48 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -5,6 +5,7 @@ from django.db.models import UniqueConstraint # Must be imported that way to avoid circular imports from .infraction import Infraction +from pydis_site.apps.api.models.mixins import ModelTimestampMixin, ModelReprMixin class FilterListType(models.IntegerChoices): @@ -14,7 +15,7 @@ class FilterListType(models.IntegerChoices): DENY = 0 -class FilterList(models.Model): +class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): """Represent a list in its allow or deny form.""" name = models.CharField(max_length=50, help_text="The unique name of this list.") @@ -119,7 +120,7 @@ class FilterList(models.Model): return f"Filter {FilterListType(self.list_type).label}list {self.name!r}" -class FilterBase(models.Model): +class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): """One specific trigger of a list.""" content = models.CharField(max_length=100, help_text="The definition of this filter.") @@ -247,7 +248,7 @@ class Filter(FilterBase): UniqueConstraint( fields=tuple( [field.name for field in FilterBase._meta.fields - if field.name != "id" and field.name != "description"] + if field.name not in ("id", "description", "created_at", "updated_at")] ), name="unique_filters"), ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a902523e..d6bae2cb 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -181,8 +181,8 @@ ALLOW_EMPTY_SETTINGS = ( ) # Required fields for custom JSON representation purposes -BASE_FILTER_FIELDS = ('id', 'content', 'description', 'additional_field') -BASE_FILTERLIST_FIELDS = ('id', 'name', 'list_type') +BASE_FILTER_FIELDS = ('id', 'created_at', 'updated_at', 'content', 'description', 'additional_field') +BASE_FILTERLIST_FIELDS = ('id', 'created_at', 'updated_at', 'name', 'list_type') BASE_SETTINGS_FIELDS = ( "bypass_roles", "filter_dm", @@ -253,7 +253,7 @@ class FilterSerializer(ModelSerializer): model = Filter fields = ( - 'id', 'content', 'description', 'additional_field', 'filter_list' + 'id', 'created_at', 'updated_at', 'content', 'description', 'additional_field', 'filter_list' ) + SETTINGS_FIELDS extra_kwargs = _create_filter_meta_extra_kwargs() @@ -331,7 +331,7 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ('id', 'name', 'list_type', 'filters') + SETTINGS_FIELDS + fields = ('id', 'created_at', 'updated_at', 'name', 'list_type', 'filters') + SETTINGS_FIELDS extra_kwargs = _create_filter_list_meta_extra_kwargs() # Ensure there can only be one filter list with the same name and type. -- cgit v1.2.3 From 1411ad02a7b98c358b215191c7ff1b3846824c9d Mon Sep 17 00:00:00 2001 From: mbaruh Date: Mon, 7 Nov 2022 22:41:50 +0200 Subject: Disable everyone filter in code jam categories This is in line with what already existed. --- pydis_site/apps/api/migrations/0087_unique_filter_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/api/migrations/0087_unique_filter_list.py b/pydis_site/apps/api/migrations/0087_unique_filter_list.py index 9db966fb..96c2b17a 100644 --- a/pydis_site/apps/api/migrations/0087_unique_filter_list.py +++ b/pydis_site/apps/api/migrations/0087_unique_filter_list.py @@ -43,6 +43,7 @@ def create_unique_list(apps: Apps, _): "Please don't try to ping `@everyone` or `@here`. Your message has been removed. " "If you believe this was a mistake, please let staff know!" ), + disabled_categories=["CODE JAM"] ) everyone.save() -- cgit v1.2.3 From c39ae63d407663f47bf2d824a259335234066801 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 9 Nov 2022 21:27:26 +0200 Subject: Rename delete_messages to the more generic remove_context --- pydis_site/apps/api/migrations/0085_new_filter_schema.py | 8 ++++---- .../apps/api/migrations/0086_unique_constraint_filters.py | 2 +- pydis_site/apps/api/migrations/0087_unique_filter_list.py | 8 ++++---- pydis_site/apps/api/migrations/0088_antispam_filter_list.py | 2 +- pydis_site/apps/api/models/bot/filters.py | 8 ++++---- pydis_site/apps/api/serializers.py | 4 ++-- pydis_site/apps/api/tests/test_filters.py | 8 ++++---- pydis_site/apps/api/viewsets/bot/filters.py | 12 ++++++------ 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pydis_site/apps/api/migrations/0085_new_filter_schema.py b/pydis_site/apps/api/migrations/0085_new_filter_schema.py index d902be7f..96d03bf4 100644 --- a/pydis_site/apps/api/migrations/0085_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0085_new_filter_schema.py @@ -41,7 +41,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: guild_pings=(["Moderators"] if name != "FILE_FORMAT" else []), filter_dm=True, dm_pings=[], - delete_messages=(True if name != "FILTER_TOKEN" else False), + remove_context=(True if name != "FILTER_TOKEN" else False), bypass_roles=["Helpers"], enabled=True, dm_content=dm_content, @@ -68,7 +68,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: guild_pings=None, filter_dm=None, dm_pings=None, - delete_messages=None, + remove_context=None, bypass_roles=None, enabled=None, dm_content=None, @@ -109,7 +109,7 @@ class Migration(migrations.Migration): ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, null=True)), - ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), + ('remove_context', models.BooleanField(help_text='Whether this filter should remove the context (such as a message) triggering it.', null=True)), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True, blank=True)), @@ -136,7 +136,7 @@ class Migration(migrations.Migration): ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None)), - ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), + ('remove_context', models.BooleanField(help_text='Whether this filter should remove the context (such as a message) triggering it.')), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, blank=True)), diff --git a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py index 6fa99e9e..b83e395c 100644 --- a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py @@ -25,7 +25,7 @@ class Migration(migrations.Migration): 'guild_pings', 'filter_dm', 'dm_pings', - 'delete_messages', + 'remove_context', 'bypass_roles', 'enabled', 'send_alert', diff --git a/pydis_site/apps/api/migrations/0087_unique_filter_list.py b/pydis_site/apps/api/migrations/0087_unique_filter_list.py index 96c2b17a..b8087d9c 100644 --- a/pydis_site/apps/api/migrations/0087_unique_filter_list.py +++ b/pydis_site/apps/api/migrations/0087_unique_filter_list.py @@ -17,7 +17,7 @@ def create_unique_list(apps: Apps, _): guild_pings=[], filter_dm=True, dm_pings=[], - delete_messages=False, + remove_context=False, bypass_roles=[], enabled=True, dm_content="", @@ -37,7 +37,7 @@ def create_unique_list(apps: Apps, _): content="everyone", filter_list=list_, description="", - delete_messages=True, + remove_context=True, bypass_roles=["Helpers"], dm_content=( "Please don't try to ping `@everyone` or `@here`. Your message has been removed. " @@ -51,7 +51,7 @@ def create_unique_list(apps: Apps, _): content="webhook", filter_list=list_, description="", - delete_messages=True, + remove_context=True, dm_content=( "Looks like you posted a Discord webhook URL. " "Therefore, your message has been removed, and your webhook has been deleted. " @@ -74,7 +74,7 @@ def create_unique_list(apps: Apps, _): content="discord_token", filter_list=list_, filter_dm=False, - delete_messages=True, + remove_context=True, dm_content=( "I noticed you posted a seemingly valid Discord API " "token in your message and have removed your message. " diff --git a/pydis_site/apps/api/migrations/0088_antispam_filter_list.py b/pydis_site/apps/api/migrations/0088_antispam_filter_list.py index 354e4520..fcb56781 100644 --- a/pydis_site/apps/api/migrations/0088_antispam_filter_list.py +++ b/pydis_site/apps/api/migrations/0088_antispam_filter_list.py @@ -17,7 +17,7 @@ def create_antispam_list(apps: Apps, _): guild_pings=["Moderators"], filter_dm=False, dm_pings=[], - delete_messages=True, + remove_context=True, bypass_roles=["Helpers"], enabled=True, dm_content="", diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 1ea21a48..4d8a4025 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -75,8 +75,8 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): help_text="Who to ping when this filter triggers on a DM.", null=False ) - delete_messages = models.BooleanField( - help_text="Whether this filter should delete messages triggering it.", + remove_context = models.BooleanField( + help_text="Whether this filter should remove the context (such as a message) triggering it.", null=False ) bypass_roles = ArrayField( @@ -185,8 +185,8 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): help_text="Who to ping when this filter triggers on a DM.", null=True ) - delete_messages = models.BooleanField( - help_text="Whether this filter should delete messages triggering it.", + remove_context = models.BooleanField( + help_text="Whether this filter should remove the context (such as a message) triggering it.", null=True ) bypass_roles = ArrayField( diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index d6bae2cb..eabca66e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -154,7 +154,7 @@ SETTINGS_FIELDS = ( 'guild_pings', 'filter_dm', 'dm_pings', - 'delete_messages', + 'remove_context', 'send_alert', 'bypass_roles', 'enabled', @@ -187,7 +187,7 @@ BASE_SETTINGS_FIELDS = ( "bypass_roles", "filter_dm", "enabled", - "delete_messages", + "remove_context", "send_alert" ) INFRACTION_AND_NOTIFICATION_FIELDS = ( diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index 5f40c6f9..f3afdaeb 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -49,7 +49,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: ping_type=[], filter_dm=False, dm_ping_type=[], - delete_messages=False, + remove_context=False, bypass_roles=[], enabled=False, default_action=FilterAction( @@ -76,7 +76,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: "ping_type": ["onduty"], "filter_dm": True, "dm_ping_type": ["123456"], - "delete_messages": True, + "remove_context": True, "bypass_roles": [123456], "enabled": True, "default_action": FilterAction( @@ -130,7 +130,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: ping_type=[], filter_dm=False, dm_ping_type=[], - delete_messages=False, + remove_context=False, bypass_roles=[], enabled=False, default_action=FilterAction( @@ -157,7 +157,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: "ping_type": ["everyone"], "filter_dm": False, "dm_ping_type": ["here"], - "delete_messages": False, + "remove_context": False, "bypass_roles": [9876], "enabled": True, "filter_action": None, diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index dd9a7d87..1eb05053 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -36,7 +36,7 @@ class FilterListViewSet(ModelViewSet): ... "filter_dm": None, ... "enabled": None ... "send_alert": True, - ... "delete_messages": None + ... "remove_context": None ... "infraction_and_notification": { ... "infraction_type": None, ... "infraction_reason": "", @@ -64,7 +64,7 @@ class FilterListViewSet(ModelViewSet): ... ], ... "filter_dm": True, ... "enabled": True - ... "delete_messages": True, + ... "remove_context": True, ... "send_alert": True ... "infraction_and_notification": { ... "infraction_type": "", @@ -111,7 +111,7 @@ class FilterListViewSet(ModelViewSet): ... "bypass_roles": None ... "filter_dm": None, ... "enabled": None - ... "delete_messages": None, + ... "remove_context": None, ... "send_alert": None ... "infraction_and_notification": { ... "infraction_type": None, @@ -140,7 +140,7 @@ class FilterListViewSet(ModelViewSet): ... ], ... "filter_dm": True, ... "enabled": True - ... "delete_messages": True + ... "remove_context": True ... "send_alert": True ... "infraction_and_notification": { ... "infraction_type": "", @@ -198,7 +198,7 @@ class FilterViewSet(ModelViewSet): ... "bypass_roles": None ... "filter_dm": None, ... "enabled": None - ... "delete_messages": True, + ... "remove_context": True, ... "send_alert": True ... "infraction": { ... "infraction_type": None, @@ -237,7 +237,7 @@ class FilterViewSet(ModelViewSet): ... "bypass_roles": None ... "filter_dm": None, ... "enabled": None - ... "delete_messages": True, + ... "remove_context": True, ... "send_alert": True ... "infraction": { ... "infraction_type": None, -- cgit v1.2.3 From d3eec93b36bd57c521e70b4001c74cb9756caf23 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 27 Jan 2023 22:19:28 +0200 Subject: Fix filter serializers validation to account for filterlist settings --- pydis_site/apps/api/serializers.py | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index eabca66e..83ab4584 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,4 +1,6 @@ """Converters from Django models to data interchange formats and back.""" +from typing import Any + from django.db.models.query import QuerySet from django.db.utils import IntegrityError from rest_framework.exceptions import NotFound @@ -220,31 +222,29 @@ def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: return extra_kwargs +def get_field_value(data: dict, field_name: str) -> Any: + """Get the value directly from the key, or from the filter list if it's missing or is None.""" + if data.get(field_name): + return data[field_name] + return getattr(data["filter_list"], field_name) + + class FilterSerializer(ModelSerializer): """A class providing (de-)serialization of `Filter` instances.""" def validate(self, data: dict) -> dict: - """Perform infraction data + allow and disallowed lists validation.""" - if ( - data.get('infraction_reason') or data.get('infraction_duration') - ) and not data.get('infraction_type'): - raise ValidationError("Infraction type is required with infraction duration or reason") - + """Perform infraction data + allowed and disallowed lists validation.""" if ( - data.get('disabled_channels') is not None - and data.get('enabled_channels') is not None + (get_field_value(data, "infraction_reason") or get_field_value(data, "infraction_duration")) + and get_field_value(data, "infraction_type") == "NONE" ): - channels_collection = data['disabled_channels'] + data['enabled_channels'] - if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Enabled and Disabled channels lists contain duplicates.") + raise ValidationError("Infraction type is required with infraction duration or reason.") - if ( - data.get('disabled_categories') is not None - and data.get('enabled_categories') is not None - ): - categories_collection = data['disabled_categories'] + data['enabled_categories'] - if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Enabled and Disabled categories lists contain duplicates.") + if set(get_field_value(data, "disabled_channels")) & set(get_field_value(data, "enabled_channels")): + raise ValidationError("You can't have the same value in both enabled and disabled channels lists.") + + if set(get_field_value(data, "disabled_categories")) & set(get_field_value(data, "enabled_categories")): + raise ValidationError("You can't have the same value in both enabled and disabled categories lists.") return data @@ -318,8 +318,8 @@ class FilterListSerializer(ModelSerializer): raise ValidationError("Enabled and Disabled channels lists contain duplicates.") if ( - data.get('disabled_categories') is not None - and data.get('enabled_categories') is not None + data.get('disabled_categories') is not None + and data.get('enabled_categories') is not None ): categories_collection = data['disabled_categories'] + data['enabled_categories'] if len(categories_collection) != len(set(categories_collection)): -- cgit v1.2.3 From 3862e051407061186609dbeaab23ec53aeca2f94 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 27 Jan 2023 22:20:10 +0200 Subject: Update tests --- pydis_site/apps/api/models/__init__.py | 1 - pydis_site/apps/api/tests/test_filters.py | 309 ++++++++++++++++-------------- pydis_site/apps/api/tests/test_models.py | 3 - 3 files changed, 170 insertions(+), 143 deletions(-) diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 580c95a0..fee4c8d5 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -6,7 +6,6 @@ from .bot import ( BumpedThread, DocumentationLink, DeletedMessage, - FilterList, Infraction, Message, MessageDeletionContext, diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index f3afdaeb..cae78cd6 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -1,19 +1,13 @@ import contextlib from dataclasses import dataclass +from datetime import timedelta from typing import Any, Dict, Tuple, Type from django.db.models import Model -from django_hosts import reverse +from django.urls import reverse -from pydis_site.apps.api.models.bot.filters import ( # noqa: I101 - Preserving the filter order - FilterList, - FilterSettings, - FilterAction, - ChannelRange, - Filter, - FilterOverride -) -from pydis_site.apps.api.tests.base import APISubdomainTestCase +from pydis_site.apps.api.models.bot.filters import FilterList, Filter +from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase @dataclass() @@ -21,99 +15,76 @@ class TestSequence: model: Type[Model] route: str object: Dict[str, Any] - ignored_fields: Tuple[str] = () + ignored_fields: Tuple[str, ...] = () def url(self, detail: bool = False) -> str: - return reverse(f'bot:{self.route}-{"detail" if detail else "list"}', host='api') + return reverse(f'api:bot:{self.route}-{"detail" if detail else "list"}') -FK_FIELDS: Dict[Type[Model], Tuple[str]] = { - FilterList: ("default_settings",), - FilterSettings: ("default_action", "default_range"), - FilterAction: (), - ChannelRange: (), +FK_FIELDS: Dict[Type[Model], Tuple[str, ...]] = { + FilterList: (), Filter: ("filter_list",), - FilterOverride: ("filter_action", "filter_range") } def get_test_sequences() -> Dict[str, TestSequence]: + filter_list1_deny_dict = { + "name": "testname", + "list_type": 0, + "guild_pings": [], + "filter_dm": True, + "dm_pings": [], + "remove_context": False, + "bypass_roles": [], + "enabled": True, + "dm_content": "", + "dm_embed": "", + "infraction_type": "NONE", + "infraction_reason": "", + "infraction_duration": timedelta(seconds=0), + "infraction_channel": 0, + "disabled_channels": [], + "disabled_categories": [], + "enabled_channels": [], + "enabled_categories": [], + "send_alert": True + } + filter_list1_allow_dict = filter_list1_deny_dict.copy() + filter_list1_allow_dict["list_type"] = 1 + filter_list1_allow = FilterList(**filter_list1_allow_dict) + return { - "filter_list": TestSequence( + "filter_list1": TestSequence( FilterList, "filterlist", - { - "name": "testname", - "list_type": 0, - "default_settings": FilterSettings( - ping_type=[], - filter_dm=False, - dm_ping_type=[], - remove_context=False, - bypass_roles=[], - enabled=False, - default_action=FilterAction( - dm_content=None, - infraction_type=None, - infraction_reason="", - infraction_duration=None - ), - default_range=ChannelRange( - disallowed_channels=[], - disallowed_categories=[], - allowed_channels=[], - allowed_categories=[], - default=False - ) - ) - }, - ignored_fields=("filters",) + filter_list1_deny_dict, + ignored_fields=("filters", "created_at", "updated_at") ), - "filter_settings": TestSequence( - FilterSettings, - "filtersettings", + "filter_list2": TestSequence( + FilterList, + "filterlist", { - "ping_type": ["onduty"], - "filter_dm": True, - "dm_ping_type": ["123456"], + "name": "testname2", + "list_type": 1, + "guild_pings": ["Moderators"], + "filter_dm": False, + "dm_pings": ["here"], "remove_context": True, - "bypass_roles": [123456], - "enabled": True, - "default_action": FilterAction( - dm_content=None, - infraction_type=None, - infraction_reason="", - infraction_duration=None - ), - "default_range": ChannelRange( - disallowed_channels=[], - disallowed_categories=[], - allowed_channels=[], - allowed_categories=[], - default=False - ) - } - ), - "filter_action": TestSequence( - FilterAction, - "filteraction", - { - "dm_content": "This is a DM message.", - "infraction_type": "Mute", - "infraction_reason": "Too long beard", - "infraction_duration": "1 02:03:00" - } - ), - "channel_range": TestSequence( - ChannelRange, - "channelrange", - { - "disallowed_channels": [1234], - "disallowed_categories": [5678], - "allowed_channels": [9101], - "allowed_categories": [1121], - "default": True - } + "bypass_roles": ["123456"], + "enabled": False, + "dm_content": "testing testing", + "dm_embed": "one two three", + "infraction_type": "MUTE", + "infraction_reason": "stop testing", + "infraction_duration": timedelta(seconds=10.5), + "infraction_channel": 123, + "disabled_channels": ["python-general"], + "disabled_categories": ["CODE JAM"], + "enabled_channels": ["mighty-mice"], + "enabled_categories": ["Lobby"], + "send_alert": False + }, + ignored_fields=("filters", "created_at", "updated_at") ), "filter": TestSequence( Filter, @@ -121,58 +92,35 @@ def get_test_sequences() -> Dict[str, TestSequence]: { "content": "bad word", "description": "This is a really bad word.", - "additional_field": None, - "override": None, - "filter_list": FilterList( - name="testname", - list_type=0, - default_settings=FilterSettings( - ping_type=[], - filter_dm=False, - dm_ping_type=[], - remove_context=False, - bypass_roles=[], - enabled=False, - default_action=FilterAction( - dm_content=None, - infraction_type=None, - infraction_reason="", - infraction_duration=None - ), - default_range=ChannelRange( - disallowed_channels=[], - disallowed_categories=[], - allowed_channels=[], - allowed_categories=[], - default=False - ) - ) - ) - } + "additional_field": "{'hi': 'there'}", + "guild_pings": None, + "filter_dm": None, + "dm_pings": None, + "remove_context": None, + "bypass_roles": None, + "enabled": None, + "dm_content": None, + "dm_embed": None, + "infraction_type": None, + "infraction_reason": None, + "infraction_duration": None, + "infraction_channel": None, + "disabled_channels": None, + "disabled_categories": None, + "enabled_channels": None, + "enabled_categories": None, + "send_alert": None, + "filter_list": filter_list1_allow + }, + ignored_fields=("created_at", "updated_at") ), - "filter_override": TestSequence( - FilterOverride, - "filteroverride", - { - "ping_type": ["everyone"], - "filter_dm": False, - "dm_ping_type": ["here"], - "remove_context": False, - "bypass_roles": [9876], - "enabled": True, - "filter_action": None, - "filter_range": None - } - ) } def save_nested_objects(object_: Model, save_root: bool = True) -> None: - for field in FK_FIELDS[object_.__class__]: + for field in FK_FIELDS.get(object_.__class__, ()): value = getattr(object_, field) - - if value is not None: - save_nested_objects(value) + save_nested_objects(value) if save_root: object_.save() @@ -182,6 +130,8 @@ def clean_test_json(json: dict) -> dict: for key, value in json.items(): if isinstance(value, Model): json[key] = value.id + elif isinstance(value, timedelta): + json[key] = str(value.total_seconds()) return json @@ -194,7 +144,22 @@ def clean_api_json(json: dict, sequence: TestSequence) -> dict: return json -class GenericFilterTest(APISubdomainTestCase): +def flatten_settings(json: dict) -> dict: + settings = json.pop("settings", {}) + flattened_settings = {} + for entry, value in settings.items(): + if isinstance(value, dict): + flattened_settings.update(value) + else: + flattened_settings[entry] = value + + json.update(flattened_settings) + + return json + + +class GenericFilterTests(AuthenticatedAPITestCase): + def test_cannot_read_unauthenticated(self) -> None: for name, sequence in get_test_sequences().items(): with self.subTest(name=name): @@ -222,7 +187,7 @@ class GenericFilterTest(APISubdomainTestCase): response = self.client.get(sequence.url()) self.assertDictEqual( clean_test_json(sequence.object), - clean_api_json(response.json()[0], sequence) + clean_api_json(flatten_settings(response.json()[0]), sequence) ) def test_fetch_by_id(self) -> None: @@ -236,7 +201,7 @@ class GenericFilterTest(APISubdomainTestCase): response = self.client.get(f"{sequence.url()}/{saved.id}") self.assertDictEqual( clean_test_json(sequence.object), - clean_api_json(response.json(), sequence) + clean_api_json(flatten_settings(response.json()), sequence) ) def test_fetch_non_existing(self) -> None: @@ -259,14 +224,15 @@ class GenericFilterTest(APISubdomainTestCase): self.assertEqual(response.status_code, 201) self.assertDictEqual( - clean_api_json(response.json(), sequence), + clean_api_json(flatten_settings(response.json()), sequence), clean_test_json(sequence.object) ) def test_creation_missing_field(self) -> None: for name, sequence in get_test_sequences().items(): with self.subTest(name=name): - save_nested_objects(sequence.model(**sequence.object), False) + saved = sequence.model(**sequence.object) + save_nested_objects(saved) data = clean_test_json(sequence.object.copy()) for field in sequence.model._meta.get_fields(): @@ -296,3 +262,68 @@ class GenericFilterTest(APISubdomainTestCase): response = self.client.delete(f"{sequence.url()}/42") self.assertEqual(response.status_code, 404) + + +class FilterValidationTests(AuthenticatedAPITestCase): + + def test_filter_validation(self) -> None: + test_sequences = get_test_sequences() + base_filter = test_sequences["filter"] + base_filter_list = test_sequences["filter_list1"] + cases = ( + ({"infraction_reason": "hi"}, {}, 400), ({"infraction_duration": timedelta(seconds=10)}, {}, 400), + ({"infraction_reason": "hi"}, {"infraction_type": "NOTE"}, 200), + ({"infraction_duration": timedelta(seconds=10)}, {"infraction_type": "MUTE"}, 200), + ({"enabled_channels": ["admins"]}, {}, 200), ({"disabled_channels": ["123"]}, {}, 200), + ({"enabled_categories": ["CODE JAM"]}, {}, 200), ({"disabled_categories": ["CODE JAM"]}, {}, 200), + ({"enabled_channels": ["admins"], "disabled_channels": ["123", "admins"]}, {}, 400), + ({"enabled_categories": ["admins"], "disabled_categories": ["123", "admins"]}, {}, 400), + ({"enabled_channels": ["admins"]}, {"disabled_channels": ["123", "admins"]}, 400), + ({"enabled_categories": ["admins"]}, {"disabled_categories": ["123", "admins"]}, 400), + ) + + for filter_settings, filter_list_settings, response_code in cases: + with self.subTest(f_settings=filter_settings, fl_settings=filter_list_settings, response=response_code): + base_filter.model.objects.all().delete() + base_filter_list.model.objects.all().delete() + + case_filter_dict = base_filter.object.copy() + case_fl_dict = base_filter_list.object.copy() + case_fl_dict.update(filter_list_settings) + + case_fl = base_filter_list.model(**case_fl_dict) + case_filter_dict["filter_list"] = case_fl + case_filter = base_filter.model(**case_filter_dict) + save_nested_objects(case_filter) + + filter_settings["filter_list"] = case_fl + response = self.client.patch( + f"{base_filter.url()}/{case_filter.id}", data=clean_test_json(filter_settings) + ) + self.assertEqual(response.status_code, response_code) + + def test_filter_list_validation(self) -> None: + test_sequences = get_test_sequences() + base_filter_list = test_sequences["filter_list1"] + cases = ( + ({"infraction_reason": "hi"}, 400), ({"infraction_duration": timedelta(seconds=10)}, 400), + ({"infraction_reason": "hi", "infraction_type": "NOTE"}, 200), + ({"infraction_duration": timedelta(seconds=10), "infraction_type": "MUTE"}, 200), + ({"enabled_channels": ["admins"]}, 200), ({"disabled_channels": ["123"]}, 200), + ({"enabled_categories": ["CODE JAM"]}, 200), ({"disabled_categories": ["CODE JAM"]}, 200), + ({"enabled_channels": ["admins"], "disabled_channels": ["123", "admins"]}, 400), + ({"enabled_categories": ["admins"], "disabled_categories": ["123", "admins"]}, 400), + ) + + for filter_list_settings, response_code in cases: + with self.subTest(fl_settings=filter_list_settings, response=response_code): + base_filter_list.model.objects.all().delete() + + case_fl_dict = base_filter_list.object.copy() + case_fl = base_filter_list.model(**case_fl_dict) + save_nested_objects(case_fl) + + response = self.client.patch( + f"{base_filter_list.url()}/{case_fl.id}", data=clean_test_json(filter_list_settings) + ) + self.assertEqual(response.status_code, response_code) diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index b9b14a84..25d771cc 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -8,7 +8,6 @@ from pydis_site.apps.api.models import ( DocumentationLink, Filter, FilterList, - FilterSettings, Infraction, MessageDeletionContext, Nomination, @@ -110,13 +109,11 @@ class StringDunderMethodTests(SimpleTestCase): FilterList( name="forbidden_duckies", list_type=0, - default_settings=FilterSettings() ), Filter( content="ducky_nsfw", description="This ducky is totally inappropriate!", additional_field=None, - override=None ), OffensiveMessage( id=602951077675139072, -- cgit v1.2.3 From 5f538e9e876adc7f4a459fe230b46bdde61b3f64 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 27 Jan 2023 23:37:26 +0200 Subject: Make filter unique constraint use NULLS NOT DISTINCT The existing constraint was ineffective as null values were considered distinct, and so two filters with the same content and no overrides were considered different. This change uses a new PSQL 15 feature unsupported in django currently, and so it is added with raw SQL. --- .../migrations/0087_unique_constraint_filters.py | 39 ++++++++-------------- pydis_site/apps/api/serializers.py | 9 +++++ pydis_site/apps/api/tests/test_filters.py | 12 +++++++ 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/pydis_site/apps/api/migrations/0087_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0087_unique_constraint_filters.py index b2ff91f1..910e7b1b 100644 --- a/pydis_site/apps/api/migrations/0087_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0087_unique_constraint_filters.py @@ -1,5 +1,3 @@ -# Generated by Django 3.1.14 on 2022-03-22 16:31 - from django.db import migrations, models @@ -10,29 +8,18 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddConstraint( - model_name='filter', - constraint=models.UniqueConstraint(fields=( - 'content', - 'additional_field', - 'filter_list', - 'dm_content', - 'dm_embed', - 'infraction_type', - 'infraction_reason', - 'infraction_duration', - 'infraction_channel', - 'guild_pings', - 'filter_dm', - 'dm_pings', - 'remove_context', - 'bypass_roles', - 'enabled', - 'send_alert', - 'enabled_channels', - 'disabled_channels', - 'enabled_categories', - 'disabled_categories' - ), name='unique_filters'), + migrations.RunSQL( + "ALTER TABLE api_filter " + "ADD CONSTRAINT unique_filters UNIQUE NULLS NOT DISTINCT " + "(content, additional_field, filter_list_id, dm_content, dm_embed, infraction_type, infraction_reason, infraction_duration, infraction_channel, guild_pings, filter_dm, dm_pings, remove_context, bypass_roles, enabled, send_alert, enabled_channels, disabled_channels, enabled_categories, disabled_categories)", + state_operations=[ + migrations.AddConstraint( + model_name='filter', + constraint=models.UniqueConstraint( + fields=('content', 'additional_field', 'filter_list', 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', 'infraction_channel', 'guild_pings', 'filter_dm', 'dm_pings', 'remove_context', 'bypass_roles', 'enabled', 'send_alert', 'enabled_channels', 'disabled_channels', 'enabled_categories', 'disabled_categories'), + name='unique_filters' + ), + ), + ], ), ] diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 7f9461ec..8da47802 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -257,6 +257,15 @@ class FilterSerializer(ModelSerializer): ) + SETTINGS_FIELDS extra_kwargs = _create_filter_meta_extra_kwargs() + def create(self, validated_data: dict) -> User: + """Override the create method to catch violations of the custom uniqueness constraint.""" + try: + return super().create(validated_data) + except IntegrityError: + raise ValidationError( + "Check if a filter with this combination of content and settings already exists in this filter list." + ) + def to_representation(self, instance: Filter) -> dict: """ Provides a custom JSON representation to the Filter Serializers. diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index cae78cd6..73c8e0d9 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -327,3 +327,15 @@ class FilterValidationTests(AuthenticatedAPITestCase): f"{base_filter_list.url()}/{case_fl.id}", data=clean_test_json(filter_list_settings) ) self.assertEqual(response.status_code, response_code) + + def test_filter_unique_constraint(self) -> None: + test_filter = get_test_sequences()["filter"] + test_filter.model.objects.all().delete() + test_filter_object = test_filter.model(**test_filter.object) + save_nested_objects(test_filter_object, False) + + response = self.client.post(test_filter.url(), data=clean_test_json(test_filter.object)) + self.assertEqual(response.status_code, 201) + + response = self.client.post(test_filter.url(), data=clean_test_json(test_filter.object)) + self.assertEqual(response.status_code, 400) -- cgit v1.2.3 From d52a8c955aceccd719dd1511700aac9f2a564b0a Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 28 Jan 2023 16:24:58 +0200 Subject: Update viewsets, fix linting --- .../apps/api/migrations/0088_unique_filter_list.py | 2 +- pydis_site/apps/api/models/bot/filters.py | 12 +- pydis_site/apps/api/serializers.py | 50 +- pydis_site/apps/api/tests/test_filters.py | 24 +- pydis_site/apps/api/viewsets/bot/filters.py | 532 ++++++++++++++------- 5 files changed, 428 insertions(+), 192 deletions(-) diff --git a/pydis_site/apps/api/migrations/0088_unique_filter_list.py b/pydis_site/apps/api/migrations/0088_unique_filter_list.py index 3f3a34bb..98d14e2b 100644 --- a/pydis_site/apps/api/migrations/0088_unique_filter_list.py +++ b/pydis_site/apps/api/migrations/0088_unique_filter_list.py @@ -99,4 +99,4 @@ class Migration(migrations.Migration): code=create_unique_list, reverse_code=None ), - ] \ No newline at end of file + ] diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 4d8a4025..1eab79ba 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -4,8 +4,8 @@ from django.db import models from django.db.models import UniqueConstraint # Must be imported that way to avoid circular imports +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin from .infraction import Infraction -from pydis_site.apps.api.models.mixins import ModelTimestampMixin, ModelReprMixin class FilterListType(models.IntegerChoices): @@ -76,7 +76,10 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): null=False ) remove_context = models.BooleanField( - help_text="Whether this filter should remove the context (such as a message) triggering it.", + help_text=( + "Whether this filter should remove the context (such as a message) " + "triggering it." + ), null=False ) bypass_roles = ArrayField( @@ -186,7 +189,10 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): null=True ) remove_context = models.BooleanField( - help_text="Whether this filter should remove the context (such as a message) triggering it.", + help_text=( + "Whether this filter should remove the context (such as a message) " + "triggering it." + ), null=True ) bypass_roles = ArrayField( diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 8da47802..a6328eff 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -21,9 +21,9 @@ from .models import ( BumpedThread, DeletedMessage, DocumentationLink, - Infraction, - FilterList, Filter, + FilterList, + Infraction, MessageDeletionContext, Nomination, NominationEntry, @@ -183,7 +183,9 @@ ALLOW_EMPTY_SETTINGS = ( ) # Required fields for custom JSON representation purposes -BASE_FILTER_FIELDS = ('id', 'created_at', 'updated_at', 'content', 'description', 'additional_field') +BASE_FILTER_FIELDS = ( + 'id', 'created_at', 'updated_at', 'content', 'description', 'additional_field' +) BASE_FILTERLIST_FIELDS = ('id', 'created_at', 'updated_at', 'name', 'list_type') BASE_SETTINGS_FIELDS = ( "bypass_roles", @@ -235,16 +237,31 @@ class FilterSerializer(ModelSerializer): def validate(self, data: dict) -> dict: """Perform infraction data + allowed and disallowed lists validation.""" if ( - (get_field_value(data, "infraction_reason") or get_field_value(data, "infraction_duration")) + ( + get_field_value(data, "infraction_reason") + or get_field_value(data, "infraction_duration") + ) and get_field_value(data, "infraction_type") == "NONE" ): - raise ValidationError("Infraction type is required with infraction duration or reason.") + raise ValidationError( + "Infraction type is required with infraction duration or reason." + ) - if set(get_field_value(data, "disabled_channels")) & set(get_field_value(data, "enabled_channels")): - raise ValidationError("You can't have the same value in both enabled and disabled channels lists.") + if ( + set(get_field_value(data, "disabled_channels")) + & set(get_field_value(data, "enabled_channels")) + ): + raise ValidationError( + "You can't have the same value in both enabled and disabled channels lists." + ) - if set(get_field_value(data, "disabled_categories")) & set(get_field_value(data, "enabled_categories")): - raise ValidationError("You can't have the same value in both enabled and disabled categories lists.") + if ( + set(get_field_value(data, "disabled_categories")) + & set(get_field_value(data, "enabled_categories")) + ): + raise ValidationError( + "You can't have the same value in both enabled and disabled categories lists." + ) return data @@ -253,7 +270,13 @@ class FilterSerializer(ModelSerializer): model = Filter fields = ( - 'id', 'created_at', 'updated_at', 'content', 'description', 'additional_field', 'filter_list' + 'id', + 'created_at', + 'updated_at', + 'content', + 'description', + 'additional_field', + 'filter_list' ) + SETTINGS_FIELDS extra_kwargs = _create_filter_meta_extra_kwargs() @@ -263,7 +286,8 @@ class FilterSerializer(ModelSerializer): return super().create(validated_data) except IntegrityError: raise ValidationError( - "Check if a filter with this combination of content and settings already exists in this filter list." + "Check if a filter with this combination of content " + "and settings already exists in this filter list." ) def to_representation(self, instance: Filter) -> dict: @@ -340,7 +364,9 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ('id', 'created_at', 'updated_at', 'name', 'list_type', 'filters') + SETTINGS_FIELDS + fields = ( + 'id', 'created_at', 'updated_at', 'name', 'list_type', 'filters' + ) + SETTINGS_FIELDS extra_kwargs = _create_filter_list_meta_extra_kwargs() # Ensure there can only be one filter list with the same name and type. diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index 73c8e0d9..62de23c4 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Tuple, Type from django.db.models import Model from django.urls import reverse -from pydis_site.apps.api.models.bot.filters import FilterList, Filter +from pydis_site.apps.api.models.bot.filters import Filter, FilterList from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase @@ -271,11 +271,14 @@ class FilterValidationTests(AuthenticatedAPITestCase): base_filter = test_sequences["filter"] base_filter_list = test_sequences["filter_list1"] cases = ( - ({"infraction_reason": "hi"}, {}, 400), ({"infraction_duration": timedelta(seconds=10)}, {}, 400), + ({"infraction_reason": "hi"}, {}, 400), + ({"infraction_duration": timedelta(seconds=10)}, {}, 400), ({"infraction_reason": "hi"}, {"infraction_type": "NOTE"}, 200), ({"infraction_duration": timedelta(seconds=10)}, {"infraction_type": "MUTE"}, 200), - ({"enabled_channels": ["admins"]}, {}, 200), ({"disabled_channels": ["123"]}, {}, 200), - ({"enabled_categories": ["CODE JAM"]}, {}, 200), ({"disabled_categories": ["CODE JAM"]}, {}, 200), + ({"enabled_channels": ["admins"]}, {}, 200), + ({"disabled_channels": ["123"]}, {}, 200), + ({"enabled_categories": ["CODE JAM"]}, {}, 200), + ({"disabled_categories": ["CODE JAM"]}, {}, 200), ({"enabled_channels": ["admins"], "disabled_channels": ["123", "admins"]}, {}, 400), ({"enabled_categories": ["admins"], "disabled_categories": ["123", "admins"]}, {}, 400), ({"enabled_channels": ["admins"]}, {"disabled_channels": ["123", "admins"]}, 400), @@ -283,7 +286,9 @@ class FilterValidationTests(AuthenticatedAPITestCase): ) for filter_settings, filter_list_settings, response_code in cases: - with self.subTest(f_settings=filter_settings, fl_settings=filter_list_settings, response=response_code): + with self.subTest( + f_settings=filter_settings, fl_settings=filter_list_settings, response=response_code + ): base_filter.model.objects.all().delete() base_filter_list.model.objects.all().delete() @@ -306,11 +311,13 @@ class FilterValidationTests(AuthenticatedAPITestCase): test_sequences = get_test_sequences() base_filter_list = test_sequences["filter_list1"] cases = ( - ({"infraction_reason": "hi"}, 400), ({"infraction_duration": timedelta(seconds=10)}, 400), + ({"infraction_reason": "hi"}, 400), + ({"infraction_duration": timedelta(seconds=10)}, 400), ({"infraction_reason": "hi", "infraction_type": "NOTE"}, 200), ({"infraction_duration": timedelta(seconds=10), "infraction_type": "MUTE"}, 200), ({"enabled_channels": ["admins"]}, 200), ({"disabled_channels": ["123"]}, 200), - ({"enabled_categories": ["CODE JAM"]}, 200), ({"disabled_categories": ["CODE JAM"]}, 200), + ({"enabled_categories": ["CODE JAM"]}, 200), + ({"disabled_categories": ["CODE JAM"]}, 200), ({"enabled_channels": ["admins"], "disabled_channels": ["123", "admins"]}, 400), ({"enabled_categories": ["admins"], "disabled_categories": ["123", "admins"]}, 400), ) @@ -324,7 +331,8 @@ class FilterValidationTests(AuthenticatedAPITestCase): save_nested_objects(case_fl) response = self.client.patch( - f"{base_filter_list.url()}/{case_fl.id}", data=clean_test_json(filter_list_settings) + f"{base_filter_list.url()}/{case_fl.id}", + data=clean_test_json(filter_list_settings) ) self.assertEqual(response.status_code, response_code) diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 1eb05053..8e677612 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -22,69 +22,79 @@ class FilterListViewSet(ModelViewSet): >>> [ ... { ... "id": 1, - ... "name": "invites", + ... "created_at": "2023-01-27T21:26:34.027293Z", + ... "updated_at": "2023-01-27T21:26:34.027308Z", + ... "name": "invite", ... "list_type": 1, ... "filters": [ ... { ... "id": 1, + ... "created_at": "2023-01-27T21:26:34.029539Z", + ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", ... "additional_field": None, - ... "filter_list": 1 + ... "filter_list": 1, ... "settings": { - ... "bypass_roles": None - ... "filter_dm": None, - ... "enabled": None - ... "send_alert": True, - ... "remove_context": None - ... "infraction_and_notification": { - ... "infraction_type": None, - ... "infraction_reason": "", - ... "infraction_duration": None - ... "dm_content": None, - ... "dm_embed": None - ... }, - ... "channel_scope": { - ... "disabled_channels": None, - ... "disabled_categories": None, - ... "enabled_channels": None - ... } - ... "mentions": { - ... "ping_type": None - ... "dm_ping_type": None - ... } - ... } - ... + ... "bypass_roles": None, + ... "filter_dm": None, + ... "enabled": None, + ... "remove_context": None, + ... "send_alert": None, + ... "infraction_and_notification": { + ... "infraction_type": None, + ... "infraction_reason": None, + ... "infraction_duration": None, + ... "infraction_channel": None, + ... "dm_content": None, + ... "dm_embed": None + ... }, + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, + ... "enabled_categories": None + ... }, + ... "mentions": { + ... "guild_pings": None, + ... "dm_pings": None + ... } + ... } ... }, ... ... ... ], - ... "settings": { - ... "bypass_roles": [ - ... "staff" - ... ], - ... "filter_dm": True, - ... "enabled": True - ... "remove_context": True, - ... "send_alert": True - ... "infraction_and_notification": { - ... "infraction_type": "", - ... "infraction_reason": "", - ... "infraction_duration": "0.0", - ... "dm_content": "", - ... "dm_embed": "" - ... } - ... "channel_scope": { + ... "settings": { + ... "bypass_roles": [ + ... "Helpers" + ... ], + ... "filter_dm": True, + ... "enabled": True, + ... "remove_context": True, + ... "send_alert": True, + ... "infraction_and_notification": { + ... "infraction_type": "NONE", + ... "infraction_reason": "", + ... "infraction_duration": "0.0", + ... "infraction_channel": 0, + ... "dm_content": "Per Rule 6, your invite link has been removed...", + ... "dm_embed": "" + ... }, + ... "channel_scope": { ... "disabled_channels": [], - ... "disabled_categories": [], - ... "enabled_channels": [] - ... } - ... "mentions": { - ... "ping_type": [ - ... "onduty" - ... ] - ... "dm_ping_type": [] - ... } - ... }, + ... "disabled_categories": [ + ... "CODE JAM" + ... ], + ... "enabled_channels": [], + ... "enabled_categories": [] + ... }, + ... "mentions": { + ... "guild_pings": [ + ... "Moderators" + ... ], + ... "dm_pings": [] + ... } + ... } + ... }, ... ... ... ] @@ -97,75 +107,205 @@ class FilterListViewSet(ModelViewSet): #### Response format >>> { - ... "id": 1, - ... "name": "invites", - ... "list_type": 1, - ... "filters": [ - ... { - ... "id": 1, - ... "filter_list": 1 - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "settings": { - ... "bypass_roles": None - ... "filter_dm": None, - ... "enabled": None - ... "remove_context": None, - ... "send_alert": None - ... "infraction_and_notification": { - ... "infraction_type": None, - ... "infraction_reason": "", - ... "infraction_duration": None - ... "dm_content": None, - ... "dm_embed": None - ... }, - ... "channel_scope": { - ... "disabled_channels": None, - ... "disabled_categories": None, - ... "enabled_channels": None - ... } - ... "mentions": { - ... "ping_type": None - ... "dm_ping_type": None - ... } - ... } - ... - ... }, - ... ... + ... "id": 1, + ... "created_at": "2023-01-27T21:26:34.027293Z", + ... "updated_at": "2023-01-27T21:26:34.027308Z", + ... "name": "invite", + ... "list_type": 1, + ... "filters": [ + ... { + ... "id": 1, + ... "created_at": "2023-01-27T21:26:34.029539Z", + ... "updated_at": "2023-01-27T21:26:34.030532Z", + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "filter_list": 1, + ... "settings": { + ... "bypass_roles": None, + ... "filter_dm": None, + ... "enabled": None, + ... "remove_context": None, + ... "send_alert": None, + ... "infraction_and_notification": { + ... "infraction_type": None, + ... "infraction_reason": None, + ... "infraction_duration": None, + ... "infraction_channel": None, + ... "dm_content": None, + ... "dm_embed": None + ... }, + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, + ... "enabled_categories": None + ... }, + ... "mentions": { + ... "guild_pings": None, + ... "dm_pings": None + ... } + ... } + ... }, + ... ... + ... ], + ... "settings": { + ... "bypass_roles": [ + ... "Helpers" ... ], - ... "settings": { - ... "bypass_roles": [ - ... "staff" - ... ], - ... "filter_dm": True, - ... "enabled": True - ... "remove_context": True - ... "send_alert": True - ... "infraction_and_notification": { - ... "infraction_type": "", - ... "infraction_reason": "", - ... "infraction_duration": "0.0", - ... "dm_content": "", - ... "dm_embed": "" - ... } - ... "channel_scope": { - ... "disabled_channels": [], - ... "disabled_categories": [], - ... "enabled_channels": [] - ... } - ... "mentions": { - ... "ping_type": [ - ... "onduty" - ... ] - ... "dm_ping_type": [] - ... } + ... "filter_dm": True, + ... "enabled": True, + ... "remove_context": True, + ... "send_alert": True, + ... "infraction_and_notification": { + ... "infraction_type": "NONE", + ... "infraction_reason": "", + ... "infraction_duration": "0.0", + ... "infraction_channel": 0, + ... "dm_content": "Per Rule 6, your invite link has been removed...", + ... "dm_embed": "" + ... }, + ... "channel_scope": { + ... "disabled_channels": [], + ... "disabled_categories": [ + ... "CODE JAM" + ... ], + ... "enabled_channels": [], + ... "enabled_categories": [] + ... }, + ... "mentions": { + ... "guild_pings": [ + ... "Moderators" + ... ], + ... "dm_pings": [] + ... } + ... } ... } #### Status codes - 200: returned on success - 404: returned if the id was not found. + ### POST /bot/filter/filter_lists + Adds a single FilterList item to the database. + + #### Request body + >>> { + ... "name": "invite", + ... "list_type": 1, + ... "bypass_roles": [ + ... "Helpers" + ... ], + ... "filter_dm": True, + ... "enabled": True, + ... "remove_context": True, + ... "send_alert": True, + ... "infraction_type": "NONE", + ... "infraction_reason": "", + ... "infraction_duration": "0.0", + ... "infraction_channel": 0, + ... "dm_content": "Per Rule 6, your invite link has been removed...", + ... "dm_embed": "", + ... "disabled_channels": [], + ... "disabled_categories": [ + ... "CODE JAM" + ... ], + ... "enabled_channels": [], + ... "enabled_categories": [] + ... "guild_pings": [ + ... "Moderators" + ... ], + ... "dm_pings": [] + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### PATCH /bot/filter/filter_lists/ + Updates a specific FilterList item from the database. + + #### Response format + >>> { + ... "id": 1, + ... "created_at": "2023-01-27T21:26:34.027293Z", + ... "updated_at": "2023-01-27T21:26:34.027308Z", + ... "name": "invite", + ... "list_type": 1, + ... "filters": [ + ... { + ... "id": 1, + ... "created_at": "2023-01-27T21:26:34.029539Z", + ... "updated_at": "2023-01-27T21:26:34.030532Z", + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "filter_list": 1, + ... "settings": { + ... "bypass_roles": None, + ... "filter_dm": None, + ... "enabled": None, + ... "remove_context": None, + ... "send_alert": None, + ... "infraction_and_notification": { + ... "infraction_type": None, + ... "infraction_reason": None, + ... "infraction_duration": None, + ... "infraction_channel": None, + ... "dm_content": None, + ... "dm_embed": None + ... }, + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, + ... "enabled_categories": None + ... }, + ... "mentions": { + ... "guild_pings": None, + ... "dm_pings": None + ... } + ... } + ... }, + ... ... + ... ], + ... "settings": { + ... "bypass_roles": [ + ... "Helpers" + ... ], + ... "filter_dm": True, + ... "enabled": True, + ... "remove_context": True, + ... "send_alert": True, + ... "infraction_and_notification": { + ... "infraction_type": "NONE", + ... "infraction_reason": "", + ... "infraction_duration": "0.0", + ... "infraction_channel": 0, + ... "dm_content": "Per Rule 6, your invite link has been removed...", + ... "dm_embed": "" + ... }, + ... "channel_scope": { + ... "disabled_channels": [], + ... "disabled_categories": [ + ... "CODE JAM" + ... ], + ... "enabled_channels": [], + ... "enabled_categories": [] + ... }, + ... "mentions": { + ... "guild_pings": [ + ... "Moderators" + ... ], + ... "dm_pings": [] + ... } + ... } + ... } + + #### Status codes + - 200: returned on success + - 400: if one of the given fields is invalid + ### DELETE /bot/filter/filter_lists/ Deletes the FilterList item with the given `id`. @@ -188,33 +328,39 @@ class FilterViewSet(ModelViewSet): #### Response format >>> [ - ... { - ... "id": 1, - ... "filter_list": 1 - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "settings": { - ... "bypass_roles": None - ... "filter_dm": None, - ... "enabled": None - ... "remove_context": True, - ... "send_alert": True - ... "infraction": { - ... "infraction_type": None, - ... "infraction_reason": None, - ... "infraction_duration": None - ... }, - ... "channel_scope": { - ... "disabled_channels": None, - ... "disabled_categories": None, - ... "enabled_channels": None - ... } - ... "mentions": { - ... "ping_type": None, - ... "dm_ping_type": None - ... } - ... } + ... { + ... "id": 1, + ... "created_at": "2023-01-27T21:26:34.029539Z", + ... "updated_at": "2023-01-27T21:26:34.030532Z", + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "filter_list": 1, + ... "settings": { + ... "bypass_roles": None, + ... "filter_dm": None, + ... "enabled": None, + ... "remove_context": None, + ... "send_alert": None, + ... "infraction_and_notification": { + ... "infraction_type": None, + ... "infraction_reason": None, + ... "infraction_duration": None, + ... "infraction_channel": None, + ... "dm_content": None, + ... "dm_embed": None + ... }, + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, + ... "enabled_categories": None + ... }, + ... "mentions": { + ... "guild_pings": None, + ... "dm_pings": None + ... } + ... } ... }, ... ... ... ] @@ -228,32 +374,38 @@ class FilterViewSet(ModelViewSet): #### Response format >>> { - ... "id": 1, - ... "filter_list": 1 - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "settings": { - ... "bypass_roles": None - ... "filter_dm": None, - ... "enabled": None - ... "remove_context": True, - ... "send_alert": True - ... "infraction": { - ... "infraction_type": None, - ... "infraction_reason": None, - ... "infraction_duration": None - ... }, - ... "channel_scope": { - ... "disabled_channels": None, - ... "disabled_categories": None, - ... "enabled_channels": None, - ... } - ... "mentions": { - ... "ping_type": None - ... "dm_ping_type": None - ... } - ... } + ... "id": 1, + ... "created_at": "2023-01-27T21:26:34.029539Z", + ... "updated_at": "2023-01-27T21:26:34.030532Z", + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "filter_list": 1, + ... "settings": { + ... "bypass_roles": None, + ... "filter_dm": None, + ... "enabled": None, + ... "remove_context": None, + ... "send_alert": None, + ... "infraction_and_notification": { + ... "infraction_type": None, + ... "infraction_reason": None, + ... "infraction_duration": None, + ... "infraction_channel": None, + ... "dm_content": None, + ... "dm_embed": None + ... }, + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, + ... "enabled_categories": None + ... }, + ... "mentions": { + ... "guild_pings": None, + ... "dm_pings": None + ... } + ... } ... } #### Status codes @@ -265,10 +417,27 @@ class FilterViewSet(ModelViewSet): #### Request body >>> { + ... "filter_list": 1, ... "content": "267624335836053506", ... "description": "Python Discord", ... "additional_field": None, - ... "override": 1 + ... "bypass_roles": None, + ... "filter_dm": None, + ... "enabled": False, + ... "remove_context": None, + ... "send_alert": None, + ... "infraction_type": None, + ... "infraction_reason": None, + ... "infraction_duration": None, + ... "infraction_channel": None, + ... "dm_content": None, + ... "dm_embed": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, + ... "enabled_categories": None + ... "guild_pings": None, + ... "dm_pings": None ... } #### Status codes @@ -281,10 +450,37 @@ class FilterViewSet(ModelViewSet): #### Response format >>> { ... "id": 1, + ... "created_at": "2023-01-27T21:26:34.029539Z", + ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", ... "additional_field": None, - ... "override": 1 + ... "filter_list": 1, + ... "settings": { + ... "bypass_roles": None, + ... "filter_dm": None, + ... "enabled": None, + ... "remove_context": None, + ... "send_alert": None, + ... "infraction_and_notification": { + ... "infraction_type": None, + ... "infraction_reason": None, + ... "infraction_duration": None, + ... "infraction_channel": None, + ... "dm_content": None, + ... "dm_embed": None + ... }, + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, + ... "enabled_categories": None + ... }, + ... "mentions": { + ... "guild_pings": None, + ... "dm_pings": None + ... } + ... } ... } #### Status codes -- cgit v1.2.3 From 4eecda92e16ffe97fabb5d2e07790357140f7bbb Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 11 Feb 2023 13:32:46 +0200 Subject: Fix documentation and import Co-authored-by: wookie184 --- pydis_site/apps/api/migrations/0086_new_filter_schema.py | 8 ++++---- pydis_site/apps/api/models/bot/filters.py | 5 ++--- pydis_site/apps/api/viewsets/bot/filters.py | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pydis_site/apps/api/migrations/0086_new_filter_schema.py b/pydis_site/apps/api/migrations/0086_new_filter_schema.py index 9067a380..5da3a3b1 100644 --- a/pydis_site/apps/api/migrations/0086_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0086_new_filter_schema.py @@ -1,4 +1,4 @@ -# Modified migration file to migrate existing filters to the new one +"""Modified migration file to migrate existing filters to the new system.""" from datetime import timedelta import django.contrib.postgres.fields @@ -8,7 +8,7 @@ from django.db import migrations, models import django.db.models.deletion from django.db.backends.base.schema import BaseDatabaseSchemaEditor -import pydis_site.apps.api.models.bot.filters +import pydis_site.apps.api.models OLD_LIST_NAMES = (('GUILD_INVITE', True), ('GUILD_INVITE', False), ('FILE_FORMAT', True), ('DOMAIN_NAME', False), ('FILTER_TOKEN', False), ('REDIRECT', False)) change_map = { @@ -116,7 +116,7 @@ class Migration(migrations.Migration): ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True, blank=True)), ('infraction_type', models.CharField(choices=[('NONE', 'None'), ('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True, blank=True)), - ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('infraction_duration', models.DurationField(help_text='The duration of the infraction. 0 for permanent.', null=True)), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", null=True, size=None)), @@ -143,7 +143,7 @@ class Migration(migrations.Migration): ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, blank=True)), ('infraction_type', models.CharField(choices=[('NONE', 'None'), ('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, blank=True)), - ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.')), + ('infraction_duration', models.DurationField(help_text='The duration of the infraction. 0 for permanent.')), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.")), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 1eab79ba..584ee726 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -52,7 +52,7 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): ) infraction_duration = models.DurationField( null=False, - help_text="The duration of the infraction. Null if permanent." + help_text="The duration of the infraction. 0 for permanent." ) infraction_channel = models.BigIntegerField( validators=( @@ -165,7 +165,7 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): ) infraction_duration = models.DurationField( null=True, - help_text="The duration of the infraction. Null if permanent." + help_text="The duration of the infraction. 0 for permanent." ) infraction_channel = models.BigIntegerField( validators=( @@ -209,7 +209,6 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): null=True ) - # Check FilterList model for information about these properties. enabled_channels = ArrayField( models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 8e677612..c84da909 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -311,7 +311,7 @@ class FilterListViewSet(ModelViewSet): #### Status codes - 204: returned on success - - 404: if a tag with the given `id` does not exist + - 404: if a FilterList with the given `id` does not exist """ serializer_class = FilterListSerializer @@ -492,7 +492,7 @@ class FilterViewSet(ModelViewSet): #### Status codes - 204: returned on success - - 404: if a tag with the given `id` does not exist + - 404: if a Filter with the given `id` does not exist """ serializer_class = FilterSerializer -- cgit v1.2.3 From be854fa3d34dac7b4b9e96b3736dd61d972f1b79 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 11 Feb 2023 13:45:31 +0200 Subject: Fix filter serializers for false-y values Co-authored-by: GDWR --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a6328eff..f4d64ad0 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -226,7 +226,7 @@ def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: def get_field_value(data: dict, field_name: str) -> Any: """Get the value directly from the key, or from the filter list if it's missing or is None.""" - if data.get(field_name): + if data.get(field_name) is not None: return data[field_name] return getattr(data["filter_list"], field_name) -- cgit v1.2.3 From 138506045acd1cf464dc63d4465d36208801a3ab Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Mar 2023 18:15:56 +0300 Subject: Make the unique constraint reversible Co-authored-by: Amrou --- pydis_site/apps/api/migrations/0089_unique_constraint_filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py index d6f32342..0bcfd8a3 100644 --- a/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py @@ -12,6 +12,7 @@ class Migration(migrations.Migration): "ALTER TABLE api_filter " "ADD CONSTRAINT unique_filters UNIQUE NULLS NOT DISTINCT " "(content, additional_field, filter_list_id, dm_content, dm_embed, infraction_type, infraction_reason, infraction_duration, infraction_channel, guild_pings, filter_dm, dm_pings, remove_context, bypass_roles, enabled, send_alert, enabled_channels, disabled_channels, enabled_categories, disabled_categories)", + reverse_sql="ALTER TABLE api_filter DROP CONSTRAINT unique_filters", state_operations=[ migrations.AddConstraint( model_name='filter', -- cgit v1.2.3 From d6565845ec07b9d76949da36505b3f53e402d230 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Mar 2023 18:29:31 +0300 Subject: Documentation improvements and fixes Co-authored-by: Amrou --- pydis_site/apps/api/migrations/0088_new_filter_schema.py | 4 ++-- pydis_site/apps/api/migrations/0091_antispam_filter_list.py | 2 +- pydis_site/apps/api/models/bot/filters.py | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pydis_site/apps/api/migrations/0088_new_filter_schema.py b/pydis_site/apps/api/migrations/0088_new_filter_schema.py index 46756781..1506e4d7 100644 --- a/pydis_site/apps/api/migrations/0088_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0088_new_filter_schema.py @@ -118,7 +118,7 @@ class Migration(migrations.Migration): ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True, blank=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. 0 for permanent.', null=True)), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), - ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), + ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter even if it's enabled in the category.", null=True, size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", null=True, size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", null=True, size=None)), ('enabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="The only categories in which to run the filter.", null=True, size=None)), @@ -145,7 +145,7 @@ class Migration(migrations.Migration): ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, blank=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. 0 for permanent.')), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.")), - ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), + ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter even if it's enabled in the category.", size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", size=None)), ('enabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="The only categories in which to run the filter.", size=None)), diff --git a/pydis_site/apps/api/migrations/0091_antispam_filter_list.py b/pydis_site/apps/api/migrations/0091_antispam_filter_list.py index 58ffa4a4..7c233142 100644 --- a/pydis_site/apps/api/migrations/0091_antispam_filter_list.py +++ b/pydis_site/apps/api/migrations/0091_antispam_filter_list.py @@ -7,7 +7,7 @@ import pydis_site.apps.api.models.bot.filters def create_antispam_list(apps: Apps, _): - """Create the 'unique' FilterList and its related Filters.""" + """Create the 'antispam' FilterList and its related Filters.""" filter_list: pydis_site.apps.api.models.FilterList = apps.get_model("api", "FilterList") filter_: pydis_site.apps.api.models.Filter = apps.get_model("api", "Filter") diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 584ee726..60ae394b 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -101,7 +101,7 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): ) disabled_channels = ArrayField( models.CharField(max_length=100), - help_text="Channels in which to not run the filter." + help_text="Channels in which to not run the filter even if it's enabled in the category." ) enabled_categories = ArrayField( models.CharField(max_length=100), @@ -216,7 +216,8 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): ) disabled_channels = ArrayField( models.CharField(max_length=100), - help_text="Channels in which to not run the filter.", null=True + help_text="Channels in which to not run the filter even if it's enabled in the category.", + null=True ) enabled_categories = ArrayField( models.CharField(max_length=100), -- cgit v1.2.3 From 43913623f87329b51cd2a6793e843c30368698aa Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Mar 2023 18:51:24 +0300 Subject: Merge the extra kwargs creation functions Co-authored-by: Amrou --- pydis_site/apps/api/serializers.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index fe3c1dd2..e8c5869f 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -211,11 +211,11 @@ CHANNEL_SCOPE_FIELDS = ( MENTIONS_FIELDS = ("guild_pings", "dm_pings") -def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: - """Create the extra kwargs of the Filter serializer's Meta class.""" +def _create_meta_extra_kwargs(*, for_filter: bool) -> dict[str, dict[str, bool]]: + """Create the extra kwargs for the Meta classes of the Filter and FilterList serializers.""" extra_kwargs = {} for field in SETTINGS_FIELDS: - field_args = {'required': False, 'allow_null': True} + field_args = {'required': False, 'allow_null': True} if for_filter else {} if field in ALLOW_BLANK_SETTINGS: field_args['allow_blank'] = True if field in ALLOW_EMPTY_SETTINGS: @@ -278,7 +278,7 @@ class FilterSerializer(ModelSerializer): 'additional_field', 'filter_list' ) + SETTINGS_FIELDS - extra_kwargs = _create_filter_meta_extra_kwargs() + extra_kwargs = _create_meta_extra_kwargs(for_filter=True) def create(self, validated_data: dict) -> User: """Override the create method to catch violations of the custom uniqueness constraint.""" @@ -317,19 +317,6 @@ class FilterSerializer(ModelSerializer): return schema -def _create_filter_list_meta_extra_kwargs() -> dict[str, dict[str, bool]]: - """Create the extra kwargs of the FilterList serializer's Meta class.""" - extra_kwargs = {} - for field in SETTINGS_FIELDS: - field_args = {} - if field in ALLOW_BLANK_SETTINGS: - field_args['allow_blank'] = True - if field in ALLOW_EMPTY_SETTINGS: - field_args['allow_empty'] = True - extra_kwargs[field] = field_args - return extra_kwargs - - class FilterListSerializer(ModelSerializer): """A class providing (de-)serialization of `FilterList` instances.""" @@ -367,7 +354,7 @@ class FilterListSerializer(ModelSerializer): fields = ( 'id', 'created_at', 'updated_at', 'name', 'list_type', 'filters' ) + SETTINGS_FIELDS - extra_kwargs = _create_filter_list_meta_extra_kwargs() + extra_kwargs = _create_meta_extra_kwargs(for_filter=False) # Ensure there can only be one filter list with the same name and type. validators = [ -- cgit v1.2.3 From 36bca58ff336f9d4b797a2c76f08775f9de7e9a7 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Mar 2023 21:26:07 +0300 Subject: Specify the common elements in the validation errors Co-authored-by: Amrou --- pydis_site/apps/api/serializers.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index e8c5869f..bfad18ab 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -247,20 +247,24 @@ class FilterSerializer(ModelSerializer): "Infraction type is required with infraction duration or reason." ) - if ( + common_channels = ( set(get_field_value(data, "disabled_channels")) & set(get_field_value(data, "enabled_channels")) - ): + ) + if common_channels: raise ValidationError( - "You can't have the same value in both enabled and disabled channels lists." + "You can't have the same value in both enabled and disabled channels lists:" + f" {', '.join(repr(channel) for channel in common_channels)}." ) - if ( + common_categories = ( set(get_field_value(data, "disabled_categories")) & set(get_field_value(data, "enabled_categories")) - ): + ) + if common_categories: raise ValidationError( - "You can't have the same value in both enabled and disabled categories lists." + "You can't have the same value in both enabled and disabled categories lists:" + f" {', '.join(repr(category) for category in common_categories)}." ) return data @@ -333,17 +337,23 @@ class FilterListSerializer(ModelSerializer): data.get('disabled_channels') is not None and data.get('enabled_channels') is not None ): - channels_collection = data['disabled_channels'] + data['enabled_channels'] - if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Enabled and Disabled channels lists contain duplicates.") + common_channels = set(data['disabled_channels']) & set(data['enabled_channels']) + if common_channels: + raise ValidationError( + "You can't have the same value in both enabled and disabled channels lists:" + f" {', '.join(repr(channel) for channel in common_channels)}." + ) if ( data.get('disabled_categories') is not None and data.get('enabled_categories') is not None ): - categories_collection = data['disabled_categories'] + data['enabled_categories'] - if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Enabled and Disabled categories lists contain duplicates.") + common_categories = set(data['disabled_categories']) & set(data['enabled_categories']) + if common_categories: + raise ValidationError( + "You can't have the same value in both enabled and disabled categories lists:" + f" {', '.join(repr(category) for category in common_categories)}." + ) return data -- cgit v1.2.3 From 91b89475913210400cf39884efe37ab5552efbf7 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Mar 2023 21:35:49 +0300 Subject: Use consistent quoting style Co-authored-by: Johannes Christ --- pydis_site/apps/api/models/bot/filters.py | 4 +- pydis_site/apps/api/serializers.py | 68 +++++++++++++++---------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 60ae394b..c6f6f851 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -253,8 +253,8 @@ class Filter(FilterBase): constraints = ( UniqueConstraint( fields=tuple( - [field.name for field in FilterBase._meta.fields - if field.name not in ("id", "description", "created_at", "updated_at")] + field.name for field in FilterBase._meta.fields + if field.name not in ("id", "description", "created_at", "updated_at") ), name="unique_filters"), ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index bfad18ab..da02c837 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -188,27 +188,27 @@ BASE_FILTER_FIELDS = ( ) BASE_FILTERLIST_FIELDS = ('id', 'created_at', 'updated_at', 'name', 'list_type') BASE_SETTINGS_FIELDS = ( - "bypass_roles", - "filter_dm", - "enabled", - "remove_context", - "send_alert" + 'bypass_roles', + 'filter_dm', + 'enabled', + 'remove_context', + 'send_alert' ) INFRACTION_AND_NOTIFICATION_FIELDS = ( - "infraction_type", - "infraction_reason", - "infraction_duration", - "infraction_channel", - "dm_content", - "dm_embed" + 'infraction_type', + 'infraction_reason', + 'infraction_duration', + 'infraction_channel', + 'dm_content', + 'dm_embed' ) CHANNEL_SCOPE_FIELDS = ( - "disabled_channels", - "disabled_categories", - "enabled_channels", - "enabled_categories" + 'disabled_channels', + 'disabled_categories', + 'enabled_channels', + 'enabled_categories' ) -MENTIONS_FIELDS = ("guild_pings", "dm_pings") +MENTIONS_FIELDS = ('guild_pings', 'dm_pings') def _create_meta_extra_kwargs(*, for_filter: bool) -> dict[str, dict[str, bool]]: @@ -228,7 +228,7 @@ def get_field_value(data: dict, field_name: str) -> Any: """Get the value directly from the key, or from the filter list if it's missing or is None.""" if data.get(field_name) is not None: return data[field_name] - return getattr(data["filter_list"], field_name) + return getattr(data['filter_list'], field_name) class FilterSerializer(ModelSerializer): @@ -238,18 +238,18 @@ class FilterSerializer(ModelSerializer): """Perform infraction data + allowed and disallowed lists validation.""" if ( ( - get_field_value(data, "infraction_reason") - or get_field_value(data, "infraction_duration") + get_field_value(data, 'infraction_reason') + or get_field_value(data, 'infraction_duration') ) - and get_field_value(data, "infraction_type") == "NONE" + and get_field_value(data, 'infraction_type') == 'NONE' ): raise ValidationError( "Infraction type is required with infraction duration or reason." ) common_channels = ( - set(get_field_value(data, "disabled_channels")) - & set(get_field_value(data, "enabled_channels")) + set(get_field_value(data, 'disabled_channels')) + & set(get_field_value(data, 'enabled_channels')) ) if common_channels: raise ValidationError( @@ -258,8 +258,8 @@ class FilterSerializer(ModelSerializer): ) common_categories = ( - set(get_field_value(data, "disabled_categories")) - & set(get_field_value(data, "enabled_categories")) + set(get_field_value(data, 'disabled_categories')) + & set(get_field_value(data, 'enabled_categories')) ) if common_categories: raise ValidationError( @@ -305,19 +305,19 @@ class FilterSerializer(ModelSerializer): into a sub-field called `settings`. """ settings = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - settings["infraction_and_notification"] = { + settings['infraction_and_notification'] = { name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS } - settings["channel_scope"] = { + settings['channel_scope'] = { name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS } - settings["mentions"] = { + settings['mentions'] = { name: getattr(instance, name) for name in MENTIONS_FIELDS } schema = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} - schema["filter_list"] = instance.filter_list.id - schema["settings"] = settings + schema['filter_list'] = instance.filter_list.id + schema['settings'] = settings return schema @@ -388,19 +388,19 @@ class FilterListSerializer(ModelSerializer): into a sub-field called `settings`. """ schema = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} - schema["filters"] = [ + schema['filters'] = [ FilterSerializer(many=False).to_representation(instance=item) for item in Filter.objects.filter(filter_list=instance.id) ] settings = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - settings["infraction_and_notification"] = { + settings['infraction_and_notification'] = { name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS } - settings["channel_scope"] = {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} - settings["mentions"] = {name: getattr(instance, name) for name in MENTIONS_FIELDS} + settings['channel_scope'] = {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} + settings['mentions'] = {name: getattr(instance, name) for name in MENTIONS_FIELDS} - schema["settings"] = settings + schema['settings'] = settings return schema # endregion -- cgit v1.2.3 From 0f40b114940164c65b10d1312b5a419ce025c799 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 26 Mar 2023 23:12:34 +0300 Subject: Rename additional_field to additional_settings --- pydis_site/apps/api/migrations/0088_new_filter_schema.py | 4 ++-- .../apps/api/migrations/0089_unique_constraint_filters.py | 4 ++-- pydis_site/apps/api/models/bot/filters.py | 4 +++- pydis_site/apps/api/serializers.py | 9 +++++---- pydis_site/apps/api/tests/test_filters.py | 2 +- pydis_site/apps/api/tests/test_models.py | 2 +- pydis_site/apps/api/viewsets/bot/filters.py | 14 +++++++------- 7 files changed, 21 insertions(+), 18 deletions(-) diff --git a/pydis_site/apps/api/migrations/0088_new_filter_schema.py b/pydis_site/apps/api/migrations/0088_new_filter_schema.py index 1506e4d7..2e1d78c9 100644 --- a/pydis_site/apps/api/migrations/0088_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0088_new_filter_schema.py @@ -64,7 +64,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: updated_at=object_.updated_at, filter_list=list_, description=object_.comment, - additional_field=None, + additional_settings=None, guild_pings=None, filter_dm=None, dm_pings=None, @@ -105,7 +105,7 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True)), ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), - ('additional_field', models.JSONField(help_text='Implementation specific field.', null=True)), + ('additional_settings', models.JSONField(help_text='Additional settings which are specific to this filter.', null=True)), ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, null=True)), diff --git a/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py index 0bcfd8a3..cb230a27 100644 --- a/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py @@ -11,13 +11,13 @@ class Migration(migrations.Migration): migrations.RunSQL( "ALTER TABLE api_filter " "ADD CONSTRAINT unique_filters UNIQUE NULLS NOT DISTINCT " - "(content, additional_field, filter_list_id, dm_content, dm_embed, infraction_type, infraction_reason, infraction_duration, infraction_channel, guild_pings, filter_dm, dm_pings, remove_context, bypass_roles, enabled, send_alert, enabled_channels, disabled_channels, enabled_categories, disabled_categories)", + "(content, additional_settings, filter_list_id, dm_content, dm_embed, infraction_type, infraction_reason, infraction_duration, infraction_channel, guild_pings, filter_dm, dm_pings, remove_context, bypass_roles, enabled, send_alert, enabled_channels, disabled_channels, enabled_categories, disabled_categories)", reverse_sql="ALTER TABLE api_filter DROP CONSTRAINT unique_filters", state_operations=[ migrations.AddConstraint( model_name='filter', constraint=models.UniqueConstraint( - fields=('content', 'additional_field', 'filter_list', 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', 'infraction_channel', 'guild_pings', 'filter_dm', 'dm_pings', 'remove_context', 'bypass_roles', 'enabled', 'send_alert', 'enabled_channels', 'disabled_channels', 'enabled_categories', 'disabled_categories'), + fields=('content', 'additional_settings', 'filter_list', 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', 'infraction_channel', 'guild_pings', 'filter_dm', 'dm_pings', 'remove_context', 'bypass_roles', 'enabled', 'send_alert', 'enabled_channels', 'disabled_channels', 'enabled_categories', 'disabled_categories'), name='unique_filters' ), ), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index c6f6f851..aadb39aa 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -131,7 +131,9 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): max_length=200, help_text="Why this filter has been added.", null=True ) - additional_field = models.JSONField(null=True, help_text="Implementation specific field.") + additional_settings = models.JSONField( + null=True, help_text="Additional settings which are specific to this filter." + ) filter_list = models.ForeignKey( FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index da02c837..a3779094 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -184,7 +184,7 @@ ALLOW_EMPTY_SETTINGS = ( # Required fields for custom JSON representation purposes BASE_FILTER_FIELDS = ( - 'id', 'created_at', 'updated_at', 'content', 'description', 'additional_field' + 'id', 'created_at', 'updated_at', 'content', 'description', 'additional_settings' ) BASE_FILTERLIST_FIELDS = ('id', 'created_at', 'updated_at', 'name', 'list_type') BASE_SETTINGS_FIELDS = ( @@ -279,7 +279,7 @@ class FilterSerializer(ModelSerializer): 'updated_at', 'content', 'description', - 'additional_field', + 'additional_settings', 'filter_list' ) + SETTINGS_FIELDS extra_kwargs = _create_meta_extra_kwargs(for_filter=True) @@ -382,9 +382,10 @@ class FilterListSerializer(ModelSerializer): Provides a custom JSON representation to the FilterList Serializers. This representation restructures how the Filter is represented. - It groups the Infraction, Channel and Mention related fields into their own separated group. + It groups the Infraction, Channel, and Mention related fields + into their own separated groups. - Furthermore, it puts the fields that meant to represent FilterList settings, + Furthermore, it puts the fields that are meant to represent FilterList settings, into a sub-field called `settings`. """ schema = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index f36e0617..3d3be51e 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -92,7 +92,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: { "content": "bad word", "description": "This is a really bad word.", - "additional_field": "{'hi': 'there'}", + "additional_settings": "{'hi': 'there'}", "guild_pings": None, "filter_dm": None, "dm_pings": None, diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index 25d771cc..d3341b35 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -113,7 +113,7 @@ class StringDunderMethodTests(SimpleTestCase): Filter( content="ducky_nsfw", description="This ducky is totally inappropriate!", - additional_field=None, + additional_settings=None, ), OffensiveMessage( id=602951077675139072, diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index c84da909..d6c2d18c 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -33,7 +33,7 @@ class FilterListViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -119,7 +119,7 @@ class FilterListViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -239,7 +239,7 @@ class FilterListViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -334,7 +334,7 @@ class FilterViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -379,7 +379,7 @@ class FilterViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -420,7 +420,7 @@ class FilterViewSet(ModelViewSet): ... "filter_list": 1, ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "bypass_roles": None, ... "filter_dm": None, ... "enabled": False, @@ -454,7 +454,7 @@ class FilterViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, -- cgit v1.2.3 From 4c923fa1cd6f1f5144036317b116aac745b3c345 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Wed, 5 Apr 2023 03:10:05 +0300 Subject: Add maximum auto-timeout duration validation --- pydis_site/apps/api/serializers.py | 31 +++++++++++++++++++++++++------ pydis_site/apps/api/tests/test_filters.py | 2 ++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a3779094..2186b02c 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,4 +1,5 @@ """Converters from Django models to data interchange formats and back.""" +from datetime import timedelta from typing import Any from django.db.models.query import QuerySet @@ -210,6 +211,8 @@ CHANNEL_SCOPE_FIELDS = ( ) MENTIONS_FIELDS = ('guild_pings', 'dm_pings') +MAX_TIMEOUT_DURATION = timedelta(days=28) + def _create_meta_extra_kwargs(*, for_filter: bool) -> dict[str, dict[str, bool]]: """Create the extra kwargs for the Meta classes of the Filter and FilterList serializers.""" @@ -236,17 +239,24 @@ class FilterSerializer(ModelSerializer): def validate(self, data: dict) -> dict: """Perform infraction data + allowed and disallowed lists validation.""" + infraction_type = get_field_value(data, 'infraction_type') + infraction_duration = get_field_value(data, 'infraction_duration') if ( - ( - get_field_value(data, 'infraction_reason') - or get_field_value(data, 'infraction_duration') - ) - and get_field_value(data, 'infraction_type') == 'NONE' + (get_field_value(data, 'infraction_reason') or infraction_duration) + and infraction_type == 'NONE' ): raise ValidationError( "Infraction type is required with infraction duration or reason." ) + if ( + infraction_type == 'TIMEOUT' + and (not infraction_duration or infraction_duration > MAX_TIMEOUT_DURATION) + ): + raise ValidationError( + f"A timeout cannot be longer than {MAX_TIMEOUT_DURATION.days} days." + ) + common_channels = ( set(get_field_value(data, 'disabled_channels')) & set(get_field_value(data, 'enabled_channels')) @@ -328,8 +338,9 @@ class FilterListSerializer(ModelSerializer): def validate(self, data: dict) -> dict: """Perform infraction data + allow and disallowed lists validation.""" + infraction_duration = data.get('infraction_duration') if ( - data.get('infraction_reason') or data.get('infraction_duration') + data.get('infraction_reason') or infraction_duration ) and not data.get('infraction_type'): raise ValidationError("Infraction type is required with infraction duration or reason") @@ -344,6 +355,14 @@ class FilterListSerializer(ModelSerializer): f" {', '.join(repr(channel) for channel in common_channels)}." ) + if ( + data.get('infraction_type') == 'TIMEOUT' + and (not infraction_duration or infraction_duration > MAX_TIMEOUT_DURATION) + ): + raise ValidationError( + f"A timeout cannot be longer than {MAX_TIMEOUT_DURATION.days} days." + ) + if ( data.get('disabled_categories') is not None and data.get('enabled_categories') is not None diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index 3d3be51e..ebc4a2cf 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -274,6 +274,7 @@ class FilterValidationTests(AuthenticatedAPITestCase): ({"infraction_reason": "hi"}, {}, 400), ({"infraction_duration": timedelta(seconds=10)}, {}, 400), ({"infraction_reason": "hi"}, {"infraction_type": "NOTE"}, 200), + ({"infraction_type": "TIMEOUT", "infraction_duration": timedelta(days=30)}, {}, 400), ({"infraction_duration": timedelta(seconds=10)}, {"infraction_type": "TIMEOUT"}, 200), ({"enabled_channels": ["admins"]}, {}, 200), ({"disabled_channels": ["123"]}, {}, 200), @@ -313,6 +314,7 @@ class FilterValidationTests(AuthenticatedAPITestCase): cases = ( ({"infraction_reason": "hi"}, 400), ({"infraction_duration": timedelta(seconds=10)}, 400), + ({"infraction_type": "TIMEOUT", "infraction_duration": timedelta(days=30)}, 400), ({"infraction_reason": "hi", "infraction_type": "NOTE"}, 200), ({"infraction_duration": timedelta(seconds=10), "infraction_type": "TIMEOUT"}, 200), ({"enabled_channels": ["admins"]}, 200), ({"disabled_channels": ["123"]}, 200), -- cgit v1.2.3 From b8ddedc31d54f46bb86a7e7d200c163ea8806ee0 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Thu, 6 Apr 2023 01:09:09 +0300 Subject: Make additional_settings non-null with dict default This makes sure that the value in the DB is always a valid JSON, ensuring the unique constraint will work properly. --- pydis_site/apps/api/migrations/0088_new_filter_schema.py | 4 ++-- pydis_site/apps/api/models/bot/filters.py | 2 +- pydis_site/apps/api/tests/test_filters.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pydis_site/apps/api/migrations/0088_new_filter_schema.py b/pydis_site/apps/api/migrations/0088_new_filter_schema.py index 2e1d78c9..675fdcec 100644 --- a/pydis_site/apps/api/migrations/0088_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0088_new_filter_schema.py @@ -64,7 +64,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: updated_at=object_.updated_at, filter_list=list_, description=object_.comment, - additional_settings=None, + additional_settings={}, guild_pings=None, filter_dm=None, dm_pings=None, @@ -105,7 +105,7 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True)), ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), - ('additional_settings', models.JSONField(help_text='Additional settings which are specific to this filter.', null=True)), + ('additional_settings', models.JSONField(help_text='Additional settings which are specific to this filter.', default=dict)), ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, null=True)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index aadb39aa..71f8771f 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -132,7 +132,7 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): help_text="Why this filter has been added.", null=True ) additional_settings = models.JSONField( - null=True, help_text="Additional settings which are specific to this filter." + help_text="Additional settings which are specific to this filter.", default=dict ) filter_list = models.ForeignKey( FilterList, models.CASCADE, related_name="filters", diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index ebc4a2cf..5059d651 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -230,6 +230,7 @@ class GenericFilterTests(AuthenticatedAPITestCase): def test_creation_missing_field(self) -> None: for name, sequence in get_test_sequences().items(): + ignored_fields = sequence.ignored_fields + ("id", "additional_settings") with self.subTest(name=name): saved = sequence.model(**sequence.object) save_nested_objects(saved) @@ -237,7 +238,7 @@ class GenericFilterTests(AuthenticatedAPITestCase): for field in sequence.model._meta.get_fields(): with self.subTest(field=field): - if field.null or field.name in sequence.ignored_fields + ("id",): + if field.null or field.name in ignored_fields: continue test_data = data.copy() -- cgit v1.2.3