Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new csv view with total for gangs #1635

Merged
merged 10 commits into from
Jan 7, 2025
Merged
3 changes: 1 addition & 2 deletions backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,6 @@
samfundet__recruitment_applications_for_position_detail = 'samfundet:recruitment_applications_for_position-detail'
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'
Expand Down Expand Up @@ -604,6 +602,7 @@
samfundet__applicants_without_interviews = 'samfundet:applicants_without_interviews'
samfundet__applicants_without_three_interview_criteria = 'samfundet:applicants_without_three_interview_criteria'
samfundet__recruitment_recruiter_dashboard = 'samfundet:recruitment_recruiter_dashboard'
samfundet__recruitment_download_applications_csv = 'samfundet:recruitment_download_applications_csv'
samfundet__recruitment_download_gang_application_csv = 'samfundet:recruitment_download_gang_application_csv'
samfundet__occupied_timeslots = 'samfundet:occupied_timeslots'
samfundet__recruitment_interview_availability = 'samfundet:recruitment_interview_availability'
Expand Down
17 changes: 17 additions & 0 deletions backend/samfundet/models/recruitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ class RecruitmentPosition(CustomBaseModel):
help_text='Shared interviewgroup for position',
)

def get_section_name(self, language: str = 'nb') -> str | None:
if not self.section:
return None
return self.section.name_nb if language == 'nb' else self.section.name_en

def resolve_section(self, *, return_id: bool = False) -> GangSection | int:
if return_id:
# noinspection PyTypeChecker
Expand Down Expand Up @@ -425,6 +430,18 @@ def save(self, *args: tuple, **kwargs: dict) -> None: # noqa: C901

super().save(*args, **kwargs)

def get_total_interviews_for_gang(self) -> int:
return (
RecruitmentApplication.objects.filter(user=self.user, recruitment=self.recruitment, recruitment_position__gang=self.resolve_gang(), withdrawn=False)
.exclude(interview=None)
.count()
)

def get_total_applications_for_gang(self) -> int:
return RecruitmentApplication.objects.filter(
user=self.user, recruitment=self.recruitment, withdrawn=False, recruitment_position__gang=self.resolve_gang()
).count()

def get_total_interviews(self) -> int:
return RecruitmentApplication.objects.filter(user=self.user, recruitment=self.recruitment, withdrawn=False).exclude(interview=None).count()

Expand Down
147 changes: 147 additions & 0 deletions backend/samfundet/models/tests/test_recruitment.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,153 @@ def test_check_withdraw_sets_unwanted(self, fixture_recruitment_application: Rec
assert fixture_recruitment_application.recruiter_status == RecruitmentStatusChoices.AUTOMATIC_REJECTION
assert fixture_recruitment_application.recruiter_priority == RecruitmentPriorityChoices.NOT_WANTED

def test_recruitmentapplication_total_applications_two_gangs(
self,
fixture_user: User,
fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment_position2: RecruitmentPosition,
fixture_gang2: Gang,
fixture_recruitment: Recruitment,
):
fixture_recruitment_position2.gang = fixture_gang2
fixture_recruitment_position2.save()

assert fixture_recruitment_position2.gang != fixture_recruitment_position.gang

test_application1 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=1,
)
test_application2 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position2,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=2,
)
test_application1.save()
test_application2.save()
assert test_application1.get_total_applications() > 0
assert test_application1.get_total_applications() == test_application2.get_total_applications()
assert test_application1.get_total_applications_for_gang() == 1
assert test_application2.get_total_applications_for_gang() == 1
assert test_application1.get_total_applications_for_gang() != test_application1.get_total_applications()
assert test_application2.get_total_applications_for_gang() != test_application2.get_total_applications()

def test_recruitmentapplication_total_applications_single_gang(
self,
fixture_user: User,
fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment_position2: RecruitmentPosition,
fixture_recruitment: Recruitment,
):
test_application1 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=1,
)
test_application2 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position2,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=2,
)
test_application1.save()
test_application2.save()
assert test_application1.get_total_applications() > 0
assert test_application1.get_total_applications() == test_application2.get_total_applications()
assert test_application1.get_total_applications_for_gang() == test_application2.get_total_applications_for_gang()
assert test_application1.get_total_applications_for_gang() == test_application1.get_total_applications()

