From 0e053d4b81a999585327e9e481407c8943d13e0b Mon Sep 17 00:00:00 2001 From: Yazan Zarka Date: Mon, 28 Oct 2024 17:28:03 +0100 Subject: [PATCH] Feat(registration)/filter participants (#895) * filtering by year and study * linting fix * added allergy filter * added filter by allergies and participants with allergy count to event. * Lint fix * Add new fixture for admin user * Start testing filtering + finished allergy filter test * Added integration test for participants filtering * lint fix * removed unused import * Update changelog * merge with dev and more filters * Fixed has_paid filter and added filter combination test * ran linting script --------- Co-authored-by: Harry Linrui XU Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- CHANGELOG.md | 1 + app/content/filters/registration.py | 64 ++++++++- app/content/serializers/event.py | 23 +++ app/tests/conftest.py | 7 + .../content/test_registration_integration.py | 134 ++++++++++++++++++ 5 files changed, 228 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1eaec33..c4c74f5f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ --- ## Neste versjon +- ✨ **Filtrering**. Admin kan nå filtere deltakere av et arrangement på studie, studieår, om deltakere har allergier, (om deltakere godtar å bli tatt bilde av, om deltakere har ankommet), i tillegg til søk på fornavn og etternavn. ## Versjon 2024.10.11 - ✨ **Tilbakemelding-funksjon**. Man kan nå opprette tilbakemeldinger for bugs og idé. diff --git a/app/content/filters/registration.py b/app/content/filters/registration.py index 94f35b037..92b7eee6a 100644 --- a/app/content/filters/registration.py +++ b/app/content/filters/registration.py @@ -1,9 +1,71 @@ +from django.db.models import Exists, OuterRef +from django_filters import rest_framework as filters from django_filters.rest_framework import FilterSet +from app.common.enums import NativeGroupType as GroupType from app.content.models.registration import Registration +from app.payment.enums import OrderStatus +from app.payment.models import Order class RegistrationFilter(FilterSet): + + study = filters.CharFilter( + field_name="user__groups__name", lookup_expr="icontains", method="filter_study" + ) + year = filters.CharFilter( + field_name="user__groups__name", lookup_expr="icontains", method="filter_year" + ) + + has_allergy = filters.BooleanFilter( + field_name="user__allergy", method="filter_has_allergy" + ) + + has_paid = filters.BooleanFilter( + field_name="event__orders__status", method="filter_has_paid" + ) + class Meta: model = Registration - fields = ["has_attended", "is_on_wait"] + fields = [ + "has_attended", + "is_on_wait", + "study", + "year", + "has_allergy", + "allow_photo", + "has_paid", + ] + + def filter_study(self, queryset, name, value): + return queryset.filter( + user__memberships__group__name__icontains=value, + user__memberships__group__type=GroupType.STUDY, + ) + + def filter_has_paid(self, queryset, name, value): + sale_order_exists = Order.objects.filter( + event=OuterRef("event_id"), + user=OuterRef("user_id"), + status=OrderStatus.SALE, + ) + + if value: + return queryset.filter(Exists(sale_order_exists)) + else: + return queryset.exclude(Exists(sale_order_exists)) + + def filter_year(self, queryset, name, value): + return queryset.filter( + user__memberships__group__name__icontains=value, + user__memberships__group__type=GroupType.STUDYYEAR, + ) + + def filter_has_allergy(self, queryset, name, value): + if value: + return queryset.exclude(user__allergy__isnull=True).exclude( + user__allergy__exact="" + ) + return queryset.filter(user__allergy__isnull=True) | queryset.filter( + user__allergy__exact="" + ) diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index f1042051f..b9e9fd4ce 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -15,6 +15,7 @@ from app.emoji.serializers.reaction import ReactionSerializer from app.group.models.group import Group from app.group.serializers.group import SimpleGroupSerializer +from app.payment.enums import OrderStatus from app.payment.models.paid_event import PaidEvent from app.payment.serializers.paid_event import PaidEventCreateSerializer @@ -263,6 +264,9 @@ class EventStatisticsSerializer(BaseModelSerializer): has_attended_count = serializers.SerializerMethodField() studyyears = serializers.SerializerMethodField() studies = serializers.SerializerMethodField() + has_allergy_count = serializers.SerializerMethodField() + has_paid_count = serializers.SerializerMethodField() + allow_photo_count = serializers.SerializerMethodField() class Meta: model = Event @@ -272,11 +276,21 @@ class Meta: "waiting_list_count", "studyyears", "studies", + "has_allergy_count", + "has_paid_count", + "allow_photo_count", ) def get_has_attended_count(self, obj, *args, **kwargs): return obj.registrations.filter(is_on_wait=False, has_attended=True).count() + def get_has_allergy_count(self, obj, *args, **kwargs): + return ( + obj.registrations.exclude(user__allergy__isnull=True) + .exclude(user__allergy__exact="") + .count() + ) + def get_studyyears(self, obj, *args, **kwargs): return filter( lambda studyyear: studyyear["amount"] > 0, @@ -304,3 +318,12 @@ def get_studies(self, obj, *args, **kwargs): Group.objects.filter(type=GroupType.STUDY), ), ) + + def get_allow_photo_count(self, obj, *args, **kwargs): + return obj.registrations.filter(allow_photo=False).count() + + def get_has_paid_count(self, obj, *args, **kwargs): + if obj.is_paid_event: + orders = obj.orders.filter(status=OrderStatus.SALE, event=obj).count() + return orders + return 0 diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 379619927..fbdccddf2 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -314,6 +314,13 @@ def codex_event_registration(): return CodexEventRegistrationFactory() +@pytest.fixture() +def new_admin_user(): + admin = UserFactory() + add_user_to_group_with_name(admin, AdminGroup.HS) + return admin + + @pytest.fixture() def feedback_bug(): return BugFactory() diff --git a/app/tests/content/test_registration_integration.py b/app/tests/content/test_registration_integration.py index 5b9f20f84..86d111158 100644 --- a/app/tests/content/test_registration_integration.py +++ b/app/tests/content/test_registration_integration.py @@ -7,12 +7,14 @@ from app.common.enums import AdminGroup from app.common.enums import NativeGroupType as GroupType from app.common.enums import NativeMembershipType as MembershipType +from app.common.enums import NativeUserStudy as StudyType from app.content.factories import EventFactory, RegistrationFactory, UserFactory from app.content.factories.priority_pool_factory import PriorityPoolFactory from app.forms.enums import NativeEventFormType as EventFormType from app.forms.tests.form_factories import EventFormFactory, SubmissionFactory from app.group.factories import GroupFactory from app.payment.enums import OrderStatus +from app.payment.factories import OrderFactory from app.util.test_utils import add_user_to_group_with_name, get_api_client from app.util.utils import now @@ -1071,3 +1073,135 @@ def test_delete_registration_with_paid_order_as_self( response = client.delete(url) assert response.status_code == status_code + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("filter_params", "participant_count", "status_code"), + [ + ({"has_allergy": True}, 2, status.HTTP_200_OK), + ({"year": "2050"}, 1, status.HTTP_200_OK), + ({"year": "2051"}, 1, status.HTTP_200_OK), + ({"study": StudyType.DATAING}, 2, status.HTTP_200_OK), + ({"year": "2050", "study": StudyType.DATAING}, 1, status.HTTP_200_OK), + ( + {"has_allergy": True, "year": "2051", "study": StudyType.DATAING}, + 1, + status.HTTP_200_OK, + ), + ( + {"has_allergy": True, "year": "2050", "study": StudyType.DATAING}, + 1, + status.HTTP_200_OK, + ), + ], +) +def test_filter_participants( + new_admin_user, member, event, filter_params, participant_count, status_code +): + """ + An admin should be able to filter the participants of an event using multiple parameters + """ + + member.allergy = "Pizza" + member.save() + + new_admin_user.allergy = "Fisk" + new_admin_user.save() + + add_user_to_group_with_name(member, StudyType.DATAING, GroupType.STUDY) + add_user_to_group_with_name(member, "2050", GroupType.STUDYYEAR) + + add_user_to_group_with_name(new_admin_user, "2051", GroupType.STUDYYEAR) + add_user_to_group_with_name(new_admin_user, StudyType.DATAING, GroupType.STUDY) + + RegistrationFactory(user=member, event=event) + RegistrationFactory(user=new_admin_user, event=event) + client = get_api_client(user=new_admin_user) + + # Build the query string with multiple filter parameters + url = ( + _get_registration_url(event) + + "?" + + "&".join([f"{key}={value}" for key, value in filter_params.items()]) + ) + response = client.get(url) + + assert participant_count == response.data["count"] + assert response.status_code == status_code + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("filter_params", "participant_count", "status_code"), + [ + ({"study": StudyType.DATAING, "has_paid": True}, 1, status.HTTP_200_OK), + ({"study": StudyType.DIGFOR, "has_paid": True}, 2, status.HTTP_200_OK), + ({"study": StudyType.DIGFOR, "has_paid": False}, 1, status.HTTP_200_OK), + ({"has_paid": True, "year": "2050"}, 1, status.HTTP_200_OK), + ({"has_paid": True, "year": "2051"}, 1, status.HTTP_200_OK), + ({"has_paid": True}, 4, status.HTTP_200_OK), + ({"has_paid": False}, 2, status.HTTP_200_OK), + ], +) +def test_filter_participants_paid_event( + new_admin_user, + member, + event, + paid_event, + filter_params, + participant_count, + status_code, +): + """ + An admin should be able to filter the participants of an event using multiple parameters + """ + + paid_event.event = event + + paid_event.save() + + member.allergy = "Pizza" + member.save() + + new_admin_user.allergy = "Fisk" + new_admin_user.save() + + new_user = UserFactory() + new_user2 = UserFactory() + new_user3 = UserFactory() + new_user4 = UserFactory() + + add_user_to_group_with_name(member, StudyType.DATAING, GroupType.STUDY) + add_user_to_group_with_name(member, "2050", GroupType.STUDYYEAR) + + add_user_to_group_with_name(new_admin_user, "2051", GroupType.STUDYYEAR) + add_user_to_group_with_name(new_admin_user, StudyType.DIGFOR, GroupType.STUDY) + add_user_to_group_with_name(new_user2, StudyType.DIGFOR, GroupType.STUDY) + add_user_to_group_with_name(new_user3, StudyType.DIGFOR, GroupType.STUDY) + + RegistrationFactory(user=member, event=event) + RegistrationFactory(user=new_admin_user, event=event) + RegistrationFactory(user=new_user, event=event) + RegistrationFactory(user=new_user2, event=event) + RegistrationFactory(user=new_user3, event=event) + RegistrationFactory(user=new_user4, event=event) + + OrderFactory(event=event, user=member, status=OrderStatus.SALE) + OrderFactory(event=event, user=new_admin_user, status=OrderStatus.SALE) + OrderFactory(event=event, user=new_user4, status=OrderStatus.SALE) + OrderFactory(event=event, user=new_user2, status=OrderStatus.SALE) + OrderFactory(event=event, user=new_user, status=OrderStatus.CANCEL) + OrderFactory(event=event, user=new_user3, status=OrderStatus.CANCEL) + + client = get_api_client(user=new_admin_user) + + # Build the query string with multiple filter parameters + url = ( + _get_registration_url(paid_event) + + "?" + + "&".join([f"{key}={value}" for key, value in filter_params.items()]) + ) + response = client.get(url) + assert participant_count == response.data["count"] + assert response.status_code == status_code