Skip to content

Commit

Permalink
feat: add new csv view with total for gangs
Browse files Browse the repository at this point in the history
  • Loading branch information
magsyg committed Dec 8, 2024
1 parent dcc7a51 commit 7d2a6c8
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 3 deletions.
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 @@ -603,6 +601,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 @@ -167,6 +167,11 @@ class RecruitmentPosition(CustomBaseModel):
# TODO: Implement interviewer functionality
interviewers = models.ManyToManyField(to=User, help_text='Interviewers for the position', blank=True, related_name='interviewers')

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 @@ -401,6 +406,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 @@ -420,6 +420,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 @@ -135,6 +135,11 @@
views.RecruitmentRecruiterDashboardView.as_view(),
name='recruitment_recruiter_dashboard',
),
path(
'recruitment-download-applications-csv/<int:recruitment_id>/',
views.DownloadRecruitmentApplicationCSV.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
57 changes: 57 additions & 0 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,63 @@ def get(
return Response(data=RecruitmentPositionSharedInterviewGroupSerializer(interview_groups, many=True).data, status=status.HTTP_200_OK)


class DownloadRecruitmentApplicationCSV(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',
'Søkers 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(),
'Søkers rangering': f'{application.applicant_priority}/{application.get_total_applications()}',
'Intervjuer satt': f'{application.get_total_interviews_for_gang()}/{application.get_total_applications_for_gang()}',
}
)

return response


class DownloadRecruitmentApplicationGangCSV(APIView):
permission_classes = [IsAuthenticated]

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/routes/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,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/',
Expand Down Expand Up @@ -601,6 +602,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-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 All @@ -610,4 +612,4 @@ export const ROUTES_BACKEND = {
samfundet__gang_application_stats: '/recruitment/:recruitmentId/gang/:gangId/stats/',
static__path: '/static/:path',
media__path: '/media/:path',
} as const;
} as const;

0 comments on commit 7d2a6c8

Please sign in to comment.