def test_recruitmentapplication_total_interviews_two_gangs(
self,
fixture_user: User,
fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment_position2: RecruitmentPosition,
fixture_gang2: Gang,
fixture_recruitment: Recruitment,
):
fixture_recruitment_position2.gang = fixture_gang2
fixture_recruitment_position2.save()
# Create two interviews with separate gangs
assert fixture_recruitment_position2.gang != fixture_recruitment_position.gang

test_application1 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=1,
)
test_application2 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position2,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=2,
)
# assign 1 interview to one of them
test_application1.interview = Interview.objects.create()
test_application1.save()
test_application2.save()
assert test_application1.get_total_interviews() > 0
assert test_application1.get_total_interviews() == test_application2.get_total_interviews()
assert test_application1.get_total_interviews_for_gang() == test_application1.get_total_interviews()
assert test_application2.get_total_interviews_for_gang() != test_application2.get_total_interviews()

# test with both having an interview each
test_application2.interview = Interview.objects.create()
test_application1.save()
test_application2.save()
assert test_application1.get_total_interviews() > 0
assert test_application1.get_total_interviews() == test_application2.get_total_interviews()
assert test_application1.get_total_interviews_for_gang() == test_application2.get_total_interviews_for_gang()
assert test_application1.get_total_interviews_for_gang() != test_application1.get_total_interviews()
assert test_application2.get_total_interviews_for_gang() != test_application2.get_total_interviews()

def test_recruitmentapplication_total_interviews_single_gang(
self,
fixture_user: User,
fixture_recruitment_position: RecruitmentPosition,
fixture_recruitment_position2: RecruitmentPosition,
fixture_recruitment: Recruitment,
):
test_application1 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=1,
)
test_application2 = RecruitmentApplication.objects.create(
user=fixture_user,
recruitment_position=fixture_recruitment_position2,
recruitment=fixture_recruitment,
application_text='I have applied',
applicant_priority=2,
)
test_application1.interview = Interview.objects.create()
test_application1.save()
test_application2.save()
assert test_application1.get_total_interviews() > 0
assert test_application1.get_total_interviews() == test_application2.get_total_interviews()
assert test_application1.get_total_interviews_for_gang() == test_application2.get_total_interviews_for_gang()
assert test_application1.get_total_interviews_for_gang() == test_application1.get_total_interviews()

test_application2.interview = Interview.objects.create()
test_application1.save()
test_application2.save()
assert test_application1.get_total_interviews() > 0
assert test_application1.get_total_interviews() == test_application2.get_total_interviews()
assert test_application1.get_total_interviews_for_gang() == test_application2.get_total_interviews_for_gang()
assert test_application1.get_total_interviews_for_gang() == test_application1.get_total_interviews()


