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 --- pydis_site/apps/api/serializers.py | 98 ++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 10 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') 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.""" -- 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/serializers.py') diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 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 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/serializers.py') diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 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/serializers.py') diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 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 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/serializers.py') 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 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/serializers.py') diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 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 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/serializers.py') 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 7d21797b8bc14b92a48bc782694e226b2562c1b5 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 29 Oct 2021 20:17:03 +0300 Subject: Fix linting --- pydis_site/apps/api/serializers.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index b5f083b0..c82b0797 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -139,17 +139,25 @@ SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS class FilterSerializer(ModelSerializer): """A class providing (de-)serialization of `Filter` instances.""" - def validate(self, data): + def validate(self, data: dict) -> dict: """Perform infraction data + allow and disallowed lists validation.""" - if (data.get('infraction_reason') or data.get('infraction_duration')) and not data.get('infraction_type'): + if ( + data.get('infraction_reason') or data.get('infraction_duration') + ) and not data.get('infraction_type'): raise ValidationError("Infraction type is required with infraction duration or reason") - if data.get('allowed_channels') is not None and data.get('disallowed_channels') is not None: + if ( + data.get('allowed_channels') is not None + and data.get('disallowed_channels') is not None + ): channels_collection = data['allowed_channels'] + data['disallowed_channels'] if len(channels_collection) != len(set(channels_collection)): raise ValidationError("Allowed and disallowed channels lists contain duplicates.") - if data.get('allowed_categories') is not None and data.get('disallowed_categories') is not None: + if ( + data.get('allowed_categories') is not None + and data.get('disallowed_categories') is not None + ): categories_collection = data['allowed_categories'] + data['disallowed_categories'] if len(categories_collection) != len(set(categories_collection)): raise ValidationError("Allowed and disallowed categories lists contain duplicates.") @@ -160,7 +168,9 @@ class FilterSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = Filter - fields = ('id', 'content', 'description', 'additional_field', 'filter_list') + SETTINGS_FIELDS + fields = ( + 'id', 'content', 'description', 'additional_field', 'filter_list' + ) + SETTINGS_FIELDS extra_kwargs = { field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS } | { @@ -177,9 +187,11 @@ class FilterListSerializer(ModelSerializer): filters = FilterSerializer(many=True, read_only=True) - def validate(self, data): + def validate(self, data: dict) -> dict: """Perform infraction data + allow and disallowed lists validation.""" - if (data['infraction_reason'] or data['infraction_duration']) and not data['infraction_type']: + if ( + data['infraction_reason'] or data['infraction_duration'] + ) and not data['infraction_type']: raise ValidationError("Infraction type is required with infraction duration or reason") channels_collection = data['allowed_channels'] + data['disallowed_channels'] -- cgit v1.2.3 From 8ab32d7820b57b9f3edb61d4bd93864b6037502b Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Sun, 5 Dec 2021 16:09:56 +0100 Subject: Adjust Filter JSON Schema From now on the Serializer will have a different JSON representation than the table schema itself, conforming to the format needed on the bot-side. --- pydis_site/apps/api/serializers.py | 30 +++++++ pydis_site/apps/api/viewsets/bot/filters.py | 127 +++++++++++++++++++++------- 2 files changed, 127 insertions(+), 30 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index c82b0797..864ab52e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -133,6 +133,17 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'allowed_categories', ) +# Required fields for custom JSON representation purposes +BASE_FIELDS = ('id', 'content', 'description', 'additional_field') +BASE_SETTINGS_FIELDS = ("ping_type", "dm_ping_type", "bypass_roles", "filter_dm") +INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") +CHANNEL_SCOPE_FIELDS = ( + "allowed_channels", + "allowed_categories", + "disallowed_channels", + "disallowed_categories" +) + SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS @@ -181,6 +192,25 @@ class FilterSerializer(ModelSerializer): 'allowed_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, } + def to_representation(self, instance: Filter) -> dict: + """ + Provides a custom JSON representation to the Filter Serializers + + That does not affect how the Serializer works in general. + """ + item = Filter.objects.get(id=instance.id) + schema_settings = { + "settings": + {name: getattr(item, name) for name in BASE_SETTINGS_FIELDS} + | {"infraction": {name: getattr(item, name) for name in INFRACTION_FIELDS}} + | {"channel_scope": {name: getattr(item, name) for name in CHANNEL_SCOPE_FIELDS}} + } + + schema_base = {name: getattr(item, name) for name in BASE_FIELDS} | \ + {"filter_list": item.filter_list.id} + + return schema_base | schema_settings + class FilterListSerializer(ModelSerializer): """A class providing (de-)serialization of `FilterList` instances.""" diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 5b21de26..64329ebe 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -27,11 +27,28 @@ class FilterListViewSet(ModelViewSet): ... "filters": [ ... { ... "id": 1, + ... "filter_list": 1 ... "content": "267624335836053506", ... "description": "Python Discord", ... "additional_field": None, - ... "override": 1, - ... "filter_list": 1 + ... "settings": { + ... "ping_type": None, + ... "dm_ping_type": None + ... "bypass_roles": None + ... "filter_dm": None, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None + ... }, + ... "channel_scope": { + ... "allowed_channels": None, + ... "allowed_categories": None, + ... "disallowed_channels": None, + ... "disallowed_categories": None + ... } + ... } + ... ... }, ... ... ... ], @@ -48,23 +65,40 @@ class FilterListViewSet(ModelViewSet): Returns a specific FilterList item from the database. #### Response format - >>> { - ... "id": 1, - ... "name": "guild_invite", - ... "list_type": 1, - ... "filters": [ - ... { - ... "id": 1, - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "override": 1, - ... "filter_list": 1 - ... }, - ... ... - ... ], - ... "default_settings": 1 - ... } + ... { + ... "id": 1, + ... "name": "guild_invite", + ... "list_type": 1, + ... "filters": [ + ... { + ... "id": 1, + ... "filter_list": 1 + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "settings": { + ... "ping_type": None, + ... "dm_ping_type": None + ... "bypass_roles": None + ... "filter_dm": None, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None + ... }, + ... "channel_scope": { + ... "allowed_channels": None, + ... "allowed_categories": None, + ... "disallowed_channels": None, + ... "disallowed_categories": None + ... } + ... } + ... + ... }, + ... ... + ... ], + ... "default_settings": 1 + ... } #### Status codes - 200: returned on success @@ -93,12 +127,28 @@ class FilterViewSet(ModelViewSet): #### Response format >>> [ ... { - ... "id": 1, - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "override": 1, - ... "filter_list": 1 + ... "id": 1, + ... "filter_list": 1 + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "settings": { + ... "ping_type": None, + ... "dm_ping_type": None + ... "bypass_roles": None + ... "filter_dm": None, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None + ... }, + ... "channel_scope": { + ... "allowed_channels": None, + ... "allowed_categories": None, + ... "disallowed_channels": None, + ... "disallowed_categories": None + ... } + ... } ... }, ... ... ... ] @@ -112,11 +162,28 @@ class FilterViewSet(ModelViewSet): #### Response format >>> { - ... "id": 1, - ... "content": "267624335836053506", - ... "description": "Python Discord", - ... "additional_field": None, - ... "override": 1 + ... "id": 1, + ... "filter_list": 1 + ... "content": "267624335836053506", + ... "description": "Python Discord", + ... "additional_field": None, + ... "settings": { + ... "ping_type": None, + ... "dm_ping_type": None + ... "bypass_roles": None + ... "filter_dm": None, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None + ... }, + ... "channel_scope": { + ... "allowed_channels": None, + ... "allowed_categories": None, + ... "disallowed_channels": None, + ... "disallowed_categories": None + ... } + ... } ... } #### Status codes -- cgit v1.2.3 From e3a45e09041898ffd0bccd3c730524e8c673e696 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:22:01 +0100 Subject: Adjust FilterList Representation From now on the FilterList Serializer will contain a settings field with all the settings that were listed previously, on the model. --- pydis_site/apps/api/serializers.py | 17 ++++++++-- pydis_site/apps/api/viewsets/bot/filters.py | 49 +++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 864ab52e..267cf761 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -181,7 +181,7 @@ class FilterSerializer(ModelSerializer): model = Filter fields = ( 'id', 'content', 'description', 'additional_field', 'filter_list' - ) + SETTINGS_FIELDS + ) extra_kwargs = { field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS } | { @@ -194,7 +194,8 @@ class FilterSerializer(ModelSerializer): def to_representation(self, instance: Filter) -> dict: """ - Provides a custom JSON representation to the Filter Serializers + + Provides a custom JSON representation to the Filter Serializers. That does not affect how the Serializer works in general. """ @@ -239,7 +240,7 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ('id', 'name', 'list_type', 'filters') + SETTINGS_FIELDS + fields = ('id', 'name', 'list_type', 'filters') extra_kwargs = { field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS } | { @@ -261,6 +262,16 @@ class FilterListSerializer(ModelSerializer): ), ] + def to_representation(self, instance: FilterList) -> dict: + """ + Provides a custom JSON representation to the FilterList Serializers. + + That does not affect how the Serializer works in general. + """ + ret = super().to_representation(instance) + ret["settings"] = {name: getattr(instance, name) for name in SETTINGS_FIELDS} + return ret + class InfractionSerializer(ModelSerializer): """A class providing (de-)serialization of `Infraction` instances.""" diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 64329ebe..cbadcf2b 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -52,7 +52,28 @@ class FilterListViewSet(ModelViewSet): ... }, ... ... ... ], - ... "default_settings": 1 + ... "settings": { + ... "dm_content": None, + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None, + ... "ping_type": [ + ... "onduty" + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty" + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601 + ... ], + ... "enabled": False, + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_categories": [] + ... } ... }, ... ... ... ] @@ -65,6 +86,7 @@ class FilterListViewSet(ModelViewSet): Returns a specific FilterList item from the database. #### Response format + >>> ... { ... "id": 1, ... "name": "guild_invite", @@ -95,9 +117,30 @@ class FilterListViewSet(ModelViewSet): ... } ... ... }, - ... ... + ... ... ], - ... "default_settings": 1 + ... "settings": { + ... "dm_content": None, + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None, + ... "ping_type": [ + ... "onduty" + ... ], + ... "filter_dm": True, + ... "dm_ping_type": [ + ... "onduty" + ... ], + ... "delete_messages": True, + ... "bypass_roles": [ + ... 267630620367257601 + ... ], + ... "enabled": False, + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_categories": [] + ... } ... } #### Status codes -- cgit v1.2.3 From 3e8f164525bdd3a728bb7383da237feb9aacb44e Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Thu, 9 Dec 2021 20:53:49 +0100 Subject: Adjust FilterList Schema to group settings into subcategories - This commit patches the FilterList serializer's schema, and puts the settings into the relevant subcategories. --- pydis_site/apps/api/serializers.py | 9 ++- pydis_site/apps/api/viewsets/bot/filters.py | 93 ++++++++++++++--------------- 2 files changed, 54 insertions(+), 48 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 267cf761..89005a9b 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -269,7 +269,14 @@ class FilterListSerializer(ModelSerializer): That does not affect how the Serializer works in general. """ ret = super().to_representation(instance) - ret["settings"] = {name: getattr(instance, name) for name in SETTINGS_FIELDS} + schema_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + schema_settings = { + "infraction": + {name: getattr(instance, name) for name in INFRACTION_FIELDS}} \ + | { + "channel_scope": + {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} + ret["settings"] = schema_base | schema_settings return ret diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index cbadcf2b..2b587696 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -52,31 +52,29 @@ class FilterListViewSet(ModelViewSet): ... }, ... ... ... ], - ... "settings": { - ... "dm_content": None, - ... "infraction_type": None, - ... "infraction_reason": "", - ... "infraction_duration": None, - ... "ping_type": [ - ... "onduty" - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty" - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601 - ... ], - ... "enabled": False, - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [] - ... } - ... }, - ... ... - ... ] + ... "settings": { + ... "ping_type": [ + ... "onduty" + ... ], + ... "dm_ping_type": [ + ... "onduty" + ... ], + ... "bypass_roles": [ + ... 267630620367257601 + ... ], + ... "filter_dm": True, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None, + ... } + ... "channel_scope": { + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_categories": [] + ... } + ... } #### Status codes - 200: returned on success @@ -120,28 +118,29 @@ class FilterListViewSet(ModelViewSet): ... ... ], ... "settings": { - ... "dm_content": None, - ... "infraction_type": None, - ... "infraction_reason": "", - ... "infraction_duration": None, - ... "ping_type": [ - ... "onduty" - ... ], - ... "filter_dm": True, - ... "dm_ping_type": [ - ... "onduty" - ... ], - ... "delete_messages": True, - ... "bypass_roles": [ - ... 267630620367257601 - ... ], - ... "enabled": False, - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [] - ... } - ... } + ... "ping_type": [ + ... "onduty" + ... ], + ... "dm_ping_type": [ + ... "onduty" + ... ], + ... "bypass_roles": [ + ... 267630620367257601 + ... ], + ... "filter_dm": True, + ... "infraction": { + ... "infraction_type": None, + ... "infraction_reason": "", + ... "infraction_duration": None, + ... } + ... "channel_scope": { + ... "disallowed_channels": [], + ... "disallowed_categories": [], + ... "allowed_channels": [], + ... "allowed_categories": [] + ... } + ... } + ... } #### Status codes - 200: returned on success -- cgit v1.2.3 From a24cac8d43893f792d4fa495cf2a9ce65f69051c Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Fri, 10 Dec 2021 22:19:09 +0100 Subject: Patch Filter and FilterList Serializer validation logic and representation - This commit patches an error with the FilterListSerializer validation logic, so that it won't raise an error when an optional field is not present. - It also adds the `enabled` and `delete_messages` fields, to the FilterSerializer's representation - Furthermore the commit introduces minor bug patches, regarding DRF Serializer Fields. --- pydis_site/apps/api/serializers.py | 73 +++++++++++++++++++---------- pydis_site/apps/api/viewsets/bot/filters.py | 12 +++++ 2 files changed, 61 insertions(+), 24 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 89005a9b..cb8313ac 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -134,8 +134,16 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( ) # Required fields for custom JSON representation purposes -BASE_FIELDS = ('id', 'content', 'description', 'additional_field') -BASE_SETTINGS_FIELDS = ("ping_type", "dm_ping_type", "bypass_roles", "filter_dm") +BASE_FILTER_FIELDS = ('id', 'content', 'description', 'additional_field') +BASE_FILTERLIST_FIELDS = ('id', 'name', 'list_type') +BASE_SETTINGS_FIELDS = ( + "ping_type", + "dm_ping_type", + "bypass_roles", + "filter_dm", + "enabled", + "delete_messages" +) INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") CHANNEL_SCOPE_FIELDS = ( "allowed_channels", @@ -181,7 +189,7 @@ class FilterSerializer(ModelSerializer): model = Filter fields = ( 'id', 'content', 'description', 'additional_field', 'filter_list' - ) + ) + SETTINGS_FIELDS extra_kwargs = { field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS } | { @@ -199,16 +207,18 @@ class FilterSerializer(ModelSerializer): That does not affect how the Serializer works in general. """ - item = Filter.objects.get(id=instance.id) schema_settings = { "settings": - {name: getattr(item, name) for name in BASE_SETTINGS_FIELDS} - | {"infraction": {name: getattr(item, name) for name in INFRACTION_FIELDS}} - | {"channel_scope": {name: getattr(item, name) for name in CHANNEL_SCOPE_FIELDS}} + {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + | {"infraction": {name: getattr(instance, name) for name in INFRACTION_FIELDS}} + | { + "channel_scope": + {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} + } } - schema_base = {name: getattr(item, name) for name in BASE_FIELDS} | \ - {"filter_list": item.filter_list.id} + schema_base = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} | \ + {"filter_list": instance.filter_list.id} return schema_base | schema_settings @@ -221,18 +231,25 @@ class FilterListSerializer(ModelSerializer): def validate(self, data: dict) -> dict: """Perform infraction data + allow and disallowed lists validation.""" if ( - data['infraction_reason'] or data['infraction_duration'] - ) and not data['infraction_type']: + data.get('infraction_reason') or data.get('infraction_duration') + ) and not data.get('infraction_type'): raise ValidationError("Infraction type is required with infraction duration or reason") - channels_collection = data['allowed_channels'] + data['disallowed_channels'] - categories_collection = data['allowed_categories'] + data['disallowed_categories'] - - if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + if ( + data.get('allowed_channels') is not None + and data.get('disallowed_channels') is not None + ): + channels_collection = data['allowed_channels'] + data['disallowed_channels'] + if len(channels_collection) != len(set(channels_collection)): + raise ValidationError("Allowed and disallowed channels lists contain duplicates.") - if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + if ( + data.get('allowed_categories') is not None + and data.get('disallowed_categories') is not None + ): + categories_collection = data['allowed_categories'] + data['disallowed_categories'] + if len(categories_collection) != len(set(categories_collection)): + raise ValidationError("Allowed and disallowed categories lists contain duplicates.") return data @@ -240,7 +257,7 @@ class FilterListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = FilterList - fields = ('id', 'name', 'list_type', 'filters') + fields = ('id', 'name', 'list_type', 'filters') + SETTINGS_FIELDS extra_kwargs = { field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS } | { @@ -268,16 +285,24 @@ class FilterListSerializer(ModelSerializer): That does not affect how the Serializer works in general. """ - ret = super().to_representation(instance) - schema_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - schema_settings = { + # Fetches the relating filters + filters = [ + FilterSerializer(many=False).to_representation( + instance=item + ) for item in Filter.objects.filter( + filter_list=instance.id + ) + ] + schema_base = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} \ + | {"filters": filters} + schema_settings_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + schema_settings_categories = { "infraction": {name: getattr(instance, name) for name in INFRACTION_FIELDS}} \ | { "channel_scope": {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} - ret["settings"] = schema_base | schema_settings - return ret + return schema_base | {"settings": schema_settings_base | schema_settings_categories} class InfractionSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 2b587696..e8f3e3d9 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -36,6 +36,8 @@ class FilterListViewSet(ModelViewSet): ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -63,6 +65,8 @@ class FilterListViewSet(ModelViewSet): ... 267630620367257601 ... ], ... "filter_dm": True, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -101,6 +105,8 @@ class FilterListViewSet(ModelViewSet): ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -128,6 +134,8 @@ class FilterListViewSet(ModelViewSet): ... 267630620367257601 ... ], ... "filter_dm": True, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -179,6 +187,8 @@ class FilterViewSet(ModelViewSet): ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -214,6 +224,8 @@ class FilterViewSet(ModelViewSet): ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, + ... "enabled": False + ... "delete_messages": True ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", -- cgit v1.2.3 From 4c93b1b9b75cce4e45bdbdae608f4497372c2b56 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Wed, 15 Dec 2021 19:25:55 +0100 Subject: Prepare FilterList and Filter models, serializers for the new filter schema - Rename channel scope fields: - "allowed" -> "disabled" eg.: "allowed_channels" -> "disabled_channels" - Rename FilterLists` names: filter_token -> tokens domain_name -> domains guild_invite -> invites file_format -> formats - Patch the docs and validators accordingly. --- ..._filter_and_filterlist_for_new_filter_schema.py | 80 ++++++++++++++++++ pydis_site/apps/api/models/bot/filters.py | 21 +++-- pydis_site/apps/api/serializers.py | 90 +++++++++++---------- pydis_site/apps/api/viewsets/bot/filters.py | 94 ++++++++++++---------- 4 files changed, 189 insertions(+), 96 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py new file mode 100644 index 00000000..30537e3d --- /dev/null +++ b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py @@ -0,0 +1,80 @@ +# Generated by Django 3.0.14 on 2021-12-11 23:14 +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + change_map = { + "filter_token": "tokens", + "domain_name": "domains", + "guild_invite": "invites", + "file_format": "formats" + } + for filter_list in FilterList.objects.all(): + if change_map.get(filter_list.name): + filter_list.name = change_map.get(filter_list.name) + filter_list.save() + + +def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + change_map = { + "tokens": "filter_token", + "domains": "domain_name", + "invites": "guild_invite", + "formats": "file_format" + } + for filter_list in FilterList.objects.all(): + if change_map.get(filter_list.name): + filter_list.name = change_map.get(filter_list.name) + filter_list.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0074_merge_20211017_0822'), + ] + + operations = [ + migrations.RenameField( + model_name='filter', + old_name='allowed_categories', + new_name='disabled_categories', + ), + migrations.RenameField( + model_name='filter', + old_name='allowed_channels', + new_name='disabled_channels', + ), + migrations.RenameField( + model_name='filter', + old_name='disallowed_channels', + new_name='enabled_channels', + ), + migrations.RenameField( + model_name='filterlist', + old_name='allowed_categories', + new_name='disabled_categories', + ), + migrations.RenameField( + model_name='filterlist', + old_name='allowed_channels', + new_name='disabled_channels', + ), + migrations.RenameField( + model_name='filterlist', + old_name='disallowed_channels', + new_name='enabled_channels', + ), + migrations.RemoveField( + model_name='filterlist', + name='disallowed_categories', + ), + migrations.RemoveField( + model_name='filter', + name='disallowed_categories', + ), + migrations.RunPython(migrate_filterlist, unmigrate_filterlist) + ] diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 3a1f3c6a..ae877685 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -99,14 +99,12 @@ class FilterList(FilterSettingsMixin): # Where a filter should apply. # # The resolution is done in the following order: - # - disallowed channels - # - disallowed categories - # - allowed categories - # - allowed channels - disallowed_channels = ArrayField(models.IntegerField()) - disallowed_categories = ArrayField(models.IntegerField()) - allowed_channels = ArrayField(models.IntegerField()) - allowed_categories = ArrayField(models.IntegerField()) + # - enabled_channels + # - disabled_categories + # - disabled_channels + enabled_channels = ArrayField(models.IntegerField()) + disabled_channels = ArrayField(models.IntegerField()) + disabled_categories = ArrayField(models.IntegerField()) class Meta: """Constrain name and list_type unique.""" @@ -157,10 +155,9 @@ class Filter(FilterSettingsMixin): ) # Check FilterList model for information about these properties. - disallowed_channels = ArrayField(models.IntegerField(), null=True) - disallowed_categories = ArrayField(models.IntegerField(), null=True) - allowed_channels = ArrayField(models.IntegerField(), null=True) - allowed_categories = ArrayField(models.IntegerField(), null=True) + enabled_channels = ArrayField(models.IntegerField(), null=True) + disabled_channels = ArrayField(models.IntegerField(), null=True) + disabled_categories = ArrayField(models.IntegerField(), null=True) def __str__(self) -> str: return f"Filter {self.content!r}" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index cb8313ac..784f8160 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -127,18 +127,15 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'delete_messages', 'bypass_roles', 'enabled', - 'disallowed_channels', - 'disallowed_categories', - 'allowed_channels', - 'allowed_categories', + 'enabled_channels', + 'disabled_channels', + 'disabled_categories', ) # Required fields for custom JSON representation purposes BASE_FILTER_FIELDS = ('id', 'content', 'description', 'additional_field') BASE_FILTERLIST_FIELDS = ('id', 'name', 'list_type') BASE_SETTINGS_FIELDS = ( - "ping_type", - "dm_ping_type", "bypass_roles", "filter_dm", "enabled", @@ -146,11 +143,11 @@ BASE_SETTINGS_FIELDS = ( ) INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") CHANNEL_SCOPE_FIELDS = ( - "allowed_channels", - "allowed_categories", - "disallowed_channels", - "disallowed_categories" + "disabled_channels", + "disabled_categories", + "enabled_channels", ) +MENTIONS_FIELDS = ("ping_type", "dm_ping_type") SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS @@ -166,20 +163,17 @@ class FilterSerializer(ModelSerializer): raise ValidationError("Infraction type is required with infraction duration or reason") if ( - data.get('allowed_channels') is not None - and data.get('disallowed_channels') is not None + data.get('disabled_channels') is not None + and data.get('enabled_channels') is not None ): - channels_collection = data['allowed_channels'] + data['disallowed_channels'] + channels_collection = data['disabled_channels'] + data['enabled_channels'] if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + raise ValidationError("Enabled and Disabled channels lists contain duplicates.") - if ( - data.get('allowed_categories') is not None - and data.get('disallowed_categories') is not None - ): - categories_collection = data['allowed_categories'] + data['disallowed_categories'] + if data.get('disabled_categories') is not None: + categories_collection = data['disabled_categories'] if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + raise ValidationError("Disabled categories lists contain duplicates.") return data @@ -194,18 +188,20 @@ class FilterSerializer(ModelSerializer): field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS } | { 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, - 'disallowed_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'disallowed_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'allowed_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'allowed_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'enabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'disabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'disabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, } def to_representation(self, instance: Filter) -> dict: """ - Provides a custom JSON representation to the Filter Serializers. - That does not affect how the Serializer works in general. + This representation restructures how the Filter is represented. + It groups the Infraction, Channel and Mention related fields into their own separated group. + + Furthermore, it puts the fields that meant to represent Filter settings, + into a sub-field called `settings`. """ schema_settings = { "settings": @@ -214,6 +210,11 @@ class FilterSerializer(ModelSerializer): | { "channel_scope": {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} + } | { + "mentions": + { + schema_field_name: getattr(instance, schema_field_name) + for schema_field_name in MENTIONS_FIELDS} } } @@ -236,20 +237,17 @@ class FilterListSerializer(ModelSerializer): raise ValidationError("Infraction type is required with infraction duration or reason") if ( - data.get('allowed_channels') is not None - and data.get('disallowed_channels') is not None + data.get('disabled_channels') is not None + and data.get('enabled_channels') is not None ): - channels_collection = data['allowed_channels'] + data['disallowed_channels'] + channels_collection = data['disabled_channels'] + data['enabled_channels'] if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Allowed and disallowed channels lists contain duplicates.") + raise ValidationError("Enabled and Disabled channels lists contain duplicates.") - if ( - data.get('allowed_categories') is not None - and data.get('disallowed_categories') is not None - ): - categories_collection = data['allowed_categories'] + data['disallowed_categories'] + if data.get('disabled_categories') is not None: + categories_collection = data['disabled_categories'] if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Allowed and disallowed categories lists contain duplicates.") + raise ValidationError("Disabled categories lists contain duplicates.") return data @@ -262,10 +260,9 @@ class FilterListSerializer(ModelSerializer): field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS } | { 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, - 'disallowed_channels': {'allow_empty': True}, - 'disallowed_categories': {'allow_empty': True}, - 'allowed_channels': {'allow_empty': True}, - 'allowed_categories': {'allow_empty': True}, + 'enabled_channels': {'allow_empty': True}, + 'disabled_channels': {'allow_empty': True}, + 'disabled_categories': {'allow_empty': True}, } # Ensure that we can only have one filter list with the same name and field @@ -283,7 +280,11 @@ class FilterListSerializer(ModelSerializer): """ Provides a custom JSON representation to the FilterList Serializers. - That does not affect how the Serializer works in general. + This representation restructures how the Filter is represented. + It groups the Infraction, Channel and Mention related fields into their own separated group. + + Furthermore, it puts the fields that meant to represent FilterList settings, + into a sub-field called `settings`. """ # Fetches the relating filters filters = [ @@ -301,7 +302,12 @@ class FilterListSerializer(ModelSerializer): {name: getattr(instance, name) for name in INFRACTION_FIELDS}} \ | { "channel_scope": - {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} + {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} | { + "mentions": { + schema_field_name: getattr(instance, schema_field_name) + for schema_field_name in MENTIONS_FIELDS + } + } return schema_base | {"settings": schema_settings_base | schema_settings_categories} diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index e8f3e3d9..20af079d 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -32,8 +32,6 @@ class FilterListViewSet(ModelViewSet): ... "description": "Python Discord", ... "additional_field": None, ... "settings": { - ... "ping_type": None, - ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, ... "enabled": False @@ -44,11 +42,14 @@ class FilterListViewSet(ModelViewSet): ... "infraction_duration": None ... }, ... "channel_scope": { - ... "allowed_channels": None, - ... "allowed_categories": None, - ... "disallowed_channels": None, - ... "disallowed_categories": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } ... } ... ... }, @@ -72,13 +73,18 @@ class FilterListViewSet(ModelViewSet): ... "infraction_reason": "", ... "infraction_duration": None, ... } - ... "channel_scope": { - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [] - ... } - ... } + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None + ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } + ... }, + ... ... + ... ] #### Status codes - 200: returned on success @@ -88,8 +94,7 @@ class FilterListViewSet(ModelViewSet): Returns a specific FilterList item from the database. #### Response format - >>> - ... { + >>> { ... "id": 1, ... "name": "guild_invite", ... "list_type": 1, @@ -101,8 +106,6 @@ class FilterListViewSet(ModelViewSet): ... "description": "Python Discord", ... "additional_field": None, ... "settings": { - ... "ping_type": None, - ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, ... "enabled": False @@ -113,15 +116,18 @@ class FilterListViewSet(ModelViewSet): ... "infraction_duration": None ... }, ... "channel_scope": { - ... "allowed_channels": None, - ... "allowed_categories": None, - ... "disallowed_channels": None, - ... "disallowed_categories": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } ... } ... ... }, - ... + ... ... ... ], ... "settings": { ... "ping_type": [ @@ -141,14 +147,16 @@ class FilterListViewSet(ModelViewSet): ... "infraction_reason": "", ... "infraction_duration": None, ... } - ... "channel_scope": { - ... "disallowed_channels": [], - ... "disallowed_categories": [], - ... "allowed_channels": [], - ... "allowed_categories": [] - ... } - ... } - ... } + ... "channel_scope": { + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None + ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } + ... } #### Status codes - 200: returned on success @@ -183,8 +191,6 @@ class FilterViewSet(ModelViewSet): ... "description": "Python Discord", ... "additional_field": None, ... "settings": { - ... "ping_type": None, - ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, ... "enabled": False @@ -195,11 +201,14 @@ class FilterViewSet(ModelViewSet): ... "infraction_duration": None ... }, ... "channel_scope": { - ... "allowed_channels": None, - ... "allowed_categories": None, - ... "disallowed_channels": None, - ... "disallowed_categories": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None ... } + ... "mentions": { + ... "ping_type": None, + ... "dm_ping_type": None + ... } ... } ... }, ... ... @@ -220,8 +229,6 @@ class FilterViewSet(ModelViewSet): ... "description": "Python Discord", ... "additional_field": None, ... "settings": { - ... "ping_type": None, - ... "dm_ping_type": None ... "bypass_roles": None ... "filter_dm": None, ... "enabled": False @@ -232,11 +239,14 @@ class FilterViewSet(ModelViewSet): ... "infraction_duration": None ... }, ... "channel_scope": { - ... "allowed_channels": None, - ... "allowed_categories": None, - ... "disallowed_channels": None, - ... "disallowed_categories": None + ... "disabled_channels": None, + ... "disabled_categories": None, + ... "enabled_channels": None, ... } + ... "mentions": { + ... "ping_type": None + ... "dm_ping_type": None + ... } ... } ... } -- cgit v1.2.3 From af3980fe65b997287ceaf68e53ce3ab7bf4607e5 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Wed, 22 Dec 2021 18:18:22 +0100 Subject: Patch Filter/FilterList's default values and add new fields - Patch default values, so that further implementations can be performed on the bot side - Add three new fields: "send_alert", and in settings under the "server_message" field: "send_message_text", and "server_message_embed" fields. - Patch documentation, and validators accordingly. - Perform further patches, and minor corrections. --- .../apps/api/migrations/0070_new_filter_schema.py | 28 ++++-- ..._filter_and_filterlist_for_new_filter_schema.py | 17 +++- .../api/migrations/0078_merge_20211218_2200.py | 14 +++ .../0079_add_server_message_and_alert_fields.py | 69 ++++++++++++++ pydis_site/apps/api/models/bot/filters.py | 44 ++++++++- pydis_site/apps/api/serializers.py | 19 +++- pydis_site/apps/api/viewsets/bot/filters.py | 104 ++++++++++++--------- 7 files changed, 234 insertions(+), 61 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0078_merge_20211218_2200.py create mode 100644 pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py index 8716cbad..f56c29f8 100644 --- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py @@ -1,4 +1,5 @@ # Modified migration file to migrate existing filters to the new one +from datetime import timedelta import django.contrib.postgres.fields from django.apps.registry import Apps @@ -18,20 +19,27 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: for name, type_ in OLD_LIST_NAMES: objects = filter_list_old.objects.filter(type=name) + if name == "DOMAIN_NAME": + dm_content = "Your URL has been removed because it matched a blacklisted domain: {match}" + elif name == "GUILD_INVITE": + dm_content = "Per Rule 6, your invite link has been removed. " \ + "Our server rules can be found here: https://pythondiscord.com/pages/rules" + else: + dm_content = "" list_ = filter_list.objects.create( name=name.lower(), list_type=1 if type_ == "ALLOW" else 0, - ping_type=["onduty"], + ping_type=(["onduty"] if name != "FILE_FORMAT" else []), filter_dm=True, - dm_ping_type=["onduty"], - delete_messages=True, - bypass_roles=[267630620367257601], - enabled=False, - dm_content=None, - infraction_type=None, + dm_ping_type=[], + delete_messages=(True if name != "FILTER_TOKEN" else False), + bypass_roles=["staff"], + enabled=True, + dm_content=dm_content, + infraction_type="", infraction_reason="", - infraction_duration=None, + infraction_duration=timedelta(seconds=0), disallowed_channels=[], disallowed_categories=[], allowed_channels=[], @@ -84,7 +92,7 @@ class Migration(migrations.Migration): ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field], null=True)), ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None, null=True)), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field], null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), @@ -106,7 +114,7 @@ class Migration(migrations.Migration): ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), ('dm_ping_type', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=20), help_text='Who to ping when this filter triggers on a DM.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_ping_field])), ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), - ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(), help_text='Roles and users who can bypass this filter.', size=None)), + ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, validators=[pydis_site.apps.api.models.bot.filters.validate_bypass_roles_field])), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), ('infraction_type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The infraction to apply to this user.', max_length=9, null=True)), diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py index 30537e3d..cc524fcb 100644 --- a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py @@ -10,12 +10,26 @@ def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N "filter_token": "tokens", "domain_name": "domains", "guild_invite": "invites", - "file_format": "formats" + "file_format": "extensions" } for filter_list in FilterList.objects.all(): if change_map.get(filter_list.name): filter_list.name = change_map.get(filter_list.name) filter_list.save() + redirects = FilterList( + name="redirects", + ping_type=[], + dm_ping_type=[], + enabled_channels=[], + disabled_channels=[], + disabled_categories=[], + list_type=0, + filter_dm=True, + delete_messages=False, + bypass_roles=[0], + enabled=True + ) + redirects.save() def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: @@ -30,6 +44,7 @@ def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> if change_map.get(filter_list.name): filter_list.name = change_map.get(filter_list.name) filter_list.save() + FilterList.objects.filter(name="redirects").delete() class Migration(migrations.Migration): diff --git a/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py b/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py new file mode 100644 index 00000000..7fe559f5 --- /dev/null +++ b/pydis_site/apps/api/migrations/0078_merge_20211218_2200.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.14 on 2021-12-18 22:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0075_prepare_filter_and_filterlist_for_new_filter_schema'), + ('api', '0077_use_generic_jsonfield'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py b/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py new file mode 100644 index 00000000..f9803bd3 --- /dev/null +++ b/pydis_site/apps/api/migrations/0079_add_server_message_and_alert_fields.py @@ -0,0 +1,69 @@ +# Generated by Django 3.1.14 on 2021-12-19 23:05 +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + change_map = { + "tokens": True, + "domains": True, + "invites": True, + "extensions": False, + "redirects": False + } + for filter_list in FilterList.objects.all(): + filter_list.send_alert = change_map.get(filter_list.name) + filter_list.server_message_text = "" + filter_list.server_message_embed = "" + filter_list.save() + + +def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + FilterList = apps.get_model("api", "FilterList") + for filter_list in FilterList.objects.all(): + filter_list.send_alert = True + filter_list.server_message_text = None + filter_list.server_message_embed = None + filter_list.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0078_merge_20211218_2200'), + ] + + operations = [ + migrations.AddField( + model_name='filter', + name='send_alert', + field=models.BooleanField(help_text='Whether alert should be sent.', null=True), + ), + migrations.AddField( + model_name='filter', + name='server_message_embed', + field=models.CharField(help_text='The content of the server message embed', max_length=100, null=True), + ), + migrations.AddField( + model_name='filter', + name='server_message_text', + field=models.CharField(help_text='The message to send on the server', max_length=100, null=True), + ), + migrations.AddField( + model_name='filterlist', + name='send_alert', + field=models.BooleanField(default=True, help_text='Whether alert should be sent.'), + ), + migrations.AddField( + model_name='filterlist', + name='server_message_embed', + field=models.CharField(help_text='The content of the server message embed', max_length=100, null=True), + ), + migrations.AddField( + model_name='filterlist', + name='server_message_text', + field=models.CharField(help_text='The message to send on the server', max_length=100, null=True), + ), + migrations.RunPython(migrate_filterlist, unmigrate_filterlist) + ] diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index ae877685..92251ee4 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -18,6 +18,7 @@ class FilterListType(models.IntegerChoices): # Valid special values in ping related fields VALID_PINGS = ("everyone", "here", "moderators", "onduty", "admins") +VALID_BYPASS_ROLES = ("staff",) def validate_ping_field(value_list: List[str]) -> None: @@ -33,6 +34,14 @@ def validate_ping_field(value_list: List[str]) -> None: raise ValidationError(f"{value!r} isn't a valid ping type.") +def validate_bypass_roles_field(value_list: List[str]) -> None: + """Validate that the vclues are either a special value or a Role ID.""" + for value in value_list: + if value.isnumeric() or value in VALID_BYPASS_ROLES: + continue + raise ValidationError(f"{value!r} isn't a valid (bypass) role.") + + class FilterSettingsMixin(models.Model): """Mixin for common settings of a filters and filter lists.""" @@ -88,14 +97,30 @@ class FilterList(FilterSettingsMixin): null=False ) bypass_roles = ArrayField( - models.BigIntegerField(), + models.CharField(max_length=100), help_text="Roles and users who can bypass this filter.", + validators=(validate_bypass_roles_field,), null=False ) enabled = models.BooleanField( help_text="Whether this filter is currently enabled.", null=False ) + send_alert = models.BooleanField( + help_text="Whether alert should be sent.", + null=False, + default=True + ) + server_message_text = models.CharField( + max_length=100, + help_text="The message to send on the server", + null=True + ) + server_message_embed = models.CharField( + max_length=100, + help_text="The content of the server message embed", + null=True + ) # Where a filter should apply. # # The resolution is done in the following order: @@ -145,14 +170,29 @@ class Filter(FilterSettingsMixin): null=True ) bypass_roles = ArrayField( - models.BigIntegerField(), + models.CharField(max_length=100), help_text="Roles and users who can bypass this filter.", + validators=(validate_bypass_roles_field,), null=True ) enabled = models.BooleanField( help_text="Whether this filter is currently enabled.", null=True ) + send_alert = models.BooleanField( + help_text="Whether alert should be sent.", + null=True + ) + server_message_text = models.CharField( + max_length=100, + help_text="The message to send on the server", + null=True + ) + server_message_embed = models.CharField( + max_length=100, + help_text="The content of the server message embed", + null=True + ) # Check FilterList model for information about these properties. enabled_channels = ArrayField(models.IntegerField(), null=True) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 784f8160..30af9512 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -139,7 +139,8 @@ BASE_SETTINGS_FIELDS = ( "bypass_roles", "filter_dm", "enabled", - "delete_messages" + "delete_messages", + "send_alert" ) INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") CHANNEL_SCOPE_FIELDS = ( @@ -147,6 +148,7 @@ CHANNEL_SCOPE_FIELDS = ( "disabled_categories", "enabled_channels", ) +SERVER_MESSAGE_FIELDS = ("server_message_text", "server_message_embed") MENTIONS_FIELDS = ("ping_type", "dm_ping_type") SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS @@ -214,10 +216,16 @@ class FilterSerializer(ModelSerializer): "mentions": { schema_field_name: getattr(instance, schema_field_name) - for schema_field_name in MENTIONS_FIELDS} + for schema_field_name in MENTIONS_FIELDS + } } + } | { + "server_message": + { + schema_field_name: getattr(instance, schema_field_name) + for schema_field_name in SERVER_MESSAGE_FIELDS + } } - schema_base = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} | \ {"filter_list": instance.filter_list.id} @@ -307,6 +315,11 @@ class FilterListSerializer(ModelSerializer): schema_field_name: getattr(instance, schema_field_name) for schema_field_name in MENTIONS_FIELDS } + } | { + "server_message": { + schema_field_name: getattr(instance, schema_field_name) + for schema_field_name in SERVER_MESSAGE_FIELDS + } } return schema_base | {"settings": schema_settings_base | schema_settings_categories} diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index 20af079d..e52cd4e5 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -22,20 +22,21 @@ class FilterListViewSet(ModelViewSet): >>> [ ... { ... "id": 1, - ... "name": "guild_invite", + ... "name": "invites", ... "list_type": 1, ... "filters": [ ... { ... "id": 1, - ... "filter_list": 1 ... "content": "267624335836053506", ... "description": "Python Discord", ... "additional_field": None, + ... "filter_list": 1 ... "settings": { ... "bypass_roles": None ... "filter_dm": None, - ... "enabled": False - ... "delete_messages": True + ... "enabled": None + ... "send_alert": True, + ... "delete_messages": None ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -50,37 +51,42 @@ class FilterListViewSet(ModelViewSet): ... "ping_type": None ... "dm_ping_type": None ... } + ... "server_message": { + ... "server_message_text": None, + ... "server_message_embed": None + ... } ... } ... ... }, ... ... ... ], ... "settings": { - ... "ping_type": [ - ... "onduty" - ... ], - ... "dm_ping_type": [ - ... "onduty" - ... ], ... "bypass_roles": [ - ... 267630620367257601 + ... "staff" ... ], ... "filter_dm": True, - ... "enabled": False - ... "delete_messages": True + ... "enabled": True + ... "delete_messages": True, + ... "send_alert": True ... "infraction": { - ... "infraction_type": None, + ... "infraction_type": "", ... "infraction_reason": "", - ... "infraction_duration": None, + ... "infraction_duration": "0.0", ... } ... "channel_scope": { - ... "disabled_channels": None, - ... "disabled_categories": None, - ... "enabled_channels": None - ... } + ... "disabled_channels": [], + ... "disabled_categories": [], + ... "enabled_channels": [] + ... } ... "mentions": { - ... "ping_type": None - ... "dm_ping_type": None + ... "ping_type": [ + ... "onduty" + ... ] + ... "dm_ping_type": [] + ... } + ... "server_message": { + ... "server_message_text": "", + ... "server_message_embed": "" ... } ... }, ... ... @@ -96,7 +102,7 @@ class FilterListViewSet(ModelViewSet): #### Response format >>> { ... "id": 1, - ... "name": "guild_invite", + ... "name": "invites", ... "list_type": 1, ... "filters": [ ... { @@ -108,8 +114,9 @@ class FilterListViewSet(ModelViewSet): ... "settings": { ... "bypass_roles": None ... "filter_dm": None, - ... "enabled": False - ... "delete_messages": True + ... "enabled": None + ... "delete_messages": None, + ... "send_alert": None ... "infraction": { ... "infraction_type": None, ... "infraction_reason": "", @@ -124,37 +131,42 @@ class FilterListViewSet(ModelViewSet): ... "ping_type": None ... "dm_ping_type": None ... } + ... "server_message": { + ... "server_message_text": None, + ... "server_message_embed": None + ... } ... } ... ... }, ... ... ... ], ... "settings": { - ... "ping_type": [ - ... "onduty" - ... ], - ... "dm_ping_type": [ - ... "onduty" - ... ], ... "bypass_roles": [ - ... 267630620367257601 + ... "staff" ... ], ... "filter_dm": True, - ... "enabled": False + ... "enabled": True ... "delete_messages": True + ... "send_alert": True ... "infraction": { - ... "infraction_type": None, + ... "infraction_type": "", ... "infraction_reason": "", - ... "infraction_duration": None, + ... "infraction_duration": "0.0", ... } ... "channel_scope": { - ... "disabled_channels": None, - ... "disabled_categories": None, - ... "enabled_channels": None + ... "disabled_channels": [], + ... "disabled_categories": [], + ... "enabled_channels": [] ... } ... "mentions": { - ... "ping_type": None - ... "dm_ping_type": None + ... "ping_type": [ + ... "onduty" + ... ] + ... "dm_ping_type": [] + ... } + ... "server_message": { + ... "server_message_text": "", + ... "server_message_embed": "" ... } ... } @@ -193,11 +205,12 @@ class FilterViewSet(ModelViewSet): ... "settings": { ... "bypass_roles": None ... "filter_dm": None, - ... "enabled": False - ... "delete_messages": True + ... "enabled": None + ... "delete_messages": True, + ... "send_alert": True ... "infraction": { ... "infraction_type": None, - ... "infraction_reason": "", + ... "infraction_reason": None, ... "infraction_duration": None ... }, ... "channel_scope": { @@ -231,11 +244,12 @@ class FilterViewSet(ModelViewSet): ... "settings": { ... "bypass_roles": None ... "filter_dm": None, - ... "enabled": False - ... "delete_messages": True + ... "enabled": None + ... "delete_messages": True, + ... "send_alert": True ... "infraction": { ... "infraction_type": None, - ... "infraction_reason": "", + ... "infraction_reason": None, ... "infraction_duration": None ... }, ... "channel_scope": { -- cgit v1.2.3 From c2aaa8d672484a698b8aec6a65c2f4af3cff18b1 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:15:52 +0100 Subject: Include 'dm_content ' field under Infraction settings in Filters/FilterLists --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 30af9512..66236d92 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -142,7 +142,7 @@ BASE_SETTINGS_FIELDS = ( "delete_messages", "send_alert" ) -INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration") +INFRACTION_FIELDS = ("infraction_type", "infraction_reason", "infraction_duration", "dm_content") CHANNEL_SCOPE_FIELDS = ( "disabled_channels", "disabled_categories", -- cgit v1.2.3 From 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/serializers.py') 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 7d22d8427fa73e6209ffcea827d9e460b6c1d985 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Tue, 15 Feb 2022 22:59:04 +0100 Subject: Patch a minor issue with FilterList field naming in migrations --- ...75_prepare_filter_and_filterlist_for_new_filter_schema.py | 12 ++++++------ .../apps/api/migrations/0079_dm_embed_and_alert_fields.py | 10 +++++----- pydis_site/apps/api/serializers.py | 3 ++- 3 files changed, 13 insertions(+), 12 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py index 2a85fa63..1e24b379 100644 --- a/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0075_prepare_filter_and_filterlist_for_new_filter_schema.py @@ -17,7 +17,7 @@ def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N filter_list.name = change_map.get(filter_list.name) filter_list.save() redirects = FilterList( - name="redirects", + name="redirect", ping_type=[], dm_ping_type=[], enabled_channels=[], @@ -35,16 +35,16 @@ def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N def unmigrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: FilterList = apps.get_model("api", "FilterList") change_map = { - "tokens": "filter_token", - "domains": "domain_name", - "invites": "guild_invite", - "formats": "file_format" + "token": "filter_token", + "domain": "domain_name", + "invite": "guild_invite", + "format": "file_format" } for filter_list in FilterList.objects.all(): if change_map.get(filter_list.name): filter_list.name = change_map.get(filter_list.name) filter_list.save() - FilterList.objects.filter(name="redirects").delete() + FilterList.objects.filter(name="redirect").delete() class Migration(migrations.Migration): diff --git a/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py b/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py index 49da62b6..cae175df 100644 --- a/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py +++ b/pydis_site/apps/api/migrations/0079_dm_embed_and_alert_fields.py @@ -7,11 +7,11 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor def migrate_filterlist(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: FilterList = apps.get_model("api", "FilterList") change_map = { - "tokens": True, - "domains": True, - "invites": True, - "extensions": False, - "redirects": False + "token": True, + "domain": True, + "invite": True, + "extension": False, + "redirect": False } for filter_list in FilterList.objects.all(): filter_list.send_alert = change_map.get(filter_list.name) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 88b6e2bd..99f2b630 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -215,7 +215,8 @@ class FilterSerializer(ModelSerializer): {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} | { "infraction_and_notification": - {name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS} + {name: getattr(instance, name) + for name in INFRACTION_AND_NOTIFICATION_FIELDS} } | { "channel_scope": {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} -- cgit v1.2.3 From 26e4f518c874cafdee594c08c01d610e88528dc7 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 20 Feb 2022 17:43:54 +0100 Subject: Prevent race condition with duplicate infractions DRF's `UniqueTogetherValidator` validates uniqueness by querying the database before running the actual insert. This is not, has not, and will never be valid, unless you happen to run a single worker, on a single thread, and your single worker running on a single thread is the only client for the database, in which case it may be valid. For any other cases, it's invalid, and it has never been valid. PostgreSQL spits out an `IntegrityError` for us if we have a duplicate entry, and PostgreSQL is the only valid and correct thing to trust here. The `UniqueTogetherValidator` is removed, and an existing test case calling into this validator to check for uniqueness is removed. Furthermore, to work around a Django quirk, `transaction.atomic()` is added to prevent one `subTest` from messing with another. Closes #665. --- pydis_site/apps/api/serializers.py | 7 --- pydis_site/apps/api/tests/test_infractions.py | 77 +++++++++++--------------- pydis_site/apps/api/viewsets/bot/infraction.py | 18 ++++++ 3 files changed, 50 insertions(+), 52 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 4a702d61..745aff42 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -156,13 +156,6 @@ class InfractionSerializer(ModelSerializer): 'hidden', 'dm_sent' ) - validators = [ - UniqueTogetherValidator( - queryset=Infraction.objects.filter(active=True), - fields=['user', 'type', 'active'], - message='This user already has an active infraction of this type.', - ) - ] def validate(self, attrs: dict) -> dict: """Validate data constraints for the given data and abort if it is invalid.""" diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index b3dd16ee..aa0604f6 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -3,6 +3,7 @@ from datetime import datetime as dt, timedelta, timezone from unittest.mock import patch from urllib.parse import quote +from django.db import transaction from django.db.utils import IntegrityError from django.urls import reverse @@ -492,6 +493,7 @@ class CreationTests(AuthenticatedAPITestCase): ) for infraction_type, hidden in restricted_types: + # https://stackoverflow.com/a/23326971 with self.subTest(infraction_type=infraction_type): invalid_infraction = { 'user': self.user.id, @@ -516,37 +518,38 @@ class CreationTests(AuthenticatedAPITestCase): for infraction_type in active_infraction_types: with self.subTest(infraction_type=infraction_type): - first_active_infraction = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': infraction_type, - 'reason': 'Take me on!', - 'active': True, - 'expires_at': '2019-10-04T12:52:00+00:00' - } - - # Post the first active infraction of a type and confirm it's accepted. - first_response = self.client.post(url, data=first_active_infraction) - self.assertEqual(first_response.status_code, 201) + with transaction.atomic(): + first_active_infraction = { + 'user': self.user.id, + 'actor': self.user.id, + 'type': infraction_type, + 'reason': 'Take me on!', + 'active': True, + 'expires_at': '2019-10-04T12:52:00+00:00' + } - second_active_infraction = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': infraction_type, - 'reason': 'Take on me!', - 'active': True, - 'expires_at': '2019-10-04T12:52:00+00:00' - } - second_response = self.client.post(url, data=second_active_infraction) - self.assertEqual(second_response.status_code, 400) - self.assertEqual( - second_response.json(), - { - 'non_field_errors': [ - 'This user already has an active infraction of this type.' - ] + # Post the first active infraction of a type and confirm it's accepted. + first_response = self.client.post(url, data=first_active_infraction) + self.assertEqual(first_response.status_code, 201) + + second_active_infraction = { + 'user': self.user.id, + 'actor': self.user.id, + 'type': infraction_type, + 'reason': 'Take on me!', + 'active': True, + 'expires_at': '2019-10-04T12:52:00+00:00' } - ) + second_response = self.client.post(url, data=second_active_infraction) + self.assertEqual(second_response.status_code, 400) + self.assertEqual( + second_response.json(), + { + 'non_field_errors': [ + 'This user already has an active infraction of this type.' + ] + } + ) def test_returns_201_for_second_active_infraction_of_different_type(self): """Test if the API accepts a second active infraction of a different type than the first.""" @@ -811,22 +814,6 @@ class SerializerTests(AuthenticatedAPITestCase): self.assertTrue(serializer.is_valid(), msg=serializer.errors) - def test_validation_error_if_active_duplicate(self): - self.create_infraction('ban', active=True) - instance = self.create_infraction('ban', active=False) - - data = {'active': True} - serializer = InfractionSerializer(instance, data=data, partial=True) - - if not serializer.is_valid(): - self.assertIn('non_field_errors', serializer.errors) - - code = serializer.errors['non_field_errors'][0].code - msg = f'Expected failure on unique validator but got {serializer.errors}' - self.assertEqual(code, 'unique', msg=msg) - else: # pragma: no cover - self.fail('Validation unexpectedly succeeded.') - def test_is_valid_for_new_active_infraction(self): self.create_infraction('ban', active=False) diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index 8a48ed1f..31e8ba40 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -1,5 +1,6 @@ from datetime import datetime +from django.db import IntegrityError from django.db.models import QuerySet from django.http.request import HttpRequest from django_filters.rest_framework import DjangoFilterBackend @@ -271,3 +272,20 @@ class InfractionViewSet( """ self.serializer_class = ExpandedInfractionSerializer return self.partial_update(*args, **kwargs) + + def create(self, request: HttpRequest, *args, **kwargs) -> Response: + """ + Create an infraction for a target user. + + Called by the Django Rest Framework in response to the corresponding HTTP request. + """ + try: + return super().create(request, *args, **kwargs) + except IntegrityError: + raise ValidationError( + { + 'non_field_errors': [ + 'This user already has an active infraction of this type.', + ] + } + ) -- cgit v1.2.3 From a6b8c27e68b529b1060b1213b465457c5c0d685a Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Mon, 7 Mar 2022 20:18:18 +0100 Subject: Add support for storing AoC related data in site --- .../apps/api/migrations/0080_add_aoc_tables.py | 33 +++++++++++ pydis_site/apps/api/models/__init__.py | 2 + pydis_site/apps/api/models/bot/__init__.py | 2 + .../apps/api/models/bot/aoc_completionist_block.py | 21 +++++++ pydis_site/apps/api/models/bot/aoc_link.py | 20 +++++++ pydis_site/apps/api/serializers.py | 22 +++++++ pydis_site/apps/api/urls.py | 10 ++++ pydis_site/apps/api/viewsets/__init__.py | 2 + pydis_site/apps/api/viewsets/bot/__init__.py | 2 + .../api/viewsets/bot/aoc_completionist_block.py | 69 ++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/aoc_link.py | 69 ++++++++++++++++++++++ 11 files changed, 252 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0080_add_aoc_tables.py create mode 100644 pydis_site/apps/api/models/bot/aoc_completionist_block.py create mode 100644 pydis_site/apps/api/models/bot/aoc_link.py create mode 100644 pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py create mode 100644 pydis_site/apps/api/viewsets/bot/aoc_link.py (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0080_add_aoc_tables.py b/pydis_site/apps/api/migrations/0080_add_aoc_tables.py new file mode 100644 index 00000000..f129d86f --- /dev/null +++ b/pydis_site/apps/api/migrations/0080_add_aoc_tables.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.14 on 2022-03-06 16:07 + +from django.db import migrations, models +import django.db.models.deletion +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0079_merge_20220125_2022'), + ] + + operations = [ + migrations.CreateModel( + name='AocCompletionistBlock', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_blocked', models.BooleanField(default=True, help_text='Whether this user is actively being blocked from getting the AoC Completionist Role', verbose_name='Blocked')), + ('user', models.ForeignKey(help_text='The user that is blocked from getting the AoC Completionist Role', on_delete=django.db.models.deletion.CASCADE, to='api.user')), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.CreateModel( + name='AocAccountLink', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('aoc_username', models.CharField(help_text='The AoC username associated with the Discord User.', max_length=120)), + ('user', models.ForeignKey(help_text='The user that is blocked from getting the AoC Completionist Role', on_delete=django.db.models.deletion.CASCADE, to='api.user')), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index fd5bf220..4f616986 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -10,6 +10,8 @@ from .bot import ( Nomination, NominationEntry, OffensiveMessage, + AocAccountLink, + AocCompletionistBlock, OffTopicChannelName, Reminder, Role, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index ac864de3..ec0e701c 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -5,6 +5,8 @@ from .deleted_message import DeletedMessage from .documentation_link import DocumentationLink from .infraction import Infraction from .message import Message +from .aoc_completionist_block import AocCompletionistBlock +from .aoc_link import AocAccountLink from .message_deletion_context import MessageDeletionContext from .nomination import Nomination, NominationEntry from .off_topic_channel_name import OffTopicChannelName diff --git a/pydis_site/apps/api/models/bot/aoc_completionist_block.py b/pydis_site/apps/api/models/bot/aoc_completionist_block.py new file mode 100644 index 00000000..cac41ff1 --- /dev/null +++ b/pydis_site/apps/api/models/bot/aoc_completionist_block.py @@ -0,0 +1,21 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.mixins import ModelReprMixin + + +class AocCompletionistBlock(ModelReprMixin, models.Model): + """A Discord user blocked from getting the AoC completionist Role.""" + + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The user that is blocked from getting the AoC Completionist Role" + ) + + is_blocked = models.BooleanField( + default=True, + help_text="Whether this user is actively being blocked " + "from getting the AoC Completionist Role", + verbose_name="Blocked" + ) diff --git a/pydis_site/apps/api/models/bot/aoc_link.py b/pydis_site/apps/api/models/bot/aoc_link.py new file mode 100644 index 00000000..6c7cc591 --- /dev/null +++ b/pydis_site/apps/api/models/bot/aoc_link.py @@ -0,0 +1,20 @@ +from django.db import models + +from pydis_site.apps.api.models.bot.user import User +from pydis_site.apps.api.models.mixins import ModelReprMixin + + +class AocAccountLink(ModelReprMixin, models.Model): + """An AoC account link for a Discord User.""" + + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="The user that is blocked from getting the AoC Completionist Role" + ) + + aoc_username = models.CharField( + max_length=120, + help_text="The AoC username associated with the Discord User.", + blank=False + ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 745aff42..0b0e4237 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -13,6 +13,8 @@ from rest_framework.settings import api_settings from rest_framework.validators import UniqueTogetherValidator from .models import ( + AocAccountLink, + AocCompletionistBlock, BotSetting, DeletedMessage, DocumentationLink, @@ -250,6 +252,26 @@ class ReminderSerializer(ModelSerializer): ) +class AocCompletionistBlockSerializer(ModelSerializer): + """A class providing (de-)serialization of `AocCompletionistBlock` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = AocCompletionistBlock + fields = ("user", "is_blocked") + + +class AocAccountLinkSerializer(ModelSerializer): + """A class providing (de-)serialization of `AocAccountLink` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = AocAccountLink + fields = ("user", "aoc_username") + + class RoleSerializer(ModelSerializer): """A class providing (de-)serialization of `Role` instances.""" diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index b0ab545b..7c55fc92 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -3,6 +3,8 @@ from rest_framework.routers import DefaultRouter from .views import HealthcheckView, RulesView from .viewsets import ( + AocAccountLinkViewSet, + AocCompletionistBlockViewSet, BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, @@ -34,6 +36,14 @@ bot_router.register( 'documentation-links', DocumentationLinkViewSet ) +bot_router.register( + "aoc-account-links", + AocAccountLinkViewSet +) +bot_router.register( + "aoc-completionist-blocks", + AocCompletionistBlockViewSet +) bot_router.register( 'infractions', InfractionViewSet diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index f133e77f..5fc1d64f 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -7,6 +7,8 @@ from .bot import ( InfractionViewSet, NominationViewSet, OffensiveMessageViewSet, + AocAccountLinkViewSet, + AocCompletionistBlockViewSet, OffTopicChannelNameViewSet, ReminderViewSet, RoleViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index 84b87eab..f1d84729 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -7,6 +7,8 @@ from .infraction import InfractionViewSet from .nomination import NominationViewSet from .off_topic_channel_name import OffTopicChannelNameViewSet from .offensive_message import OffensiveMessageViewSet +from .aoc_link import AocAccountLinkViewSet +from .aoc_completionist_block import AocCompletionistBlockViewSet from .reminder import ReminderViewSet from .role import RoleViewSet from .user import UserViewSet diff --git a/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py new file mode 100644 index 00000000..53bcb546 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py @@ -0,0 +1,69 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.mixins import ( + CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot import AocCompletionistBlock +from pydis_site.apps.api.serializers import AocCompletionistBlockSerializer + + +class AocCompletionistBlockViewSet( + GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin +): + """ + View providing management for Users blocked from gettign the AoC completionist Role. + + ## Routes + + ### GET /bot/aoc-completionist-blocks/ + Returns all the AoC completionist blocks + + #### Response format + >>> [ + ... { + ... "user": 2, + ... "is_blocked": False + ... } + ... ] + + + ### GET /bot/aoc-completionist-blocks/ + Retrieve a single Block by User ID + + #### Response format + >>> + ... { + ... "user": 2, + ... "is_blocked": False + ... } + + #### Status codes + - 200: returned on success + - 404: returned if an AoC completionist block with the given user__id was not found. + + ### POST /bot/aoc-completionist-blocks + Adds a single AoC completionist block + + #### Request body + >>> { + ... 'user': int, + ... 'is_blocked': bool + ... } + + #### Status codes + - 204: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/aoc-completionist-blocks/ + Deletes the AoC Completionist block item with the given `user__id`. + #### Status codes + - 204: returned on success + - 404: if the AoC Completionist block with the given user__id does not exist + + """ + + serializer_class = AocCompletionistBlockSerializer + queryset = AocCompletionistBlock.objects.all() + filter_backends = (DjangoFilterBackend,) + filter_fields = ("user__id",) diff --git a/pydis_site/apps/api/viewsets/bot/aoc_link.py b/pydis_site/apps/api/viewsets/bot/aoc_link.py new file mode 100644 index 00000000..b5b5420e --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/aoc_link.py @@ -0,0 +1,69 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.mixins import ( + CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot import AocAccountLink +from pydis_site.apps.api.serializers import AocAccountLinkSerializer + + +class AocAccountLinkViewSet( + GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin +): + """ + View providing management for Users who linked their AoC accounts to their Discord Account. + + ## Routes + + ### GET /bot/aoc-account-links + Returns all the AoC account links + + #### Response format + >>> [ + ... { + ... "user": 2, + ... "aoc_username": "AoCUser1" + ... } + ... ] + + + ### GET /bot/aoc-account-links + Retrieve a AoC account link by User ID + + #### Response format + >>> + ... { + ... "user": 2, + ... "aoc_username": "AoCUser1" + ... } + + #### Status codes + - 200: returned on success + - 404: returned if an AoC account link with the given user__id was not found. + + ### POST /bot/aoc-account-links + Adds a single AoC account link block + + #### Request body + >>> { + ... 'user': int, + ... 'aoc_username': str + ... } + + #### Status codes + - 204: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/aoc-account-links/ + Deletes the AoC account link item with the given `user__id`. + #### Status codes + - 204: returned on success + - 404: if the AoC account link with the given user__id does not exist + + """ + + serializer_class = AocAccountLinkSerializer + queryset = AocAccountLink.objects.all() + filter_backends = (DjangoFilterBackend,) + filter_fields = ("user__id",) -- 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/serializers.py') 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 b93dce5abcf225579b9407358f938ca3932e67a2 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Wed, 9 Mar 2022 19:37:30 +0100 Subject: Add reason field to AoC completionist block table --- pydis_site/apps/api/migrations/0080_add_aoc_tables.py | 1 + pydis_site/apps/api/models/bot/aoc_completionist_block.py | 4 ++++ pydis_site/apps/api/serializers.py | 2 +- pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py | 7 +++++-- 4 files changed, 11 insertions(+), 3 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0080_add_aoc_tables.py b/pydis_site/apps/api/migrations/0080_add_aoc_tables.py index c58a5d29..917c5b7f 100644 --- a/pydis_site/apps/api/migrations/0080_add_aoc_tables.py +++ b/pydis_site/apps/api/migrations/0080_add_aoc_tables.py @@ -17,6 +17,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('is_blocked', models.BooleanField(default=True, help_text='Whether this user is actively being blocked from getting the AoC Completionist Role', verbose_name='Blocked')), + ('reason', models.TextField(help_text='The reason for the AoC Completionist Role Block.', null=True)), ('user', models.OneToOneField(help_text='The user that is blocked from getting the AoC Completionist Role', on_delete=django.db.models.deletion.CASCADE, to='api.user')), ], bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), diff --git a/pydis_site/apps/api/models/bot/aoc_completionist_block.py b/pydis_site/apps/api/models/bot/aoc_completionist_block.py index a89f9760..6605cbc4 100644 --- a/pydis_site/apps/api/models/bot/aoc_completionist_block.py +++ b/pydis_site/apps/api/models/bot/aoc_completionist_block.py @@ -19,3 +19,7 @@ class AocCompletionistBlock(ModelReprMixin, models.Model): "from getting the AoC Completionist Role", verbose_name="Blocked" ) + reason = models.TextField( + null=True, + help_text="The reason for the AoC Completionist Role Block." + ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0b0e4237..c97f7dba 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -259,7 +259,7 @@ class AocCompletionistBlockSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = AocCompletionistBlock - fields = ("user", "is_blocked") + fields = ("user", "is_blocked", "reason") class AocAccountLinkSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py index c5568129..8e7d821c 100644 --- a/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py +++ b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py @@ -24,6 +24,7 @@ class AocCompletionistBlockViewSet( ... { ... "user": 2, ... "is_blocked": False + ... "reason": "Too good to be true" ... } ... ] @@ -36,6 +37,7 @@ class AocCompletionistBlockViewSet( ... { ... "user": 2, ... "is_blocked": False + ... "reason": "Too good to be true" ... } #### Status codes @@ -47,8 +49,9 @@ class AocCompletionistBlockViewSet( #### Request body >>> { - ... 'user': int, - ... 'is_blocked': bool + ... "user": int, + ... "is_blocked": bool + ... "reason": string ... } #### Status codes -- cgit v1.2.3 From 0aed5f7913e7ce268ddb56127f84a5386ede5739 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 19 Feb 2022 17:59:26 +0000 Subject: Add support for BumpedThreads to be stored in site Following our move to use Redis as just a cache, this PR allows the site to store a list of threads that need to be bumped. The bot will interact with this within the ThreadBumper cog. --- .../apps/api/migrations/0081_bumpedthread.py | 22 ++++++++ pydis_site/apps/api/models/__init__.py | 1 + pydis_site/apps/api/models/bot/__init__.py | 1 + pydis_site/apps/api/models/bot/bumped_thread.py | 22 ++++++++ pydis_site/apps/api/serializers.py | 11 ++++ pydis_site/apps/api/urls.py | 5 ++ pydis_site/apps/api/viewsets/__init__.py | 1 + pydis_site/apps/api/viewsets/bot/__init__.py | 1 + pydis_site/apps/api/viewsets/bot/bumped_thread.py | 65 ++++++++++++++++++++++ 9 files changed, 129 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0081_bumpedthread.py create mode 100644 pydis_site/apps/api/models/bot/bumped_thread.py create mode 100644 pydis_site/apps/api/viewsets/bot/bumped_thread.py (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0081_bumpedthread.py b/pydis_site/apps/api/migrations/0081_bumpedthread.py new file mode 100644 index 00000000..03e66cc1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0081_bumpedthread.py @@ -0,0 +1,22 @@ +# Generated by Django 3.1.14 on 2022-02-19 16:26 + +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0080_add_aoc_tables'), + ] + + operations = [ + migrations.CreateModel( + name='BumpedThread', + fields=[ + ('thread_id', models.BigIntegerField(help_text='The thread ID that should be bumped.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Thread IDs cannot be negative.')], verbose_name='Thread ID')), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index e83473c9..a197e988 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa from .bot import ( BotSetting, + BumpedThread, DocumentationLink, DeletedMessage, FilterList, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 64676fdb..013bb85e 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa from .bot_setting import BotSetting +from .bumped_thread import BumpedThread from .deleted_message import DeletedMessage from .documentation_link import DocumentationLink from .filter_list import FilterList diff --git a/pydis_site/apps/api/models/bot/bumped_thread.py b/pydis_site/apps/api/models/bot/bumped_thread.py new file mode 100644 index 00000000..cdf9a950 --- /dev/null +++ b/pydis_site/apps/api/models/bot/bumped_thread.py @@ -0,0 +1,22 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from pydis_site.apps.api.models.mixins import ModelReprMixin + + +class BumpedThread(ModelReprMixin, models.Model): + """A list of thread IDs to be bumped.""" + + thread_id = models.BigIntegerField( + primary_key=True, + help_text=( + "The thread ID that should be bumped." + ), + validators=( + MinValueValidator( + limit_value=0, + message="Thread IDs cannot be negative." + ), + ), + verbose_name="Thread ID", + ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index c97f7dba..b9e06081 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -16,6 +16,7 @@ from .models import ( AocAccountLink, AocCompletionistBlock, BotSetting, + BumpedThread, DeletedMessage, DocumentationLink, FilterList, @@ -41,6 +42,16 @@ class BotSettingSerializer(ModelSerializer): fields = ('name', 'data') +class BumpedThreadSerializer(ModelSerializer): + """A class providing (de-)serialization of `BumpedThread` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = BumpedThread + fields = ('thread_id',) + + class DeletedMessageSerializer(ModelSerializer): """ A class providing (de-)serialization of `DeletedMessage` instances. diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 6b881fac..1e564b29 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -6,6 +6,7 @@ from .viewsets import ( AocAccountLinkViewSet, AocCompletionistBlockViewSet, BotSettingViewSet, + BumpedThreadViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, FilterListViewSet, @@ -32,6 +33,10 @@ bot_router.register( 'bot-settings', BotSettingViewSet ) +bot_router.register( + 'bumped-threads', + BumpedThreadViewSet +) bot_router.register( 'deleted-messages', DeletedMessageViewSet diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index a62a9c01..ec52416a 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa from .bot import ( BotSettingViewSet, + BumpedThreadViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, FilterListViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index f1d84729..262aa59f 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa from .filter_list import FilterListViewSet from .bot_setting import BotSettingViewSet +from .bumped_thread import BumpedThreadViewSet from .deleted_message import DeletedMessageViewSet from .documentation_link import DocumentationLinkViewSet from .infraction import InfractionViewSet diff --git a/pydis_site/apps/api/viewsets/bot/bumped_thread.py b/pydis_site/apps/api/viewsets/bot/bumped_thread.py new file mode 100644 index 00000000..6594ac6e --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/bumped_thread.py @@ -0,0 +1,65 @@ +from rest_framework.mixins import ( + CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot import BumpedThread +from pydis_site.apps.api.serializers import BumpedThreadSerializer + + +class BumpedThreadViewSet( + GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin +): + """ + View providing CRUD (Minus the U) operations on threads to be bumped. + + ## Routes + ### GET /bot/bumped-threads + Returns all BumpedThread items in the database. + + #### Response format + >>> [ + ... { + ... 'thread_id': "941705627405811793", + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/bumped-threads/ + Returns a specific BumpedThread item from the database. + + #### Response format + >>> { + ... 'thread_id': "941705627405811793", + ... } + + #### Status codes + - 200: returned on success + - 404: returned if a BumpedThread with the given thread_id was not found. + + ### POST /bot/bumped-threads + Adds a single BumpedThread item to the database. + + #### Request body + >>> { + ... 'thread_id': int, + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/bumped-threads/ + Deletes the BumpedThread item with the given `thread_id`. + + #### Status codes + - 204: returned on success + - 404: if a BumpedThread with the given `thread_id` does not exist + """ + + serializer_class = BumpedThreadSerializer + queryset = BumpedThread.objects.all() -- cgit v1.2.3 From 3e9557056c06a39c077b76d718eb35b99a365711 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 16 Mar 2022 21:46:46 +0000 Subject: Only return list of ints when retrieving all BumpedThreads --- pydis_site/apps/api/serializers.py | 17 +++++++++++++++++ pydis_site/apps/api/viewsets/bot/bumped_thread.py | 7 +------ 2 files changed, 18 insertions(+), 6 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index b9e06081..dfdda915 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -42,12 +42,29 @@ class BotSettingSerializer(ModelSerializer): fields = ('name', 'data') +class ListBumpedThreadSerializer(ListSerializer): + """Custom ListSerializer to override to_representation() when list views are triggered.""" + + def to_representation(self, objects: list[BumpedThread]) -> int: + """ + Used by the `ListModelMixin` to return just the list of bumped thread ids. + + We want to only return the thread_id attribute, hence it is unnecessary + to create a nested dictionary. + + Additionally, this allows bumped thread routes to simply return an + array of thread_id ints instead of objects, saving on bandwidth. + """ + return [obj.thread_id for obj in objects] + + class BumpedThreadSerializer(ModelSerializer): """A class providing (de-)serialization of `BumpedThread` instances.""" class Meta: """Metadata defined for the Django REST Framework.""" + list_serializer_class = ListBumpedThreadSerializer model = BumpedThread fields = ('thread_id',) diff --git a/pydis_site/apps/api/viewsets/bot/bumped_thread.py b/pydis_site/apps/api/viewsets/bot/bumped_thread.py index 6594ac6e..0972379b 100644 --- a/pydis_site/apps/api/viewsets/bot/bumped_thread.py +++ b/pydis_site/apps/api/viewsets/bot/bumped_thread.py @@ -18,12 +18,7 @@ class BumpedThreadViewSet( Returns all BumpedThread items in the database. #### Response format - >>> [ - ... { - ... 'thread_id': "941705627405811793", - ... }, - ... ... - ... ] + >>> list[int] #### Status codes - 200: returned on success -- cgit v1.2.3 From 429d98a5349b55e63f93e6192d8b2b35262dc60b Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Fri, 18 Mar 2022 15:11:34 +0000 Subject: fixup: don't use "We" in docstring Co-authored-by: Mark <1515135+MarkKoz@users.noreply.github.com> --- pydis_site/apps/api/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index dfdda915..e53ccffa 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -49,8 +49,7 @@ class ListBumpedThreadSerializer(ListSerializer): """ Used by the `ListModelMixin` to return just the list of bumped thread ids. - We want to only return the thread_id attribute, hence it is unnecessary - to create a nested dictionary. + Only the thread_id field is useful, hence it is unnecessary to create a nested dictionary. Additionally, this allows bumped thread routes to simply return an array of thread_id ints instead of objects, saving on bandwidth. -- cgit v1.2.3 From 562b6f0d783583838e51a86086aa441f093de102 Mon Sep 17 00:00:00 2001 From: ionite34 Date: Wed, 3 Aug 2022 17:15:05 -0400 Subject: Added `last_applied` to `serializers` --- pydis_site/apps/api/serializers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index e53ccffa..9228c1f4 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -176,6 +176,7 @@ class InfractionSerializer(ModelSerializer): fields = ( 'id', 'inserted_at', + 'last_applied', 'expires_at', 'active', 'user', -- cgit v1.2.3 From 855ce1e018bbb3a489c28768f60300c297890281 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 30 Sep 2022 21:51:25 +0300 Subject: Fix send_alert not being added correctly in serializers --- pydis_site/apps/api/serializers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0976ed29..50200035 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -154,6 +154,7 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'filter_dm', 'dm_pings', 'delete_messages', + 'send_alert', 'bypass_roles', 'enabled', 'enabled_channels', -- cgit v1.2.3 From 1970a3651db1e1a4f2ef92c85a0a733fa23fa6f0 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 1 Oct 2022 19:48:25 +0300 Subject: Bring back enabled categories There needs to be a way to only enable a filter in a specific category, so this setting now fulfills that role. Disabled channels can be used to disable a filter in a specific channel within the category. --- pydis_site/apps/api/migrations/0084_new_filter_schema.py | 4 ++++ .../apps/api/migrations/0085_unique_constraint_filters.py | 1 + pydis_site/apps/api/models/bot/filters.py | 14 +++++++++----- pydis_site/apps/api/serializers.py | 6 ++++-- 4 files changed, 18 insertions(+), 7 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0084_new_filter_schema.py b/pydis_site/apps/api/migrations/0084_new_filter_schema.py index ba228d70..74e1f009 100644 --- a/pydis_site/apps/api/migrations/0084_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0084_new_filter_schema.py @@ -51,6 +51,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: disabled_channels=[], disabled_categories=(["CODE JAM"] if name in ("FILE_FORMAT", "GUILD_INVITE") else []), enabled_channels=[], + enabled_categories=[], send_alert=(name in ('GUILD_INVITE', 'DOMAIN_NAME', 'FILTER_TOKEN')) ) @@ -74,6 +75,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: disabled_channels=None, disabled_categories=None, enabled_channels=None, + enabled_categories=None, send_alert=None, ) new_object.save() @@ -111,6 +113,7 @@ class Migration(migrations.Migration): ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", null=True, size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", null=True, size=None)), + ('enabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="The only categories in which to run the filter.", null=True, size=None)), ('send_alert', models.BooleanField(help_text='Whether an alert should be sent.', null=True)), ], ), @@ -134,6 +137,7 @@ class Migration(migrations.Migration): ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", size=None)), + ('enabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="The only categories in which to run the filter.", size=None)), ('send_alert', models.BooleanField(help_text='Whether an alert should be sent.')), ], ), diff --git a/pydis_site/apps/api/migrations/0085_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0085_unique_constraint_filters.py index 418c6e71..55ede901 100644 --- a/pydis_site/apps/api/migrations/0085_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0085_unique_constraint_filters.py @@ -30,6 +30,7 @@ class Migration(migrations.Migration): 'send_alert', 'enabled_channels', 'disabled_channels', + 'enabled_categories', 'disabled_categories' ), name='unique_filters'), ), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 1fb9707d..95a10e42 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -84,11 +84,6 @@ class FilterList(FilterSettingsMixin): help_text="Whether an alert should be sent.", ) # Where a filter should apply. - # - # The resolution is done in the following order: - # - enabled_channels - # - disabled_categories - # - disabled_channels enabled_channels = ArrayField( models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category." @@ -97,6 +92,10 @@ class FilterList(FilterSettingsMixin): models.CharField(max_length=100), help_text="Channels in which to not run the filter." ) + enabled_categories = ArrayField( + models.CharField(max_length=100), + help_text="The only categories in which to run the filter." + ) disabled_categories = ArrayField( models.CharField(max_length=100), help_text="Categories in which to not run the filter." @@ -165,6 +164,11 @@ class FilterBase(FilterSettingsMixin): models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True ) + enabled_categories = ArrayField( + models.CharField(max_length=100), + help_text="The only categories in which to run the filter.", + null=True + ) disabled_categories = ArrayField( models.CharField(max_length=100), help_text="Categories in which to not run the filter.", diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 50200035..26bda035 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -159,6 +159,7 @@ REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'enabled', 'enabled_channels', 'disabled_channels', + 'enabled_categories', 'disabled_categories', ) @@ -183,6 +184,7 @@ CHANNEL_SCOPE_FIELDS = ( "disabled_channels", "disabled_categories", "enabled_channels", + "enabled_categories" ) MENTIONS_FIELDS = ("guild_pings", "dm_pings") @@ -208,9 +210,9 @@ class FilterSerializer(ModelSerializer): raise ValidationError("Enabled and Disabled channels lists contain duplicates.") if data.get('disabled_categories') is not None: - categories_collection = data['disabled_categories'] + categories_collection = data['disabled_categories'] + data['enabled_categories'] if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Disabled categories lists contain duplicates.") + raise ValidationError("Enabled and Disabled categories lists contain duplicates.") return data -- cgit v1.2.3 From 862d00162309f4c061508545a377309bbd1871eb Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 7 Oct 2022 16:51:49 +0300 Subject: Properly add dm_embed to serializers --- pydis_site/apps/api/serializers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 7c1c107a..0dcbf2ee 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -144,6 +144,7 @@ class DocumentationLinkSerializer(ModelSerializer): ALWAYS_OPTIONAL_SETTINGS = ( 'dm_content', + 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', -- cgit v1.2.3 From e5d655a81f71c4b5bfb15d567bc11f88023e5879 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 8 Oct 2022 01:18:59 +0300 Subject: Add infraction channel setting --- pydis_site/apps/api/migrations/0085_new_filter_schema.py | 5 +++++ .../apps/api/migrations/0086_unique_constraint_filters.py | 1 + pydis_site/apps/api/models/bot/filters.py | 11 +++++++++++ pydis_site/apps/api/serializers.py | 6 +++++- 4 files changed, 22 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0085_new_filter_schema.py b/pydis_site/apps/api/migrations/0085_new_filter_schema.py index d16c26ac..2e721df4 100644 --- a/pydis_site/apps/api/migrations/0085_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0085_new_filter_schema.py @@ -3,6 +3,7 @@ from datetime import timedelta import django.contrib.postgres.fields from django.apps.registry import Apps +from django.core.validators import MinValueValidator from django.db import migrations, models import django.db.models.deletion from django.db.backends.base.schema import BaseDatabaseSchemaEditor @@ -48,6 +49,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: infraction_type="", infraction_reason="", infraction_duration=timedelta(seconds=0), + infraction_channel=None, disabled_channels=[], disabled_categories=(["CODE JAM"] if name in ("FILE_FORMAT", "GUILD_INVITE") else []), enabled_channels=[], @@ -72,6 +74,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: infraction_type=None, infraction_reason=None, infraction_duration=None, + infraction_channel=None, disabled_channels=None, disabled_categories=None, enabled_channels=None, @@ -110,6 +113,7 @@ class Migration(migrations.Migration): ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", null=True, size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", null=True, size=None)), @@ -134,6 +138,7 @@ class Migration(migrations.Migration): ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), + ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), ('disabled_categories', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Categories in which to not run the filter.", size=None)), ('enabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to run the filter even if it's disabled in the category.", size=None)), diff --git a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py index 8072ed2e..e7816e19 100644 --- a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py @@ -18,6 +18,7 @@ class Migration(migrations.Migration): 'infraction_type', 'infraction_reason', 'infraction_duration', + 'infraction_channel', 'content', 'additional_field', 'filter_list', diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 95a10e42..22482870 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -1,4 +1,5 @@ from django.contrib.postgres.fields import ArrayField +from django.core.validators import MinValueValidator from django.db import models from django.db.models import UniqueConstraint @@ -41,6 +42,16 @@ class FilterSettingsMixin(models.Model): null=True, help_text="The duration of the infraction. Null if permanent." ) + infraction_channel = models.BigIntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Channel IDs cannot be negative." + ), + ), + help_text="Channel in which to send the infraction.", + null=True + ) class Meta: """Metaclass for settings mixin.""" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0dcbf2ee..83471ca2 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -148,6 +148,7 @@ ALWAYS_OPTIONAL_SETTINGS = ( 'infraction_type', 'infraction_reason', 'infraction_duration', + 'infraction_channel', ) REQUIRED_FOR_FILTER_LIST_SETTINGS = ( @@ -178,6 +179,7 @@ INFRACTION_AND_NOTIFICATION_FIELDS = ( "infraction_type", "infraction_reason", "infraction_duration", + "infraction_channel", "dm_content", "dm_embed" ) @@ -230,6 +232,7 @@ class FilterSerializer(ModelSerializer): 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, 'enabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, 'disabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'enabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, 'disabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, } @@ -305,6 +308,7 @@ class FilterListSerializer(ModelSerializer): 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, 'enabled_channels': {'allow_empty': True}, 'disabled_channels': {'allow_empty': True}, + 'enabled_categories': {'allow_empty': True}, 'disabled_categories': {'allow_empty': True}, } @@ -314,7 +318,7 @@ class FilterListSerializer(ModelSerializer): queryset=FilterList.objects.all(), fields=('name', 'list_type'), message=( - "A filterlist with the same name and type already exist." + "A filterlist with the same name and type already exists." ) ), ] -- cgit v1.2.3 From e23fcc3f1d8575243bb4acee3b8747d05e21ef22 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 8 Oct 2022 12:02:56 +0300 Subject: Fix categories validation --- pydis_site/apps/api/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 83471ca2..13cd7fea 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -212,7 +212,10 @@ class FilterSerializer(ModelSerializer): if len(channels_collection) != len(set(channels_collection)): raise ValidationError("Enabled and Disabled channels lists contain duplicates.") - if data.get('disabled_categories') is not None: + if ( + data.get('disabled_categories') is not None + and data.get('enabled_categories') is not None + ): categories_collection = data['disabled_categories'] + data['enabled_categories'] if len(categories_collection) != len(set(categories_collection)): raise ValidationError("Enabled and Disabled categories lists contain duplicates.") -- cgit v1.2.3 From 88a2be8ec1dc0bb85d3ac50f3f24b70a8ce12b3e Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 8 Oct 2022 12:07:38 +0300 Subject: Allow ping arrays to be empty --- pydis_site/apps/api/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 13cd7fea..7a5e76b7 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -237,6 +237,8 @@ class FilterSerializer(ModelSerializer): 'disabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, 'enabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, 'disabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'guild_pings': {'allow_empty': True, 'allow_null': True, 'required': False}, + 'dm_pings': {'allow_empty': True, 'allow_null': True, 'required': False}, } def to_representation(self, instance: Filter) -> dict: @@ -313,6 +315,8 @@ class FilterListSerializer(ModelSerializer): 'disabled_channels': {'allow_empty': True}, 'enabled_categories': {'allow_empty': True}, 'disabled_categories': {'allow_empty': True}, + 'guild_pings': {'allow_empty': True}, + 'dm_pings': {'allow_empty': True}, } # Ensure that we can only have one filter list with the same name and field -- cgit v1.2.3 From c3747b6d09ff968858eab698eb5fcffb9c3fbd1f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 8 Oct 2022 15:44:37 +0300 Subject: Allow char fields to be blank This is necessary allow filters to define a blank message when the default is not blank. Additionally allows bypass_roles to be empty like the other array fields --- pydis_site/apps/api/migrations/0085_new_filter_schema.py | 12 ++++++------ pydis_site/apps/api/models/bot/filters.py | 5 ++++- pydis_site/apps/api/serializers.py | 12 +++++++++++- 3 files changed, 21 insertions(+), 8 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0085_new_filter_schema.py b/pydis_site/apps/api/migrations/0085_new_filter_schema.py index 2e721df4..a38194ef 100644 --- a/pydis_site/apps/api/migrations/0085_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0085_new_filter_schema.py @@ -108,10 +108,10 @@ class Migration(migrations.Migration): ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), - ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), - ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True, blank=True)), + ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True, blank=True)), ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True, blank=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", null=True, size=None)), @@ -133,10 +133,10 @@ class Migration(migrations.Migration): ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), - ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True)), - ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True)), + ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True, blank=True)), + ('dm_embed', models.CharField(help_text='The content of the DM embed', max_length=2000, null=True, blank=True)), ('infraction_type', models.CharField(choices=[('NOTE', 'Note'), ('WARNING', 'Warning'), ('WATCH', 'Watch'), ('MUTE', 'Mute'), ('KICK', 'Kick'), ('BAN', 'Ban'), ('SUPERSTAR', 'Superstar'), ('VOICE_BAN', 'Voice Ban'), ('VOICE_MUTE', 'Voice Mute')], help_text='The infraction to apply to this user.', max_length=10, null=True)), - ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True)), + ('infraction_reason', models.CharField(help_text='The reason to give for the infraction.', max_length=1000, null=True, blank=True)), ('infraction_duration', models.DurationField(help_text='The duration of the infraction. Null if permanent.', null=True)), ('infraction_channel', models.BigIntegerField(validators=(MinValueValidator(limit_value=0, message="Channel IDs cannot be negative."),), help_text="Channel in which to send the infraction.", null=True)), ('disabled_channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text="Channels in which to not run the filter.", size=None)), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 22482870..81b72c6e 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -20,12 +20,14 @@ class FilterSettingsMixin(models.Model): dm_content = models.CharField( max_length=1000, null=True, + blank=True, help_text="The DM to send to a user triggering this filter." ) dm_embed = models.CharField( max_length=2000, help_text="The content of the DM embed", - null=True + null=True, + blank=True ) infraction_type = models.CharField( choices=[(choices[0].upper(), choices[1]) for choices in Infraction.TYPE_CHOICES], @@ -36,6 +38,7 @@ class FilterSettingsMixin(models.Model): infraction_reason = models.CharField( max_length=1000, help_text="The reason to give for the infraction.", + blank=True, null=True ) infraction_duration = models.DurationField( diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 7a5e76b7..a42d567b 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -151,6 +151,13 @@ ALWAYS_OPTIONAL_SETTINGS = ( 'infraction_channel', ) +ALWAYS_BLANKABLE_SETTINGS = ( + 'dm_content', + 'dm_embed', + 'infraction_type', + 'infraction_reason', +) + REQUIRED_FOR_FILTER_LIST_SETTINGS = ( 'guild_pings', 'filter_dm', @@ -310,7 +317,10 @@ class FilterListSerializer(ModelSerializer): extra_kwargs = { field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS } | { - 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, + field: {'allow_blank': True, 'allow_null': True, 'required': False} + for field in ALWAYS_BLANKABLE_SETTINGS + } | { + 'bypass_roles': {'allow_empty': True}, 'enabled_channels': {'allow_empty': True}, 'disabled_channels': {'allow_empty': True}, 'enabled_categories': {'allow_empty': True}, -- cgit v1.2.3 From 65559eee45f0b17e4db3c80ad8b147d5413fab6f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 18 Oct 2022 18:59:32 +0300 Subject: Refactors filters serialier --- pydis_site/apps/api/serializers.py | 170 +++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 81 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a42d567b..aac8d06e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -142,7 +142,24 @@ class DocumentationLinkSerializer(ModelSerializer): fields = ('package', 'base_url', 'inventory_url') -ALWAYS_OPTIONAL_SETTINGS = ( +# region: filters serializers + + +REQUIRED_FOR_FILTER_LIST_SETTINGS = ( + 'guild_pings', + 'filter_dm', + 'dm_pings', + 'delete_messages', + 'send_alert', + 'bypass_roles', + 'enabled', + 'enabled_channels', + 'disabled_channels', + 'enabled_categories', + 'disabled_categories', +) + +OPTIONAL_FOR_FILTER_LIST_SETTINGS = ( 'dm_content', 'dm_embed', 'infraction_type', @@ -151,25 +168,21 @@ ALWAYS_OPTIONAL_SETTINGS = ( 'infraction_channel', ) -ALWAYS_BLANKABLE_SETTINGS = ( +ALLOW_BLANK_SETTINGS = ( 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', ) -REQUIRED_FOR_FILTER_LIST_SETTINGS = ( - 'guild_pings', - 'filter_dm', - 'dm_pings', - 'delete_messages', - 'send_alert', - 'bypass_roles', - 'enabled', +ALLOW_EMPTY_SETTINGS = ( 'enabled_channels', 'disabled_channels', 'enabled_categories', 'disabled_categories', + 'guild_pings', + 'dm_pings', + 'bypass_roles', ) # Required fields for custom JSON representation purposes @@ -198,7 +211,20 @@ CHANNEL_SCOPE_FIELDS = ( ) MENTIONS_FIELDS = ("guild_pings", "dm_pings") -SETTINGS_FIELDS = ALWAYS_OPTIONAL_SETTINGS + REQUIRED_FOR_FILTER_LIST_SETTINGS +SETTINGS_FIELDS = REQUIRED_FOR_FILTER_LIST_SETTINGS + OPTIONAL_FOR_FILTER_LIST_SETTINGS + + +def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: + """Create the extra kwargs of the Filter serializer's Meta class.""" + extra_kwargs = {} + for field in SETTINGS_FIELDS: + field_args = {'required': False, 'allow_null': True} + if field in ALLOW_BLANK_SETTINGS: + field_args['allow_blank'] = True + if field in ALLOW_EMPTY_SETTINGS: + field_args['allow_empty'] = True + extra_kwargs[field] = field_args + return extra_kwargs class FilterSerializer(ModelSerializer): @@ -236,17 +262,7 @@ class FilterSerializer(ModelSerializer): fields = ( 'id', 'content', 'description', 'additional_field', 'filter_list' ) + SETTINGS_FIELDS - extra_kwargs = { - field: {'required': False, 'allow_null': True} for field in SETTINGS_FIELDS - } | { - 'infraction_reason': {'allow_blank': True, 'allow_null': True, 'required': False}, - 'enabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'disabled_channels': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'enabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'disabled_categories': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'guild_pings': {'allow_empty': True, 'allow_null': True, 'required': False}, - 'dm_pings': {'allow_empty': True, 'allow_null': True, 'required': False}, - } + extra_kwargs = _create_filter_meta_extra_kwargs() def to_representation(self, instance: Filter) -> dict: """ @@ -258,28 +274,36 @@ class FilterSerializer(ModelSerializer): Furthermore, it puts the fields that meant to represent Filter settings, into a sub-field called `settings`. """ - schema_settings = { - "settings": - {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - | { - "infraction_and_notification": - {name: getattr(instance, name) - for name in INFRACTION_AND_NOTIFICATION_FIELDS} - } | { - "channel_scope": - {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} - } | { - "mentions": - { - schema_field_name: getattr(instance, schema_field_name) - for schema_field_name in MENTIONS_FIELDS - } - } + settings = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + settings["infraction_and_notification"] = { + name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS + } + settings["channel_scope"] = { + name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS } - schema_base = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} | \ - {"filter_list": instance.filter_list.id} + settings["mentions"] = { + name: getattr(instance, name) for name in MENTIONS_FIELDS + } + + schema = {name: getattr(instance, name) for name in BASE_FILTER_FIELDS} + schema["filter_list"] = instance.filter_list.id + schema["settings"] = settings + return schema - return schema_base | schema_settings + +def _create_filter_list_meta_extra_kwargs() -> dict[str, dict[str, bool]]: + """Create the extra kwargs of the FilterList serializer's Meta class.""" + extra_kwargs = {} + for field in SETTINGS_FIELDS: + field_args = {} + if field in OPTIONAL_FOR_FILTER_LIST_SETTINGS: + field_args = {'required': False, 'allow_null': True} + if field in ALLOW_BLANK_SETTINGS: + field_args['allow_blank'] = True + if field in ALLOW_EMPTY_SETTINGS: + field_args['allow_empty'] = True + extra_kwargs[field] = field_args + return extra_kwargs class FilterListSerializer(ModelSerializer): @@ -302,10 +326,13 @@ class FilterListSerializer(ModelSerializer): if len(channels_collection) != len(set(channels_collection)): raise ValidationError("Enabled and Disabled channels lists contain duplicates.") - if data.get('disabled_categories') is not None: - categories_collection = data['disabled_categories'] + if ( + data.get('disabled_categories') is not None + and data.get('enabled_categories') is not None + ): + categories_collection = data['disabled_categories'] + data['enabled_categories'] if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Disabled categories lists contain duplicates.") + raise ValidationError("Enabled and Disabled categories lists contain duplicates.") return data @@ -314,22 +341,9 @@ class FilterListSerializer(ModelSerializer): model = FilterList fields = ('id', 'name', 'list_type', 'filters') + SETTINGS_FIELDS - extra_kwargs = { - field: {'required': False, 'allow_null': True} for field in ALWAYS_OPTIONAL_SETTINGS - } | { - field: {'allow_blank': True, 'allow_null': True, 'required': False} - for field in ALWAYS_BLANKABLE_SETTINGS - } | { - 'bypass_roles': {'allow_empty': True}, - 'enabled_channels': {'allow_empty': True}, - 'disabled_channels': {'allow_empty': True}, - 'enabled_categories': {'allow_empty': True}, - 'disabled_categories': {'allow_empty': True}, - 'guild_pings': {'allow_empty': True}, - 'dm_pings': {'allow_empty': True}, - } + extra_kwargs = _create_filter_list_meta_extra_kwargs() - # Ensure that we can only have one filter list with the same name and field + # Ensure there can only be one filter list with the same name and type. validators = [ UniqueTogetherValidator( queryset=FilterList.objects.all(), @@ -350,29 +364,23 @@ class FilterListSerializer(ModelSerializer): Furthermore, it puts the fields that meant to represent FilterList settings, into a sub-field called `settings`. """ - # Fetches the relating filters - filters = [ - FilterSerializer(many=False).to_representation( - instance=item - ) for item in Filter.objects.filter( - filter_list=instance.id - ) + schema = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} + schema["filters"] = [ + FilterSerializer(many=False).to_representation(instance=item) + for item in Filter.objects.filter(filter_list=instance.id) ] - schema_base = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} \ - | {"filters": filters} - schema_settings_base = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} - schema_settings_categories = { - "infraction_and_notification": - {name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS}} \ - | { - "channel_scope": - {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS}} | { - "mentions": { - schema_field_name: getattr(instance, schema_field_name) - for schema_field_name in MENTIONS_FIELDS - } + + settings = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} + settings["infraction_and_notification"] = { + name: getattr(instance, name) for name in INFRACTION_AND_NOTIFICATION_FIELDS } - return schema_base | {"settings": schema_settings_base | schema_settings_categories} + settings["channel_scope"] = {name: getattr(instance, name) for name in CHANNEL_SCOPE_FIELDS} + settings["mentions"] = {name: getattr(instance, name) for name in MENTIONS_FIELDS} + + schema["settings"] = settings + return schema + +# endregion class InfractionSerializer(ModelSerializer): -- cgit v1.2.3 From 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/serializers.py') 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/serializers.py') 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 bb160ae0700ef3c739b9c622c3b90a26e576ac96 Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Sun, 6 Nov 2022 02:29:39 +0100 Subject: add thread_id to serializer's fields --- pydis_site/apps/api/serializers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 9228c1f4..4303e7d0 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -432,7 +432,15 @@ class NominationSerializer(ModelSerializer): model = Nomination fields = ( - 'id', 'active', 'user', 'inserted_at', 'end_reason', 'ended_at', 'reviewed', 'entries' + 'id', + 'active', + 'user', + 'inserted_at', + 'end_reason', + 'ended_at', + 'reviewed', + 'entries', + 'thread_id' ) -- 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/serializers.py') diff --git a/pydis_site/apps/api/migrations/0085_new_filter_schema.py b/pydis_site/apps/api/migrations/0085_new_filter_schema.py index d902be7f..96d03bf4 100644 --- a/pydis_site/apps/api/migrations/0085_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0085_new_filter_schema.py @@ -41,7 +41,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: guild_pings=(["Moderators"] if name != "FILE_FORMAT" else []), filter_dm=True, dm_pings=[], - delete_messages=(True if name != "FILTER_TOKEN" else False), + remove_context=(True if name != "FILTER_TOKEN" else False), bypass_roles=["Helpers"], enabled=True, dm_content=dm_content, @@ -68,7 +68,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: guild_pings=None, filter_dm=None, dm_pings=None, - delete_messages=None, + remove_context=None, bypass_roles=None, enabled=None, dm_content=None, @@ -109,7 +109,7 @@ class Migration(migrations.Migration): ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, null=True)), - ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.', null=True)), + ('remove_context', models.BooleanField(help_text='Whether this filter should remove the context (such as a message) triggering it.', null=True)), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None, null=True)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.', null=True)), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, null=True, blank=True)), @@ -136,7 +136,7 @@ class Migration(migrations.Migration): ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.')), ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None)), - ('delete_messages', models.BooleanField(help_text='Whether this filter should delete messages triggering it.')), + ('remove_context', models.BooleanField(help_text='Whether this filter should remove the context (such as a message) triggering it.')), ('bypass_roles', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Roles and users who can bypass this filter.', size=None)), ('enabled', models.BooleanField(help_text='Whether this filter is currently enabled.')), ('dm_content', models.CharField(help_text='The DM to send to a user triggering this filter.', max_length=1000, blank=True)), diff --git a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py index 6fa99e9e..b83e395c 100644 --- a/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0086_unique_constraint_filters.py @@ -25,7 +25,7 @@ class Migration(migrations.Migration): 'guild_pings', 'filter_dm', 'dm_pings', - 'delete_messages', + 'remove_context', 'bypass_roles', 'enabled', 'send_alert', diff --git a/pydis_site/apps/api/migrations/0087_unique_filter_list.py b/pydis_site/apps/api/migrations/0087_unique_filter_list.py index 96c2b17a..b8087d9c 100644 --- a/pydis_site/apps/api/migrations/0087_unique_filter_list.py +++ b/pydis_site/apps/api/migrations/0087_unique_filter_list.py @@ -17,7 +17,7 @@ def create_unique_list(apps: Apps, _): guild_pings=[], filter_dm=True, dm_pings=[], - delete_messages=False, + remove_context=False, bypass_roles=[], enabled=True, dm_content="", @@ -37,7 +37,7 @@ def create_unique_list(apps: Apps, _): content="everyone", filter_list=list_, description="", - delete_messages=True, + remove_context=True, bypass_roles=["Helpers"], dm_content=( "Please don't try to ping `@everyone` or `@here`. Your message has been removed. " @@ -51,7 +51,7 @@ def create_unique_list(apps: Apps, _): content="webhook", filter_list=list_, description="", - delete_messages=True, + remove_context=True, dm_content=( "Looks like you posted a Discord webhook URL. " "Therefore, your message has been removed, and your webhook has been deleted. " @@ -74,7 +74,7 @@ def create_unique_list(apps: Apps, _): content="discord_token", filter_list=list_, filter_dm=False, - delete_messages=True, + remove_context=True, dm_content=( "I noticed you posted a seemingly valid Discord API " "token in your message and have removed your message. " diff --git a/pydis_site/apps/api/migrations/0088_antispam_filter_list.py b/pydis_site/apps/api/migrations/0088_antispam_filter_list.py index 354e4520..fcb56781 100644 --- a/pydis_site/apps/api/migrations/0088_antispam_filter_list.py +++ b/pydis_site/apps/api/migrations/0088_antispam_filter_list.py @@ -17,7 +17,7 @@ def create_antispam_list(apps: Apps, _): guild_pings=["Moderators"], filter_dm=False, dm_pings=[], - delete_messages=True, + remove_context=True, bypass_roles=["Helpers"], enabled=True, dm_content="", diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index 1ea21a48..4d8a4025 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -75,8 +75,8 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): help_text="Who to ping when this filter triggers on a DM.", null=False ) - delete_messages = models.BooleanField( - help_text="Whether this filter should delete messages triggering it.", + remove_context = models.BooleanField( + help_text="Whether this filter should remove the context (such as a message) triggering it.", null=False ) bypass_roles = ArrayField( @@ -185,8 +185,8 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): help_text="Who to ping when this filter triggers on a DM.", null=True ) - delete_messages = models.BooleanField( - help_text="Whether this filter should delete messages triggering it.", + remove_context = models.BooleanField( + help_text="Whether this filter should remove the context (such as a message) triggering it.", null=True ) bypass_roles = ArrayField( diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index d6bae2cb..eabca66e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -154,7 +154,7 @@ SETTINGS_FIELDS = ( 'guild_pings', 'filter_dm', 'dm_pings', - 'delete_messages', + 'remove_context', 'send_alert', 'bypass_roles', 'enabled', @@ -187,7 +187,7 @@ BASE_SETTINGS_FIELDS = ( "bypass_roles", "filter_dm", "enabled", - "delete_messages", + "remove_context", "send_alert" ) INFRACTION_AND_NOTIFICATION_FIELDS = ( diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index 5f40c6f9..f3afdaeb 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -49,7 +49,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: ping_type=[], filter_dm=False, dm_ping_type=[], - delete_messages=False, + remove_context=False, bypass_roles=[], enabled=False, default_action=FilterAction( @@ -76,7 +76,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: "ping_type": ["onduty"], "filter_dm": True, "dm_ping_type": ["123456"], - "delete_messages": True, + "remove_context": True, "bypass_roles": [123456], "enabled": True, "default_action": FilterAction( @@ -130,7 +130,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: ping_type=[], filter_dm=False, dm_ping_type=[], - delete_messages=False, + remove_context=False, bypass_roles=[], enabled=False, default_action=FilterAction( @@ -157,7 +157,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: "ping_type": ["everyone"], "filter_dm": False, "dm_ping_type": ["here"], - "delete_messages": False, + "remove_context": False, "bypass_roles": [9876], "enabled": True, "filter_action": None, diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index dd9a7d87..1eb05053 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -36,7 +36,7 @@ class FilterListViewSet(ModelViewSet): ... "filter_dm": None, ... "enabled": None ... "send_alert": True, - ... "delete_messages": None + ... "remove_context": None ... "infraction_and_notification": { ... "infraction_type": None, ... "infraction_reason": "", @@ -64,7 +64,7 @@ class FilterListViewSet(ModelViewSet): ... ], ... "filter_dm": True, ... "enabled": True - ... "delete_messages": True, + ... "remove_context": True, ... "send_alert": True ... "infraction_and_notification": { ... "infraction_type": "", @@ -111,7 +111,7 @@ class FilterListViewSet(ModelViewSet): ... "bypass_roles": None ... "filter_dm": None, ... "enabled": None - ... "delete_messages": None, + ... "remove_context": None, ... "send_alert": None ... "infraction_and_notification": { ... "infraction_type": None, @@ -140,7 +140,7 @@ class FilterListViewSet(ModelViewSet): ... ], ... "filter_dm": True, ... "enabled": True - ... "delete_messages": True + ... "remove_context": True ... "send_alert": True ... "infraction_and_notification": { ... "infraction_type": "", @@ -198,7 +198,7 @@ class FilterViewSet(ModelViewSet): ... "bypass_roles": None ... "filter_dm": None, ... "enabled": None - ... "delete_messages": True, + ... "remove_context": True, ... "send_alert": True ... "infraction": { ... "infraction_type": None, @@ -237,7 +237,7 @@ class FilterViewSet(ModelViewSet): ... "bypass_roles": None ... "filter_dm": None, ... "enabled": None - ... "delete_messages": True, + ... "remove_context": True, ... "send_alert": True ... "infraction": { ... "infraction_type": None, -- cgit v1.2.3 From d3eec93b36bd57c521e70b4001c74cb9756caf23 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 27 Jan 2023 22:19:28 +0200 Subject: Fix filter serializers validation to account for filterlist settings --- pydis_site/apps/api/serializers.py | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index eabca66e..83ab4584 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,4 +1,6 @@ """Converters from Django models to data interchange formats and back.""" +from typing import Any + from django.db.models.query import QuerySet from django.db.utils import IntegrityError from rest_framework.exceptions import NotFound @@ -220,31 +222,29 @@ def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: return extra_kwargs +def get_field_value(data: dict, field_name: str) -> Any: + """Get the value directly from the key, or from the filter list if it's missing or is None.""" + if data.get(field_name): + return data[field_name] + return getattr(data["filter_list"], field_name) + + class FilterSerializer(ModelSerializer): """A class providing (de-)serialization of `Filter` instances.""" def validate(self, data: dict) -> dict: - """Perform infraction data + allow and disallowed lists validation.""" - if ( - data.get('infraction_reason') or data.get('infraction_duration') - ) and not data.get('infraction_type'): - raise ValidationError("Infraction type is required with infraction duration or reason") - + """Perform infraction data + allowed and disallowed lists validation.""" if ( - data.get('disabled_channels') is not None - and data.get('enabled_channels') is not None + (get_field_value(data, "infraction_reason") or get_field_value(data, "infraction_duration")) + and get_field_value(data, "infraction_type") == "NONE" ): - channels_collection = data['disabled_channels'] + data['enabled_channels'] - if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Enabled and Disabled channels lists contain duplicates.") + raise ValidationError("Infraction type is required with infraction duration or reason.") - if ( - data.get('disabled_categories') is not None - and data.get('enabled_categories') is not None - ): - categories_collection = data['disabled_categories'] + data['enabled_categories'] - if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Enabled and Disabled categories lists contain duplicates.") + if set(get_field_value(data, "disabled_channels")) & set(get_field_value(data, "enabled_channels")): + raise ValidationError("You can't have the same value in both enabled and disabled channels lists.") + + if set(get_field_value(data, "disabled_categories")) & set(get_field_value(data, "enabled_categories")): + raise ValidationError("You can't have the same value in both enabled and disabled categories lists.") return data @@ -318,8 +318,8 @@ class FilterListSerializer(ModelSerializer): raise ValidationError("Enabled and Disabled channels lists contain duplicates.") if ( - data.get('disabled_categories') is not None - and data.get('enabled_categories') is not None + data.get('disabled_categories') is not None + and data.get('enabled_categories') is not None ): categories_collection = data['disabled_categories'] + data['enabled_categories'] if len(categories_collection) != len(set(categories_collection)): -- cgit v1.2.3 From 5f538e9e876adc7f4a459fe230b46bdde61b3f64 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 27 Jan 2023 23:37:26 +0200 Subject: Make filter unique constraint use NULLS NOT DISTINCT The existing constraint was ineffective as null values were considered distinct, and so two filters with the same content and no overrides were considered different. This change uses a new PSQL 15 feature unsupported in django currently, and so it is added with raw SQL. --- .../migrations/0087_unique_constraint_filters.py | 39 ++++++++-------------- pydis_site/apps/api/serializers.py | 9 +++++ pydis_site/apps/api/tests/test_filters.py | 12 +++++++ 3 files changed, 34 insertions(+), 26 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0087_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0087_unique_constraint_filters.py index b2ff91f1..910e7b1b 100644 --- a/pydis_site/apps/api/migrations/0087_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0087_unique_constraint_filters.py @@ -1,5 +1,3 @@ -# Generated by Django 3.1.14 on 2022-03-22 16:31 - from django.db import migrations, models @@ -10,29 +8,18 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddConstraint( - model_name='filter', - constraint=models.UniqueConstraint(fields=( - 'content', - 'additional_field', - 'filter_list', - 'dm_content', - 'dm_embed', - 'infraction_type', - 'infraction_reason', - 'infraction_duration', - 'infraction_channel', - 'guild_pings', - 'filter_dm', - 'dm_pings', - 'remove_context', - 'bypass_roles', - 'enabled', - 'send_alert', - 'enabled_channels', - 'disabled_channels', - 'enabled_categories', - 'disabled_categories' - ), name='unique_filters'), + migrations.RunSQL( + "ALTER TABLE api_filter " + "ADD CONSTRAINT unique_filters UNIQUE NULLS NOT DISTINCT " + "(content, additional_field, filter_list_id, dm_content, dm_embed, infraction_type, infraction_reason, infraction_duration, infraction_channel, guild_pings, filter_dm, dm_pings, remove_context, bypass_roles, enabled, send_alert, enabled_channels, disabled_channels, enabled_categories, disabled_categories)", + state_operations=[ + migrations.AddConstraint( + model_name='filter', + constraint=models.UniqueConstraint( + fields=('content', 'additional_field', 'filter_list', 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', 'infraction_channel', 'guild_pings', 'filter_dm', 'dm_pings', 'remove_context', 'bypass_roles', 'enabled', 'send_alert', 'enabled_channels', 'disabled_channels', 'enabled_categories', 'disabled_categories'), + name='unique_filters' + ), + ), + ], ), ] diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 7f9461ec..8da47802 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -257,6 +257,15 @@ class FilterSerializer(ModelSerializer): ) + SETTINGS_FIELDS extra_kwargs = _create_filter_meta_extra_kwargs() + def create(self, validated_data: dict) -> User: + """Override the create method to catch violations of the custom uniqueness constraint.""" + try: + return super().create(validated_data) + except IntegrityError: + raise ValidationError( + "Check if a filter with this combination of content and settings already exists in this filter list." + ) + def to_representation(self, instance: Filter) -> dict: """ Provides a custom JSON representation to the Filter Serializers. diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index cae78cd6..73c8e0d9 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -327,3 +327,15 @@ class FilterValidationTests(AuthenticatedAPITestCase): f"{base_filter_list.url()}/{case_fl.id}", data=clean_test_json(filter_list_settings) ) self.assertEqual(response.status_code, response_code) + + def test_filter_unique_constraint(self) -> None: + test_filter = get_test_sequences()["filter"] + test_filter.model.objects.all().delete() + test_filter_object = test_filter.model(**test_filter.object) + save_nested_objects(test_filter_object, False) + + response = self.client.post(test_filter.url(), data=clean_test_json(test_filter.object)) + self.assertEqual(response.status_code, 201) + + response = self.client.post(test_filter.url(), data=clean_test_json(test_filter.object)) + self.assertEqual(response.status_code, 400) -- cgit v1.2.3 From d52a8c955aceccd719dd1511700aac9f2a564b0a Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 28 Jan 2023 16:24:58 +0200 Subject: Update viewsets, fix linting --- .../apps/api/migrations/0088_unique_filter_list.py | 2 +- pydis_site/apps/api/models/bot/filters.py | 12 +- pydis_site/apps/api/serializers.py | 50 +- pydis_site/apps/api/tests/test_filters.py | 24 +- pydis_site/apps/api/viewsets/bot/filters.py | 532 ++++++++++++++------- 5 files changed, 428 insertions(+), 192 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') 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 be854fa3d34dac7b4b9e96b3736dd61d972f1b79 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 11 Feb 2023 13:45:31 +0200 Subject: Fix filter serializers for false-y values Co-authored-by: GDWR --- pydis_site/apps/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a6328eff..f4d64ad0 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -226,7 +226,7 @@ def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: def get_field_value(data: dict, field_name: str) -> Any: """Get the value directly from the key, or from the filter list if it's missing or is None.""" - if data.get(field_name): + if data.get(field_name) is not None: return data[field_name] return getattr(data["filter_list"], field_name) -- cgit v1.2.3 From 8a954029a2f0f22cde599afee3ff8195680e621e Mon Sep 17 00:00:00 2001 From: vivekashok1221 Date: Fri, 17 Feb 2023 11:40:29 +0530 Subject: Add jump_url field to infraction model --- .../apps/api/migrations/0086_infraction_jump_url.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/infraction.py | 8 ++++++++ pydis_site/apps/api/serializers.py | 3 ++- pydis_site/apps/api/viewsets/bot/infraction.py | 8 +++++--- 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0086_infraction_jump_url.py (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/migrations/0086_infraction_jump_url.py b/pydis_site/apps/api/migrations/0086_infraction_jump_url.py new file mode 100644 index 00000000..e32219c8 --- /dev/null +++ b/pydis_site/apps/api/migrations/0086_infraction_jump_url.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.6 on 2023-02-13 22:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0085_add_thread_id_to_nominations'), + ] + + operations = [ + migrations.AddField( + model_name='infraction', + name='jump_url', + field=models.CharField(default='', help_text='The jump url to message invoking the infraction.', max_length=88), + ), + ] diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 218ee5ec..ea0277c3 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -69,6 +69,14 @@ class Infraction(ModelReprMixin, models.Model): help_text="Whether a DM was sent to the user when infraction was applied." ) + jump_url = models.CharField( + default='', + max_length=88, + help_text=( + "The jump url to message invoking the infraction." + ) + ) + def __str__(self): """Returns some info on the current infraction, for display purposes.""" s = f"#{self.id}: {self.type} on {self.user_id}" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 4303e7d0..e74ca102 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -184,7 +184,8 @@ class InfractionSerializer(ModelSerializer): 'type', 'reason', 'hidden', - 'dm_sent' + 'dm_sent', + 'jump_url' ) def validate(self, attrs: dict) -> dict: diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index 93d29391..9c21733b 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -72,7 +72,8 @@ class InfractionViewSet( ... 'type': 'ban', ... 'reason': 'He terk my jerb!', ... 'hidden': True, - ... 'dm_sent': True + ... 'dm_sent': True, + ... 'jump_url': '' ... } ... ] @@ -103,7 +104,8 @@ class InfractionViewSet( ... 'type': 'ban', ... 'reason': 'He terk my jerb!', ... 'user': 172395097705414656, - ... 'dm_sent': False + ... 'dm_sent': False, + ... 'jump_url': ''x ... } #### Response format @@ -138,7 +140,7 @@ class InfractionViewSet( #### Status codes - 204: returned on success - - 404: if a infraction with the given `id` does not exist + - 404: if an infraction with the given `id` does not exist ### Expanded routes All routes support expansion of `user` and `actor` in responses. To use an expanded route, -- cgit v1.2.3 From 43913623f87329b51cd2a6793e843c30368698aa Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Mar 2023 18:51:24 +0300 Subject: Merge the extra kwargs creation functions Co-authored-by: Amrou --- pydis_site/apps/api/serializers.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index fe3c1dd2..e8c5869f 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -211,11 +211,11 @@ CHANNEL_SCOPE_FIELDS = ( MENTIONS_FIELDS = ("guild_pings", "dm_pings") -def _create_filter_meta_extra_kwargs() -> dict[str, dict[str, bool]]: - """Create the extra kwargs of the Filter serializer's Meta class.""" +def _create_meta_extra_kwargs(*, for_filter: bool) -> dict[str, dict[str, bool]]: + """Create the extra kwargs for the Meta classes of the Filter and FilterList serializers.""" extra_kwargs = {} for field in SETTINGS_FIELDS: - field_args = {'required': False, 'allow_null': True} + field_args = {'required': False, 'allow_null': True} if for_filter else {} if field in ALLOW_BLANK_SETTINGS: field_args['allow_blank'] = True if field in ALLOW_EMPTY_SETTINGS: @@ -278,7 +278,7 @@ class FilterSerializer(ModelSerializer): 'additional_field', 'filter_list' ) + SETTINGS_FIELDS - extra_kwargs = _create_filter_meta_extra_kwargs() + extra_kwargs = _create_meta_extra_kwargs(for_filter=True) def create(self, validated_data: dict) -> User: """Override the create method to catch violations of the custom uniqueness constraint.""" @@ -317,19 +317,6 @@ class FilterSerializer(ModelSerializer): return schema -def _create_filter_list_meta_extra_kwargs() -> dict[str, dict[str, bool]]: - """Create the extra kwargs of the FilterList serializer's Meta class.""" - extra_kwargs = {} - for field in SETTINGS_FIELDS: - field_args = {} - if field in ALLOW_BLANK_SETTINGS: - field_args['allow_blank'] = True - if field in ALLOW_EMPTY_SETTINGS: - field_args['allow_empty'] = True - extra_kwargs[field] = field_args - return extra_kwargs - - class FilterListSerializer(ModelSerializer): """A class providing (de-)serialization of `FilterList` instances.""" @@ -367,7 +354,7 @@ class FilterListSerializer(ModelSerializer): fields = ( 'id', 'created_at', 'updated_at', 'name', 'list_type', 'filters' ) + SETTINGS_FIELDS - extra_kwargs = _create_filter_list_meta_extra_kwargs() + extra_kwargs = _create_meta_extra_kwargs(for_filter=False) # Ensure there can only be one filter list with the same name and type. validators = [ -- cgit v1.2.3 From 36bca58ff336f9d4b797a2c76f08775f9de7e9a7 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Mar 2023 21:26:07 +0300 Subject: Specify the common elements in the validation errors Co-authored-by: Amrou --- pydis_site/apps/api/serializers.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index e8c5869f..bfad18ab 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -247,20 +247,24 @@ class FilterSerializer(ModelSerializer): "Infraction type is required with infraction duration or reason." ) - if ( + common_channels = ( set(get_field_value(data, "disabled_channels")) & set(get_field_value(data, "enabled_channels")) - ): + ) + if common_channels: raise ValidationError( - "You can't have the same value in both enabled and disabled channels lists." + "You can't have the same value in both enabled and disabled channels lists:" + f" {', '.join(repr(channel) for channel in common_channels)}." ) - if ( + common_categories = ( set(get_field_value(data, "disabled_categories")) & set(get_field_value(data, "enabled_categories")) - ): + ) + if common_categories: raise ValidationError( - "You can't have the same value in both enabled and disabled categories lists." + "You can't have the same value in both enabled and disabled categories lists:" + f" {', '.join(repr(category) for category in common_categories)}." ) return data @@ -333,17 +337,23 @@ class FilterListSerializer(ModelSerializer): data.get('disabled_channels') is not None and data.get('enabled_channels') is not None ): - channels_collection = data['disabled_channels'] + data['enabled_channels'] - if len(channels_collection) != len(set(channels_collection)): - raise ValidationError("Enabled and Disabled channels lists contain duplicates.") + common_channels = set(data['disabled_channels']) & set(data['enabled_channels']) + if common_channels: + raise ValidationError( + "You can't have the same value in both enabled and disabled channels lists:" + f" {', '.join(repr(channel) for channel in common_channels)}." + ) if ( data.get('disabled_categories') is not None and data.get('enabled_categories') is not None ): - categories_collection = data['disabled_categories'] + data['enabled_categories'] - if len(categories_collection) != len(set(categories_collection)): - raise ValidationError("Enabled and Disabled categories lists contain duplicates.") + common_categories = set(data['disabled_categories']) & set(data['enabled_categories']) + if common_categories: + raise ValidationError( + "You can't have the same value in both enabled and disabled categories lists:" + f" {', '.join(repr(category) for category in common_categories)}." + ) return data -- cgit v1.2.3 From 91b89475913210400cf39884efe37ab5552efbf7 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 25 Mar 2023 21:35:49 +0300 Subject: Use consistent quoting style Co-authored-by: Johannes Christ --- pydis_site/apps/api/models/bot/filters.py | 4 +- pydis_site/apps/api/serializers.py | 68 +++++++++++++++---------------- 2 files changed, 36 insertions(+), 36 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') 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/serializers.py') diff --git a/pydis_site/apps/api/migrations/0088_new_filter_schema.py b/pydis_site/apps/api/migrations/0088_new_filter_schema.py index 1506e4d7..2e1d78c9 100644 --- a/pydis_site/apps/api/migrations/0088_new_filter_schema.py +++ b/pydis_site/apps/api/migrations/0088_new_filter_schema.py @@ -64,7 +64,7 @@ def forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: updated_at=object_.updated_at, filter_list=list_, description=object_.comment, - additional_field=None, + additional_settings=None, guild_pings=None, filter_dm=None, dm_pings=None, @@ -105,7 +105,7 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True)), ('content', models.CharField(help_text='The definition of this filter.', max_length=100)), ('description', models.CharField(help_text='Why this filter has been added.', max_length=200, null=True)), - ('additional_field', models.JSONField(help_text='Implementation specific field.', null=True)), + ('additional_settings', models.JSONField(help_text='Additional settings which are specific to this filter.', null=True)), ('guild_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers.', size=None, null=True)), ('filter_dm', models.BooleanField(help_text='Whether DMs should be filtered.', null=True)), ('dm_pings', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), help_text='Who to ping when this filter triggers on a DM.', size=None, null=True)), diff --git a/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py b/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py index 0bcfd8a3..cb230a27 100644 --- a/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py +++ b/pydis_site/apps/api/migrations/0089_unique_constraint_filters.py @@ -11,13 +11,13 @@ class Migration(migrations.Migration): migrations.RunSQL( "ALTER TABLE api_filter " "ADD CONSTRAINT unique_filters UNIQUE NULLS NOT DISTINCT " - "(content, additional_field, filter_list_id, dm_content, dm_embed, infraction_type, infraction_reason, infraction_duration, infraction_channel, guild_pings, filter_dm, dm_pings, remove_context, bypass_roles, enabled, send_alert, enabled_channels, disabled_channels, enabled_categories, disabled_categories)", + "(content, additional_settings, filter_list_id, dm_content, dm_embed, infraction_type, infraction_reason, infraction_duration, infraction_channel, guild_pings, filter_dm, dm_pings, remove_context, bypass_roles, enabled, send_alert, enabled_channels, disabled_channels, enabled_categories, disabled_categories)", reverse_sql="ALTER TABLE api_filter DROP CONSTRAINT unique_filters", state_operations=[ migrations.AddConstraint( model_name='filter', constraint=models.UniqueConstraint( - fields=('content', 'additional_field', 'filter_list', 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', 'infraction_channel', 'guild_pings', 'filter_dm', 'dm_pings', 'remove_context', 'bypass_roles', 'enabled', 'send_alert', 'enabled_channels', 'disabled_channels', 'enabled_categories', 'disabled_categories'), + fields=('content', 'additional_settings', 'filter_list', 'dm_content', 'dm_embed', 'infraction_type', 'infraction_reason', 'infraction_duration', 'infraction_channel', 'guild_pings', 'filter_dm', 'dm_pings', 'remove_context', 'bypass_roles', 'enabled', 'send_alert', 'enabled_channels', 'disabled_channels', 'enabled_categories', 'disabled_categories'), name='unique_filters' ), ), diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py index c6f6f851..aadb39aa 100644 --- a/pydis_site/apps/api/models/bot/filters.py +++ b/pydis_site/apps/api/models/bot/filters.py @@ -131,7 +131,9 @@ class FilterBase(ModelTimestampMixin, ModelReprMixin, models.Model): max_length=200, help_text="Why this filter has been added.", null=True ) - additional_field = models.JSONField(null=True, help_text="Implementation specific field.") + additional_settings = models.JSONField( + null=True, help_text="Additional settings which are specific to this filter." + ) filter_list = models.ForeignKey( FilterList, models.CASCADE, related_name="filters", help_text="The filter list containing this filter." diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index da02c837..a3779094 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -184,7 +184,7 @@ ALLOW_EMPTY_SETTINGS = ( # Required fields for custom JSON representation purposes BASE_FILTER_FIELDS = ( - 'id', 'created_at', 'updated_at', 'content', 'description', 'additional_field' + 'id', 'created_at', 'updated_at', 'content', 'description', 'additional_settings' ) BASE_FILTERLIST_FIELDS = ('id', 'created_at', 'updated_at', 'name', 'list_type') BASE_SETTINGS_FIELDS = ( @@ -279,7 +279,7 @@ class FilterSerializer(ModelSerializer): 'updated_at', 'content', 'description', - 'additional_field', + 'additional_settings', 'filter_list' ) + SETTINGS_FIELDS extra_kwargs = _create_meta_extra_kwargs(for_filter=True) @@ -382,9 +382,10 @@ class FilterListSerializer(ModelSerializer): Provides a custom JSON representation to the FilterList Serializers. This representation restructures how the Filter is represented. - It groups the Infraction, Channel and Mention related fields into their own separated group. + It groups the Infraction, Channel, and Mention related fields + into their own separated groups. - Furthermore, it puts the fields that meant to represent FilterList settings, + Furthermore, it puts the fields that are meant to represent FilterList settings, into a sub-field called `settings`. """ schema = {name: getattr(instance, name) for name in BASE_FILTERLIST_FIELDS} diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index f36e0617..3d3be51e 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -92,7 +92,7 @@ def get_test_sequences() -> Dict[str, TestSequence]: { "content": "bad word", "description": "This is a really bad word.", - "additional_field": "{'hi': 'there'}", + "additional_settings": "{'hi': 'there'}", "guild_pings": None, "filter_dm": None, "dm_pings": None, diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index 25d771cc..d3341b35 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -113,7 +113,7 @@ class StringDunderMethodTests(SimpleTestCase): Filter( content="ducky_nsfw", description="This ducky is totally inappropriate!", - additional_field=None, + additional_settings=None, ), OffensiveMessage( id=602951077675139072, diff --git a/pydis_site/apps/api/viewsets/bot/filters.py b/pydis_site/apps/api/viewsets/bot/filters.py index c84da909..d6c2d18c 100644 --- a/pydis_site/apps/api/viewsets/bot/filters.py +++ b/pydis_site/apps/api/viewsets/bot/filters.py @@ -33,7 +33,7 @@ class FilterListViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -119,7 +119,7 @@ class FilterListViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -239,7 +239,7 @@ class FilterListViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -334,7 +334,7 @@ class FilterViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -379,7 +379,7 @@ class FilterViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, @@ -420,7 +420,7 @@ class FilterViewSet(ModelViewSet): ... "filter_list": 1, ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "bypass_roles": None, ... "filter_dm": None, ... "enabled": False, @@ -454,7 +454,7 @@ class FilterViewSet(ModelViewSet): ... "updated_at": "2023-01-27T21:26:34.030532Z", ... "content": "267624335836053506", ... "description": "Python Discord", - ... "additional_field": None, + ... "additional_settings": None, ... "filter_list": 1, ... "settings": { ... "bypass_roles": None, -- cgit v1.2.3 From 4c923fa1cd6f1f5144036317b116aac745b3c345 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Wed, 5 Apr 2023 03:10:05 +0300 Subject: Add maximum auto-timeout duration validation --- pydis_site/apps/api/serializers.py | 31 +++++++++++++++++++++++++------ pydis_site/apps/api/tests/test_filters.py | 2 ++ 2 files changed, 27 insertions(+), 6 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index a3779094..2186b02c 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,4 +1,5 @@ """Converters from Django models to data interchange formats and back.""" +from datetime import timedelta from typing import Any from django.db.models.query import QuerySet @@ -210,6 +211,8 @@ CHANNEL_SCOPE_FIELDS = ( ) MENTIONS_FIELDS = ('guild_pings', 'dm_pings') +MAX_TIMEOUT_DURATION = timedelta(days=28) + def _create_meta_extra_kwargs(*, for_filter: bool) -> dict[str, dict[str, bool]]: """Create the extra kwargs for the Meta classes of the Filter and FilterList serializers.""" @@ -236,17 +239,24 @@ class FilterSerializer(ModelSerializer): def validate(self, data: dict) -> dict: """Perform infraction data + allowed and disallowed lists validation.""" + infraction_type = get_field_value(data, 'infraction_type') + infraction_duration = get_field_value(data, 'infraction_duration') if ( - ( - get_field_value(data, 'infraction_reason') - or get_field_value(data, 'infraction_duration') - ) - and get_field_value(data, 'infraction_type') == 'NONE' + (get_field_value(data, 'infraction_reason') or infraction_duration) + and infraction_type == 'NONE' ): raise ValidationError( "Infraction type is required with infraction duration or reason." ) + if ( + infraction_type == 'TIMEOUT' + and (not infraction_duration or infraction_duration > MAX_TIMEOUT_DURATION) + ): + raise ValidationError( + f"A timeout cannot be longer than {MAX_TIMEOUT_DURATION.days} days." + ) + common_channels = ( set(get_field_value(data, 'disabled_channels')) & set(get_field_value(data, 'enabled_channels')) @@ -328,8 +338,9 @@ class FilterListSerializer(ModelSerializer): def validate(self, data: dict) -> dict: """Perform infraction data + allow and disallowed lists validation.""" + infraction_duration = data.get('infraction_duration') if ( - data.get('infraction_reason') or data.get('infraction_duration') + data.get('infraction_reason') or infraction_duration ) and not data.get('infraction_type'): raise ValidationError("Infraction type is required with infraction duration or reason") @@ -344,6 +355,14 @@ class FilterListSerializer(ModelSerializer): f" {', '.join(repr(channel) for channel in common_channels)}." ) + if ( + data.get('infraction_type') == 'TIMEOUT' + and (not infraction_duration or infraction_duration > MAX_TIMEOUT_DURATION) + ): + raise ValidationError( + f"A timeout cannot be longer than {MAX_TIMEOUT_DURATION.days} days." + ) + if ( data.get('disabled_categories') is not None and data.get('enabled_categories') is not None diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index 3d3be51e..ebc4a2cf 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -274,6 +274,7 @@ class FilterValidationTests(AuthenticatedAPITestCase): ({"infraction_reason": "hi"}, {}, 400), ({"infraction_duration": timedelta(seconds=10)}, {}, 400), ({"infraction_reason": "hi"}, {"infraction_type": "NOTE"}, 200), + ({"infraction_type": "TIMEOUT", "infraction_duration": timedelta(days=30)}, {}, 400), ({"infraction_duration": timedelta(seconds=10)}, {"infraction_type": "TIMEOUT"}, 200), ({"enabled_channels": ["admins"]}, {}, 200), ({"disabled_channels": ["123"]}, {}, 200), @@ -313,6 +314,7 @@ class FilterValidationTests(AuthenticatedAPITestCase): cases = ( ({"infraction_reason": "hi"}, 400), ({"infraction_duration": timedelta(seconds=10)}, 400), + ({"infraction_type": "TIMEOUT", "infraction_duration": timedelta(days=30)}, 400), ({"infraction_reason": "hi", "infraction_type": "NOTE"}, 200), ({"infraction_duration": timedelta(seconds=10), "infraction_type": "TIMEOUT"}, 200), ({"enabled_channels": ["admins"]}, 200), ({"disabled_channels": ["123"]}, 200), -- cgit v1.2.3 From e8f8161e41a4735897b3038e202107f5d55ec96e Mon Sep 17 00:00:00 2001 From: jchristgit Date: Mon, 11 Dec 2023 18:09:39 +0100 Subject: Unify frozen fields logic into serializer mixin (#1169) Additionally, implement frozen fields on the offensive message serializer. --- pydis_site/apps/api/serializers.py | 34 ++++++++++++++++++++-- pydis_site/apps/api/tests/test_nominations.py | 2 +- .../apps/api/tests/test_offensive_message.py | 8 +++++ pydis_site/apps/api/viewsets/bot/infraction.py | 5 ---- pydis_site/apps/api/viewsets/bot/nomination.py | 5 ---- 5 files changed, 40 insertions(+), 14 deletions(-) (limited to 'pydis_site/apps/api/serializers.py') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 2186b02c..87fd6190 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -2,6 +2,7 @@ from datetime import timedelta from typing import Any +from django.db import models from django.db.models.query import QuerySet from django.db.utils import IntegrityError from rest_framework.exceptions import NotFound @@ -35,6 +36,30 @@ from .models import ( User ) +class FrozenFieldsMixin: + """ + Serializer mixin that allows adding non-updateable fields to a serializer. + + To use, inherit from the mixin and specify the fields that should only be + written to on creation in the `frozen_fields` attribute of the `Meta` class + in a serializer. + + See also the DRF discussion for this feature at + https://github.com/encode/django-rest-framework/discussions/8606, which may + eventually provide an official way to implement this. + """ + + def update(self, instance: models.Model, validated_data: dict) -> models.Model: + """Validate that no frozen fields were changed and update the instance.""" + for field_name in getattr(self.Meta, 'frozen_fields', ()): + if field_name in validated_data: + raise ValidationError( + { + field_name: ["This field cannot be updated."] + } + ) + return super().update(instance, validated_data) + class BotSettingSerializer(ModelSerializer): """A class providing (de-)serialization of `BotSetting` instances.""" @@ -426,7 +451,7 @@ class FilterListSerializer(ModelSerializer): # endregion -class InfractionSerializer(ModelSerializer): +class InfractionSerializer(FrozenFieldsMixin, ModelSerializer): """A class providing (de-)serialization of `Infraction` instances.""" class Meta: @@ -447,6 +472,7 @@ class InfractionSerializer(ModelSerializer): 'dm_sent', 'jump_url' ) + frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden') def validate(self, attrs: dict) -> dict: """Validate data constraints for the given data and abort if it is invalid.""" @@ -683,7 +709,7 @@ class NominationEntrySerializer(ModelSerializer): fields = ('nomination', 'actor', 'reason', 'inserted_at') -class NominationSerializer(ModelSerializer): +class NominationSerializer(FrozenFieldsMixin, ModelSerializer): """A class providing (de-)serialization of `Nomination` instances.""" entries = NominationEntrySerializer(many=True, read_only=True) @@ -703,9 +729,10 @@ class NominationSerializer(ModelSerializer): 'entries', 'thread_id' ) + frozen_fields = ('id', 'inserted_at', 'user', 'ended_at') -class OffensiveMessageSerializer(ModelSerializer): +class OffensiveMessageSerializer(FrozenFieldsMixin, ModelSerializer): """A class providing (de-)serialization of `OffensiveMessage` instances.""" class Meta: @@ -713,3 +740,4 @@ class OffensiveMessageSerializer(ModelSerializer): model = OffensiveMessage fields = ('id', 'channel_id', 'delete_date') + frozen_fields = ('id', 'channel_id') diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 7fe2f0a8..e4dfe36a 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -254,7 +254,7 @@ class NominationTests(AuthenticatedAPITestCase): def test_returns_400_on_frozen_field_update(self): url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) data = { - 'user': "Theo Katzman" + 'user': 1234 } response = self.client.patch(url, data=data) diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py index d01231f1..2dc60bc3 100644 --- a/pydis_site/apps/api/tests/test_offensive_message.py +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -156,6 +156,14 @@ class UpdateOffensiveMessageTestCase(AuthenticatedAPITestCase): delta=datetime.timedelta(seconds=1), ) + def test_updating_write_once_fields(self): + """Fields such as the channel ID may not be updated.""" + url = reverse('api:bot:offensivemessage-detail', args=(self.message.id,)) + data = {'channel_id': self.message.channel_id + 1} + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), {'channel_id': ["This field cannot be updated."]}) + def test_updating_nonexistent_message(self): url = reverse('api:bot:offensivemessage-detail', args=(self.message.id + 1,)) data = {'delete_date': self.in_one_week} diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index 26cae3ad..09c05a74 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -157,14 +157,9 @@ class InfractionViewSet( filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) filterset_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type') search_fields = ('$reason',) - frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden') def partial_update(self, request: HttpRequest, *_args, **_kwargs) -> Response: """Method that handles the nuts and bolts of updating an Infraction.""" - for field in request.data: - if field in self.frozen_fields: - raise ValidationError({field: ['This field cannot be updated.']}) - instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 78687e0e..953513e0 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -173,7 +173,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge queryset = Nomination.objects.all() filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) filterset_fields = ('user__id', 'active') - frozen_fields = ('id', 'inserted_at', 'user', 'ended_at') frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at', 'reviewed') def create(self, request: HttpRequest, *args, **kwargs) -> Response: @@ -238,10 +237,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge Called by the Django Rest Framework in response to the corresponding HTTP request. """ - for field in request.data: - if field in self.frozen_fields: - raise ValidationError({field: ['This field cannot be updated.']}) - instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) -- cgit v1.2.3