Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
4 changes: 3 additions & 1 deletion backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,9 @@
samfundet__recruitment_list = 'samfundet:recruitment-list'
samfundet__recruitment_detail = 'samfundet:recruitment-detail'
samfundet__recruitment_gangs = 'samfundet:recruitment-gangs'
samfundet__active_recruitment_list = 'samfundet:active_recruitment-list'
samfundet__active_recruitment_get_active_samf_recruitments = 'samfundet:active_recruitment-get-active-samf-recruitments'
samfundet__active_recruitment_detail = 'samfundet:active_recruitment-detail'
samfundet__recruitment_for_recruiter_list = 'samfundet:recruitment_for_recruiter-list'
samfundet__recruitment_for_recruiter_detail = 'samfundet:recruitment_for_recruiter-detail'
samfundet__recruitment_stats_list = 'samfundet:recruitment_stats-list'
Expand Down Expand Up @@ -586,7 +589,6 @@
samfundet__gangsorganized = 'samfundet:gangsorganized'
samfundet__check_reservation = 'samfundet:check_reservation'
samfundet__reservation_create = 'samfundet:reservation-create'
samfundet__active_recruitments = 'samfundet:active_recruitments'
samfundet__recruitment_positions = 'samfundet:recruitment_positions'
samfundet__recruitment_show_unprocessed_applicants = 'samfundet:recruitment_show_unprocessed_applicants'
samfundet__recruitment_positions_gang_for_applicants = 'samfundet:recruitment_positions_gang_for_applicants'
Expand Down
2 changes: 1 addition & 1 deletion backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

