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

Applicant overview page #829

Merged
merged 27 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
07c1aa6
get data into frontend
Mathias-a Nov 7, 2023
b03e18d
Add communication with backend
Mathias-a Nov 15, 2023
aac9c49
fix sorting
Mathias-a Nov 15, 2023
7880b4a
improve position seed script
Mathias-a Nov 15, 2023
68ec617
fix priority
Mathias-a Nov 15, 2023
b1688a2
fix tests, and move choices to global file
Mathias-a Nov 16, 2023
427963e
cleanup
Mathias-a Nov 16, 2023
457f4ae
Fix tests, improve error handling
Mathias-a Nov 16, 2023
ef46566
Merge branch 'master' into 687-view-and-manage-own-admission-applicat…
magsyg Dec 20, 2023
6053abd
fix migration
Dec 20, 2023
6c4fdc8
Merge branch 'master' into 687-view-and-manage-own-admission-applicat…
magsyg Feb 8, 2024
2a5f30e
finalize view own applications
Feb 8, 2024
691f890
finalize view own applications
Feb 8, 2024
daaefdc
Merge branch '687-view-and-manage-own-admission-applications' of http…
Feb 8, 2024
799f982
ruff
Feb 8, 2024
4017879
mmore ruff
Feb 8, 2024
3965a03
Merge branch 'master' into 687-view-and-manage-own-admission-applicat…
magsyg Feb 8, 2024
3a356aa
more ruff
Feb 8, 2024
c53abe0
Merge branch '687-view-and-manage-own-admission-applications' of http…
Feb 8, 2024
5b334f8
mmore ruff :)
Feb 8, 2024
f4d26c3
fix traslat
Feb 8, 2024
247a3be
fix
Feb 8, 2024
c1e5201
reformat again
Feb 8, 2024
575eba0
ruff
Feb 8, 2024
b5af892
Merge branch 'master' into 687-view-and-manage-own-admission-applicat…
magsyg Feb 8, 2024
4b3ce89
fun merge
Feb 8, 2024
206a786
Merge branch 'master' into 687-view-and-manage-own-admission-applicat…
magsyg Feb 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ def seed():
for recruitment_index, recruitment in enumerate(recruitments):
for i in range(2): # Create 2 instances for each gang and recruitment
position_data = POSITION_DATA.copy()
position_data.update({
'name_nb': f'Stilling {i}',
'name_en': f'Position {i}',
'gang': gang,
'recruitment': recruitment,
})
position_data.update(
{
'name_nb': f'{gang.abbreviation} stilling {i}',
'name_en': f'{gang.abbreviation} position {i}',
'gang': gang,
'recruitment': recruitment,
}
)
position, created = RecruitmentPosition.objects.get_or_create(**position_data)

if created:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.6 on 2023-11-07 20:58

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0040_alter_notification_options_and_more'),
]

operations = [
migrations.AlterField(
model_name='recruitmentadmission',
name='recruiter_priority',
field=models.IntegerField(choices=[(0, 'Not Set'), (1, 'Not Wanted'), (2, 'Wanted'), (3, 'Reserve')], default=1, help_text='The priority of the admission'),
),
]
19 changes: 19 additions & 0 deletions backend/samfundet/migrations/0042_alter_recruitmentadmission_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.6 on 2023-11-15 11:32

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

dependencies = [
('samfundet', '0041_alter_recruitmentadmission_recruiter_priority'),
]

operations = [
migrations.AlterField(
model_name='recruitmentadmission',
name='id',
field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
),
]
13 changes: 10 additions & 3 deletions backend/samfundet/models/recruitment.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#
# This file contains models spesific to the recruitment system
#

from __future__ import annotations
import uuid

from django.core.exceptions import ValidationError
from django.utils import timezone

from django.db import models

from root.utils.mixins import FullCleanSaveMixin
Expand Down Expand Up @@ -133,6 +133,8 @@ class Interview(FullCleanSaveMixin):


class RecruitmentAdmission(FullCleanSaveMixin):

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
admission_text = models.TextField(help_text='Admission text for the admission')
recruitment_position = models.ForeignKey(
RecruitmentPosition, on_delete=models.CASCADE, help_text='The recruitment position that is recruiting', related_name='admissions'
Expand Down Expand Up @@ -160,7 +162,7 @@ class RecruitmentAdmission(FullCleanSaveMixin):
]

# TODO: Important that the following is not sent along with the rest of the object whenever a user retrieves its admission
recruiter_priority = models.IntegerField(choices=PRIORITY_CHOICES, default=0, help_text='The priority of the admission')
recruiter_priority = models.IntegerField(choices=PRIORITY_CHOICES, help_text='The priority of the admission', default=1)

recruiter_status = models.IntegerField(choices=STATUS_CHOICES, default=0, help_text='The status of the admission')

