From 269b1520fb12b9b996ac0be1e37df1533ead596b Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Tue, 5 Sep 2023 21:36:29 +0200 Subject: [PATCH 1/3] Create overview of users without interviews --- backend/root/utils/routes.py | 23 ++++++- backend/samfundet/serializers.py | 19 ++++++ backend/samfundet/urls.py | 1 + backend/samfundet/views.py | 26 ++++++++ frontend/src/AppRoutes.tsx | 5 ++ .../Pages/ApiTestingPage/ApiTestingPage.tsx | 8 +++ .../RecruitmentGangOverviewPage.tsx | 6 +- ...cruitmentUsersWithoutInterview.module.scss | 3 + .../RecruitmentUsersWithoutInterview.tsx | 64 +++++++++++++++++++ .../RecruitmentUsersWithoutInterview/index.ts | 1 + frontend/src/PagesAdmin/index.ts | 1 + frontend/src/api.ts | 14 ++++ frontend/src/dto.ts | 9 +++ frontend/src/i18n/constants.ts | 2 + frontend/src/i18n/translations.ts | 4 ++ frontend/src/routes/backend.ts | 25 +++++++- frontend/src/routes/frontend.ts | 1 + 17 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/RecruitmentUsersWithoutInterview.module.scss create mode 100644 frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/RecruitmentUsersWithoutInterview.tsx create mode 100644 frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/index.ts diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 56d96342b..9755f9833 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -5,7 +5,7 @@ DO NOT WRITE IN THIS FILE, AS IT WILL BE OVERWRITTEN ON NEXT UPDATE. THIS FILE WAS GENERATED BY: root.management.commands.generate_routes -LAST UPDATE: 2023-08-17 17:48:38.961443+00:00 +LAST UPDATE: 2023-09-04 15:22:16.450169+00:00 """ ############################################################ @@ -308,18 +308,36 @@ admin__samfundet_keyvalue_delete = 'admin:samfundet_keyvalue_delete' admin__samfundet_keyvalue_change = 'admin:samfundet_keyvalue_change' adminsamfundetkeyvalue__objectId = '' +admin__samfundet_recruitment_permissions = 'admin:samfundet_recruitment_permissions' +admin__samfundet_recruitment_permissions_manage_user = 'admin:samfundet_recruitment_permissions_manage_user' +admin__samfundet_recruitment_permissions_manage_group = 'admin:samfundet_recruitment_permissions_manage_group' admin__samfundet_recruitment_changelist = 'admin:samfundet_recruitment_changelist' admin__samfundet_recruitment_add = 'admin:samfundet_recruitment_add' admin__samfundet_recruitment_history = 'admin:samfundet_recruitment_history' admin__samfundet_recruitment_delete = 'admin:samfundet_recruitment_delete' admin__samfundet_recruitment_change = 'admin:samfundet_recruitment_change' adminsamfundetrecruitment__objectId = '' +admin__samfundet_recruitmentposition_permissions = 'admin:samfundet_recruitmentposition_permissions' +admin__samfundet_recruitmentposition_permissions_manage_user = 'admin:samfundet_recruitmentposition_permissions_manage_user' +admin__samfundet_recruitmentposition_permissions_manage_group = 'admin:samfundet_recruitmentposition_permissions_manage_group' admin__samfundet_recruitmentposition_changelist = 'admin:samfundet_recruitmentposition_changelist' admin__samfundet_recruitmentposition_add = 'admin:samfundet_recruitmentposition_add' admin__samfundet_recruitmentposition_history = 'admin:samfundet_recruitmentposition_history' admin__samfundet_recruitmentposition_delete = 'admin:samfundet_recruitmentposition_delete' admin__samfundet_recruitmentposition_change = 'admin:samfundet_recruitmentposition_change' adminsamfundetrecruitmentposition__objectId = '' +admin__samfundet_recruitmentadmission_permissions = 'admin:samfundet_recruitmentadmission_permissions' +admin__samfundet_recruitmentadmission_permissions_manage_user = 'admin:samfundet_recruitmentadmission_permissions_manage_user' +admin__samfundet_recruitmentadmission_permissions_manage_group = 'admin:samfundet_recruitmentadmission_permissions_manage_group' +admin__samfundet_recruitmentadmission_changelist = 'admin:samfundet_recruitmentadmission_changelist' +admin__samfundet_recruitmentadmission_add = 'admin:samfundet_recruitmentadmission_add' +admin__samfundet_recruitmentadmission_history = 'admin:samfundet_recruitmentadmission_history' +admin__samfundet_recruitmentadmission_delete = 'admin:samfundet_recruitmentadmission_delete' +admin__samfundet_recruitmentadmission_change = 'admin:samfundet_recruitmentadmission_change' +adminsamfundetrecruitmentadmission__objectId = '' +admin__samfundet_organization_permissions = 'admin:samfundet_organization_permissions' +admin__samfundet_organization_permissions_manage_user = 'admin:samfundet_organization_permissions_manage_user' +admin__samfundet_organization_permissions_manage_group = 'admin:samfundet_organization_permissions_manage_group' admin__samfundet_organization_changelist = 'admin:samfundet_organization_changelist' admin__samfundet_organization_add = 'admin:samfundet_organization_add' admin__samfundet_organization_history = 'admin:samfundet_organization_history' @@ -386,6 +404,8 @@ samfundet__table_detail = 'samfundet:table-detail' samfundet__text_item_list = 'samfundet:text_item-list' samfundet__text_item_detail = 'samfundet:text_item-detail' +samfundet__infobox_list = 'samfundet:infobox-list' +samfundet__infobox_detail = 'samfundet:infobox-detail' samfundet__key_value_list = 'samfundet:key_value-list' samfundet__key_value_detail = 'samfundet:key_value-detail' samfundet__organizations_list = 'samfundet:organizations-list' @@ -413,5 +433,6 @@ samfundet__assign_group = 'samfundet:assign_group' samfundet__recruitment_positions = 'samfundet:recruitment_positions' samfundet__active_recruitment_positions = 'samfundet:active_recruitment_positions' +samfundet__applicants_without_interviews = 'samfundet:applicants_without_interviews/' static__path = '' media__path = '' diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 32e0a1ee2..a96499a22 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -492,6 +492,25 @@ class Meta: fields = '__all__' +class UserForRecruitmentSerializer(serializers.ModelSerializer): + recruitment_admission_ids = serializers.SerializerMethodField() + + class Meta: + model = User + fields = [ + 'id', + 'first_name', + 'last_name', + 'username', + 'email', + 'recruitment_admission_ids', # Add this to the fields list + ] + + def get_recruitment_admission_ids(self, obj: User) -> list[int]: + """Return list of recruitment admission IDs for the user.""" + return RecruitmentAdmission.objects.filter(user=obj).values_list('id', flat=True) + + class RecruitmentPositionSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/samfundet/urls.py b/backend/samfundet/urls.py index a84a4dadd..2985c5d94 100644 --- a/backend/samfundet/urls.py +++ b/backend/samfundet/urls.py @@ -57,4 +57,5 @@ ########## Recruitment ########## path('recruitment-positions/', views.RecruitmentPositionsPerRecruitmentView.as_view(), name='recruitment_positions'), path('active-recruitment-positions/', views.ActiveRecruitmentPositionsView.as_view(), name='active_recruitment_positions'), + path('applicants-without-interviews/', views.ApplicantsWithoutInterviewsView.as_view(), name='applicants_without_interviews/'), ] diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 0e72f4242..b8742557d 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -1,5 +1,6 @@ from typing import Type +from django.db.models import Count from django.contrib.auth import login, logout from django.contrib.auth.models import Group from django.db.models import QuerySet @@ -78,6 +79,7 @@ FoodPreferenceSerializer, UserPreferenceSerializer, InformationPageSerializer, + UserForRecruitmentSerializer, RecruitmentPositionSerializer, RecruitmentAdmissionForGangSerializer, RecruitmentAdmissionForApplicantSerializer, @@ -464,6 +466,30 @@ def get_queryset(self) -> Response: return None +class ApplicantsWithoutInterviewsView(ListAPIView): + permission_classes = [AllowAny] + serializer_class = UserForRecruitmentSerializer + + def get_queryset(self) -> QuerySet[User]: + """ + Optionally restricts the returned positions to a given recruitment, + by filtering against a `recruitment` query parameter in the URL. + """ + recruitment = self.request.query_params.get('recruitment', None) + if recruitment is not None: + # Users who have admissions for the given recruitment + users_with_admissions = User.objects.filter(admissions__recruitment=recruitment) + + # Exclude users who have any admissions for the given recruitment that have an interview_time + users_without_interviews = users_with_admissions.annotate(num_interviews=Count('admissions__interview_time') + ).filter(admissions__recruitment=recruitment, num_interviews=0) + + return users_without_interviews + + else: + return User.objects.none() # Return an empty queryset instead of None + + class RecruitmentAdmissionForApplicantView(ModelViewSet): permission_classes = [IsAuthenticated] serializer_class = RecruitmentAdmissionForApplicantSerializer diff --git a/frontend/src/AppRoutes.tsx b/frontend/src/AppRoutes.tsx index dc0f84a5a..29e759353 100644 --- a/frontend/src/AppRoutes.tsx +++ b/frontend/src/AppRoutes.tsx @@ -35,6 +35,7 @@ import { RecruitmentGangAdminPage, RecruitmentGangOverviewPage, RecruitmentPositionFormAdminPage, + RecruitmentUsersWithoutInterview, SaksdokumentFormAdminPage, } from '~/PagesAdmin'; import { useGoatCounter } from '~/hooks'; @@ -170,6 +171,10 @@ export function AppRoutes() { path={ROUTES.frontend.admin_recruitment_edit} element={} /> + } + /> {/* TODO ADD PERMISSIONS */} get Rec admissions for gang + ); } diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.tsx b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.tsx index 7136d779f..b8bc74835 100644 --- a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.tsx +++ b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.tsx @@ -44,7 +44,11 @@ export function RecruitmentGangOverviewPage() { -