diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 74cc0fc7e..54672e72e 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -564,6 +564,7 @@ samfundet__interview_list = 'samfundet:interview-list' samfundet__interview_detail = 'samfundet:interview-detail' samfundet__api_root = 'samfundet:api-root' +samfundet__api_root = 'samfundet:api-root' samfundet__schema = 'samfundet:schema' samfundet__swagger_ui = 'samfundet:swagger_ui' samfundet__redoc = 'samfundet:redoc' @@ -591,6 +592,7 @@ samfundet__recruitment_set_interview = 'samfundet:recruitment_set_interview' samfundet__recruitment_application_states_choices = 'samfundet:recruitment_application_states_choices' samfundet__recruitment_application_update_state_gang = 'samfundet:recruitment_application_update_state_gang' +samfundet__recruitment_position_organized_applications = 'samfundet:recruitment_position_organized_applications' samfundet__recruitment_application_update_state_position = 'samfundet:recruitment_application_update_state_position' samfundet__recruitment_applications_recruiter = 'samfundet:recruitment_applications_recruiter' samfundet__recruitment_withdraw_application = 'samfundet:recruitment_withdraw_application' diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 3e8488380..96bb7f5b5 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -1094,6 +1094,37 @@ def update(self, instance: RecruitmentApplication, validated_data: dict) -> Recr def get_application_count(self, application: RecruitmentApplication) -> int: return application.user.applications.filter(recruitment=application.recruitment).count() +class RecruitmentPositionOrganizedApplications(CustomBaseSerializer): + applicationSerializer = RecruitmentApplicationForGangSerializer + unprocessed = serializers.SerializerMethodField(method_name='get_unprocessed', read_only=True) + withdrawn = serializers.SerializerMethodField(method_name='get_withdrawn', read_only=True) + accepted = serializers.SerializerMethodField(method_name='get_accepted', read_only=True) + rejected = serializers.SerializerMethodField(method_name='get_rejected', read_only=True) + hardtoget = serializers.SerializerMethodField(method_name='get_hardtoget', read_only=True) + + class Meta: + model = RecruitmentPosition + fields = ['unprocessed', 'withdrawn', 'accepted', 'rejected', 'hardtoget'] + + def get_unprocessed(self, instance: RecruitmentPosition): + unprocessed = instance.applications.filter(withdrawn=False, recruiter_status=RecruitmentStatusChoices.NOT_SET) + return self.applicationSerializer(unprocessed, many=True).data + + def get_withdrawn(self, instance: RecruitmentPosition): + withdrawn = instance.applications.filter(withdrawn=True) + return self.applicationSerializer(withdrawn, many=True).data + + def get_rejected(self, instance: RecruitmentPosition): + rejected = instance.applications.filter(withdrawn=False, recruiter_status__in=[RecruitmentStatusChoices.AUTOMATIC_REJECTION, RecruitmentStatusChoices.REJECTION]) + return self.applicationSerializer(rejected, many=True).data + + def get_accepted(self, instance: RecruitmentPosition): + accepted = instance.applications.filter(withdrawn=False, recruiter_status=RecruitmentStatusChoices.CALLED_AND_ACCEPTED) + return self.applicationSerializer(accepted, many=True).data + + def get_hardtoget(self, instance: RecruitmentPosition): + hardtoget = instance.applications.filter(withdrawn=False, recruiter_status=RecruitmentStatusChoices.CALLED_AND_REJECTED) + return self.applicationSerializer(hardtoget, many=True).data class RecruitmentApplicationUpdateForGangSerializer(serializers.Serializer): recruiter_priority = serializers.ChoiceField(choices=RecruitmentPriorityChoices.choices, required=False) diff --git a/backend/samfundet/urls.py b/backend/samfundet/urls.py index 3761d75c3..3bcda7a60 100644 --- a/backend/samfundet/urls.py +++ b/backend/samfundet/urls.py @@ -105,6 +105,11 @@ views.RecruitmentApplicationForGangUpdateStateView.as_view(), name='recruitment_application_update_state_gang', ), + path( + 'recruitment-position-organized-applications//', + views.RecruitmentPositionOrganizedApplicationsView.as_view(), + name='recruitment_position_organized_applications', + ), path( 'recruitment-application-update-state-position//', views.RecruitmentApplicationForPositionUpdateStateView.as_view(), diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 875f80229..9287a0bbb 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -42,6 +42,7 @@ from .homepage import homepage from .models.role import Role from .serializers import ( + RecruitmentPositionOrganizedApplications, TagSerializer, GangSerializer, MenuSerializer, @@ -1043,13 +1044,21 @@ def put(self, request: Request, pk: int) -> Response: return Response(update_serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class RecruitmentPositionOrganizedApplicationsView(APIView): + permission_classes = [IsAuthenticated] + serializer_class = RecruitmentPositionOrganizedApplications + + def get(self, request: Request, pk: int) -> Response: + position = get_object_or_404(RecruitmentPosition, pk=pk) + serializer = self.serializer_class(position) + return Response(serializer.data, status=status.HTTP_200_OK) + class RecruitmentApplicationForPositionUpdateStateView(APIView): permission_classes = [IsAuthenticated] serializer_class = RecruitmentApplicationUpdateForGangSerializer def put(self, request: Request, pk: int) -> Response: application = get_object_or_404(RecruitmentApplication, pk=pk) - # TODO add check if user has permission to update for GANG update_serializer = self.serializer_class(data=request.data) if update_serializer.is_valid(): @@ -1060,12 +1069,9 @@ def put(self, request: Request, pk: int) -> Response: application.recruiter_status = update_serializer.data['recruiter_status'] application.save() application.update_applicant_state() - applications = RecruitmentApplication.objects.filter( - recruitment_position=application.recruitment_position, # Only change from above - recruitment=application.recruitment, - ) - serializer = RecruitmentApplicationForGangSerializer(applications, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + position = get_object_or_404(RecruitmentPosition, pk=application.recruitment_position.id) + organized_serializer = RecruitmentPositionOrganizedApplications(position) + return Response(organized_serializer.data, status=status.HTTP_200_OK) return Response(update_serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/frontend/src/PagesAdmin/RecruitmentPositionOverviewPage/RecruitmentPositionOverviewPage.tsx b/frontend/src/PagesAdmin/RecruitmentPositionOverviewPage/RecruitmentPositionOverviewPage.tsx index 9d90f823c..3af1c196c 100644 --- a/frontend/src/PagesAdmin/RecruitmentPositionOverviewPage/RecruitmentPositionOverviewPage.tsx +++ b/frontend/src/PagesAdmin/RecruitmentPositionOverviewPage/RecruitmentPositionOverviewPage.tsx @@ -4,7 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import { Button, RecruitmentApplicantsStatus } from '~/Components'; import { Text } from '~/Components/Text/Text'; -import { getRecruitmentApplicationsForGang, updateRecruitmentApplicationStateForPosition } from '~/api'; +import { getRecruitmentApplicationsForGang, getRecruitmentPositionOrganizedApplications, updateRecruitmentApplicationStateForPosition } from '~/api'; import type { RecruitmentApplicationDto, RecruitmentApplicationStateDto } from '~/dto'; import { useTitle } from '~/hooks'; import { STATUS } from '~/http_status_codes'; @@ -33,46 +33,22 @@ export function RecruitmentPositionOverviewPage() { if (!recruitmentId || !gangId || !positionId) { return; } - getRecruitmentApplicationsForGang(gangId, recruitmentId) - .then((data) => { + getRecruitmentPositionOrganizedApplications(positionId) + .then((response) => { setRecruitmentApplicants( - data.data.filter( - (recruitmentApplicant) => - !recruitmentApplicant.withdrawn && - recruitmentApplicant.recruiter_status === 0 && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.unprocessed ); setWithdrawnApplicants( - data.data.filter( - (recruitmentApplicant) => - recruitmentApplicant.withdrawn && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.withdrawn ); setHardtogetApplicants( - data.data.filter( - (recruitmentApplicant) => - !recruitmentApplicant.withdrawn && - recruitmentApplicant.recruiter_status === 2 && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.hardtoget ); setRejectedApplicants( - data.data.filter( - (recruitmentApplicant) => - !recruitmentApplicant.withdrawn && - recruitmentApplicant.recruiter_status === 3 && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.rejected ); setAcceptedApplicants( - data.data.filter( - (recruitmentApplicant) => - !recruitmentApplicant.withdrawn && - recruitmentApplicant.recruiter_status === 1 && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.accepted ); setShowSpinner(false); }) @@ -91,45 +67,21 @@ export function RecruitmentPositionOverviewPage() { const updateApplicationState = (id: string, data: RecruitmentApplicationStateDto) => { positionId && updateRecruitmentApplicationStateForPosition(id, data) - .then((data) => { + .then((response) => { setRecruitmentApplicants( - data.data.filter( - (recruitmentApplicant) => - !recruitmentApplicant.withdrawn && - recruitmentApplicant.recruiter_status === 0 && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.unprocessed ); setWithdrawnApplicants( - data.data.filter( - (recruitmentApplicant) => - recruitmentApplicant.withdrawn && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.withdrawn ); setHardtogetApplicants( - data.data.filter( - (recruitmentApplicant) => - !recruitmentApplicant.withdrawn && - recruitmentApplicant.recruiter_status === 2 && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.hardtoget ); setRejectedApplicants( - data.data.filter( - (recruitmentApplicant) => - !recruitmentApplicant.withdrawn && - recruitmentApplicant.recruiter_status === 3 && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.rejected ); setAcceptedApplicants( - data.data.filter( - (recruitmentApplicant) => - !recruitmentApplicant.withdrawn && - recruitmentApplicant.recruiter_status === 1 && - recruitmentApplicant.recruitment_position?.id === Number.parseInt(positionId), - ), + response.data.accepted ); setShowSpinner(false); }) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 9a7997519..dae569074 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -30,6 +30,7 @@ import type { RecruitmentDto, RecruitmentGangDto, RecruitmentPositionDto, + RecruitmentPositionOrganizedApplicationsDto, RecruitmentPositionPostDto, RecruitmentPositionPutDto, RecruitmentSeparatePositionDto, @@ -819,6 +820,20 @@ export async function getRecruitmentApplicationsForGang( return await axios.get(url, { withCredentials: true }); } +export async function getRecruitmentPositionOrganizedApplications( + positionId: string, +): Promise> { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_position_organized_applications, + urlParams: { + pk: positionId + }, + }); + return await axios.get(url, { withCredentials: true }); +} + export async function getRecruitmentSharedInterviewGroups( recruitmentId: string, ): Promise> { @@ -888,7 +903,7 @@ export async function updateRecruitmentApplicationStateForGang( export async function updateRecruitmentApplicationStateForPosition( applicationId: string, application: Partial, -): Promise> { +): Promise> { const url = BACKEND_DOMAIN + reverse({ diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 4cd54cb1c..53d9da5e2 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -496,6 +496,14 @@ export type InterviewDto = { interviewers?: UserDto[]; }; +export type RecruitmentPositionOrganizedApplicationsDto = { + unprocessed: RecruitmentApplicationDto[]; + withdrawn: RecruitmentApplicationDto[]; + rejected: RecruitmentApplicationDto[]; + accepted: RecruitmentApplicationDto[]; + hardtoget: RecruitmentApplicationDto[]; +} + export type RecruitmentApplicationDto = { id: string; interview?: InterviewDto; diff --git a/frontend/src/routes/backend.ts b/frontend/src/routes/backend.ts index f1b8b9b81..5ca8ce229 100644 --- a/frontend/src/routes/backend.ts +++ b/frontend/src/routes/backend.ts @@ -563,6 +563,7 @@ export const ROUTES_BACKEND = { samfundet__interview_list: '/api/interview/', samfundet__interview_detail: '/api/interview/:pk/', samfundet__api_root: '/api/', + samfundet__api_root: '/api/:format', samfundet__schema: '/schema/', samfundet__swagger_ui: '/schema/swagger-ui/', samfundet__redoc: '/schema/redoc/', @@ -590,6 +591,7 @@ export const ROUTES_BACKEND = { samfundet__recruitment_set_interview: '/recruitment-set-interview/:pk/', samfundet__recruitment_application_states_choices: '/recruitment-application-states-choices', samfundet__recruitment_application_update_state_gang: '/recruitment-application-update-state-gang/:pk/', + samfundet__recruitment_position_organized_applications: '/recruitment-position-organized-applications/:pk/', samfundet__recruitment_application_update_state_position: '/recruitment-application-update-state-position/:pk/', samfundet__recruitment_applications_recruiter: '/recruitment-application-recruiter/:applicationId/', samfundet__recruitment_withdraw_application: '/recruitment-withdraw-application/:pk/',