Expand All @@ -171,6 +173,11 @@ def save(self, *args: tuple, **kwargs: dict) -> None:
"""
If the admission is saved without an interview, try to find an interview from a shared position.
"""
if not self.applicant_priority:
current_applications_count = RecruitmentAdmission.objects.filter(user=self.user).count()
# Set the applicant_priority to the number of applications + 1 (for the current application)
self.applicant_priority = current_applications_count + 1

if not self.interview:
# Check if there is already an interview for the same user in shared positions
shared_interview_positions = self.recruitment_position.shared_interview_positions.all()
Expand Down
36 changes: 36 additions & 0 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,13 +524,49 @@ class Meta:
fields = '__all__'


class ApplicantInterviewSerializer(serializers.ModelSerializer):

class Meta:
model = Interview
fields = [
'id',
'interview_time',
'interview_location',
]


class RecruitmentPositionForApplicantSerializer(serializers.ModelSerializer):

class Meta:
model = RecruitmentPosition
fields = [
'id',
'name_nb',
'name_en',
'short_description_nb',
'short_description_en',
'long_description_nb',
'long_description_en',
'is_funksjonaer_position',
'default_admission_letter_nb',
'default_admission_letter_en',
'gang',
'recruitment',
]


class RecruitmentAdmissionForApplicantSerializer(serializers.ModelSerializer):
interview = ApplicantInterviewSerializer(read_only=True)
recruitment_position = RecruitmentPositionForApplicantSerializer(read_only=True)

class Meta:
model = RecruitmentAdmission
fields = [
'id',
'admission_text',
'recruitment_position',
'applicant_priority',
'interview',
]

