diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index fc4be2097..4f5acb4a9 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -1,3 +1,5 @@ +from functools import lru_cache + from django.contrib.postgres.fields import ArrayField from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -70,6 +72,12 @@ def __str__(self): return f"{self.name}#{self.discriminator:04d}" return self.name + @staticmethod + @lru_cache(maxsize=256) + def _get_roles(role_ids: tuple[int, ...]) -> list[Role]: + """Fetch the given roles, cached to avoid refetching the same roles for every user.""" + return list(Role.objects.filter(id__in=role_ids)) + @property def top_role(self) -> Role: """ @@ -77,7 +85,7 @@ def top_role(self) -> Role: This will fall back to the Developers role if the user does not have any roles. """ - roles = Role.objects.filter(id__in=self.roles) + roles = self._get_roles(tuple(sorted(self.roles))) if not roles: return Role.objects.get(name="Developers") return max(roles) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 1f1fd41f9..4031c1e33 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -435,10 +435,11 @@ def to_representation(self, instance: FilterList) -> dict: into a sub-field called `settings`. """ 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).prefetch_related('filter_list') - ] + filters = [] + for item in instance.filters.all(): + item.filter_list = instance + filters.append(FilterSerializer(many=False).to_representation(instance=item)) + schema['filters'] = filters settings = {name: getattr(instance, name) for name in BASE_SETTINGS_FIELDS} settings['infraction_and_notification'] = { diff --git a/pydis_site/apps/api/signals.py b/pydis_site/apps/api/signals.py index 5c26bfb63..50b61649d 100644 --- a/pydis_site/apps/api/signals.py +++ b/pydis_site/apps/api/signals.py @@ -1,4 +1,4 @@ -from django.db.models.signals import post_delete +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from pydis_site.apps.api.models.bot import Role, User @@ -10,3 +10,10 @@ def delete_role_from_user(sender: Role, instance: Role, **kwargs) -> None: for user in User.objects.filter(roles__contains=[instance.id]): del user.roles[user.roles.index(instance.id)] user.save() + + +@receiver(signal=post_save, sender=Role) +@receiver(signal=post_delete, sender=Role) +def clear_top_role_cache(sender: Role, **kwargs) -> None: + """Clear the cached role lookups used by `User.top_role` when a role changes.""" + User._get_roles.cache_clear() diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index 831c4649c..859a43b0b 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -358,6 +358,20 @@ def test_filter_unique_constraint(self) -> None: response = self.client.post(test_filter.url(), data=clean_test_json(test_filter.object)) self.assertEqual(response.status_code, 400) + def test_filter_list_includes_nested_filters(self) -> None: + filter_list = FilterList.objects.create(**get_test_sequences()["filter_list1"].object) + filter_ = Filter.objects.create( + filter_list=filter_list, content="bad word", description="This is a bad word." + ) + + response = self.client.get(f"{reverse('api:bot:filterlist-list')}/{filter_list.id}") + + self.assertEqual(response.status_code, 200) + filters = response.json()["filters"] + self.assertEqual(len(filters), 1) + self.assertEqual(filters[0]["id"], filter_.id) + self.assertEqual(filters[0]["filter_list"], filter_list.id) + class FilterCreationMissingOptionalFieldsTestCase(AuthenticatedAPITestCase): @classmethod diff --git a/pydis_site/apps/staff/views.py b/pydis_site/apps/staff/views.py index 22dede952..c78ea4fd1 100644 --- a/pydis_site/apps/staff/views.py +++ b/pydis_site/apps/staff/views.py @@ -1,5 +1,7 @@ +from django.db.models import Prefetch from django.views.generic.detail import DetailView +from pydis_site.apps.api.models.bot.deleted_message import DeletedMessage from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext @@ -9,3 +11,9 @@ class LogView(DetailView): model = MessageDeletionContext context_object_name = "deletion_context" template_name = "staff/logs.html" + queryset = MessageDeletionContext.objects.select_related("actor").prefetch_related( + Prefetch( + "deletedmessage_set", + queryset=DeletedMessage.objects.select_related("author"), + ), + )