diff options
Diffstat (limited to 'pydis_site/apps/api')
31 files changed, 376 insertions, 97 deletions
diff --git a/pydis_site/apps/api/__init__.py b/pydis_site/apps/api/__init__.py index e69de29b..afa5b4d5 100644 --- a/pydis_site/apps/api/__init__.py +++ b/pydis_site/apps/api/__init__.py @@ -0,0 +1 @@ +default_app_config = 'pydis_site.apps.api.apps.ApiConfig' diff --git a/pydis_site/apps/api/apps.py b/pydis_site/apps/api/apps.py index 76810b2e..18eda9e3 100644 --- a/pydis_site/apps/api/apps.py +++ b/pydis_site/apps/api/apps.py @@ -4,4 +4,12 @@ from django.apps import AppConfig class ApiConfig(AppConfig): """Django AppConfig for the API app.""" - name = 'api' + name = 'pydis_site.apps.api' + + def ready(self) -> None: + """ + Gets called as soon as the registry is fully populated. + + https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.ready + """ + import pydis_site.apps.api.signals # noqa: F401 diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py index 8c550191..273db3d1 100644 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -60,35 +60,35 @@ domain_name_blacklist = [ ] filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), + (r"\bgoo+ks*\b", None, False), + (r"\bky+s+\b", None, False), + (r"\bki+ke+s*\b", None, False), + (r"\bbeaner+s?\b", None, False), + (r"\bcoo+ns*\b", None, False), + (r"\bnig+lets*\b", None, False), + (r"\bslant-eyes*\b", None, False), + (r"\btowe?l-?head+s*\b", None, False), + (r"\bchi*n+k+s*\b", None, False), + (r"\bspick*s*\b", None, False), + (r"\bkill* +(?:yo)?urself+\b", None, False), + (r"\bjew+s*\b", None, False), + (r"\bsuicide\b", None, False), + (r"\brape\b", None, False), + (r"\b(re+)tar+(d+|t+)(ed)?\b", None, False), + (r"\bta+r+d+\b", None, False), + (r"\bcunts*\b", None, False), + (r"\btrann*y\b", None, False), + (r"\bshemale\b", None, False), + (r"fa+g+s*", None, False), + (r"卐", None, False), + (r"卍", None, False), + (r"࿖", None, False), + (r"࿕", None, False), + (r"࿘", None, False), + (r"࿗", None, False), + (r"cuck(?!oo+)", None, False), + (r"nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + (r"fag+o+t+s*", None, False), ] file_format_whitelist = [ diff --git a/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py b/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py new file mode 100644 index 00000000..dbd7ac91 --- /dev/null +++ b/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.14 on 2021-05-19 05:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0069_documentationlink_validators'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='active', + field=models.BooleanField(default=True, help_text='Whether or not this name should be considered for naming channels.'), + ), + migrations.AlterField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation.'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py b/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py new file mode 100644 index 00000000..f12efab5 --- /dev/null +++ b/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.14 on 2021-07-24 13:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0071_increase_message_content_4000'), + ('api', '0070_auto_20210519_0545'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py b/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py new file mode 100644 index 00000000..ebf5ae15 --- /dev/null +++ b/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.14 on 2021-11-05 05:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0072_merge_20210724_1354'), + ('api', '0073_otn_allow_GT_and_LT'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0074_reminder_failures.py b/pydis_site/apps/api/migrations/0074_reminder_failures.py new file mode 100644 index 00000000..2860046e --- /dev/null +++ b/pydis_site/apps/api/migrations/0074_reminder_failures.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.14 on 2021-10-27 17:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0073_otn_allow_GT_and_LT'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='failures', + field=models.IntegerField(default=0, help_text='Number of times we attempted to send the reminder and failed.'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0075_add_redirects_filter.py b/pydis_site/apps/api/migrations/0075_add_redirects_filter.py new file mode 100644 index 00000000..23dc176f --- /dev/null +++ b/pydis_site/apps/api/migrations/0075_add_redirects_filter.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.14 on 2021-11-17 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0074_reminder_failures'), + ] + + operations = [ + migrations.AlterField( + model_name='filterlist', + name='type', + field=models.CharField(choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token'), ('REDIRECT', 'Redirect')], help_text='The type of allowlist this is on.', max_length=50), + ), + ] diff --git a/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py b/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py new file mode 100644 index 00000000..c0ac709d --- /dev/null +++ b/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.14 on 2021-11-10 22:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0074_reminder_failures'), + ] + + operations = [ + migrations.AddField( + model_name='infraction', + name='dm_sent', + field=models.BooleanField(help_text='Whether a DM was sent to the user when infraction was applied.', null=True), + ), + ] diff --git a/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py b/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py new file mode 100644 index 00000000..097d0a0c --- /dev/null +++ b/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.14 on 2021-11-25 19:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0075_infraction_dm_sent'), + ('api', '0075_add_redirects_filter'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py b/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py new file mode 100644 index 00000000..9e8f2fb9 --- /dev/null +++ b/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.13 on 2021-11-27 12:27 + +import django.contrib.postgres.fields +from django.db import migrations, models +import pydis_site.apps.api.models.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0076_merge_20211125_1941'), + ] + + operations = [ + migrations.AlterField( + model_name='botsetting', + name='data', + field=models.JSONField(help_text='The actual settings of this setting.'), + ), + migrations.AlterField( + model_name='deletedmessage', + name='embeds', + field=django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), blank=True, help_text='Embeds attached to this message.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py b/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py new file mode 100644 index 00000000..5ce0e871 --- /dev/null +++ b/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.14 on 2021-12-13 05:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0077_use_generic_jsonfield'), + ('api', '0074_merge_20211105_0518'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py index 2a3944f8..1bcb1ae6 100644 --- a/pydis_site/apps/api/models/bot/bot_setting.py +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -1,4 +1,3 @@ -from django.contrib.postgres import fields as pgfields from django.core.exceptions import ValidationError from django.db import models @@ -24,6 +23,6 @@ class BotSetting(ModelReprMixin, models.Model): max_length=50, validators=(validate_bot_setting_name,) ) - data = pgfields.JSONField( + data = models.JSONField( help_text="The actual settings of this setting." ) diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index d279e137..d30f7213 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -12,6 +12,7 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): 'FILE_FORMAT ' 'DOMAIN_NAME ' 'FILTER_TOKEN ' + 'REDIRECT ' ) type = models.CharField( max_length=50, diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 63745490..c9303024 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -58,6 +58,10 @@ class Infraction(ModelReprMixin, models.Model): default=False, help_text="Whether the infraction is a shadow infraction." ) + dm_sent = models.BooleanField( + null=True, + help_text="Whether a DM was sent to the user when infraction was applied." + ) def __str__(self): """Returns some info on the current infraction, for display purposes.""" diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 60e2a553..bab3368d 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -48,7 +48,7 @@ class Message(ModelReprMixin, models.Model): blank=True ) embeds = pgfields.ArrayField( - pgfields.JSONField( + models.JSONField( validators=(validate_embed,) ), blank=True, diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 33fb7ad7..901f191a 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -4,10 +4,10 @@ from django.db import connections BLOCK_INTERVAL = 10 * 60 # 10 minute blocks -EXCLUDE_CHANNELS = [ +EXCLUDE_CHANNELS = ( "267659945086812160", # Bot commands "607247579608121354" # SeasonalBot commands -] +) class NotFoundError(Exception): @@ -46,12 +46,12 @@ class Metricity: self.cursor.execute( """ SELECT - COUNT(*) + COUNT(*) FROM messages WHERE - author_id = '%s' - AND NOT is_deleted - AND NOT %s::varchar[] @> ARRAY[channel_id] + author_id = '%s' + AND NOT is_deleted + AND channel_id NOT IN %s """, [user_id, EXCLUDE_CHANNELS] ) @@ -79,7 +79,7 @@ class Metricity: WHERE author_id='%s' AND NOT is_deleted - AND NOT %s::varchar[] @> ARRAY[channel_id] + AND channel_id NOT IN %s GROUP BY interval ) block_query; """, diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 8999e560..e9fec114 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -18,7 +18,12 @@ class OffTopicChannelName(ModelReprMixin, models.Model): used = models.BooleanField( default=False, - help_text="Whether or not this name has already been used during this rotation", + help_text="Whether or not this name has already been used during this rotation.", + ) + + active = models.BooleanField( + default=True, + help_text="Whether or not this name should be considered for naming channels." ) def __str__(self): diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index 7d968a0e..173900ee 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -59,6 +59,10 @@ class Reminder(ModelReprMixin, models.Model): blank=True, help_text="IDs of roles or users to ping with the reminder." ) + failures = models.IntegerField( + default=0, + help_text="Number of times we attempted to send the reminder and failed." + ) def __str__(self): """Returns some info on the current reminder, for display purposes.""" diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 0e220a1d..859394d2 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -103,11 +103,10 @@ def validate_embed(embed: Any) -> None: Example: - >>> from django.contrib.postgres import fields as pgfields >>> from django.db import models >>> from pydis_site.apps.api.models.utils import validate_embed >>> class MyMessage(models.Model): - ... embed = pgfields.JSONField( + ... embed = models.JSONField( ... validators=( ... validate_embed, ... ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 8484e561..4a702d61 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -145,7 +145,16 @@ class InfractionSerializer(ModelSerializer): model = Infraction fields = ( - 'id', 'inserted_at', 'expires_at', 'active', 'user', 'actor', 'type', 'reason', 'hidden' + 'id', + 'inserted_at', + 'expires_at', + 'active', + 'user', + 'actor', + 'type', + 'reason', + 'hidden', + 'dm_sent' ) validators = [ UniqueTogetherValidator( @@ -200,25 +209,30 @@ class ExpandedInfractionSerializer(InfractionSerializer): return ret +class OffTopicChannelNameListSerializer(ListSerializer): + """Custom ListSerializer to override to_representation() when list views are triggered.""" + + def to_representation(self, objects: list[OffTopicChannelName]) -> list[str]: + """ + Return a list with all `OffTopicChannelName`s in the database. + + This returns the list of off topic channel names. We want to only return + the name attribute, hence it is unnecessary to create a nested dictionary. + Additionally, this allows off topic channel name routes to simply return an + array of names instead of objects, saving on bandwidth. + """ + return [obj.name for obj in objects] + + class OffTopicChannelNameSerializer(ModelSerializer): """A class providing (de-)serialization of `OffTopicChannelName` instances.""" class Meta: """Metadata defined for the Django REST Framework.""" + list_serializer_class = OffTopicChannelNameListSerializer model = OffTopicChannelName - fields = ('name',) - - def to_representation(self, obj: OffTopicChannelName) -> str: - """ - Return the representation of this `OffTopicChannelName`. - - This only returns the name of the off topic channel name. As the model - only has a single attribute, it is unnecessary to create a nested dictionary. - Additionally, this allows off topic channel name routes to simply return an - array of names instead of objects, saving on bandwidth. - """ - return obj.name + fields = ('name', 'used', 'active') class ReminderSerializer(ModelSerializer): @@ -231,7 +245,15 @@ class ReminderSerializer(ModelSerializer): model = Reminder fields = ( - 'active', 'author', 'jump_url', 'channel_id', 'content', 'expiration', 'id', 'mentions' + 'active', + 'author', + 'jump_url', + 'channel_id', + 'content', + 'expiration', + 'id', + 'mentions', + 'failures' ) diff --git a/pydis_site/apps/api/signals.py b/pydis_site/apps/api/signals.py new file mode 100644 index 00000000..5c26bfb6 --- /dev/null +++ b/pydis_site/apps/api/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import post_delete +from django.dispatch import receiver + +from pydis_site.apps.api.models.bot import Role, User + + +@receiver(signal=post_delete, sender=Role) +def delete_role_from_user(sender: Role, instance: Role, **kwargs) -> None: + """Unassigns the Role (instance) that is being deleted from every user that has it.""" + for user in User.objects.filter(roles__contains=[instance.id]): + del user.roles[user.roles.index(instance.id)] + user.save() diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 63993978..2d273756 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -65,8 +65,15 @@ class EmptyDatabaseTests(AuthenticatedAPITestCase): class ListTests(AuthenticatedAPITestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) + cls.test_name = OffTopicChannelName.objects.create( + name='lemons-lemonade-stand', used=False, active=True + ) + cls.test_name_2 = OffTopicChannelName.objects.create( + name='bbq-with-bisk', used=False, active=True + ) + cls.test_name_3 = OffTopicChannelName.objects.create( + name="frozen-with-iceman", used=True, active=False + ) def test_returns_name_in_list(self): """Return all off-topic channel names.""" @@ -75,29 +82,55 @@ class ListTests(AuthenticatedAPITestCase): self.assertEqual(response.status_code, 200) self.assertEqual( - response.json(), - [ + set(response.json()), + { self.test_name.name, - self.test_name_2.name - ] + self.test_name_2.name, + self.test_name_3.name + } ) - def test_returns_single_item_with_random_items_param_set_to_1(self): + def test_returns_two_items_with_random_items_param_set_to_2(self): """Return not-used name instead used.""" url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=1') + response = self.client.get(f'{url}?random_items=2') self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json(), [self.test_name.name]) + self.assertEqual(len(response.json()), 2) + self.assertEqual(set(response.json()), {self.test_name.name, self.test_name_2.name}) def test_running_out_of_names_with_random_parameter(self): """Reset names `used` parameter to `False` when running out of names.""" url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=2') + response = self.client.get(f'{url}?random_items=3') self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) + self.assertEqual( + set(response.json()), + {self.test_name.name, self.test_name_2.name, self.test_name_3.name} + ) + + def test_returns_inactive_ot_names(self): + """Return inactive off topic names.""" + url = reverse('api:bot:offtopicchannelname-list') + response = self.client.get(f"{url}?active=false") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + [self.test_name_3.name] + ) + + def test_returns_active_ot_names(self): + """Return active off topic names.""" + url = reverse('api:bot:offtopicchannelname-list') + response = self.client.get(f"{url}?active=true") + + self.assertEqual(response.status_code, 200) + self.assertEqual( + set(response.json()), + {self.test_name.name, self.test_name_2.name} + ) class CreationTests(AuthenticatedAPITestCase): @@ -154,7 +187,7 @@ class DeletionTests(AuthenticatedAPITestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): - """Return 404 reponse when trying to delete unknown name.""" + """Return 404 response when trying to delete unknown name.""" url = reverse('api:bot:offtopicchannelname-detail', args=('unknown-name',)) response = self.client.delete(url) diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py index 9b79b38c..3cf95b75 100644 --- a/pydis_site/apps/api/tests/test_offensive_message.py +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -58,7 +58,7 @@ class CreationTests(AuthenticatedAPITestCase): ) for field, invalid_value in cases: - with self.subTest(fied=field, invalid_value=invalid_value): + with self.subTest(field=field, invalid_value=invalid_value): test_data = data.copy() test_data.update({field: invalid_value}) diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py index d39cea4d..73c80c77 100644 --- a/pydis_site/apps/api/tests/test_roles.py +++ b/pydis_site/apps/api/tests/test_roles.py @@ -1,7 +1,7 @@ from django.urls import reverse from .base import AuthenticatedAPITestCase -from ..models import Role +from ..models import Role, User class CreationTests(AuthenticatedAPITestCase): @@ -35,6 +35,20 @@ class CreationTests(AuthenticatedAPITestCase): permissions=6, position=0, ) + cls.role_to_delete = Role.objects.create( + id=7, + name="role to delete", + colour=7, + permissions=7, + position=0, + ) + cls.role_unassigned_test_user = User.objects.create( + id=8, + name="role_unassigned_test_user", + discriminator="0000", + roles=[cls.role_to_delete.id], + in_guild=True + ) def _validate_roledict(self, role_dict: dict) -> None: """Helper method to validate a dict representing a role.""" @@ -81,11 +95,11 @@ class CreationTests(AuthenticatedAPITestCase): url = reverse('api:bot:role-list') response = self.client.get(url) - self.assertContains(response, text="id", count=4, status_code=200) + self.assertContains(response, text="id", count=5, status_code=200) roles = response.json() self.assertIsInstance(roles, list) - self.assertEqual(len(roles), 4) + self.assertEqual(len(roles), 5) for role in roles: self._validate_roledict(role) @@ -181,6 +195,12 @@ class CreationTests(AuthenticatedAPITestCase): response = self.client.delete(url) self.assertEqual(response.status_code, 204) + def test_role_delete_unassigned(self): + """Tests if the deleted Role gets unassigned from the user.""" + self.role_to_delete.delete() + self.role_unassigned_test_user.refresh_from_db() + self.assertEqual(self.role_unassigned_test_user.roles, []) + def test_role_detail_404_all_methods(self): """Tests detail view with non-existing ID.""" url = reverse('api:bot:role-detail', args=(20190815,)) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 295bcf64..9b91380b 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -421,7 +421,7 @@ class UserMetricityTests(AuthenticatedAPITestCase): # Then self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), { + self.assertCountEqual(response.json(), { "joined_at": joined_at, "total_messages": total_messages, "voice_banned": False, diff --git a/pydis_site/apps/api/viewsets/bot/filter_list.py b/pydis_site/apps/api/viewsets/bot/filter_list.py index 2cb21ab9..4b05acee 100644 --- a/pydis_site/apps/api/viewsets/bot/filter_list.py +++ b/pydis_site/apps/api/viewsets/bot/filter_list.py @@ -59,7 +59,8 @@ class FilterListViewSet(ModelViewSet): ... ["GUILD_INVITE","Guild Invite"], ... ["FILE_FORMAT","File Format"], ... ["DOMAIN_NAME","Domain Name"], - ... ["FILTER_TOKEN","Filter Token"] + ... ["FILTER_TOKEN","Filter Token"], + ... ["REDIRECT", "Redirect"] ... ] #### Status codes diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index f8b0cb9d..8a48ed1f 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -70,7 +70,8 @@ class InfractionViewSet( ... 'actor': 125435062127820800, ... 'type': 'ban', ... 'reason': 'He terk my jerb!', - ... 'hidden': True + ... 'hidden': True, + ... 'dm_sent': True ... } ... ] @@ -100,7 +101,8 @@ class InfractionViewSet( ... 'hidden': True, ... 'type': 'ban', ... 'reason': 'He terk my jerb!', - ... 'user': 172395097705414656 + ... 'user': 172395097705414656, + ... 'dm_sent': False ... } #### Response format @@ -118,7 +120,8 @@ class InfractionViewSet( >>> { ... 'active': True, ... 'expires_at': '4143-02-15T21:04:31+00:00', - ... 'reason': 'durka derr' + ... 'reason': 'durka derr', + ... 'dm_sent': True ... } #### Response format diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 922e6555..78f8c340 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,18 +1,17 @@ from django.db.models import Case, Value, When from django.db.models.query import QuerySet -from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 from rest_framework.exceptions import ParseError -from rest_framework.mixins import DestroyModelMixin +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.status import HTTP_201_CREATED -from rest_framework.viewsets import ViewSet +from rest_framework.viewsets import ModelViewSet from pydis_site.apps.api.models.bot.off_topic_channel_name import OffTopicChannelName from pydis_site.apps.api.serializers import OffTopicChannelNameSerializer -class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): +class OffTopicChannelNameViewSet(ModelViewSet): """ View of off-topic channel names used by the bot to rotate our off-topic names on a daily basis. @@ -58,6 +57,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): lookup_field = 'name' serializer_class = OffTopicChannelNameSerializer + queryset = OffTopicChannelName.objects.all() def get_object(self) -> OffTopicChannelName: """ @@ -65,15 +65,14 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): If it doesn't, a HTTP 404 is returned by way of throwing an exception. """ - queryset = self.get_queryset() name = self.kwargs[self.lookup_field] - return get_object_or_404(queryset, name=name) + return get_object_or_404(self.queryset, name=name) def get_queryset(self) -> QuerySet: """Returns a queryset that covers the entire OffTopicChannelName table.""" return OffTopicChannelName.objects.all() - def create(self, request: HttpRequest) -> Response: + def create(self, request: Request, *args, **kwargs) -> Response: """ DRF method for creating a new OffTopicChannelName. @@ -91,7 +90,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'name': ["This query parameter is required."] }) - def list(self, request: HttpRequest) -> Response: + def list(self, request: Request, *args, **kwargs) -> Response: """ DRF method for listing OffTopicChannelName entries. @@ -109,13 +108,13 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('used', '?')[:random_count] + queryset = self.queryset.order_by('used', '?')[:random_count] # When any name is used in our listing then this means we reached end of round # and we need to reset all other names `used` to False if any(offtopic_name.used for offtopic_name in queryset): # These names that we just got have to be excluded from updating used to False - self.get_queryset().update( + self.queryset.update( used=Case( When( name__in=(offtopic_name.name for offtopic_name in queryset), @@ -126,13 +125,18 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): ) else: # Otherwise mark selected names `used` to True - self.get_queryset().filter( + self.queryset.filter( name__in=(offtopic_name.name for offtopic_name in queryset) ).update(used=True) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) - queryset = self.get_queryset() + params = {} + + if active_param := request.query_params.get("active"): + params["active"] = active_param.lower() == "true" + + queryset = self.queryset.filter(**params) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 111660d9..78d7cb3b 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -42,7 +42,8 @@ class ReminderViewSet( ... 'expiration': '5018-11-20T15:52:00Z', ... 'id': 11, ... 'channel_id': 634547009956872193, - ... 'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>" + ... 'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>", + ... 'failures': 3 ... }, ... ... ... ] @@ -67,7 +68,8 @@ class ReminderViewSet( ... 'expiration': '5018-11-20T15:52:00Z', ... 'id': 11, ... 'channel_id': 634547009956872193, - ... 'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>" + ... 'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>", + ... 'failures': 3 ... } #### Status codes @@ -80,7 +82,7 @@ class ReminderViewSet( #### Request body >>> { ... 'author': int, - ... 'mentions': List[int], + ... 'mentions': list[int], ... 'content': str, ... 'expiration': str, # ISO-formatted datetime ... 'channel_id': int, @@ -98,9 +100,10 @@ class ReminderViewSet( #### Request body >>> { - ... 'mentions': List[int], + ... 'mentions': list[int], ... 'content': str, - ... 'expiration': str # ISO-formatted datetime + ... 'expiration': str, # ISO-formatted datetime + ... 'failures': int ... } #### Status codes diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 22d13dc4..1a5e79f8 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -271,9 +271,11 @@ class UserViewSet(ModelViewSet): with Metricity() as metricity: try: data = metricity.user(user.id) + data["total_messages"] = metricity.total_messages(user.id) - data["voice_banned"] = voice_banned data["activity_blocks"] = metricity.total_message_blocks(user.id) + + data["voice_banned"] = voice_banned return Response(data, status=status.HTTP_200_OK) except NotFoundError: return Response(dict(detail="User not found in metricity"), |