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

Create overview of users without interviews #664

Merged
merged 4 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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