def create(self, validated_data: dict) -> RecruitmentAdmission:
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AboutPage,
AdminPage,
ApiTestingPage,
ApplicantApplicationOverviewPage,
ComponentPage,
EventPage,
EventsPage,
Expand Down Expand Up @@ -82,6 +83,7 @@ export function AppRoutes() {
<Route path={ROUTES.frontend.route_overview} element={<RouteOverviewPage />} />
<Route path={ROUTES.frontend.recruitment} element={<RecruitmentPage />} />
<Route path={ROUTES.frontend.recruitment_application} element={<RecruitmentAdmissionFormPage />} />
<Route path={ROUTES.frontend.recruitment_application_overview} element={<ApplicantApplicationOverviewPage />} />
</Route>
{/*
ADMIN ROUTES
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/Components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ type TableProps = {
// Data can either be a table cell with separated value and content, or just the raw value
// For instance ["a", "b"] or [ {value: "a", content: <div>a</div>}, {value: "b", content: <div>b</div>} ]
data: TableDataType;
defaultSortColumn?: number;
};

export function Table({ className, columns, data }: TableProps) {
const [sortColumn, setSortColumn] = useState(-1);
export function Table({ className, columns, data, defaultSortColumn }: TableProps) {
const [sortColumn, setSortColumn] = useState(defaultSortColumn || -1);
const [sortInverse, setSortInverse] = useState(false);

function sort(column: number) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
$back-button-width: 5rem;

.container {
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}

.header {
font-size: 2em;
font-weight: 700;
}

.back_button {
max-width: $back-button-width;
}

.top_container {
display: flex;
justify-content: space-between;
}

.empty_div {
width: $back-button-width;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Icon } from '@iconify/react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { Button, Page } from '~/Components';
import { Table } from '~/Components/Table';
import { getRecruitmentAdmissionsForApplicant, putRecruitmentAdmission } from '~/api';
import { RecruitmentAdmissionDto } from '~/dto';
import { KEY } from '~/i18n/constants';
import { ROUTES } from '~/routes';
import { dbT } from '~/utils';
import styles from './ApplicantApplicationOverviewPage.module.scss';

export function ApplicantApplicationOverviewPage() {
const { recruitmentID } = useParams();
const [admissions, setAdmissions] = useState<RecruitmentAdmissionDto[]>([]);
const { t } = useTranslation();

function handleChangePriority(id: number, direction: 'up' | 'down') {
const newAdmissions = [...admissions];
const index = newAdmissions.findIndex((admission) => admission.id === id);
const directionIncrement = direction === 'up' ? -1 : 1;
if (newAdmissions[index].applicant_priority === 1 && direction === 'up') return;
if (newAdmissions[index].applicant_priority === newAdmissions.length && direction === 'down') return;
const targetIndex = newAdmissions.findIndex(
(admission) => admission.applicant_priority === newAdmissions[index].applicant_priority + directionIncrement,
);

const old_priority = newAdmissions[index].applicant_priority;
const new_priority = newAdmissions[targetIndex].applicant_priority;

console.log('old priority', old_priority);
console.log('new priority', new_priority);

newAdmissions[index].applicant_priority = new_priority;
newAdmissions[targetIndex].applicant_priority = old_priority;

// TODO: Make this a single API call
putRecruitmentAdmission(newAdmissions[index]);
putRecruitmentAdmission(newAdmissions[targetIndex]).then(() => {
setAdmissions(newAdmissions);
});
}

function upDownArrow(id: number) {
return (
<>
<Icon icon="bxs:up-arrow" onClick={() => handleChangePriority(id, 'up')} />
<Icon icon="bxs:down-arrow" onClick={() => handleChangePriority(id, 'down')} />
</>
);
}

useEffect(() => {
if (recruitmentID) {
getRecruitmentAdmissionsForApplicant(recruitmentID).then((response) => {
setAdmissions(response.data);
});
}
}, [recruitmentID]);

useEffect(() => {
console.log(admissions);
}, [admissions]);

const tableColumns = [
{ sortable: false, content: 'Recruitment Position' },
{ sortable: false, content: 'Interview Date' },
{ sortable: false, content: 'Interview Location' },
{ sortable: true, content: 'Applicant Priority' },
{ sortable: false, content: '' },
];

function admissionToTableRow(admission: RecruitmentAdmissionDto) {
return [
dbT(admission.recruitment_position, 'name'),
admission.interview.interview_time,
admission.interview.interview_location,
admission.applicant_priority,
{ content: upDownArrow(admission.id) },
];
}

return (
<Page>
<div className={styles.container}>
<div className={styles.top_container}>
<Button link={ROUTES.frontend.recruitment} className={styles.back_button} theme="green">
{t(KEY.common_go_back)}
</Button>
<h1 className={styles.header}>My applications</h1>
<div className={styles.empty_div}></div>
</div>
<p>All info related to the applications will be anonymized three weeks after the recruitment is over</p>
{admissions ? (
<Table data={admissions.map(admissionToTableRow)} columns={tableColumns} defaultSortColumn={3}></Table>
) : (
<p>You have not applied to any positions yet</p>
)}
</div>
</Page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ApplicantApplicationOverviewPage } from './ApplicantApplicationOverviewPage';
3 changes: 2 additions & 1 deletion frontend/src/Pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { AboutPage } from './AboutPage';
export { AdminPage } from './AdminPage';
export { ApiTestingPage } from './ApiTestingPage';
export { ApplicantApplicationOverviewPage } from './ApplicantApplicationOverviewPage';
export { ComponentPage } from './ComponentPage';
export { EventPage } from './EventPage';
export { EventsPage } from './EventsPage';
Expand All @@ -10,8 +11,8 @@ export { HomePage } from './HomePage';
export { InformationListPage } from './InformationListPage';
export { InformationPage } from './InformationPage';
export { LoginPage } from './LoginPage';
export { LycheContactPage } from './LycheContactPage';
export { LycheAboutPage } from './LycheAboutPage';
export { LycheContactPage } from './LycheContactPage';
export { LycheHomePage } from './LycheHomePage';
export { NotFoundPage } from './NotFoundPage';
export { RecruitmentAdmissionFormPage } from './RecruitmentAdmissionFormPage';
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,3 +649,20 @@ export async function postRecruitmentAdmission(admission: Partial<RecruitmentAdm

return response;
}

export async function putRecruitmentAdmission(admission: Partial<RecruitmentAdmissionDto>): Promise<AxiosResponse> {
const url =
BACKEND_DOMAIN +
reverse({
pattern: ROUTES.backend.samfundet__recruitment_admissions_for_applicant_detail,
urlParams: { pk: admission.id },
});
const data = {
id: admission.id,
admission_text: admission.admission_text,
applicant_priority: admission.applicant_priority,
};
const response = await axios.put(url, data, { withCredentials: true });

return response;
}
6 changes: 5 additions & 1 deletion frontend/src/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,10 @@ export type NotificationDto = {
// TODO: There are more fields than this.
};

// ############################################################
// Recruitment
// ############################################################

export type RecruitmentDto = {
id: string | undefined;
name_nb: string;
Expand Down Expand Up @@ -353,7 +357,7 @@ export type RecruitmentAdmissionDto = {
id: number;
interview: InterviewDto;
admission_text: string;
recruitment_position?: number;
recruitment_position?: RecruitmentPositionDto;
recruitment: number;
user: UserDto;
applicant_priority: number;
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/routes/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export const ROUTES_FRONTEND = {
information_page_list: '/information/',
information_page_detail: '/information/:slugField/',
saksdokumenter: '/saksdokumenter/',
// Recruitment:
recruitment: '/recruitment/',
recruitment_application: '/recruitment/position/:positionID/',
recruitment_application_overview: '/recruitment/:recruitmentID/my-applications/',
// ==================== //
// Sulten //
// ==================== //
Expand Down