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 ++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 43 deletions(-) delete mode 100644 pydis_site/apps/api/models/bot/filter_list.py create mode 100644 pydis_site/apps/api/models/bot/filters.py (limited to 'pydis_site/apps/api/models') 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) -- 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 (limited to 'pydis_site/apps/api/models') 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 (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(+) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(+) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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 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 (limited to 'pydis_site/apps/api/models') 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 (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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 (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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(-) (limited to 'pydis_site/apps/api/models') 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 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(-) (limited to 'pydis_site/apps/api/models') 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