class TestRecruitmentApplicationStatus:
def test_check_called_accepted_sets_auto_rejection(
Expand Down
5 changes: 5 additions & 0 deletions backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@
views.RecruitmentRecruiterDashboardView.as_view(),
name='recruitment_recruiter_dashboard',
),
path(
'recruitment-download-all-applications-csv/<int:recruitment_id>/',
views.DownloadAllRecruitmentApplicationCSV.as_view(),
name='recruitment_download_applications_csv',
),
path(
'recruitment-download-gang-application-csv/<int:recruitment_id>/<int:gang_id>',
views.DownloadRecruitmentApplicationGangCSV.as_view(),
Expand Down
65 changes: 61 additions & 4 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,63 @@ def get(
return Response(data=RecruitmentPositionSharedInterviewGroupSerializer(interview_groups, many=True).data, status=status.HTTP_200_OK)


class DownloadAllRecruitmentApplicationCSV(APIView):
permission_classes = [IsAuthenticated]

def get(
self,
request: Request,
recruitment_id: int,
) -> HttpResponse:
recruitment = get_object_or_404(Recruitment, id=recruitment_id)
applications = RecruitmentApplication.objects.filter(recruitment=recruitment)

filename = f"opptak_{recruitment.name_nb}_{recruitment.organization.name}_{timezone.now().strftime('%Y-%m-%d %H.%M')}.csv"
response = HttpResponse(
content_type='text/csv',
headers={'Content-Disposition': f'Attachment; filename="{filename}"'},
)
writer = csv.DictWriter(
response,
fieldnames=[
'Navn',
'Telefon',
'Epost',
'Campus',
'Stilling',
'Gjeng',
'Seksjon',
'Intervjutid',
'Intervjusted',
'Prioritet',
'Status',
'Sokers rangering',
'Intervjuer satt',
],
)
writer.writeheader()
for application in applications:
writer.writerow(
{
'Navn': application.user.get_full_name(),
'Telefon': application.user.phone_number,
'Epost': application.user.email,
'Campus': application.user.campus.name_en if application.user.campus else '',
'Stilling': application.recruitment_position.name_nb,
'Gjeng': application.recruitment_position.gang.name_nb,
'Seksjon': application.recruitment_position.get_section_name('nb'),
'Intervjutid': application.interview.interview_time if application.interview else '',
'Intervjusted': application.interview.interview_location if application.interview else '',
'Prioritet': application.get_recruiter_priority_display(),
'Status': application.get_recruiter_status_display(),
'Sokers rangering': f'{application.applicant_priority}/{application.get_total_applications()}',
'Intervjuer satt': f'{application.get_total_interviews()}/{application.get_total_applications()}',
}
)

return response


class DownloadRecruitmentApplicationGangCSV(APIView):
Snorre98 marked this conversation as resolved.
Show resolved Hide resolved
permission_classes = [IsAuthenticated]

Expand Down Expand Up @@ -1195,8 +1252,8 @@ def get(
'Intervjusted',
'Prioritet',
'Status',
'Søkers rangering',
'Intervjuer satt',
'Sokers rangering (Hele Opptak)',
'Intervjuer satt (For Gjeng)',
],
)
writer.writeheader()
Expand All @@ -1212,8 +1269,8 @@ def get(
'Intervjusted': application.interview.interview_location if application.interview else '',
'Prioritet': application.get_recruiter_priority_display(),
'Status': application.get_recruiter_status_display(),
'Søkers rangering': f'{application.applicant_priority}/{application.get_total_applications()}',
'Intervjuer satt': f'{application.get_total_interviews()}/{application.get_total_applications()}',
'Sokers rangering (Hele Opptak)': f'{application.applicant_priority}/{application.get_total_applications()}',
'Intervjuer satt (For Gjeng)': f'{application.get_total_interviews_for_gang()}/{application.get_total_applications_for_gang()}',
}
)

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export const nb = prepareTranslations({
[KEY.recruitment_no_gangs]: 'Ingen gjenger',
[KEY.recruitment_no_active]: 'Ingen aktive opptak',
[KEY.recruitment_interview_notes]: 'Intervju notater',
[KEY.recruitment_priority]: 'Søkers prioritet',
[KEY.recruitment_priority]: 'Sokers prioritet',
[KEY.recruitment_recruiter_priority]: 'Prioritet',
[KEY.recruitment_recruiter_status]: 'Status',
[KEY.common_not_set]: 'Ikke satt',
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,6 @@ export const ROUTES_BACKEND = {
samfundet__recruitment_applications_for_position_detail: '/api/recruitment-applications-for-position/:pk/',
samfundet__interview_list: '/api/interview/',
samfundet__interview_detail: '/api/interview/:pk/',
samfundet__api_root: '/api/',
samfundet__schema: '/schema/',
samfundet__swagger_ui: '/schema/swagger-ui/',
samfundet__redoc: '/schema/redoc/',
Expand Down Expand Up @@ -602,6 +601,7 @@ export const ROUTES_BACKEND = {
samfundet__applicants_without_interviews: '/recruitment-applicants-without-interviews/:pk/',
samfundet__applicants_without_three_interview_criteria: '/recruitment-applicants-without-three-interview-criteria/:pk/',
samfundet__recruitment_recruiter_dashboard: '/recruitment-recruiter-dashboard/:pk/',
samfundet__recruitment_download_applications_csv: '/recruitment-download-all-applications-csv/:recruitmentId/',
samfundet__recruitment_download_gang_application_csv: '/recruitment-download-gang-application-csv/:recruitmentId/:gangId',
samfundet__occupied_timeslots: '/occupiedtimeslot/',
samfundet__recruitment_interview_availability: '/recruitment-interview-availability/',
Expand Down
Loading