########## Recruitment ##########
router.register('recruitment', recruitment_views.RecruitmentView, 'recruitment')
router.register('active-recruitment', recruitment_views.ActiveRecruitmentsView, 'active_recruitment')
router.register('recruitment-for-recruiter', recruitment_views.RecruitmentForRecruiterView, 'recruitment_for_recruiter')
router.register('recruitment-stats', views.RecruitmentStatisticsView, 'recruitment_stats')
router.register('recruitment-separateposition', views.RecruitmentSeparatePositionView, 'recruitment_separateposition')
Expand Down Expand Up @@ -88,7 +89,6 @@
path('check-reservation/', samfundet.view.sulten_views.ReservationCheckAvailabilityView.as_view(), name='check_reservation'),
path('reservations/', samfundet.view.sulten_views.ReservationCreateView.as_view(), name='reservation-create'),
########## Recruitment ##########
path('active-recruitments/', views.ActiveRecruitmentsView.as_view(), name='active_recruitments'),
path('recruitment-positions/', views.RecruitmentPositionsPerRecruitmentView.as_view(), name='recruitment_positions'),
path(
'recruitment-show-unprocessed-applicants/',
Expand Down
37 changes: 35 additions & 2 deletions backend/samfundet/view/recruitment_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from rest_framework import status
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissionsOrAnonReadOnly

from django.utils import timezone
from django.db.models import QuerySet
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
Expand All @@ -20,7 +21,7 @@
from root.custom_classes.permission_classes import RoleProtectedObjectPermissions, filter_queryset_by_permissions

from samfundet.serializers import RecruitmentSerializer, RecruitmentGangSerializer, RecruitmentForRecruiterSerializer, RecruitmentApplicationForGangSerializer
from samfundet.models.general import Gang
from samfundet.models.general import Gang, Organization
from samfundet.models.recruitment import Recruitment, RecruitmentApplication

# =============================== #
Expand All @@ -46,6 +47,38 @@ def gangs(self, request: Request, **kwargs: Any) -> Response:
return Response(serializer.data)


@method_decorator(ensure_csrf_cookie, 'dispatch')
class ActiveRecruitmentsView(ReadOnlyModelViewSet):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
serializer_class = RecruitmentSerializer
queryset = Recruitment.objects.all()

def get_queryset(self) -> QuerySet[Recruitment]:
"""Default queryset to show only active recruitments"""
now = timezone.now()
return Recruitment.objects.filter(
visible_from__lte=now, # __lte: less than or equal to (Django lookup type)
actual_application_deadline__gte=now, # __gte: greater than or equal to (Django lookup type)
)

@action(detail=False, methods=['get'], url_path='samfundet')
def get_active_samf_recruitments(self, request: Request, **kwargs: Any) -> Response:
try:
samfundet_org = Organization.objects.get(name='Samfundet')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finn eller flytt name inn i en konstant noe sted

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sånn her lissom? 8ff2c70

Copy link
Contributor Author

@Snorre98 Snorre98 Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^det ble feil,. Dette funker: e81d635


# Get active recruitments for Samfundet, using the overriden get_queryset method
active_samfundet_recruitments = self.get_queryset().filter(organization=samfundet_org)

if not active_samfundet_recruitments:
return Response({'message': 'No active recruitment for Samfundet'}, status=status.HTTP_404_NOT_FOUND)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Syntes det virker riktigere å returnere en tom liste her enn en 404 feil?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dvs bare fjerne dette?

Copy link
Contributor Author

@Snorre98 Snorre98 Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tror jeg er uenig. Er det ikke bedre at det er tydelig at det ikke finnes opptak, i stede for å returnere en tom liste. Jeg føler det å returnere en tom liste mer sannsynlig gjøre at fremtidige devs kan komme til å tro at det er noe feil i koden hvis det returneres en tom liste sammenlignet med melding som sier at det ikke finnes opptak som etterspørres her.

Nå gjorde jeg forresten en delvis urelatert endring (her fe23411)hvor jeg sjekker om det eksistere ne organisasjon med navn Samfundet med en if-statement istedefor en try/catch. Føler det er litt mer lesbart og gjøre kan "flyte" litt bedre enn exceptions.

Jeg mener i hvertfall man burde sjekke om organisasjonen med navn Samfundet eksistere, slik at får en feil hvis Samf av en eller annen grunn heter noe annet, f.eks. Samf eller Studentersamfundet i Trondhjem.


serializer = self.get_serializer(active_samfundet_recruitments, many=True)
return Response(serializer.data)

except Organization.DoesNotExist:
return Response({'error': 'No organization named Samfundet exists'}, status=status.HTTP_404_NOT_FOUND)


# =============================== #
# Auth protected views #
# =============================== #
Expand Down
12 changes: 1 addition & 11 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import AllowAny, IsAuthenticated, DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly
from rest_framework.permissions import AllowAny, IsAuthenticated, DjangoModelPermissions

from django.conf import settings
from django.http import QueryDict, HttpResponse
Expand Down Expand Up @@ -581,16 +581,6 @@ def get_queryset(self) -> Response:
return RecruitmentPosition.objects.filter(recruitment__visible_from__lte=timezone.now(), recruitment__actual_application_deadline__gte=timezone.now())


class ActiveRecruitmentsView(ListAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
serializer_class = RecruitmentSerializer

def get_queryset(self) -> Response:
"""Returns all active recruitments"""
# TODO Use is not completed instead of actual_application_deadline__gte
return Recruitment.objects.filter(visible_from__lte=timezone.now(), actual_application_deadline__gte=timezone.now())


class RecruitmentInterviewGroupView(APIView):
permission_classes = [IsAuthenticated]

Expand Down
43 changes: 27 additions & 16 deletions frontend/src/Components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query';
import { default as classNames } from 'classnames';
import { useEffect, useState } from 'react';
import { useCookies } from 'react-cookie';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router';
import { Button, Link, ThemeSwitch } from '~/Components';
import { getActiveRecruitments, logout, stopImpersonatingUser } from '~/api';
import { getActiveSamfRecruitments, logout, stopImpersonatingUser } from '~/api';
import { logoWhite } from '~/assets';
import { useAuthContext } from '~/context/AuthContext';
import { useGlobalContext } from '~/context/GlobalContextProvider';
import type { RecruitmentDto } from '~/dto';
import { useDesktop, useScrollY } from '~/hooks';
import { STATUS } from '~/http_status_codes';
import { KEY } from '~/i18n/constants';
import { reverse } from '~/named-urls';
import { recruitmentKeys } from '~/queryKeys';
import { ROUTES } from '~/routes';
import styles from './Navbar.module.scss';
import { HamburgerMenu, LanguageButton, NavbarItem } from './components';
Expand All @@ -23,7 +25,6 @@ export function Navbar() {
const { isMobileNavigation, setIsMobileNavigation } = useGlobalContext();
const { t, i18n } = useTranslation();
const { user, setUser } = useAuthContext();
const [activeRecruitments, setActiveRecruitments] = useState<RecruitmentDto[]>();
const navigate = useNavigate();
const isDesktop = useDesktop();
const [cookies, setCookie, removeCookie] = useCookies();
Expand All @@ -50,13 +51,12 @@ export function Navbar() {
}
}, [isMobileNavigation, isDesktop]);

useEffect(() => {
getActiveRecruitments().then((response) => {
setActiveRecruitments(response.data);
});
}, []);
const { data: activeSamfRecruitments } = useQuery({
queryKey: recruitmentKeys.all,
queryFn: getActiveSamfRecruitments,
});

const showActiveRecruitments = activeRecruitments !== undefined && activeRecruitments?.length > 0;
const showActiveRecruitments = activeSamfRecruitments !== undefined && activeSamfRecruitments?.length > 0;

// Return profile button for navbar if logged in.
const mobileProfileButton = (
Expand Down Expand Up @@ -130,13 +130,24 @@ export function Navbar() {
route={ROUTES.frontend.sulten}
label={t(KEY.common_restaurant)}
/>
<NavbarItem
setExpandedDropdown={setExpandedDropdown}
expandedDropdown={expandedDropdown}
route={ROUTES.frontend.recruitment}
label={t(KEY.common_volunteer)}
labelClassName={showActiveRecruitments ? styles.active_recruitment : ''}
/>
{showActiveRecruitments && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nå forsvinner knappen når det ikke er opptak. Det er ikke ønsket, man burde kunne trykke seg inn når som helst for å få info om opptak generelt, og når neste opptak evt. er

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Endret her 1c52c77

<NavbarItem
setExpandedDropdown={setExpandedDropdown}
expandedDropdown={expandedDropdown}
route={
activeSamfRecruitments.length === 1
? // goes to the one samf recruitment if there is only one
reverse({
pattern: ROUTES.frontend.organization_recruitment,
urlParams: { recruitmentId: activeSamfRecruitments[0].id },
})
: // goes to the page with recruitment cards if there is multiple samf recruitments
ROUTES.frontend.recruitment
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dette kan gå i en konstant

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done d70b36d

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

og med en oppfølger her 1ec48cf

label={t(KEY.common_volunteer)}
labelClassName={styles.active_recruitment}
/>
)}
</div>
);

Expand Down
29 changes: 12 additions & 17 deletions frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Page, SamfundetLogoSpinner } from '~/Components';
import { getActiveRecruitments } from '~/api';
import { getActiveSamfRecruitments } from '~/api';
import type { RecruitmentDto } from '~/dto';
import { useTitle } from '~/hooks';
import { KEY } from '~/i18n/constants';
import { recruitmentKeys } from '~/queryKeys';
import { NoPositions, RecruitmentCard } from './Components';
import styles from './RecruitmentPage.module.scss';

export function RecruitmentPage() {
const [recruitments, setRecruitments] = useState<RecruitmentDto[]>([]);
const [loading, setLoading] = useState(true);
const { t } = useTranslation();
useTitle(t(KEY.common_recruitment));

useEffect(() => {
getActiveRecruitments()
.then((response) => {
setRecruitments(response.data);
})
.catch((error) => {
console.log('Error fetching data:', error);
});
setLoading(false);
}, []);
const { data: activeSamfRecruitments, isLoading } = useQuery({
/* Important! Navbar navigates to this page if there are multiple active Samf recruitments! */
/* Take it into consideration if recruitments for all organizations should be fetched here in the future */
queryKey: recruitmentKeys.all,
queryFn: getActiveSamfRecruitments,
});

return (
<Page>
<div className={styles.container}>
<div className={styles.cardContainer}>
{loading ? (
{isLoading ? (
<SamfundetLogoSpinner />
) : recruitments && recruitments.length > 0 ? (
recruitments.map((recruitment: RecruitmentDto) => (
) : activeSamfRecruitments && activeSamfRecruitments.length > 0 ? (
activeSamfRecruitments.map((recruitment: RecruitmentDto) => (
<RecruitmentCard recruitment={recruitment} key={recruitment.id} />
))
) : (
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -974,13 +974,20 @@ export async function getActiveRecruitmentPositions(): Promise<AxiosResponse<Rec
return response;
}

export async function getActiveRecruitments(): Promise<AxiosResponse<RecruitmentDto[]>> {
const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__active_recruitments;
export async function getAllActiveRecruitments(): Promise<AxiosResponse<RecruitmentDto[]>> {
const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__active_recruitment_list;
const response = await axios.get(url, { withCredentials: true });

return response;
}

export async function getActiveSamfRecruitments(): Promise<RecruitmentDto[]> {
const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__active_recruitment_get_active_samf_recruitments;
const response = await axios.get<RecruitmentDto[]>(url, { withCredentials: true });

return response.data;
}

export async function getApplicantsWithoutInterviews(
recruitmentId: string,
gangId: string | null = null,
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ export const permissionKeys = {
details: () => [...permissionKeys.all, 'detail'] as const,
detail: (id: number) => [...permissionKeys.details(), id] as const,
};

export const recruitmentKeys = {
all: ['recruitments'] as const,
lists: () => [...recruitmentKeys.all, 'list'] as const,
list: (filters: unknown[]) => [...recruitmentKeys.lists(), { filters }] as const,
details: () => [...recruitmentKeys.all, 'detail'] as const,
detail: (id: number) => [...recruitmentKeys.details(), id] as const,
};
35 changes: 33 additions & 2 deletions frontend/src/router/loaders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import type { LoaderFunctionArgs } from 'react-router';
import { getGang, getRecruitment, getRecruitmentPosition, getRecruitmentSeparatePosition, getRole } from '~/api';
import { type LoaderFunctionArgs, redirect } from 'react-router';
import {
getActiveSamfRecruitments,
getGang,
getRecruitment,
getRecruitmentPosition,
getRecruitmentSeparatePosition,
getRole,
} from '~/api';
import type { GangDto, RecruitmentDto, RecruitmentPositionDto, RecruitmentSeparatePositionDto, RoleDto } from '~/dto';
import { reverse } from '~/named-urls';
import { ROUTES } from '~/routes';

export type RecruitmentLoader = {
recruitment: RecruitmentDto | undefined;
Expand All @@ -26,6 +35,28 @@ export async function roleLoader({ params }: LoaderFunctionArgs): Promise<RoleLo
return { role: await getRole(Number.parseInt(params.roleId as string)) };
}

export async function samfRecruitmentLoader() {
try {
const activeSamfRecruitments = await getActiveSamfRecruitments();
Comment on lines +39 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tanstack?

Copy link
Contributor Author

@Snorre98 Snorre98 Apr 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Det er ikke tsx, altså, kan ikke kjøre en hook her


// Check if there's only one recruitment and redirect if needed
if (activeSamfRecruitments?.length === 1) {
return redirect(
reverse({
pattern: ROUTES.frontend.recruitment_application_overview,
urlParams: { recruitmentId: activeSamfRecruitments[0].id },
}),
);
}

// Otherwise, return the recruitments data
return { activeSamfRecruitments };
} catch (error) {
console.error('Error fetching recruitments:', error);
return { activeSamfRecruitments: [] };
}
}

export async function recruitmentLoader({ params }: LoaderFunctionArgs): Promise<RecruitmentLoader> {
return { recruitment: (await getRecruitment(params.recruitmentId as string)).data };
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import {
recruitmentGangPositionLoader,
recruitmentLoader,
roleLoader,
samfRecruitmentLoader,
separatePositionLoader,
} from '~/router/loaders';
import { dbT, lowerCapitalize } from '~/utils';
Expand Down Expand Up @@ -125,7 +126,7 @@ export const router = createBrowserRouter(
<Route path={ROUTES.frontend.contact} element={<div />} />
<Route path={ROUTES.frontend.luka} element={<div />} />
{/* Recruitment */}
<Route path={ROUTES.frontend.recruitment} element={<RecruitmentPage />} />
<Route path={ROUTES.frontend.recruitment} loader={samfRecruitmentLoader} element={<RecruitmentPage />} />
</Route>
</Route>
{/* Specific recruitment */}
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 @@ -543,6 +543,9 @@ export const ROUTES_BACKEND = {
samfundet__recruitment_list: '/api/recruitment/',
samfundet__recruitment_detail: '/api/recruitment/:pk/',
samfundet__recruitment_gangs: '/api/recruitment/:pk/gangs/',
samfundet__active_recruitment_list: '/api/active-recruitment/',
samfundet__active_recruitment_get_active_samf_recruitments: '/api/active-recruitment/samfundet/',
samfundet__active_recruitment_detail: '/api/active-recruitment/:pk/',
samfundet__recruitment_for_recruiter_list: '/api/recruitment-for-recruiter/',
samfundet__recruitment_for_recruiter_detail: '/api/recruitment-for-recruiter/:pk/',
samfundet__recruitment_stats_list: '/api/recruitment-stats/',
Expand Down Expand Up @@ -585,7 +588,6 @@ export const ROUTES_BACKEND = {
samfundet__gangsorganized: '/gangtypes/:organization/',
samfundet__check_reservation: '/check-reservation/',
samfundet__reservation_create: '/reservations/',
samfundet__active_recruitments: '/active-recruitments/',
samfundet__recruitment_positions: '/recruitment-positions/',
samfundet__recruitment_show_unprocessed_applicants: '/recruitment-show-unprocessed-applicants/',
samfundet__recruitment_positions_gang_for_applicants: '/recruitment-positions-gang-for-applicant/',
Expand Down