diff --git a/backend/Pipfile b/backend/Pipfile index b2cbb7c72..5c2d3550d 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -3,6 +3,28 @@ url = "https://pypi.org/simple" verify_ssl = true name = "pypi" +[scripts] +# See '/docs/pipenv.md'. +"pipenv:install" = "pipenv install" +"pipenv:update" = "pipenv update" +"pipenv:sync" = "bash -c \"pipenv clean; pipenv sync --dev\"" +"pipenv:docker-install-dev" = "pipenv install --deploy --ignore-pipfile --dev" # 'deploy' means abort if outdated lock file. 'ignore-pipfile' means only install using the lock file. 'dev' means install dev dependencies. +"pipenv:docker-install-prod" = "pipenv install --deploy --ignore-pipfile" # 'deploy' means abort if outdated lock file. 'ignore-pipfile' means only install using the lock file. +"pipenv:outdated" = "pipenv update --outdated" # Show outdated dependencies. +"pipenv:graph" = "pipenv graph" # Show dependency graph. +"pipenv:where" = "pipenv --where" # Show location of virtual environment. +"pipenv:rm" = "pipenv --rm" # Completely remove virtual environment. +"pipenv:shell" = "pipenv shell" # Opens a shell within the virtual environment. +"mypy:run" = "pipenv run mypy --config-file mypy.ini ." +"migrations:verify" = "pipenv run python manage.py makemigrations --check --dry-run --noinput --verbosity 2" +"bandit:run" = "pipenv run bandit --recursive --ini .bandit ." +"flake8:run" = "pipenv run flake8 --config=.flake8 ." +"yapf:diff" = "pipenv run yapf --parallel --recursive --diff ." # Dry-run yapf on all files in the project. +"yapf:apply" = "pipenv run yapf --parallel --recursive -i ." # Applies yapf to all files in the project. +"pytest:run" = "pipenv run pytest" +"pipeline:run" = "bash -c \"pipenv run mypy:run && pipenv run yapf:diff && pipenv run migrations:verify && pipenv run bandit:run && pipenv run flake8:run && pipenv run pytest:run\"" +"seed:run" = "pipenv run python manage.py seed" + [packages] django = "*" pytest = "*" diff --git a/backend/root/management/commands/seed_scripts/__init__.py b/backend/root/management/commands/seed_scripts/__init__.py index e80702a9a..ced9e69b5 100755 --- a/backend/root/management/commands/seed_scripts/__init__.py +++ b/backend/root/management/commands/seed_scripts/__init__.py @@ -8,6 +8,7 @@ billig, menu, documents, + textitems, example, samf3, recruitment, @@ -32,6 +33,7 @@ ('menu', menu.seed), ('documents', documents.seed), ('information_page', information_pages.seed), + ('textitems', textitems.seed), ('blogposts', blogposts.seed), ('organization', oganizations.seed), ('recruitment', recruitment.seed), diff --git a/backend/root/management/commands/seed_scripts/textitems.py b/backend/root/management/commands/seed_scripts/textitems.py new file mode 100644 index 000000000..9fa2bd21b --- /dev/null +++ b/backend/root/management/commands/seed_scripts/textitems.py @@ -0,0 +1,44 @@ +from samfundet.models.general import TextItem + + +def seed(): + text_items = [ + { + 'key': 'welcome_message', + 'text_nb': 'Velkommen til Studentersamfundet i Trondhjem!', + 'text_en': 'Welcome to the Student Society in Trondheim!', + }, + { + 'key': 'upcoming_events', + 'text_nb': 'Sjekk ut våre kommende arrangementer og konsertene vi har planlagt!', + 'text_en': 'Check out our upcoming events and concerts we have planned!', + }, + { + 'key': 'join_us', + 'text_nb': 'Bli medlem av Studentersamfundet og nyt godt av medlemsfordelene!', + 'text_en': 'Join the Student Society and enjoy the benefits of membership!', + }, + { + 'key': 'volunteer', + 'text_nb': 'Vil du bli frivillig? Bli med i vårt fantastiske team og bidra til studentkulturen i Trondheim!', + 'text_en': 'Want to volunteer? Join our amazing team and contribute to the student culture in Trondheim!', + }, + { + 'key': 'about_us', + 'text_nb': 'Studentersamfundet i Trondhjem er et kulturelt senter for studenter og en viktig del av studentlivet i Trondheim.', + 'text_en': 'The Student Society in Trondheim is a cultural center for students and an essential part of student life in Trondheim.', + }, + { + 'key': 'contact_us', + 'text_nb': 'Har du spørsmål eller ønsker å komme i kontakt med oss? Ikke nøl med å ta kontakt!', + 'text_en': 'Do you have any questions or want to get in touch with us? Don"t hesitate to contact us!', + }, + ] + + TextItem.objects.all().delete() + yield 0, 'Deleted old textitems' + + for i, item in enumerate(text_items): + text_item, created = TextItem.objects.get_or_create(key=item['key'], text_nb=item['text_nb'], text_en=item['text_en']) + if created: + yield (100 * (i + 1) // len(text_items), f'Created {len(TextItem.objects.all())} textitems') diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 56d96342b..9755f9833 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -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-17 17:48:38.961443+00:00 +LAST UPDATE: 2023-09-04 15:22:16.450169+00:00 """ ############################################################ @@ -308,18 +308,36 @@ admin__samfundet_keyvalue_delete = 'admin:samfundet_keyvalue_delete' admin__samfundet_keyvalue_change = 'admin:samfundet_keyvalue_change' adminsamfundetkeyvalue__objectId = '' +admin__samfundet_recruitment_permissions = 'admin:samfundet_recruitment_permissions' +admin__samfundet_recruitment_permissions_manage_user = 'admin:samfundet_recruitment_permissions_manage_user' +admin__samfundet_recruitment_permissions_manage_group = 'admin:samfundet_recruitment_permissions_manage_group' admin__samfundet_recruitment_changelist = 'admin:samfundet_recruitment_changelist' admin__samfundet_recruitment_add = 'admin:samfundet_recruitment_add' admin__samfundet_recruitment_history = 'admin:samfundet_recruitment_history' admin__samfundet_recruitment_delete = 'admin:samfundet_recruitment_delete' admin__samfundet_recruitment_change = 'admin:samfundet_recruitment_change' adminsamfundetrecruitment__objectId = '' +admin__samfundet_recruitmentposition_permissions = 'admin:samfundet_recruitmentposition_permissions' +admin__samfundet_recruitmentposition_permissions_manage_user = 'admin:samfundet_recruitmentposition_permissions_manage_user' +admin__samfundet_recruitmentposition_permissions_manage_group = 'admin:samfundet_recruitmentposition_permissions_manage_group' admin__samfundet_recruitmentposition_changelist = 'admin:samfundet_recruitmentposition_changelist' admin__samfundet_recruitmentposition_add = 'admin:samfundet_recruitmentposition_add' admin__samfundet_recruitmentposition_history = 'admin:samfundet_recruitmentposition_history' admin__samfundet_recruitmentposition_delete = 'admin:samfundet_recruitmentposition_delete' admin__samfundet_recruitmentposition_change = 'admin:samfundet_recruitmentposition_change' adminsamfundetrecruitmentposition__objectId = '' +admin__samfundet_recruitmentadmission_permissions = 'admin:samfundet_recruitmentadmission_permissions' +admin__samfundet_recruitmentadmission_permissions_manage_user = 'admin:samfundet_recruitmentadmission_permissions_manage_user' +admin__samfundet_recruitmentadmission_permissions_manage_group = 'admin:samfundet_recruitmentadmission_permissions_manage_group' +admin__samfundet_recruitmentadmission_changelist = 'admin:samfundet_recruitmentadmission_changelist' +admin__samfundet_recruitmentadmission_add = 'admin:samfundet_recruitmentadmission_add' +admin__samfundet_recruitmentadmission_history = 'admin:samfundet_recruitmentadmission_history' +admin__samfundet_recruitmentadmission_delete = 'admin:samfundet_recruitmentadmission_delete' +admin__samfundet_recruitmentadmission_change = 'admin:samfundet_recruitmentadmission_change' +adminsamfundetrecruitmentadmission__objectId = '' +admin__samfundet_organization_permissions = 'admin:samfundet_organization_permissions' +admin__samfundet_organization_permissions_manage_user = 'admin:samfundet_organization_permissions_manage_user' +admin__samfundet_organization_permissions_manage_group = 'admin:samfundet_organization_permissions_manage_group' admin__samfundet_organization_changelist = 'admin:samfundet_organization_changelist' admin__samfundet_organization_add = 'admin:samfundet_organization_add' admin__samfundet_organization_history = 'admin:samfundet_organization_history' @@ -386,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' @@ -413,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 = '' diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index 267eb3929..9f5c1e650 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -1,9 +1,12 @@ +from django.urls import reverse from django.contrib import admin +from django.utils.html import format_html from django.contrib.admin.models import LogEntry from django.contrib.auth.models import Permission, Group from django.contrib.contenttypes.models import ContentType from django.contrib.sessions.models import Session from guardian import models as guardian_models +from root.utils.routes import admin__samfundet_recruitmentadmission_change from root.custom_classes.admin_classes import ( CustomGuardedUserAdmin, @@ -488,6 +491,25 @@ class RecruitmentAdmin(CustomGuardedModelAdmin): list_select_related = True +class RecruitmentAdmissionInline(admin.TabularInline): + """ + Inline admin interface for RecruitmentAdmission. + + Displays a link to the detailed admin page of each admission along with its user and applicant priority. + """ + model = RecruitmentAdmission + extra = 0 + readonly_fields = ['linked_admission_text', 'user', 'applicant_priority'] + fields = ['linked_admission_text', 'user', 'applicant_priority'] + + def linked_admission_text(self, obj: RecruitmentAdmission) -> str: + """ + Returns a clickable link leading to the admin change page of the RecruitmentAdmission instance. + """ + url = reverse(admin__samfundet_recruitmentadmission_change, args=[obj.pk]) + return format_html('{obj}', url=url, obj=obj.admission_text) + + @admin.register(RecruitmentPosition) class RecruitmentPositionAdmin(CustomGuardedModelAdmin): sortable_by = [ @@ -496,11 +518,17 @@ class RecruitmentPositionAdmin(CustomGuardedModelAdmin): 'gang', 'id', ] - list_display = ['name_nb', 'is_funksjonaer_position', 'gang', 'id'] + list_display = ['name_nb', 'is_funksjonaer_position', 'gang', 'id', 'admissions_count'] search_fields = ['name_nb', 'is_funksjonaer_position', 'gang', 'id'] filter_horizontal = ['interviewers'] list_select_related = True + inlines = [RecruitmentAdmissionInline] + + def admissions_count(self, obj: RecruitmentPosition) -> int: + count = obj.admissions.all().count() + return count + @admin.register(RecruitmentAdmission) class RecruitmentAdmissionAdmin(CustomGuardedModelAdmin): diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 8f5df7969..7a5c3338a 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -504,6 +504,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: @@ -518,14 +537,38 @@ class Meta: fields = [ 'admission_text', 'recruitment_position', - 'user', - 'applicant_priority', 'interview_time', 'interview_location', ] + def create(self, validated_data: dict) -> RecruitmentAdmission: + recruitment_position = validated_data['recruitment_position'] + recruitment = recruitment_position.recruitment + user = self.context['request'].user + applicant_priority = 1 + + recruitment_admission = RecruitmentAdmission.objects.create( + admission_text=validated_data.get('admission_text'), + recruitment_position=recruitment_position, + recruitment=recruitment, + user=user, + applicant_priority=applicant_priority, + interview_time=validated_data.get('interview_time'), + interview_location=validated_data.get('interview_location') + ) + + return recruitment_admission + + +class ApplicantInfoSerializer(serializers.ModelSerializer): + + class Meta: + model = User + fields = ['id', 'first_name', 'last_name', 'email'] + class RecruitmentAdmissionForGangSerializer(serializers.ModelSerializer): + user = ApplicantInfoSerializer(read_only=True) class Meta: model = RecruitmentAdmission diff --git a/backend/samfundet/urls.py b/backend/samfundet/urls.py index a84a4dadd..2985c5d94 100644 --- a/backend/samfundet/urls.py +++ b/backend/samfundet/urls.py @@ -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/'), ] diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index c354babfa..8b422b47e 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -1,5 +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 @@ -78,6 +79,7 @@ FoodPreferenceSerializer, UserPreferenceSerializer, InformationPageSerializer, + UserForRecruitmentSerializer, RecruitmentPositionSerializer, RecruitmentAdmissionForGangSerializer, RecruitmentAdmissionForApplicantSerializer, @@ -448,6 +450,13 @@ class RecruitmentPositionView(ModelViewSet): queryset = RecruitmentPosition.objects.all() +@method_decorator(ensure_csrf_cookie, 'dispatch') +class RecruitmentAdmissionView(ModelViewSet): + permission_classes = [AllowAny] + serializer_class = RecruitmentAdmissionForGangSerializer + queryset = RecruitmentAdmission.objects.all() + + @method_decorator(ensure_csrf_cookie, 'dispatch') class RecruitmentPositionsPerRecruitmentView(ListAPIView): permission_classes = [AllowAny] @@ -465,9 +474,30 @@ 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 + queryset = RecruitmentAdmission.objects.all() def list(self, request: Request) -> Response: """ @@ -495,6 +525,7 @@ def list(self, request: Request) -> Response: class RecruitmentAdmissionForGangView(ModelViewSet): permission_classes = [IsAuthenticated] serializer_class = RecruitmentAdmissionForGangSerializer + queryset = RecruitmentAdmission.objects.all() # TODO: User should only be able to edit the fields that are allowed diff --git a/frontend/src/AppRoutes.tsx b/frontend/src/AppRoutes.tsx index dc0f84a5a..2a735f5ff 100644 --- a/frontend/src/AppRoutes.tsx +++ b/frontend/src/AppRoutes.tsx @@ -14,6 +14,7 @@ import { LoginPage, LychePage, NotFoundPage, + RecruitmentAdmissionFormPage, RecruitmentPage, RouteOverviewPage, SaksdokumenterPage, @@ -35,6 +36,7 @@ import { RecruitmentGangAdminPage, RecruitmentGangOverviewPage, RecruitmentPositionFormAdminPage, + RecruitmentUsersWithoutInterview, SaksdokumentFormAdminPage, } from '~/PagesAdmin'; import { useGoatCounter } from '~/hooks'; @@ -47,6 +49,7 @@ import { RecruitmentFormAdminPage } from './PagesAdmin/RecruitmentFormAdminPage' import { SaksdokumentAdminPage } from './PagesAdmin/SaksdokumentAdminPage'; import { PERM } from './permissions'; import { ROUTES } from './routes'; +import { RecruitmentPositionOverviewPage } from './PagesAdmin/RecruitmentPositionOverviewPage/RecruitmentPositionOverviewPage'; export function AppRoutes() { // Must be called within because it uses hook useLocation(). @@ -74,6 +77,7 @@ export function AppRoutes() { } /> } /> } /> + } /> {/* ADMIN ROUTES @@ -170,6 +174,10 @@ export function AppRoutes() { path={ROUTES.frontend.admin_recruitment_edit} element={} /> + } + /> {/* TODO ADD PERMISSIONS */} } /> + } + /> } diff --git a/frontend/src/Components/Dropdown/Dropdown.tsx b/frontend/src/Components/Dropdown/Dropdown.tsx index 77e321e14..1b62660db 100644 --- a/frontend/src/Components/Dropdown/Dropdown.tsx +++ b/frontend/src/Components/Dropdown/Dropdown.tsx @@ -12,6 +12,7 @@ export type DropDownOption = { type DropdownProps = { className?: string; defaultValue?: DropDownOption; + initialValue?: T; options?: DropDownOption[]; label?: string | ReactElement; disabled?: boolean; @@ -22,6 +23,7 @@ type DropdownProps = { export function Dropdown({ options = [], defaultValue, + initialValue, onChange, className, label, @@ -50,9 +52,9 @@ export function Dropdown({ className={classNames(styles.samf_select, error && styles.error)} onChange={handleChange} disabled={disabled} - defaultValue={-1} + defaultValue={initialValue !== undefined ? options.map((e) => e.value).indexOf(initialValue) : -1} > - {defaultValue ? {defaultValue.label} : } + {defaultValue ? {defaultValue.label} : } {options.map((opt, index) => { return ( diff --git a/frontend/src/Components/InputField/InputField.tsx b/frontend/src/Components/InputField/InputField.tsx index 8ec25bfd6..ec7fdf86d 100644 --- a/frontend/src/Components/InputField/InputField.tsx +++ b/frontend/src/Components/InputField/InputField.tsx @@ -11,6 +11,7 @@ type InputFieldProps = { labelClassName?: string; inputClassName?: string; onChange?: (value: T) => void; + onBlur?: (value: T) => void; placeholder?: string | null; type?: InputFieldType; disabled?: boolean; @@ -25,6 +26,7 @@ export function InputField({ labelClassName, inputClassName, onChange, + onBlur, placeholder, disabled, value, @@ -33,7 +35,7 @@ export function InputField({ type = 'text', icon, }: InputFieldProps) { - function handleChange(e?: ChangeEvent) { + function preprocessValue(e?: ChangeEvent) { let value: string | number | undefined = e?.currentTarget.value ?? ''; if (type === 'number') { if (value.length > 0) { @@ -42,13 +44,14 @@ export function InputField({ value = undefined; } } - onChange?.(value as T); + return value as T; } return ( {children} onChange?.(preprocessValue(e))} + onBlur={(e) => onBlur?.(preprocessValue(e))} className={classNames(styles.input_field, inputClassName, error && styles.error)} placeholder={placeholder || ''} disabled={disabled} diff --git a/frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx b/frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx index 53291dbba..e458d46c7 100644 --- a/frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx +++ b/frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx @@ -1,5 +1,6 @@ import { assignUserToGroup, + getApplicantsWithoutInterviews, getCsrfToken, getInformationPage, getInformationPages, @@ -100,6 +101,13 @@ export function ApiTestingPage() { > get Rec admissions for gang + getApplicantsWithoutInterviews('1').then(console.log).catch(console.error)} + > + get users without interviews + ); } diff --git a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.module.scss b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.module.scss new file mode 100644 index 000000000..abde53a3a --- /dev/null +++ b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.module.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx new file mode 100644 index 000000000..9e0726812 --- /dev/null +++ b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { Page, SamfundetLogoSpinner } from '~/Components'; +import { SamfForm } from '~/Forms/SamfForm'; +import { SamfFormField } from '~/Forms/SamfFormField'; +import { getRecruitmentPosition, postRecruitmentAdmission } from '~/api'; +import { RecruitmentAdmissionDto, RecruitmentPositionDto } from '~/dto'; +import { KEY } from '~/i18n/constants'; +import { ROUTES } from '~/routes'; +import { dbT } from '~/utils'; +import styles from './RecruitmentAdmissionFormPage.module.scss'; + +export function RecruitmentAdmissionFormPage() { + const navigate = useNavigate(); + const { t } = useTranslation(); + + const [recruitmentPosition, setRecruitmentPosition] = useState(); + const [loading, setLoading] = useState(true); + const { positionID, id } = useParams(); + + useEffect(() => { + getRecruitmentPosition('1').then((res) => { + setRecruitmentPosition(res.data); + setLoading(false); + }); + }, []); + + function handleOnSubmit(data: RecruitmentAdmissionDto) { + data.recruitment_position = positionID ? +positionID : 1; + postRecruitmentAdmission(data) + .then(() => { + navigate(ROUTES.frontend.home); + toast.success(t(KEY.common_creation_successful)); + }) + .catch(() => { + toast.error(t(KEY.common_something_went_wrong)); + }); + } + + if (loading) { + return ( + + + + ); + } + + if (!positionID || isNaN(Number(positionID))) { + return ( + + + {t(KEY.recruitment_admission)} + The position id is invalid, please enter another position id + + + ); + } + + const submitText = t(KEY.common_send) + ' ' + t(KEY.recruitment_admission); + + return ( + + + {dbT(recruitmentPosition, 'name')} + {dbT(recruitmentPosition, 'long_description')} + + {' '} + + + + ); +} diff --git a/frontend/src/Pages/RecruitmentAdmissionFormPage/index.ts b/frontend/src/Pages/RecruitmentAdmissionFormPage/index.ts new file mode 100644 index 000000000..06791d57e --- /dev/null +++ b/frontend/src/Pages/RecruitmentAdmissionFormPage/index.ts @@ -0,0 +1 @@ +export { RecruitmentAdmissionFormPage } from './RecruitmentAdmissionFormPage'; diff --git a/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.tsx b/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.tsx index 9c791cfb9..12ab63ace 100644 --- a/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.tsx +++ b/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.tsx @@ -1,5 +1,7 @@ -import { ExpandableHeader } from '~/Components'; +import { ExpandableHeader, Link } from '~/Components'; import { GangTypeDto, RecruitmentPositionDto } from '~/dto'; +import { reverse } from '~/named-urls'; +import { ROUTES } from '~/routes'; import { dbT } from '~/utils'; import styles from './GangPosition.module.scss'; @@ -23,7 +25,15 @@ export function GangPosition({ type, recruitmentPositions }: GangItemProps) { > {filteredPositions.map((pos) => ( - {dbT(pos, 'name')} + + {dbT(pos, 'name')} + {dbT(pos, 'short_description')} ))} diff --git a/frontend/src/Pages/index.ts b/frontend/src/Pages/index.ts index 8a45011d9..0be139c90 100644 --- a/frontend/src/Pages/index.ts +++ b/frontend/src/Pages/index.ts @@ -12,6 +12,7 @@ export { InformationPage } from './InformationPage'; export { LoginPage } from './LoginPage'; export { LychePage } from './LychePage'; export { NotFoundPage } from './NotFoundPage'; +export { RecruitmentAdmissionFormPage } from './RecruitmentAdmissionFormPage'; export { RecruitmentPage } from './RecruitmentPage'; export { RouteOverviewPage } from './RouteOverviewPage'; export { SaksdokumenterPage } from './SaksdokumenterPage'; diff --git a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx index c986a8e6b..f46042e34 100644 --- a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx +++ b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Icon } from '@iconify/react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; @@ -55,10 +56,10 @@ export function AdminLayout() { {/* Applets */} {appletCategories.map((category) => { return ( - <> + {dbT(category, 'title')} {category.applets.map((applet, index) => makeAppletShortcut(applet, index))} - > + ); })} diff --git a/frontend/src/PagesAdmin/RecruitmentGangAdminPage/RecruitmentGangAdminPage.tsx b/frontend/src/PagesAdmin/RecruitmentGangAdminPage/RecruitmentGangAdminPage.tsx index 62287e6ed..8ec575e0d 100644 --- a/frontend/src/PagesAdmin/RecruitmentGangAdminPage/RecruitmentGangAdminPage.tsx +++ b/frontend/src/PagesAdmin/RecruitmentGangAdminPage/RecruitmentGangAdminPage.tsx @@ -31,8 +31,14 @@ export function RecruitmentGangAdminPage() { const tableColumns = [{ content: t(KEY.recruitment_position), sortable: true }]; const data = recruitmentPositions.map(function (recruitmentPosition) { + const pageUrl = reverse({ + pattern: ROUTES.frontend.admin_recruitment_gang_position_applicants_overview, + urlParams: { recruitmentId: recruitmentId, gangId: gangId, positionId: recruitmentPosition.id }, + }); return [ - { content: {dbT(recruitmentPosition, 'name')} }, + { + content: {dbT(recruitmentPosition, 'name')}, + }, { content: ( navigate(ROUTES.frontend.admin_information_create)}> {t(KEY.common_overview)} - navigate(ROUTES.frontend.admin_information_create)}> + navigate(ROUTES.frontend.admin_recruitment_users_without_interview)} + > {t(KEY.recruitment_show_applicants_without_interview)} navigate(ROUTES.frontend.admin_information_create)}> diff --git a/frontend/src/PagesAdmin/RecruitmentPositionOverviewPage/RecruitmentPositionOverviewPage.tsx b/frontend/src/PagesAdmin/RecruitmentPositionOverviewPage/RecruitmentPositionOverviewPage.tsx new file mode 100644 index 000000000..ef7b8e299 --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentPositionOverviewPage/RecruitmentPositionOverviewPage.tsx @@ -0,0 +1,178 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Button, Dropdown, InputField, Link } from '~/Components'; +import { DropDownOption } from '~/Components/Dropdown/Dropdown'; +import { Table } from '~/Components/Table'; +import { getRecruitmentAdmissionsForGang, putRecruitmentAdmissionForGang } from '~/api'; +import { RecruitmentAdmissionDto } from '~/dto'; +import { KEY } from '~/i18n/constants'; +import { reverse } from '~/named-urls'; +import { ROUTES } from '~/routes'; +import { utcTimestampToLocal } from '~/utils'; +import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; + +// TODO: Fetch from backend +const priorityOptions: DropDownOption[] = [ + { label: 'Not Set', value: 0 }, + { label: 'Not Wanted', value: 1 }, + { label: 'Wanted', value: 2 }, + { label: 'Reserve', value: 3 }, +]; + +const statusOptions: DropDownOption[] = [ + { label: 'Nothing', value: 0 }, + { label: 'Called and accepted', value: 1 }, + { label: 'Called and rejected', value: 2 }, + { label: 'Automatic rejection', value: 3 }, +]; + +function immutableSet( + list: RecruitmentAdmissionDto[], + oldValue: RecruitmentAdmissionDto, + newValue: RecruitmentAdmissionDto, +) { + return list.map((element: RecruitmentAdmissionDto) => { + if (element.id === oldValue.id) { + return newValue; + } else { + return element; + } + }); +} + +export function RecruitmentPositionOverviewPage() { + const recruitmentId = useParams().recruitmentId; + const gangId = useParams().gangId; + const positionId = useParams().positionId; + const navigate = useNavigate(); + const [recruitmentApplicants, setRecruitmentApplicants] = useState([]); + const [showSpinner, setShowSpinner] = useState(true); + const { t } = useTranslation(); + useEffect(() => { + recruitmentId && + gangId && + getRecruitmentAdmissionsForGang(gangId, recruitmentId).then((data) => { + setRecruitmentApplicants( + data.data.filter( + (recruitmentApplicant) => recruitmentApplicant.recruitment_position?.toString() == positionId, + ), + ); + setShowSpinner(false); + }); + }, [recruitmentId, gangId, positionId]); + + const tableColumns = [ + { content: t(KEY.recruitment_applicant), sortable: true }, + { content: t(KEY.recruitment_priority), sortable: true }, + { content: t(KEY.recruitment_interview_time), sortable: true }, + { content: t(KEY.recruitment_interview_location), sortable: true }, + { content: t(KEY.recruitment_recruiter_priority), sortable: true }, + { content: t(KEY.recruitment_recruiter_status), sortable: true }, + ]; + const data = recruitmentApplicants.map(function (admission) { + return [ + { + content: ( + + {`${admission.user.first_name} ${admission.user.last_name}`} + + ), + }, + { content: admission.applicant_priority }, + { + content: ( + putRecruitmentAdmissionForGang(admission.id.toString(), admission)} + onChange={(value: string) => { + const newAdmission = { ...admission, interview_time: value.toString() }; + setRecruitmentApplicants(immutableSet(recruitmentApplicants, admission, newAdmission)); + }} + type="datetime-local" + /> + ), + }, + { + content: ( + putRecruitmentAdmissionForGang(admission.id.toString(), admission)} + onChange={(value: string) => { + const newAdmission = { ...admission, interview_location: value.toString() }; + setRecruitmentApplicants(immutableSet(recruitmentApplicants, admission, newAdmission)); + }} + /> + ), + }, + { + content: ( + { + const newAdmission = { ...admission, recruiter_priority: value }; + setRecruitmentApplicants(immutableSet(recruitmentApplicants, admission, newAdmission)); + putRecruitmentAdmissionForGang(admission.id.toString(), newAdmission); + }} + /> + ), + }, + { + content: ( + { + const newAdmission = { ...admission, recruiter_status: value }; + setRecruitmentApplicants(immutableSet(recruitmentApplicants, admission, newAdmission)); + putRecruitmentAdmissionForGang(admission.id.toString(), newAdmission); + }} + /> + ), + }, + ]; + }); + const title = t(KEY.admin_information_manage_title); + const backendUrl = reverse({ + pattern: ROUTES.backend.admin__samfundet_recruitmentposition_change, + urlParams: { + objectId: positionId, + }, + }); + + const header = ( + + navigate( + reverse({ + pattern: ROUTES.frontend.admin_recruitment_gang_position_overview, + urlParams: { + gangId: gangId, + recruitmentId: recruitmentId, + }, + }), + ) + } + > + {t(KEY.common_go_back)} + + ); + + return ( + + + + ); +} diff --git a/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/RecruitmentUsersWithoutInterview.module.scss b/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/RecruitmentUsersWithoutInterview.module.scss new file mode 100644 index 000000000..efb4e94ea --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/RecruitmentUsersWithoutInterview.module.scss @@ -0,0 +1,3 @@ +.table_container { + margin-top: 1.5em; +} diff --git a/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/RecruitmentUsersWithoutInterview.tsx b/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/RecruitmentUsersWithoutInterview.tsx new file mode 100644 index 000000000..053b26b6d --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/RecruitmentUsersWithoutInterview.tsx @@ -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([]); + const [showSpinner, setShowSpinner] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + 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 ( + + + + userToTableRow(user))} /> + + + ); +} diff --git a/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/index.ts b/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/index.ts new file mode 100644 index 000000000..9871e6895 --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterview/index.ts @@ -0,0 +1 @@ +export { RecruitmentUsersWithoutInterview } from './RecruitmentUsersWithoutInterview'; diff --git a/frontend/src/PagesAdmin/index.ts b/frontend/src/PagesAdmin/index.ts index c8d8a6f6d..9fba79c85 100644 --- a/frontend/src/PagesAdmin/index.ts +++ b/frontend/src/PagesAdmin/index.ts @@ -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'; diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1a12e10f6..6b56a69b3 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -590,9 +590,52 @@ export async function getRecruitmentAdmissionsForGang( return response; } +export async function putRecruitmentAdmissionForGang( + admissionId: string, + admission: Partial, +): Promise { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_admissions_for_gang_detail, + urlParams: { pk: admissionId }, + }); + const response = await axios.put(url, admission, { withCredentials: true }); + return response; +} + export async function getActiveRecruitmentPositions(): Promise> { const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__active_recruitment_positions; const response = await axios.get(url, { withCredentials: true }); return response; } + +export async function getApplicantsWithoutInterviews(recruitmentId: string): Promise> { + 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): Promise { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_admissions_for_applicant_list, + }); + const data = { + admission_text: admission.admission_text, + recruitment_position: admission.recruitment_position, + }; + const response = await axios.post(url, data, { withCredentials: true }); + + return response; +} diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 16c243963..93b3e31b0 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -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[]; @@ -333,13 +342,14 @@ export type RecruitmentPositionDto = { }; export type RecruitmentAdmissionDto = { + id: number; admission_text: string; - recruitment_position: number; + recruitment_position?: number; recruitment: number; - user: number; - priority: number; + user: UserDto; + applicant_priority: number; interview_time?: string; interview_location?: string; recruiter_priority?: number; - recruiter_status?: string; + recruiter_status?: number; }; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index e6710f53b..5451b3378 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -58,6 +58,7 @@ export const KEY = { common_tags: 'common_tags', common_gang: 'common_gang', common_next: 'common_next', + common_send: 'common_send', common_event: 'common_event', common_login: 'common_login', common_image: 'common_image', @@ -68,6 +69,7 @@ export const KEY = { common_venues: 'common_venues', common_sulten: 'common_sulten', common_logout: 'common_logout', + common_go_back: 'common_go_back', common_create: 'common_create', common_search: 'common_search', common_choose: 'common_choose', @@ -78,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_email: 'common_email', @@ -149,13 +152,21 @@ export const KEY = { // Recruitment: recruitment_tags: 'recruitment_tags', recruitment_position: 'recruitment_position', + recruitment_applicant: 'recruitment_applicant', + recruitment_interview_time: 'recruitment_interview_time', + recruitment_interview_location: 'recruitment_interview_location', + recruitment_priority: 'recruitment_priority', + recruitment_recruiter_priority: 'recruitment_recruiter_priority', + recruitment_recruiter_status: 'recruitment_recruiter_status', recruitment_duration: 'recruitment_duration', + recruitment_admission: 'recruitment_admission', recruitment_funksjonaer: 'recruitment_funksjonaer', recruitment_organization: 'recruitment_organization', recruitment_visible_from: 'recruitment_visible_from', 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', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 13a36be49..b3107b077 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -40,6 +40,7 @@ export const nb: Record = { [KEY.common_here]: 'her', [KEY.common_show]: 'Vis', [KEY.common_date]: 'Dato', + [KEY.common_send]: 'Send', [KEY.common_menu]: 'Meny', [KEY.common_name]: 'Navn', [KEY.common_next]: 'Neste', @@ -58,10 +59,10 @@ export const nb: Record = { [KEY.common_create]: 'Opprett', [KEY.common_delete]: 'Slett', [KEY.common_logout]: 'Logg ut', + [KEY.common_go_back]: 'Tilbake', [KEY.common_search]: 'Søk', [KEY.common_choose]: 'Velg', [KEY.common_sulten]: 'Lyche', - [KEY.common_delete]: 'Slett', [KEY.common_missing]: 'Mangler', [KEY.common_contact]: 'Kontakt', [KEY.common_english]: 'Engelsk', @@ -85,6 +86,7 @@ export const nb: Record = { [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', @@ -141,13 +143,21 @@ export const nb: Record = { // Recruitment: [KEY.recruitment_tags]: 'Tags', [KEY.recruitment_position]: 'Stilling', + [KEY.recruitment_applicant]: 'Søker', + [KEY.recruitment_interview_time]: 'Intervjutid', + [KEY.recruitment_interview_location]: 'Intervjusted', + [KEY.recruitment_priority]: 'Søkers prioritet', + [KEY.recruitment_recruiter_priority]: 'Prioritet', + [KEY.recruitment_recruiter_status]: 'Status', [KEY.recruitment_duration]: 'Varighet', + [KEY.recruitment_admission]: 'Søknad', [KEY.recruitment_funksjonaer]: 'Funksjonær', [KEY.recruitment_visible_from]: 'Synlig fra', [KEY.recruitment_organization]: 'Organisasjon', [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', @@ -252,6 +262,7 @@ export const en: Record = { [KEY.common_save]: 'Save', [KEY.common_from]: 'From', [KEY.common_date]: 'Date', + [KEY.common_send]: 'Send', [KEY.common_edit]: 'Edit', [KEY.common_show]: 'Show', [KEY.common_tags]: 'Tags', @@ -270,10 +281,10 @@ export const en: Record = { [KEY.common_create]: 'Create', [KEY.common_delete]: 'Delete', [KEY.common_logout]: 'Log out', + [KEY.common_go_back]: 'Go back', [KEY.common_sulten]: 'Lyche', [KEY.common_search]: 'Search', [KEY.common_choose]: 'Choose', - [KEY.common_delete]: 'Delete', [KEY.common_missing]: 'Missing', [KEY.common_message]: 'Message', [KEY.common_sponsor]: 'Sponsors', @@ -293,6 +304,7 @@ export const en: Record = { [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', @@ -353,13 +365,21 @@ export const en: Record = { // Recruitment: [KEY.recruitment_tags]: 'Tags', [KEY.recruitment_position]: 'Position', + [KEY.recruitment_applicant]: 'Applicant', + [KEY.recruitment_interview_time]: 'Intervjutid', + [KEY.recruitment_interview_location]: 'Intervjusted', + [KEY.recruitment_priority]: 'Søkers prioritet', + [KEY.recruitment_recruiter_priority]: 'Prioritet', + [KEY.recruitment_recruiter_status]: 'Status', [KEY.recruitment_duration]: 'Duration', + [KEY.recruitment_admission]: 'Admission', [KEY.recruitment_funksjonaer]: 'Functionary', [KEY.recruitment_organization]: 'Organization', [KEY.recruitment_visible_from]: 'Visible from', [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', diff --git a/frontend/src/routes/backend.ts b/frontend/src/routes/backend.ts index b71365332..311f2eeb2 100644 --- a/frontend/src/routes/backend.ts +++ b/frontend/src/routes/backend.ts @@ -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-17 17:48:38.961443+00:00 +LAST UPDATE: 2023-09-04 15:22:16.450169+00:00 """ */ // ############################################################ @@ -307,18 +307,36 @@ export const ROUTES_BACKEND = { admin__samfundet_keyvalue_delete: '/admin/samfundet/keyvalue/:objectId/delete/', admin__samfundet_keyvalue_change: '/admin/samfundet/keyvalue/:objectId/change/', adminsamfundetkeyvalue__objectId: '/admin/samfundet/keyvalue/:objectId/', + admin__samfundet_recruitment_permissions: '/admin/samfundet/recruitment/:objectPk/permissions/', + admin__samfundet_recruitment_permissions_manage_user: '/admin/samfundet/recruitment/:objectPk/permissions/user-manage/:userId/', + admin__samfundet_recruitment_permissions_manage_group: '/admin/samfundet/recruitment/:objectPk/permissions/group-manage/:groupId/', admin__samfundet_recruitment_changelist: '/admin/samfundet/recruitment/', admin__samfundet_recruitment_add: '/admin/samfundet/recruitment/add/', admin__samfundet_recruitment_history: '/admin/samfundet/recruitment/:objectId/history/', admin__samfundet_recruitment_delete: '/admin/samfundet/recruitment/:objectId/delete/', admin__samfundet_recruitment_change: '/admin/samfundet/recruitment/:objectId/change/', adminsamfundetrecruitment__objectId: '/admin/samfundet/recruitment/:objectId/', + admin__samfundet_recruitmentposition_permissions: '/admin/samfundet/recruitmentposition/:objectPk/permissions/', + admin__samfundet_recruitmentposition_permissions_manage_user: '/admin/samfundet/recruitmentposition/:objectPk/permissions/user-manage/:userId/', + admin__samfundet_recruitmentposition_permissions_manage_group: '/admin/samfundet/recruitmentposition/:objectPk/permissions/group-manage/:groupId/', admin__samfundet_recruitmentposition_changelist: '/admin/samfundet/recruitmentposition/', admin__samfundet_recruitmentposition_add: '/admin/samfundet/recruitmentposition/add/', admin__samfundet_recruitmentposition_history: '/admin/samfundet/recruitmentposition/:objectId/history/', admin__samfundet_recruitmentposition_delete: '/admin/samfundet/recruitmentposition/:objectId/delete/', admin__samfundet_recruitmentposition_change: '/admin/samfundet/recruitmentposition/:objectId/change/', adminsamfundetrecruitmentposition__objectId: '/admin/samfundet/recruitmentposition/:objectId/', + admin__samfundet_recruitmentadmission_permissions: '/admin/samfundet/recruitmentadmission/:objectPk/permissions/', + admin__samfundet_recruitmentadmission_permissions_manage_user: '/admin/samfundet/recruitmentadmission/:objectPk/permissions/user-manage/:userId/', + admin__samfundet_recruitmentadmission_permissions_manage_group: '/admin/samfundet/recruitmentadmission/:objectPk/permissions/group-manage/:groupId/', + admin__samfundet_recruitmentadmission_changelist: '/admin/samfundet/recruitmentadmission/', + admin__samfundet_recruitmentadmission_add: '/admin/samfundet/recruitmentadmission/add/', + admin__samfundet_recruitmentadmission_history: '/admin/samfundet/recruitmentadmission/:objectId/history/', + admin__samfundet_recruitmentadmission_delete: '/admin/samfundet/recruitmentadmission/:objectId/delete/', + admin__samfundet_recruitmentadmission_change: '/admin/samfundet/recruitmentadmission/:objectId/change/', + adminsamfundetrecruitmentadmission__objectId: '/admin/samfundet/recruitmentadmission/:objectId/', + admin__samfundet_organization_permissions: '/admin/samfundet/organization/:objectPk/permissions/', + admin__samfundet_organization_permissions_manage_user: '/admin/samfundet/organization/:objectPk/permissions/user-manage/:userId/', + admin__samfundet_organization_permissions_manage_group: '/admin/samfundet/organization/:objectPk/permissions/group-manage/:groupId/', admin__samfundet_organization_changelist: '/admin/samfundet/organization/', admin__samfundet_organization_add: '/admin/samfundet/organization/add/', admin__samfundet_organization_history: '/admin/samfundet/organization/:objectId/history/', @@ -385,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/', @@ -412,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; diff --git a/frontend/src/routes/frontend.ts b/frontend/src/routes/frontend.ts index 2aec8600a..098d5b4ce 100644 --- a/frontend/src/routes/frontend.ts +++ b/frontend/src/routes/frontend.ts @@ -15,6 +15,7 @@ export const ROUTES_FRONTEND = { information_page_detail: '/information/:slugField/', saksdokumenter: '/saksdokumenter/', recruitment: '/recruitment/', + recruitment_application: '/recruitment/position/:positionID/', // ==================== // // Sulten // // ==================== // @@ -57,11 +58,13 @@ 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/', admin_recruitment_gang_position_edit: '/control-panel/recruitment/:recruitmentId/gang/:gangId/edit/:positionId', - + admin_recruitment_gang_position_applicants_overview: + '/control-panel/recruitment/:recruitmentId/gang/:gangId/position/:positionId', // ==================== // // Development // // ==================== //
The position id is invalid, please enter another position id
{dbT(recruitmentPosition, 'long_description')}