Skip to content

Commit

Permalink
Create overview of users without interviews (#664)
Browse files Browse the repository at this point in the history
* Create overview of users without interviews
  • Loading branch information
Mathias-a authored Sep 19, 2023
1 parent 77f00ce commit cb8212f
Show file tree
Hide file tree
Showing 17 changed files with 168 additions and 3 deletions.
5 changes: 4 additions & 1 deletion backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-29 08:57:57.685488+00:00
LAST UPDATE: 2023-09-04 15:22:16.450169+00:00
"""

############################################################
Expand Down Expand Up @@ -404,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'
Expand Down Expand Up @@ -431,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 = ''
19 changes: 19 additions & 0 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/'),
]
23 changes: 23 additions & 0 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Type

from django.db.models import Count, Case, When
from django.contrib.auth import login, logout
from django.contrib.auth.models import Group
from django.db.models import QuerySet
Expand Down Expand Up @@ -77,6 +79,7 @@
FoodPreferenceSerializer,
UserPreferenceSerializer,
InformationPageSerializer,
UserForRecruitmentSerializer,
RecruitmentPositionSerializer,
RecruitmentAdmissionForGangSerializer,
RecruitmentAdmissionForApplicantSerializer,
Expand Down Expand Up @@ -471,6 +474,26 @@ 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 None:
return User.objects.none() # Return an empty queryset instead of None

# Exclude users who have any admissions for the given recruitment that have an interview_time
users_without_interviews = User.objects.filter(admissions__recruitment=recruitment).annotate(
num_interviews=Count(Case(When(admissions__recruitment=recruitment, then='admissions__interview_time'), default=None, output_field=None))
).filter(num_interviews=0)
return users_without_interviews


class RecruitmentAdmissionForApplicantView(ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = RecruitmentAdmissionForApplicantSerializer
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
RecruitmentGangAdminPage,
RecruitmentGangOverviewPage,
RecruitmentPositionFormAdminPage,
RecruitmentUsersWithoutInterview,
SaksdokumentFormAdminPage,
} from '~/PagesAdmin';
import { useGoatCounter } from '~/hooks';
Expand Down Expand Up @@ -173,6 +174,10 @@ export function AppRoutes() {
path={ROUTES.frontend.admin_recruitment_edit}
element={<ProtectedRoute perms={[PERM.SAMFUNDET_CHANGE_RECRUITMENT]} Page={RecruitmentFormAdminPage} />}
/>
<Route
path={ROUTES.frontend.admin_recruitment_users_without_interview}
element={<RecruitmentUsersWithoutInterview />}
/>
{/* TODO ADD PERMISSIONS */}
<Route
path={ROUTES.frontend.admin_recruitment_gang_overview}
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
assignUserToGroup,
getApplicantsWithoutInterviews,
getCsrfToken,
getInformationPage,
getInformationPages,
Expand Down Expand Up @@ -100,6 +101,13 @@ export function ApiTestingPage() {
>
get Rec admissions for gang
</Button>
<Button
theme="samf"
className={styles.btn}
onClick={() => getApplicantsWithoutInterviews('1').then(console.log).catch(console.error)}
>
get users without interviews
</Button>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ export function RecruitmentGangOverviewPage() {
<Button theme="success" rounded={true} onClick={() => navigate(ROUTES.frontend.admin_information_create)}>
{t(KEY.common_overview)}
</Button>
<Button theme="blue" rounded={true} onClick={() => navigate(ROUTES.frontend.admin_information_create)}>
<Button
theme="blue"
rounded={true}
onClick={() => navigate(ROUTES.frontend.admin_recruitment_users_without_interview)}
>
{t(KEY.recruitment_show_applicants_without_interview)}
</Button>
<Button theme="white" rounded={true} onClick={() => navigate(ROUTES.frontend.admin_information_create)}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.table_container {
margin-top: 1.5em;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InputField } from '~/Components';
import { Table } from '~/Components/Table';
import { getApplicantsWithoutInterviews } from '~/api';
import { RecruitmentUserDto } from '~/dto';
import { KEY } from '~/i18n/constants';
import { ROUTES } from '~/routes';
import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout';
import styles from './RecruitmentUsersWithoutInterview.module.scss';

export function RecruitmentUsersWithoutInterview() {
const [users, setUsers] = useState<RecruitmentUserDto[]>([]);
const [showSpinner, setShowSpinner] = useState<boolean>(true);
const [searchQuery, setSearchQuery] = useState<string>('');
const { t } = useTranslation();

useEffect(() => {
getApplicantsWithoutInterviews('1').then((response) => {
setUsers(response.data);
setShowSpinner(false);
});
}, []);

const tableColumns = [
{ content: t(KEY.common_username), sortable: true },
{ content: t(KEY.common_firstname), sortable: true },
{ content: t(KEY.common_lastname), sortable: true },
{ content: t(KEY.recruitment_number_of_applications), sortable: true },
];

function filterUsers(): RecruitmentUserDto[] {
if (searchQuery === '') return users;
const keywords = searchQuery.split(' ');
return users.filter((user) => {
const fieldsToSearch = [user.username, user.first_name, user.last_name].join(' ').toLowerCase();
for (const kw of keywords) {
if (!fieldsToSearch.includes(kw.toLowerCase())) {
return false;
}
}
return true;
});
}

function userToTableRow(user: RecruitmentUserDto) {
console.log(user.recruitment_admission_ids);
return [
user.username,
user.first_name,
user.last_name,
user.recruitment_admission_ids ? user.recruitment_admission_ids.length : 0,
];
}

return (
<AdminPageLayout title={'Test'} backendUrl={ROUTES.backend.samfundet__user} header={'Test'} loading={showSpinner}>
<InputField icon="mdi:search" onChange={setSearchQuery} />
<div className={styles.table_container}>
<Table columns={tableColumns} data={filterUsers().map((user) => userToTableRow(user))} />
</div>
</AdminPageLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RecruitmentUsersWithoutInterview } from './RecruitmentUsersWithoutInterview';
1 change: 1 addition & 0 deletions frontend/src/PagesAdmin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export { RecruitmentAdminPage } from './RecruitmentAdminPage';
export { RecruitmentGangAdminPage } from './RecruitmentGangAdminPage';
export { RecruitmentGangOverviewPage } from './RecruitmentGangOverviewPage';
export { RecruitmentPositionFormAdminPage } from './RecruitmentPositionFormAdminPage';
export { RecruitmentUsersWithoutInterview } from './RecruitmentUsersWithoutInterview';
export { SaksdokumentFormAdminPage } from './SaksdokumentFormAdminPage';
14 changes: 14 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,20 @@ export async function getActiveRecruitmentPositions(): Promise<AxiosResponse<Rec
return response;
}

export async function getApplicantsWithoutInterviews(recruitmentId: string): Promise<AxiosResponse<UserDto[]>> {
const url =
BACKEND_DOMAIN +
reverse({
pattern: ROUTES.backend.samfundet__applicants_without_interviews,
queryParams: {
recruitment: recruitmentId,
},
});
const response = await axios.get(url, { withCredentials: true });

return response;
}

export async function postRecruitmentAdmission(admission: Partial<RecruitmentAdmissionDto>): Promise<AxiosResponse> {
const url =
BACKEND_DOMAIN +
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export type UserDto = {
object_permissions?: ObjectPermissionDto[];
};

export type RecruitmentUserDto = {
id: number;
username: string;
first_name: string;
last_name: string;
email: string;
recruitment_admission_ids?: string[];
};

export type HomePageDto = {
// Array of events used for splash
splash: EventDto[];
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/i18n/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const KEY = {
common_whatsup: 'common_whatsup',
common_contact: 'common_contact',
common_sponsor: 'common_sponsors',
common_username: 'common_username',
common_lastname: 'common_lastname',
common_register: 'common_register',
common_password: 'common_password',
Expand Down Expand Up @@ -163,6 +164,7 @@ export const KEY = {
recruitment_administrate: 'recruitment_administrate',
shown_application_deadline: 'shown_application_deadline',
actual_application_deadlin: 'actual_application_deadline',
recruitment_number_of_applications: 'recruitment_number_of_applications',
recrutment_default_admission_letter: 'recrutment_default_admission_letter',
reprioritization_deadline_for_groups: 'reprioritization_deadline_for_groups',
reprioritization_deadline_for_applicant: 'reprioritization_deadline_for_applicant',
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const nb: Record<KeyValues, string> = {
[KEY.common_membership]: 'Medlemskap',
[KEY.common_restaurant]: 'Restaurant',
[KEY.common_contact_us]: 'Kontakt oss',
[KEY.common_username]: 'Brukernavn',
[KEY.common_recruitment]: 'Opptak',
[KEY.common_information]: 'Informasjon',
[KEY.common_description]: 'Beskrivelse',
Expand Down Expand Up @@ -154,6 +155,7 @@ export const nb: Record<KeyValues, string> = {
[KEY.recruitment_administrate]: 'Administrer opptak',
[KEY.shown_application_deadline]: 'Vist søknadsfrist',
[KEY.actual_application_deadlin]: 'Faktisk søknadsfrist',
[KEY.recruitment_number_of_applications]: 'Antall søknader',
[KEY.recrutment_default_admission_letter]: 'Standard søknadstekst',
[KEY.reprioritization_deadline_for_groups]: 'Flaggefrist',
[KEY.reprioritization_deadline_for_applicant]: 'Omprioriteringsfrist',
Expand Down Expand Up @@ -298,6 +300,7 @@ export const en: Record<KeyValues, string> = {
[KEY.common_more_info]: 'More info',
[KEY.common_firstname]: 'First name',
[KEY.common_norwegian]: 'Norwegian',
[KEY.common_username]: 'Brukernavn',
[KEY.common_volunteer]: 'Volunteer',
[KEY.common_membership]: 'Membership',
[KEY.common_restaurant]: 'Restaurant',
Expand Down Expand Up @@ -372,6 +375,7 @@ export const en: Record<KeyValues, string> = {
[KEY.recruitment_administrate]: 'Administrate recruitment',
[KEY.actual_application_deadlin]: 'Actual deadline',
[KEY.shown_application_deadline]: 'Displayed deadline',
[KEY.recruitment_number_of_applications]: 'Number of applications',
[KEY.recrutment_default_admission_letter]: 'Default admission letter',
[KEY.reprioritization_deadline_for_groups]: 'Group reprioritization deadline',
[KEY.reprioritization_deadline_for_applicant]: 'Reprioritization deadline',
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/routes/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ THIS FILE IS AUTOGENERATED.
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-29 08:57:57.685488+00:00
LAST UPDATE: 2023-09-04 15:22:16.450169+00:00
"""
*/
// ############################################################
Expand Down Expand Up @@ -403,6 +403,8 @@ export const ROUTES_BACKEND = {
samfundet__table_detail: '/api/table/:pk/',
samfundet__text_item_list: '/api/textitem/',
samfundet__text_item_detail: '/api/textitem/:pk/',
samfundet__infobox_list: '/api/infobox/',
samfundet__infobox_detail: '/api/infobox/:pk/',
samfundet__key_value_list: '/api/key-value/',
samfundet__key_value_detail: '/api/key-value/:key/',
samfundet__organizations_list: '/api/organizations/',
Expand Down Expand Up @@ -430,6 +432,7 @@ export const ROUTES_BACKEND = {
samfundet__assign_group: '/assign_group/',
samfundet__recruitment_positions: '/recruitment-positions/',
samfundet__active_recruitment_positions: '/active-recruitment-positions/',
samfundet__applicants_without_interviews: '/applicants-without-interviews/',
static__path: '/static/:path',
media__path: '/media/:path',
} as const;
1 change: 1 addition & 0 deletions frontend/src/routes/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const ROUTES_FRONTEND = {
admin_recruitment: '/control-panel/recruitment/',
admin_recruitment_edit: '/control-panel/recruitment/edit/:id',
admin_recruitment_create: '/control-panel/recruitment/create/',
admin_recruitment_users_without_interview: '/control-panel/recruitment/:recruitmentId/users-without-admissions/',
admin_recruitment_gang_overview: '/control-panel/recruitment/:recruitmentId/gang-overview/',
admin_recruitment_gang_position_overview: '/control-panel/recruitment/:recruitmentId/gang/:gangId',
admin_recruitment_gang_position_create: '/control-panel/recruitment/:recruitmentId/gang/:gangId/create/',
Expand Down

0 comments on commit cb8212f

Please sign in to comment.