From 07c1aa68623f17a8a3b116474ff5b5767b134ed2 Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Tue, 7 Nov 2023 22:02:13 +0100 Subject: [PATCH 01/20] get data into frontend --- ...recruitmentadmission_recruiter_priority.py | 18 +++++++++ backend/samfundet/models/recruitment.py | 7 +++- backend/samfundet/serializers.py | 14 +++++++ frontend/src/AppRoutes.tsx | 2 + ...plicantApplicationOverviewPage.module.scss | 0 .../ApplicantApplicationOverviewPage.tsx | 40 +++++++++++++++++++ .../ApplicantApplicationOverviewPage/index.ts | 1 + frontend/src/Pages/index.ts | 3 +- frontend/src/dto.ts | 4 ++ frontend/src/routes/frontend.ts | 2 + 10 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 backend/samfundet/migrations/0041_alter_recruitmentadmission_recruiter_priority.py create mode 100644 frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss create mode 100644 frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx create mode 100644 frontend/src/Pages/ApplicantApplicationOverviewPage/index.ts diff --git a/backend/samfundet/migrations/0041_alter_recruitmentadmission_recruiter_priority.py b/backend/samfundet/migrations/0041_alter_recruitmentadmission_recruiter_priority.py new file mode 100644 index 000000000..4ba1e435f --- /dev/null +++ b/backend/samfundet/migrations/0041_alter_recruitmentadmission_recruiter_priority.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-11-07 20:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('samfundet', '0040_alter_notification_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='recruitmentadmission', + name='recruiter_priority', + field=models.IntegerField(choices=[(0, 'Not Set'), (1, 'Not Wanted'), (2, 'Wanted'), (3, 'Reserve')], default=1, help_text='The priority of the admission'), + ), + ] diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 333ea7cc6..47d426436 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -160,7 +160,7 @@ class RecruitmentAdmission(FullCleanSaveMixin): ] # TODO: Important that the following is not sent along with the rest of the object whenever a user retrieves its admission - recruiter_priority = models.IntegerField(choices=PRIORITY_CHOICES, default=0, help_text='The priority of the admission') + recruiter_priority = models.IntegerField(choices=PRIORITY_CHOICES, help_text='The priority of the admission', default=1) recruiter_status = models.IntegerField(choices=STATUS_CHOICES, default=0, help_text='The status of the admission') @@ -171,6 +171,11 @@ def save(self, *args: tuple, **kwargs: dict) -> None: """ If the admission is saved without an interview, try to find an interview from a shared position. """ + if not self.applicant_priority: + current_applications_count = RecruitmentAdmission.objects.filter(user=self.user).count() + # Set the applicant_priority to the number of applications + 1 (for the current application) + self.applicant_priority = current_applications_count + 1 + if not self.interview: # Check if there is already an interview for the same user in shared positions shared_interview_positions = self.recruitment_position.shared_interview_positions.all() diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 2a1204f8d..5a06c194a 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -524,13 +524,27 @@ class Meta: fields = '__all__' +class ApplicantInterviewSerializer(serializers.ModelSerializer): + + class Meta: + model = Interview + fields = [ + 'id', + 'interview_time', + 'interview_location', + ] + + class RecruitmentAdmissionForApplicantSerializer(serializers.ModelSerializer): + interview = ApplicantInterviewSerializer(read_only=True) class Meta: model = RecruitmentAdmission fields = [ 'admission_text', 'recruitment_position', + 'applicant_priority', + 'interview', ] def create(self, validated_data: dict) -> RecruitmentAdmission: diff --git a/frontend/src/AppRoutes.tsx b/frontend/src/AppRoutes.tsx index 9570877ba..2c4c2494b 100644 --- a/frontend/src/AppRoutes.tsx +++ b/frontend/src/AppRoutes.tsx @@ -3,6 +3,7 @@ import { AboutPage, AdminPage, ApiTestingPage, + ApplicantApplicationOverviewPage, ComponentPage, EventPage, EventsPage, @@ -82,6 +83,7 @@ export function AppRoutes() { } /> } /> } /> + } /> {/* ADMIN ROUTES diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx new file mode 100644 index 000000000..69fd232f4 --- /dev/null +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { Table } from '~/Components/Table'; +import { getRecruitmentAdmissionsForApplicant } from '~/api'; +import { RecruitmentAdmissionDto } from '~/dto'; + +export function ApplicantApplicationOverviewPage() { + const { recruitmentID } = useParams(); + const [admissions, setAdmissions] = useState([]); + + useEffect(() => { + if (recruitmentID) { + getRecruitmentAdmissionsForApplicant(recruitmentID).then((response) => { + setAdmissions(response.data); + }); + } + }, [recruitmentID]); + + useEffect(() => { + console.log(admissions); + }, [admissions]); + + const tableColumns = [ + { sortable: true, content: 'Recruitment Position' }, + { sortable: true, content: 'Interview Date' }, + { sortable: true, content: 'Interview Location' }, + { sortable: true, content: 'Applicant Priority' }, + ]; + + function admissionToTableRow(admission: RecruitmentAdmissionDto) { + return [ + admission.recruitment_position, + admission.interview.interview_time, + admission.interview.interview_location, + admission.applicant_priority, + ]; + } + + return
; +} diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/index.ts b/frontend/src/Pages/ApplicantApplicationOverviewPage/index.ts new file mode 100644 index 000000000..abe5c5db3 --- /dev/null +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/index.ts @@ -0,0 +1 @@ +export { ApplicantApplicationOverviewPage } from './ApplicantApplicationOverviewPage'; diff --git a/frontend/src/Pages/index.ts b/frontend/src/Pages/index.ts index 9811b99e8..b526d8774 100644 --- a/frontend/src/Pages/index.ts +++ b/frontend/src/Pages/index.ts @@ -1,6 +1,7 @@ export { AboutPage } from './AboutPage'; export { AdminPage } from './AdminPage'; export { ApiTestingPage } from './ApiTestingPage'; +export { ApplicantApplicationOverviewPage } from './ApplicantApplicationOverviewPage'; export { ComponentPage } from './ComponentPage'; export { EventPage } from './EventPage'; export { EventsPage } from './EventsPage'; @@ -10,8 +11,8 @@ export { HomePage } from './HomePage'; export { InformationListPage } from './InformationListPage'; export { InformationPage } from './InformationPage'; export { LoginPage } from './LoginPage'; -export { LycheContactPage } from './LycheContactPage'; export { LycheAboutPage } from './LycheAboutPage'; +export { LycheContactPage } from './LycheContactPage'; export { LycheHomePage } from './LycheHomePage'; export { NotFoundPage } from './NotFoundPage'; export { RecruitmentAdmissionFormPage } from './RecruitmentAdmissionFormPage'; diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 3688d5c83..2a69c80f8 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -305,6 +305,10 @@ export type NotificationDto = { // TODO: There are more fields than this. }; +// ############################################################ +// Recruitment +// ############################################################ + export type RecruitmentDto = { id: string | undefined; name_nb: string; diff --git a/frontend/src/routes/frontend.ts b/frontend/src/routes/frontend.ts index 6a573dfdb..3efd3fe82 100644 --- a/frontend/src/routes/frontend.ts +++ b/frontend/src/routes/frontend.ts @@ -14,8 +14,10 @@ export const ROUTES_FRONTEND = { information_page_list: '/information/', information_page_detail: '/information/:slugField/', saksdokumenter: '/saksdokumenter/', + // Recruitment: recruitment: '/recruitment/', recruitment_application: '/recruitment/position/:positionID/', + recruitment_application_overview: '/recruitment/:recruitmentID/my-applications/', // ==================== // // Sulten // // ==================== // From b03e18d082daafc6378e0e790fa7c96fe4248afc Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Wed, 15 Nov 2023 13:37:22 +0100 Subject: [PATCH 02/20] Add communication with backend --- .../0042_alter_recruitmentadmission_id.py | 19 +++++ backend/samfundet/models/recruitment.py | 6 +- backend/samfundet/serializers.py | 22 ++++++ frontend/src/Components/Table/Table.tsx | 5 +- ...plicantApplicationOverviewPage.module.scss | 26 +++++++ .../ApplicantApplicationOverviewPage.tsx | 71 +++++++++++++++++-- frontend/src/api.ts | 17 +++++ frontend/src/dto.ts | 2 +- 8 files changed, 156 insertions(+), 12 deletions(-) create mode 100644 backend/samfundet/migrations/0042_alter_recruitmentadmission_id.py diff --git a/backend/samfundet/migrations/0042_alter_recruitmentadmission_id.py b/backend/samfundet/migrations/0042_alter_recruitmentadmission_id.py new file mode 100644 index 000000000..4f0beeb0b --- /dev/null +++ b/backend/samfundet/migrations/0042_alter_recruitmentadmission_id.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.6 on 2023-11-15 11:32 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('samfundet', '0041_alter_recruitmentadmission_recruiter_priority'), + ] + + operations = [ + migrations.AlterField( + model_name='recruitmentadmission', + name='id', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + ] diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 47d426436..553f9bc83 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -1,11 +1,11 @@ # # This file contains models spesific to the recruitment system # - from __future__ import annotations +import uuid + from django.core.exceptions import ValidationError from django.utils import timezone - from django.db import models from root.utils.mixins import FullCleanSaveMixin @@ -133,6 +133,8 @@ class Interview(FullCleanSaveMixin): class RecruitmentAdmission(FullCleanSaveMixin): + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) admission_text = models.TextField(help_text='Admission text for the admission') recruitment_position = models.ForeignKey( RecruitmentPosition, on_delete=models.CASCADE, help_text='The recruitment position that is recruiting', related_name='admissions' diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 5a06c194a..198a998f0 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -535,12 +535,34 @@ class Meta: ] +class RecruitmentPositionForApplicantSerializer(serializers.ModelSerializer): + + class Meta: + model = RecruitmentPosition + fields = [ + 'id', + 'name_nb', + 'name_en', + 'short_description_nb', + 'short_description_en', + 'long_description_nb', + 'long_description_en', + 'is_funksjonaer_position', + 'default_admission_letter_nb', + 'default_admission_letter_en', + 'gang', + 'recruitment', + ] + + class RecruitmentAdmissionForApplicantSerializer(serializers.ModelSerializer): interview = ApplicantInterviewSerializer(read_only=True) + recruitment_position = RecruitmentPositionForApplicantSerializer(read_only=True) class Meta: model = RecruitmentAdmission fields = [ + 'id', 'admission_text', 'recruitment_position', 'applicant_priority', diff --git a/frontend/src/Components/Table/Table.tsx b/frontend/src/Components/Table/Table.tsx index 5ba9762e8..9361f6612 100644 --- a/frontend/src/Components/Table/Table.tsx +++ b/frontend/src/Components/Table/Table.tsx @@ -32,10 +32,11 @@ type TableProps = { // Data can either be a table cell with separated value and content, or just the raw value // For instance ["a", "b"] or [ {value: "a", content:
a
}, {value: "b", content:
b
} ] data: TableDataType; + defaultSortColumn?: number; }; -export function Table({ className, columns, data }: TableProps) { - const [sortColumn, setSortColumn] = useState(-1); +export function Table({ className, columns, data, defaultSortColumn }: TableProps) { + const [sortColumn, setSortColumn] = useState(defaultSortColumn || -1); const [sortInverse, setSortInverse] = useState(false); function sort(column: number) { diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss index e69de29bb..7c8a1cc89 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss @@ -0,0 +1,26 @@ +$back-button-width: 5rem; + +.container { + padding: 2rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.header { + font-size: 2em; + font-weight: 700; +} + +.back_button { + max-width: $back-button-width; +} + +.top_container { + display: flex; + justify-content: space-between; +} + +.empty_div { + width: $back-button-width; +} diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx index 69fd232f4..dc468efbb 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx @@ -1,12 +1,49 @@ +import { Icon } from '@iconify/react'; import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import { Button, Page } from '~/Components'; import { Table } from '~/Components/Table'; -import { getRecruitmentAdmissionsForApplicant } from '~/api'; +import { getRecruitmentAdmissionsForApplicant, putRecruitmentAdmission } from '~/api'; import { RecruitmentAdmissionDto } from '~/dto'; +import { KEY } from '~/i18n/constants'; +import { ROUTES } from '~/routes'; +import { dbT } from '~/utils'; +import styles from './ApplicantApplicationOverviewPage.module.scss'; export function ApplicantApplicationOverviewPage() { const { recruitmentID } = useParams(); const [admissions, setAdmissions] = useState([]); + const { t } = useTranslation(); + + function handleChangePriority(id: number, direction: 'up' | 'down') { + const index = admissions.findIndex((admission) => admission.id === id); + if (index < 0) return; // ID not found + + const targetIndex = direction === 'up' ? index - 1 : index + 1; + if (targetIndex < 0 || targetIndex >= admissions.length) return; + + const newAdmissions = [...admissions]; + const temp = newAdmissions[index].applicant_priority; + + // Swap priorities + newAdmissions[index].applicant_priority = newAdmissions[targetIndex].applicant_priority; + newAdmissions[targetIndex].applicant_priority = temp; + + // Update database + putRecruitmentAdmission(newAdmissions[index]); + putRecruitmentAdmission(newAdmissions[targetIndex]); + setAdmissions(newAdmissions); + } + + function upDownArrow(id: number) { + return ( + <> + handleChangePriority(id, 'up')} /> + handleChangePriority(id, 'down')} /> + + ); + } useEffect(() => { if (recruitmentID) { @@ -21,20 +58,40 @@ export function ApplicantApplicationOverviewPage() { }, [admissions]); const tableColumns = [ - { sortable: true, content: 'Recruitment Position' }, - { sortable: true, content: 'Interview Date' }, - { sortable: true, content: 'Interview Location' }, - { sortable: true, content: 'Applicant Priority' }, + { sortable: false, content: 'Recruitment Position' }, + { sortable: false, content: 'Interview Date' }, + { sortable: false, content: 'Interview Location' }, + { sortable: false, content: 'Applicant Priority' }, + { sortable: false, content: '' }, ]; function admissionToTableRow(admission: RecruitmentAdmissionDto) { return [ - admission.recruitment_position, + dbT(admission.recruitment_position, 'name'), admission.interview.interview_time, admission.interview.interview_location, admission.applicant_priority, + { content: upDownArrow(admission.id) }, ]; } - return
; + return ( + +
+
+ +

My applications

+
+
+

All info related to the applications will be anonymized three weeks after the recruitment is over

+ {admissions ? ( +
+ ) : ( +

You have not applied to any positions yet

+ )} +
+
+ ); } diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1200aab1c..313eb3d8f 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -649,3 +649,20 @@ export async function postRecruitmentAdmission(admission: Partial): Promise { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_admissions_for_applicant_detail, + urlParams: { pk: admission.id }, + }); + const data = { + id: admission.id, + admission_text: admission.admission_text, + applicant_priority: admission.applicant_priority, + }; + const response = await axios.put(url, data, { withCredentials: true }); + + return response; +} diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 2a69c80f8..9db7a7b67 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -357,7 +357,7 @@ export type RecruitmentAdmissionDto = { id: number; interview: InterviewDto; admission_text: string; - recruitment_position?: number; + recruitment_position?: RecruitmentPositionDto; recruitment: number; user: UserDto; applicant_priority: number; From aac9c49691d4f05085215100c7cd7a57645edcf0 Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Wed, 15 Nov 2023 13:45:06 +0100 Subject: [PATCH 03/20] fix sorting --- .../ApplicantApplicationOverviewPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx index dc468efbb..a4f70744f 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx @@ -61,7 +61,7 @@ export function ApplicantApplicationOverviewPage() { { sortable: false, content: 'Recruitment Position' }, { sortable: false, content: 'Interview Date' }, { sortable: false, content: 'Interview Location' }, - { sortable: false, content: 'Applicant Priority' }, + { sortable: true, content: 'Applicant Priority' }, { sortable: false, content: '' }, ]; From 7880b4a67d43e351f2927210966bc1c2a5009e8b Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Wed, 15 Nov 2023 13:47:31 +0100 Subject: [PATCH 04/20] improve position seed script --- .../commands/seed_scripts/recruitment_position.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/root/management/commands/seed_scripts/recruitment_position.py b/backend/root/management/commands/seed_scripts/recruitment_position.py index 29f0ac56e..2c82c9739 100644 --- a/backend/root/management/commands/seed_scripts/recruitment_position.py +++ b/backend/root/management/commands/seed_scripts/recruitment_position.py @@ -26,12 +26,14 @@ def seed(): for recruitment_index, recruitment in enumerate(recruitments): for i in range(2): # Create 2 instances for each gang and recruitment position_data = POSITION_DATA.copy() - position_data.update({ - 'name_nb': f'Stilling {i}', - 'name_en': f'Position {i}', - 'gang': gang, - 'recruitment': recruitment, - }) + position_data.update( + { + 'name_nb': f'{gang.abbreviation} stilling {i}', + 'name_en': f'{gang.abbreviation} position {i}', + 'gang': gang, + 'recruitment': recruitment, + } + ) position, created = RecruitmentPosition.objects.get_or_create(**position_data) if created: From 68ec617cee377ab516efb8327c89e3573eec8773 Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Wed, 15 Nov 2023 14:11:31 +0100 Subject: [PATCH 05/20] fix priority --- .../ApplicantApplicationOverviewPage.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx index a4f70744f..8174626c9 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx @@ -17,23 +17,29 @@ export function ApplicantApplicationOverviewPage() { const { t } = useTranslation(); function handleChangePriority(id: number, direction: 'up' | 'down') { - const index = admissions.findIndex((admission) => admission.id === id); - if (index < 0) return; // ID not found + const newAdmissions = [...admissions]; + const index = newAdmissions.findIndex((admission) => admission.id === id); + const directionIncrement = direction === 'up' ? -1 : 1; + if (newAdmissions[index].applicant_priority === 1 && direction === 'up') return; + if (newAdmissions[index].applicant_priority === newAdmissions.length && direction === 'down') return; + const targetIndex = newAdmissions.findIndex( + (admission) => admission.applicant_priority === newAdmissions[index].applicant_priority + directionIncrement, + ); - const targetIndex = direction === 'up' ? index - 1 : index + 1; - if (targetIndex < 0 || targetIndex >= admissions.length) return; + const old_priority = newAdmissions[index].applicant_priority; + const new_priority = newAdmissions[targetIndex].applicant_priority; - const newAdmissions = [...admissions]; - const temp = newAdmissions[index].applicant_priority; + console.log('old priority', old_priority); + console.log('new priority', new_priority); - // Swap priorities - newAdmissions[index].applicant_priority = newAdmissions[targetIndex].applicant_priority; - newAdmissions[targetIndex].applicant_priority = temp; + newAdmissions[index].applicant_priority = new_priority; + newAdmissions[targetIndex].applicant_priority = old_priority; - // Update database + // TODO: Make this a single API call putRecruitmentAdmission(newAdmissions[index]); - putRecruitmentAdmission(newAdmissions[targetIndex]); - setAdmissions(newAdmissions); + putRecruitmentAdmission(newAdmissions[targetIndex]).then(() => { + setAdmissions(newAdmissions); + }); } function upDownArrow(id: number) { From b1688a27e8a75e6c58ec26a3163b77514741f043 Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Thu, 16 Nov 2023 13:08:59 +0100 Subject: [PATCH 06/20] fix tests, and move choices to global file --- ...alter_recruitmentadmission_id_and_more.py} | 7 ++++++- backend/samfundet/model_choices.py | 15 ++++++++++++++ backend/samfundet/models/recruitment.py | 20 +++---------------- backend/samfundet/tests/test_views.py | 2 +- frontend/src/Components/Table/Table.tsx | 4 ++-- .../RecruitmentAdmissionFormPage.tsx | 2 +- frontend/src/dto.ts | 2 +- 7 files changed, 29 insertions(+), 23 deletions(-) rename backend/samfundet/migrations/{0042_alter_recruitmentadmission_id.py => 0042_alter_recruitmentadmission_id_and_more.py} (54%) create mode 100644 backend/samfundet/model_choices.py diff --git a/backend/samfundet/migrations/0042_alter_recruitmentadmission_id.py b/backend/samfundet/migrations/0042_alter_recruitmentadmission_id_and_more.py similarity index 54% rename from backend/samfundet/migrations/0042_alter_recruitmentadmission_id.py rename to backend/samfundet/migrations/0042_alter_recruitmentadmission_id_and_more.py index 4f0beeb0b..1ee64a30e 100644 --- a/backend/samfundet/migrations/0042_alter_recruitmentadmission_id.py +++ b/backend/samfundet/migrations/0042_alter_recruitmentadmission_id_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.6 on 2023-11-15 11:32 +# Generated by Django 4.2.6 on 2023-11-16 12:05 from django.db import migrations, models import uuid @@ -16,4 +16,9 @@ class Migration(migrations.Migration): name='id', field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), ), + migrations.AlterField( + model_name='recruitmentadmission', + name='recruiter_status', + field=models.IntegerField(choices=[(0, 'Not Set'), (1, 'Called and Accepted'), (2, 'Called and Rejected'), (3, 'Automatic Rejection')], default=0, help_text='The status of the admission'), + ), ] diff --git a/backend/samfundet/model_choices.py b/backend/samfundet/model_choices.py new file mode 100644 index 000000000..419f18e06 --- /dev/null +++ b/backend/samfundet/model_choices.py @@ -0,0 +1,15 @@ +from django.db import models + + +class RecruitmentPriorityChoices(models.IntegerChoices): + NOT_SET = 0, 'Not Set' + NOT_WANTED = 1, 'Not Wanted' + WANTED = 2, 'Wanted' + RESERVE = 3, 'Reserve' + + +class RecruitmentStatusChoices(models.IntegerChoices): + NOT_SET = 0, 'Not Set' + CALLED_AND_ACCEPTED = 1, 'Called and Accepted' + CALLED_AND_REJECTED = 2, 'Called and Rejected' + AUTOMATIC_REJECTION = 3, 'Automatic Rejection' diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 553f9bc83..dac792519 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError from django.utils import timezone from django.db import models +from samfundet.model_choices import RecruitmentPriorityChoices, RecruitmentStatusChoices from root.utils.mixins import FullCleanSaveMixin from .general import Organization, User, Gang @@ -147,24 +148,9 @@ class RecruitmentAdmission(FullCleanSaveMixin): Interview, on_delete=models.SET_NULL, null=True, blank=True, help_text='The interview for the admission', related_name='admissions' ) - PRIORITY_CHOICES = [ - (0, 'Not Set'), - (1, 'Not Wanted'), - (2, 'Wanted'), - (3, 'Reserve'), - ] + recruiter_priority = models.IntegerField(choices=RecruitmentPriorityChoices.choices, help_text='The priority of the admission', default=1) - STATUS_CHOICES = [ - (0, 'Nothing'), - (1, 'Called and Accepted'), - (2, 'Called and Rejected'), - (3, 'Automatic Rejection'), - ] - - # TODO: Important that the following is not sent along with the rest of the object whenever a user retrieves its admission - recruiter_priority = models.IntegerField(choices=PRIORITY_CHOICES, help_text='The priority of the admission', default=1) - - recruiter_status = models.IntegerField(choices=STATUS_CHOICES, default=0, help_text='The status of the admission') + recruiter_status = models.IntegerField(choices=RecruitmentStatusChoices.choices, default=0, help_text='The status of the admission') def __str__(self) -> str: return f'Admission: {self.user} for {self.recruitment_position} in {self.recruitment}' diff --git a/backend/samfundet/tests/test_views.py b/backend/samfundet/tests/test_views.py index 8bc0940fc..8fe3dda38 100644 --- a/backend/samfundet/tests/test_views.py +++ b/backend/samfundet/tests/test_views.py @@ -613,4 +613,4 @@ def test_recruitment_admission_for_applicant( # Assert the returned data based on the logic in the view assert len(response.data) == 1 assert (response.data[0]['admission_text'] == fixture_recruitment_admission.admission_text) - assert (response.data[0]['recruitment_position'] == fixture_recruitment_admission.recruitment_position.id) + assert (response.data[0]['recruitment_position']['id'] == fixture_recruitment_admission.recruitment_position.id) diff --git a/frontend/src/Components/Table/Table.tsx b/frontend/src/Components/Table/Table.tsx index 9361f6612..123e67411 100644 --- a/frontend/src/Components/Table/Table.tsx +++ b/frontend/src/Components/Table/Table.tsx @@ -35,8 +35,8 @@ type TableProps = { defaultSortColumn?: number; }; -export function Table({ className, columns, data, defaultSortColumn }: TableProps) { - const [sortColumn, setSortColumn] = useState(defaultSortColumn || -1); +export function Table({ className, columns, data, defaultSortColumn = -1 }: TableProps) { + const [sortColumn, setSortColumn] = useState(defaultSortColumn); const [sortInverse, setSortInverse] = useState(false); function sort(column: number) { diff --git a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx index 96d37cf58..1185532b1 100644 --- a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx +++ b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx @@ -29,7 +29,7 @@ export function RecruitmentAdmissionFormPage() { }, []); function handleOnSubmit(data: RecruitmentAdmissionDto) { - data.recruitment_position = positionID ? +positionID : 1; + data.recruitment_position.id = positionID ? positionID : '1'; postRecruitmentAdmission(data) .then(() => { navigate({ url: ROUTES.frontend.home }); diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 9db7a7b67..24dd7615a 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -357,7 +357,7 @@ export type RecruitmentAdmissionDto = { id: number; interview: InterviewDto; admission_text: string; - recruitment_position?: RecruitmentPositionDto; + recruitment_position: RecruitmentPositionDto; recruitment: number; user: UserDto; applicant_priority: number; From 427963ee8e91107b98ab7152a65665a95ec74a42 Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Thu, 16 Nov 2023 13:43:51 +0100 Subject: [PATCH 07/20] cleanup --- ..._alter_recruitmentadmission_id_and_more.py} | 4 ++-- ..._recruitmentadmission_recruiter_priority.py | 18 ------------------ backend/samfundet/models/recruitment.py | 8 ++++++-- 3 files changed, 8 insertions(+), 22 deletions(-) rename backend/samfundet/migrations/{0042_alter_recruitmentadmission_id_and_more.py => 0041_alter_recruitmentadmission_id_and_more.py} (84%) delete mode 100644 backend/samfundet/migrations/0041_alter_recruitmentadmission_recruiter_priority.py diff --git a/backend/samfundet/migrations/0042_alter_recruitmentadmission_id_and_more.py b/backend/samfundet/migrations/0041_alter_recruitmentadmission_id_and_more.py similarity index 84% rename from backend/samfundet/migrations/0042_alter_recruitmentadmission_id_and_more.py rename to backend/samfundet/migrations/0041_alter_recruitmentadmission_id_and_more.py index 1ee64a30e..70c6f14dd 100644 --- a/backend/samfundet/migrations/0042_alter_recruitmentadmission_id_and_more.py +++ b/backend/samfundet/migrations/0041_alter_recruitmentadmission_id_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.6 on 2023-11-16 12:05 +# Generated by Django 4.2.6 on 2023-11-16 12:43 from django.db import migrations, models import uuid @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('samfundet', '0041_alter_recruitmentadmission_recruiter_priority'), + ('samfundet', '0040_alter_notification_options_and_more'), ] operations = [ diff --git a/backend/samfundet/migrations/0041_alter_recruitmentadmission_recruiter_priority.py b/backend/samfundet/migrations/0041_alter_recruitmentadmission_recruiter_priority.py deleted file mode 100644 index 4ba1e435f..000000000 --- a/backend/samfundet/migrations/0041_alter_recruitmentadmission_recruiter_priority.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.6 on 2023-11-07 20:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('samfundet', '0040_alter_notification_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='recruitmentadmission', - name='recruiter_priority', - field=models.IntegerField(choices=[(0, 'Not Set'), (1, 'Not Wanted'), (2, 'Wanted'), (3, 'Reserve')], default=1, help_text='The priority of the admission'), - ), - ] diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index dac792519..1ee274d25 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -148,9 +148,13 @@ class RecruitmentAdmission(FullCleanSaveMixin): Interview, on_delete=models.SET_NULL, null=True, blank=True, help_text='The interview for the admission', related_name='admissions' ) - recruiter_priority = models.IntegerField(choices=RecruitmentPriorityChoices.choices, help_text='The priority of the admission', default=1) + recruiter_priority = models.IntegerField( + choices=RecruitmentPriorityChoices.choices, help_text='The priority of the admission', default=RecruitmentPriorityChoices.NOT_SET + ) - recruiter_status = models.IntegerField(choices=RecruitmentStatusChoices.choices, default=0, help_text='The status of the admission') + recruiter_status = models.IntegerField( + choices=RecruitmentStatusChoices.choices, default=RecruitmentStatusChoices.NOT_SET, help_text='The status of the admission' + ) def __str__(self) -> str: return f'Admission: {self.user} for {self.recruitment_position} in {self.recruitment}' From 457f4aee4ec8ff46cffb74555414e9d1455940db Mon Sep 17 00:00:00 2001 From: Mathias Aas Date: Thu, 16 Nov 2023 13:56:19 +0100 Subject: [PATCH 08/20] Fix tests, improve error handling --- backend/samfundet/conftest.py | 5 ++- .../RecruitmentAdmissionFormPage.tsx | 40 +++++++++++++------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index 56c0affbc..333c466bb 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -7,6 +7,7 @@ from django.test import Client from rest_framework.test import APIClient from django.contrib.auth.models import Group +from samfundet.model_choices import RecruitmentPriorityChoices, RecruitmentStatusChoices from root.settings import BASE_DIR from samfundet.contants import DEV_PASSWORD @@ -273,8 +274,8 @@ def fixture_recruitment_admission(fixture_user: User, fixture_recruitment_positi recruitment=fixture_recruitment, user=fixture_user, applicant_priority=1, - recruiter_priority=RecruitmentAdmission.PRIORITY_CHOICES[0][0], - recruiter_status=RecruitmentAdmission.STATUS_CHOICES[0][0], + recruiter_priority=RecruitmentPriorityChoices.NOT_SET, + recruiter_status=RecruitmentStatusChoices.NOT_SET ) yield admission admission.delete() diff --git a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx index 1185532b1..cb9b7cd0b 100644 --- a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx +++ b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx @@ -22,22 +22,36 @@ export function RecruitmentAdmissionFormPage() { const { positionID, id } = useParams(); useEffect(() => { - getRecruitmentPosition('1').then((res) => { - setRecruitmentPosition(res.data); + if (positionID && !isNaN(Number(positionID))) { + getRecruitmentPosition(positionID) + .then((res) => { + setRecruitmentPosition(res.data); + setLoading(false); + }) + .catch(() => { + // Handle the case where the positionID is invalid or the request fails + toast.error(t(KEY.common_something_went_wrong)); + navigate({ url: ROUTES.frontend.home }); + }); + } else { setLoading(false); - }); - }, []); + } + }, [positionID, navigate, t]); function handleOnSubmit(data: RecruitmentAdmissionDto) { - data.recruitment_position.id = positionID ? positionID : '1'; - postRecruitmentAdmission(data) - .then(() => { - navigate({ url: ROUTES.frontend.home }); - toast.success(t(KEY.common_creation_successful)); - }) - .catch(() => { - toast.error(t(KEY.common_something_went_wrong)); - }); + if (positionID && !isNaN(Number(positionID))) { + data.recruitment_position.id = positionID; + postRecruitmentAdmission(data) + .then(() => { + navigate({ url: ROUTES.frontend.home }); + toast.success(t(KEY.common_creation_successful)); + }) + .catch(() => { + toast.error(t(KEY.common_something_went_wrong)); + }); + } else { + toast.error(t(KEY.common_something_went_wrong)); + } } if (loading) { From 6053abdc9a00fcfa80a529342df36338a2e087ef Mon Sep 17 00:00:00 2001 From: magsyg Date: Wed, 20 Dec 2023 22:33:36 +0100 Subject: [PATCH 09/20] fix migration --- ..._alter_recruitmentadmission_id_and_more.py | 34 +++++++++++++++++++ ..._alter_recruitmentadmission_id_and_more.py | 24 ------------- 2 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py delete mode 100644 backend/samfundet/migrations/0041_alter_recruitmentadmission_id_and_more.py diff --git a/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py b/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py new file mode 100644 index 000000000..85e7ff65e --- /dev/null +++ b/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0 on 2023-12-20 21:31 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("samfundet", "0002_alter_notification_options_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="recruitmentadmission", + name="id", + field=models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name="recruitmentadmission", + name="recruiter_status", + field=models.IntegerField( + choices=[ + (0, "Not Set"), + (1, "Called and Accepted"), + (2, "Called and Rejected"), + (3, "Automatic Rejection"), + ], + default=0, + help_text="The status of the admission", + ), + ), + ] diff --git a/backend/samfundet/migrations/0041_alter_recruitmentadmission_id_and_more.py b/backend/samfundet/migrations/0041_alter_recruitmentadmission_id_and_more.py deleted file mode 100644 index 70c6f14dd..000000000 --- a/backend/samfundet/migrations/0041_alter_recruitmentadmission_id_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.6 on 2023-11-16 12:43 - -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('samfundet', '0040_alter_notification_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='recruitmentadmission', - name='id', - field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='recruitmentadmission', - name='recruiter_status', - field=models.IntegerField(choices=[(0, 'Not Set'), (1, 'Called and Accepted'), (2, 'Called and Rejected'), (3, 'Automatic Rejection')], default=0, help_text='The status of the admission'), - ), - ] From 2a5f30e35a38f297dab21394ccad9d67ea8082ea Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 19:04:41 +0100 Subject: [PATCH 10/20] finalize view own applications --- backend/root/custom_classes/admin_classes.py | 4 +- backend/root/custom_classes/middlewares.py | 1 + .../root/custom_classes/permission_classes.py | 1 + backend/root/db_router.py | 1 + .../root/management/commands/cleanstart.py | 1 + .../management/commands/deletemigrations.py | 1 + .../management/commands/deploymigrations.py | 1 + .../commands/generate_permissions.py | 1 - .../root/management/commands/migratezero.py | 1 + backend/root/management/commands/seed.py | 3 +- .../commands/seed_scripts/events.py | 3 +- .../management/commands/seed_scripts/gangs.py | 32 +-- .../commands/seed_scripts/images.py | 12 +- .../management/commands/seed_scripts/menu.py | 11 +- .../seed_scripts/recruitment_admissions.py | 3 +- .../management/commands/seed_scripts/samf3.py | 4 +- .../commands/seed_scripts/textitems.py | 18 +- .../management/commands/setupmigrations.py | 1 + .../management/commands/setupmigrations2.py | 1 + backend/root/settings/__init__.py | 3 +- backend/root/settings/base.py | 199 +++++++++--------- backend/root/settings/prod.py | 34 +-- backend/root/utils/mixins.py | 4 +- backend/samfundet/admin.py | 3 - backend/samfundet/conftest.py | 2 - backend/samfundet/homepage/homepage.py | 38 ++-- ..._alter_recruitmentadmission_id_and_more.py | 34 --- .../0008_alter_recruitmentadmission_id.py | 20 ++ backend/samfundet/models/general.py | 1 + backend/samfundet/models/recruitment.py | 1 - .../models/tests/test_recruitment.py | 2 + .../models/tests/test_reservation.py | 1 + backend/samfundet/models/utils/fields.py | 2 + backend/samfundet/serializers.py | 31 ++- backend/samfundet/tests/test_signals.py | 2 + backend/samfundet/tests/test_views.py | 5 + backend/samfundet/utils.py | 11 +- backend/samfundet/views.py | 44 ++-- ...plicantApplicationOverviewPage.module.scss | 11 + .../ApplicantApplicationOverviewPage.tsx | 33 +-- .../RecruitmentPage.module.scss | 5 + .../Pages/RecruitmentPage/RecruitmentPage.tsx | 23 +- frontend/src/Pages/index.ts | 1 - frontend/src/utils.ts | 15 ++ 44 files changed, 355 insertions(+), 270 deletions(-) delete mode 100644 backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py create mode 100644 backend/samfundet/migrations/0008_alter_recruitmentadmission_id.py diff --git a/backend/root/custom_classes/admin_classes.py b/backend/root/custom_classes/admin_classes.py index 8ba2af927..80906cf14 100644 --- a/backend/root/custom_classes/admin_classes.py +++ b/backend/root/custom_classes/admin_classes.py @@ -54,7 +54,7 @@ def user_link(self, obj: Contact) -> str: def get_admin_url(*, obj: Any) -> str: """https://stackoverflow.com/questions/10420271/django-how-to-get-admin-url-from-model-instance""" info = (obj._meta.app_label, obj._meta.model_name) - admin_url = reverse('admin:%s_%s_change' % info, args=(obj.pk,)) + admin_url = reverse('admin:%s_%s_change' % info, args=(obj.pk, )) return admin_url @@ -258,7 +258,7 @@ def _insert_link(*, field: str, related_links: list[str]) -> str: def autocomplete_filter(**kwargs: Any) -> AutocompleteFilter: """Simple AutocompleteFilter factory.""" - return type('AutocompleteFilter', (AutocompleteFilter,), kwargs) + return type('AutocompleteFilter', (AutocompleteFilter, ), kwargs) class CustomGuardedUserAdmin(CustomGuardedModelAdmin, UserAdmin): diff --git a/backend/root/custom_classes/middlewares.py b/backend/root/custom_classes/middlewares.py index c208d3118..c894b34a7 100644 --- a/backend/root/custom_classes/middlewares.py +++ b/backend/root/custom_classes/middlewares.py @@ -53,6 +53,7 @@ def process_exception(self, request: HttpRequest, exception: Exception) -> None: class ImpersonateUserMiddleware: + def __init__(self, get_response: HttpResponse) -> None: self.get_response = get_response diff --git a/backend/root/custom_classes/permission_classes.py b/backend/root/custom_classes/permission_classes.py index 70f987f6c..f72892103 100644 --- a/backend/root/custom_classes/permission_classes.py +++ b/backend/root/custom_classes/permission_classes.py @@ -14,6 +14,7 @@ class SuperUserPermission(BasePermission): + def has_permission(self, request: Request, view: APIView) -> bool: # noqa: PLR0917 user: User = request.user return user.is_active and user.is_superuser diff --git a/backend/root/db_router.py b/backend/root/db_router.py index 9fb9a8268..4ee9f6db9 100644 --- a/backend/root/db_router.py +++ b/backend/root/db_router.py @@ -23,6 +23,7 @@ class SamfundetDatabaseRouter: + def db_for_read(self, model: Type[models.Model], **hints: dict[str, Any]) -> str | None: if model in BILLIG_MODELS: return 'billig' diff --git a/backend/root/management/commands/cleanstart.py b/backend/root/management/commands/cleanstart.py index b653bff92..d48b2014b 100644 --- a/backend/root/management/commands/cleanstart.py +++ b/backend/root/management/commands/cleanstart.py @@ -8,6 +8,7 @@ class Command(BaseCommand): + def add_arguments(self, parser): parser.add_argument( '--noinput', diff --git a/backend/root/management/commands/deletemigrations.py b/backend/root/management/commands/deletemigrations.py index 86b1e3e85..10f76bbda 100644 --- a/backend/root/management/commands/deletemigrations.py +++ b/backend/root/management/commands/deletemigrations.py @@ -12,6 +12,7 @@ class Command(BaseCommand): + def add_arguments(self, parser): parser.add_argument( '--noinput', diff --git a/backend/root/management/commands/deploymigrations.py b/backend/root/management/commands/deploymigrations.py index 18ec99279..4e58d35b4 100644 --- a/backend/root/management/commands/deploymigrations.py +++ b/backend/root/management/commands/deploymigrations.py @@ -8,6 +8,7 @@ class Command(BaseCommand): + def add_arguments(self, parser): parser.add_argument( '--noinput', diff --git a/backend/root/management/commands/generate_permissions.py b/backend/root/management/commands/generate_permissions.py index f8ddfbb70..55a0ce921 100644 --- a/backend/root/management/commands/generate_permissions.py +++ b/backend/root/management/commands/generate_permissions.py @@ -4,7 +4,6 @@ from django.utils import timezone from django.contrib.auth.models import Permission from django.core.management.base import BaseCommand - """ NOTE: This command cannot run within docker container because the backend has no access to the frontend. Use on host machine. diff --git a/backend/root/management/commands/migratezero.py b/backend/root/management/commands/migratezero.py index d404bc231..07d401f3b 100644 --- a/backend/root/management/commands/migratezero.py +++ b/backend/root/management/commands/migratezero.py @@ -9,6 +9,7 @@ class Command(BaseCommand): + def add_arguments(self, parser): parser.add_argument( '--noinput', diff --git a/backend/root/management/commands/seed.py b/backend/root/management/commands/seed.py index b724bcb62..4202c7f98 100755 --- a/backend/root/management/commands/seed.py +++ b/backend/root/management/commands/seed.py @@ -56,7 +56,8 @@ def run_seed_script(*, target: tuple, index: int, count: int): elif type(step) in [int, float]: print_progress(progress=step, prefix=prefix, start_time=start_time) else: - raise Exception(f"Seed script {target[0]} yielded wrong type '{type(step)}', " 'expected number type or tuple of (number, str)') + raise Exception(f"Seed script {target[0]} yielded wrong type '{type(step)}', " + 'expected number type or tuple of (number, str)') # Final output 100%. if isinstance(step, tuple): diff --git a/backend/root/management/commands/seed_scripts/events.py b/backend/root/management/commands/seed_scripts/events.py index 10e9176fc..77220be37 100755 --- a/backend/root/management/commands/seed_scripts/events.py +++ b/backend/root/management/commands/seed_scripts/events.py @@ -150,8 +150,7 @@ def do_seed(): name_nb=f'Billett {i + 1}', name_en=f'Ticket {i + 1}', price=random.randint(50, 200), - ) - for i in range(0, random.randint(2, 4)) + ) for i in range(0, random.randint(2, 4)) ] # Create event(s) diff --git a/backend/root/management/commands/seed_scripts/gangs.py b/backend/root/management/commands/seed_scripts/gangs.py index 0810c1644..0b601ba8c 100755 --- a/backend/root/management/commands/seed_scripts/gangs.py +++ b/backend/root/management/commands/seed_scripts/gangs.py @@ -9,21 +9,23 @@ ('Lørdagskomiteen', 'LØK'), ('Klubbstyret', 'KLST'), ], - 'Drift': [ - ('Markedsføringsgjengen', 'MG'), - ('Fotogjengen', 'FG'), - ('Diversegjengen', 'DG'), - ('Forsterkerkomiteen', 'FK'), - ('Regi', None), - ('Videokomiteen', 'VK'), - ], - 'Kunstneriske': [ - ('Studentersamfundets interne teater', 'SIT'), - ('Studentersamfundets Symfoniorkester', 'Symforch'), - ('Strindens promenadeorkester', 'SPO'), - ('Pirum', None), - ('Candiss', None), - ], + 'Drift': + [ + ('Markedsføringsgjengen', 'MG'), + ('Fotogjengen', 'FG'), + ('Diversegjengen', 'DG'), + ('Forsterkerkomiteen', 'FK'), + ('Regi', None), + ('Videokomiteen', 'VK'), + ], + 'Kunstneriske': + [ + ('Studentersamfundets interne teater', 'SIT'), + ('Studentersamfundets Symfoniorkester', 'Symforch'), + ('Strindens promenadeorkester', 'SPO'), + ('Pirum', None), + ('Candiss', None), + ], 'Styrende': [ ('Finansstyret', 'FS'), ('Styret', None), diff --git a/backend/root/management/commands/seed_scripts/images.py b/backend/root/management/commands/seed_scripts/images.py index 727ccfbc6..5b148d88c 100644 --- a/backend/root/management/commands/seed_scripts/images.py +++ b/backend/root/management/commands/seed_scripts/images.py @@ -40,14 +40,10 @@ def do_seed(): random_image = ImageFile(image_file, name=f'img_{i}') title = words(random.randint(1, 2)) image = Image.objects.create(title=title, image=random_image) - image.tags.set( - random.choices( - Tag.objects.all().values_list( - flat=True, - ), - k=random.randint(1, 4), - ) - ) + image.tags.set(random.choices( + Tag.objects.all().values_list(flat=True, ), + k=random.randint(1, 4), + )) yield int(i / COUNT * 100), 'Creating images' # Remember to close files! diff --git a/backend/root/management/commands/seed_scripts/menu.py b/backend/root/management/commands/seed_scripts/menu.py index a6159a024..30469418c 100755 --- a/backend/root/management/commands/seed_scripts/menu.py +++ b/backend/root/management/commands/seed_scripts/menu.py @@ -38,13 +38,10 @@ def seed(): FoodPreference.objects.all().delete() # Create food preferences - prefs = [ - FoodPreference.objects.create( - name_nb=p_name[0], - name_en=p_name[1], - ) - for p_name in preferences - ] + prefs = [FoodPreference.objects.create( + name_nb=p_name[0], + name_en=p_name[1], + ) for p_name in preferences] yield 10, f'Created {len(preferences)} food preferences' # Create menu categories diff --git a/backend/root/management/commands/seed_scripts/recruitment_admissions.py b/backend/root/management/commands/seed_scripts/recruitment_admissions.py index d777be46d..ac1ab854a 100644 --- a/backend/root/management/commands/seed_scripts/recruitment_admissions.py +++ b/backend/root/management/commands/seed_scripts/recruitment_admissions.py @@ -27,7 +27,8 @@ def seed(): { 'recruitment_position': position, 'recruitment': position.recruitment, - 'user': users[randint(0, len(users) - 1)], # random user from all users + 'user': users[randint(0, + len(users) - 1)], # random user from all users } ) _admission, created = RecruitmentAdmission.objects.get_or_create(**admission_data) diff --git a/backend/root/management/commands/seed_scripts/samf3.py b/backend/root/management/commands/seed_scripts/samf3.py index 9232d864f..715d226ec 100755 --- a/backend/root/management/commands/seed_scripts/samf3.py +++ b/backend/root/management/commands/seed_scripts/samf3.py @@ -123,7 +123,7 @@ def seed() -> Iterator[tuple[int, str]]: with open(event_path, 'r') as event_file: with open(image_path, 'r') as image_file: events = list(reversed(list(csv.DictReader(event_file)))) - events = events[0 : min(max_events, len(events))] + events = events[0:min(max_events, len(events))] images = list(csv.DictReader(image_file)) event_models = [] @@ -132,7 +132,7 @@ def seed() -> Iterator[tuple[int, str]]: for chunk in range(len(events) // chunk_size): start = chunk * chunk_size - events_in_chunk = events[start : min(start + chunk_size, len(events))] + events_in_chunk = events[start:min(start + chunk_size, len(events))] jobs = [(images, event) for event in events_in_chunk] models = pool.starmap(add_event, jobs) models = [e for e in models if e is not None] diff --git a/backend/root/management/commands/seed_scripts/textitems.py b/backend/root/management/commands/seed_scripts/textitems.py index 81c7ce8a1..a9b90e79c 100644 --- a/backend/root/management/commands/seed_scripts/textitems.py +++ b/backend/root/management/commands/seed_scripts/textitems.py @@ -38,17 +38,23 @@ def seed(): 'text_en': 'Do you have any questions or want to get in touch with us? Don"t hesitate to contact us!', }, { - 'key': 'sulten_reservation_help', - 'text_nb': """Bord må reserveres minst en dag i forveien. Mat kan forhåndsbestilles slik at dere ikke trenger å vente når dere kommer. + 'key': + 'sulten_reservation_help', + 'text_nb': + """Bord må reserveres minst en dag i forveien. Mat kan forhåndsbestilles slik at dere ikke trenger å vente når dere kommer. Merk at flertallet av personer må være medlem for å reservere og at alle må være over 20 år etter kl 20:00 i helger.""", - 'text_en': """Tables must be reserved at least one day in advance. Food can be pre-ordered so you do not have to wait when you arrive. + 'text_en': + """Tables must be reserved at least one day in advance. Food can be pre-ordered so you do not have to wait when you arrive. Note that the majority of people must be a member of the Student Society to reserve and that all must be over 20 years after 20:00 on weekends.""", }, { - 'key': 'sulten_reservation_contact', - 'text_nb': 'Reservasjonssystemet vårt er fortsatt under utvikling, og vi ber om forbehold om at feil kan forekomme. Klikk her for å bestille via epost: ', - 'text_en': 'Our reservation system is still under development, and reservation errors may therefore occur. Click here to order via email: ', + 'key': + 'sulten_reservation_contact', + 'text_nb': + 'Reservasjonssystemet vårt er fortsatt under utvikling, og vi ber om forbehold om at feil kan forekomme. Klikk her for å bestille via epost: ', + 'text_en': + 'Our reservation system is still under development, and reservation errors may therefore occur. Click here to order via email: ', }, ] diff --git a/backend/root/management/commands/setupmigrations.py b/backend/root/management/commands/setupmigrations.py index 29001c5e0..ea3c6242e 100644 --- a/backend/root/management/commands/setupmigrations.py +++ b/backend/root/management/commands/setupmigrations.py @@ -10,6 +10,7 @@ class Command(BaseCommand): + def handle(self, *args, **options): for app in settings.PROJECT_APPS: try: diff --git a/backend/root/management/commands/setupmigrations2.py b/backend/root/management/commands/setupmigrations2.py index d7016da01..bd1dcc45b 100644 --- a/backend/root/management/commands/setupmigrations2.py +++ b/backend/root/management/commands/setupmigrations2.py @@ -9,6 +9,7 @@ class Command(BaseCommand): + def handle(self, *args, **options): for app in settings.INSTALLED_APPS: appname = app.split('.')[-1] diff --git a/backend/root/settings/__init__.py b/backend/root/settings/__init__.py index 7ece708e7..e5e20c2d7 100644 --- a/backend/root/settings/__init__.py +++ b/backend/root/settings/__init__.py @@ -12,7 +12,8 @@ # Raise exception if ENV is invalid (and show possible options). if ENV not in Environment.VALID: ENV_OPTIONS = ''.join([f'\n\t{env}' for env in Environment.VALID]) - raise Exception(f"Environment variable 'ENV' is required to import this module ('{__name__}')." f'Possible values: {ENV_OPTIONS}') + raise Exception(f"Environment variable 'ENV' is required to import this module ('{__name__}')." + f'Possible values: {ENV_OPTIONS}') if ENV == Environment.DEV: from .dev import * # type: ignore[assignment] # noqa: F403 diff --git a/backend/root/settings/base.py b/backend/root/settings/base.py index c1fb44bbf..f613da71f 100644 --- a/backend/root/settings/base.py +++ b/backend/root/settings/base.py @@ -113,14 +113,16 @@ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, + 'OPTIONS': + { + 'context_processors': + [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, }, ] @@ -170,12 +172,13 @@ # 'rest_framework.permissions.IsAuthenticated', 'rest_framework.authentication.SessionAuthentication', ], - 'DEFAULT_PERMISSION_CLASSES': [ - # 'rest_framework.permissions.IsAuthenticated', - # 'rest_framework.permissions.DjangoObjectPermissions', - 'root.custom_classes.permission_classes.SuperUserPermission', - # 'root.custom_classes.permission_classes.CustomDjangoObjectPermissions', - ], + 'DEFAULT_PERMISSION_CLASSES': + [ + # 'rest_framework.permissions.IsAuthenticated', + # 'rest_framework.permissions.DjangoObjectPermissions', + 'root.custom_classes.permission_classes.SuperUserPermission', + # 'root.custom_classes.permission_classes.CustomDjangoObjectPermissions', + ], } ### End: DRF ### @@ -214,91 +217,97 @@ LOGGING = { 'version': 1, 'disable_existing_loggers': False, - 'formatters': { - 'json': { - # Need to be a callable in order to use init parameters. - '()': lambda: JsonFormatter(indent=4 if ENV == Environment.DEV else None), - }, - 'file': { - 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - }, - }, - 'filters': { - 'request_context_filter': { - '()': RequestContextFilter, - }, - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, - }, - 'handlers': { - 'null': { - 'class': 'logging.NullHandler', + 'formatters': + { + 'json': { + # Need to be a callable in order to use init parameters. + '()': lambda: JsonFormatter(indent=4 if ENV == Environment.DEV else None), + }, + 'file': { + 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', + }, }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'filters': ['require_debug_true'], + 'filters': + { + 'request_context_filter': { + '()': RequestContextFilter, + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, }, - 'file': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'formatter': 'json', - 'filename': LOGFILENAME, - 'filters': ['request_context_filter'], + 'handlers': + { + 'null': { + 'class': 'logging.NullHandler', + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'filters': ['require_debug_true'], + }, + 'file': { + 'level': 'INFO', + 'class': 'logging.FileHandler', + 'formatter': 'json', + 'filename': LOGFILENAME, + 'filters': ['request_context_filter'], + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], + }, + 'humio': + { + 'level': 'DEBUG' if ENV == Environment.DEV else 'INFO', + 'formatter': 'json', + 'class': 'logging.StreamHandler', + 'stream': sys.stdout, + 'filters': ['request_context_filter'], + }, + 'sql_file': + { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'mode': 'w', + 'filename': SQL_LOG_FILE, # Added to '.gitignore'. + 'filters': ['require_debug_true'], + }, }, - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler', - 'filters': ['require_debug_false'], + 'loggers': + { + # Default logger. + '': { + 'handlers': ['humio', 'file'], + 'propagate': True, + 'level': 'INFO', + }, + # Catch all from django unless explicitly prevented propagation. + 'django': { + 'handlers': ['console', 'mail_admins'], + 'propagate': True, + 'level': 'DEBUG', + }, + 'django.db.backends': { + 'handlers': ['sql_file'], + 'propagate': False, # Don't pass up to 'django'. + 'level': 'DEBUG', + }, + 'django.server': { + 'handlers': ['console'], + 'propagate': False, # Don't pass up to 'django'. + 'level': 'INFO', + }, + 'django.utils.autoreload': { + 'handlers': ['console'], + 'propagate': False, # Don't pass up to 'django'. + 'level': 'INFO', + }, }, - 'humio': { - 'level': 'DEBUG' if ENV == Environment.DEV else 'INFO', - 'formatter': 'json', - 'class': 'logging.StreamHandler', - 'stream': sys.stdout, - 'filters': ['request_context_filter'], - }, - 'sql_file': { - 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'mode': 'w', - 'filename': SQL_LOG_FILE, # Added to '.gitignore'. - 'filters': ['require_debug_true'], - }, - }, - 'loggers': { - # Default logger. - '': { - 'handlers': ['humio', 'file'], - 'propagate': True, - 'level': 'INFO', - }, - # Catch all from django unless explicitly prevented propagation. - 'django': { - 'handlers': ['console', 'mail_admins'], - 'propagate': True, - 'level': 'DEBUG', - }, - 'django.db.backends': { - 'handlers': ['sql_file'], - 'propagate': False, # Don't pass up to 'django'. - 'level': 'DEBUG', - }, - 'django.server': { - 'handlers': ['console'], - 'propagate': False, # Don't pass up to 'django'. - 'level': 'INFO', - }, - 'django.utils.autoreload': { - 'handlers': ['console'], - 'propagate': False, # Don't pass up to 'django'. - 'level': 'INFO', - }, - }, } # Quick fix for avoiding concurrency issues related to db access diff --git a/backend/root/settings/prod.py b/backend/root/settings/prod.py index cd712e696..1555e49d1 100644 --- a/backend/root/settings/prod.py +++ b/backend/root/settings/prod.py @@ -27,21 +27,23 @@ DATABASES = { # The default database used for all Samf4 models - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ['DB_NAME'], - 'USER': os.environ['DB_USER'], - 'PASSWORD': os.environ['DB_PASSWORD'], - 'HOST': os.environ['DB_HOST'], - 'PORT': os.environ['DB_PORT'], - }, + 'default': + { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ['DB_NAME'], + 'USER': os.environ['DB_USER'], + 'PASSWORD': os.environ['DB_PASSWORD'], + 'HOST': os.environ['DB_HOST'], + 'PORT': os.environ['DB_PORT'], + }, # The database for the billig system - 'billig': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ['BILLIG_DB_NAME'], - 'USER': os.environ['BILLIG_DB_USER'], - 'PASSWORD': os.environ['BILLIG_DB_PASSWORD'], - 'HOST': os.environ['BILLIG_DB_HOST'], - 'PORT': os.environ['BILLIG_DB_PORT'], - }, + 'billig': + { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ['BILLIG_DB_NAME'], + 'USER': os.environ['BILLIG_DB_USER'], + 'PASSWORD': os.environ['BILLIG_DB_PASSWORD'], + 'HOST': os.environ['BILLIG_DB_HOST'], + 'PORT': os.environ['BILLIG_DB_PORT'], + }, } diff --git a/backend/root/utils/mixins.py b/backend/root/utils/mixins.py index 4ed6c8c4d..fe52dd818 100644 --- a/backend/root/utils/mixins.py +++ b/backend/root/utils/mixins.py @@ -140,7 +140,9 @@ def save(self, *args: Any, **kwargs: Any) -> None: else: # Log changes. LOG.info(f'{self} has changed:\n\nold: {dirty_fields_old}\n\n new:{dirty_fields_new}') LOG.info( - f'{self} has changed:\n\n' f'old: {self.ftm_log_parse(fields=dirty_fields_old)}\n\n' f'new:{self.ftm_log_parse(fields=dirty_fields_new)}' + f'{self} has changed:\n\n' + f'old: {self.ftm_log_parse(fields=dirty_fields_old)}\n\n' + f'new:{self.ftm_log_parse(fields=dirty_fields_new)}' ) except Exception as e: # Get all changes. diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index c49b79d8f..69b2cf515 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -587,19 +587,16 @@ def admissions_count(self, obj: RecruitmentPosition) -> int: @admin.register(RecruitmentAdmission) class RecruitmentAdmissionAdmin(CustomBaseAdmin): sortable_by = [ - 'id', 'recruitment_position', 'recruitment', 'user', ] list_display = [ - 'id', 'recruitment_position', 'recruitment', 'user', ] search_fields = [ - 'id', 'recruitment_position', 'recruitment', 'user', diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index 77dc26756..afbed63ac 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -22,7 +22,6 @@ from samfundet.models.general import Gang, User, Image, Table, Venue, BlogPost, TextItem, Reservation, Organization, InformationPage from samfundet.models.recruitment import Recruitment, RecruitmentPosition, RecruitmentAdmission from samfundet.models.model_choices import EventTicketType, EventAgeRestriction, RecruitmentStatusChoices, RecruitmentPriorityChoices - """ This module contains fixtures available in pytests. These do not need to be imported. @@ -32,7 +31,6 @@ https://docs.pytest.org/en/7.1.x/how-to/fixtures.html """ - TestCase.databases = {'default', 'billig'} diff --git a/backend/samfundet/homepage/homepage.py b/backend/samfundet/homepage/homepage.py index 535d4c485..32ecc5fe5 100644 --- a/backend/samfundet/homepage/homepage.py +++ b/backend/samfundet/homepage/homepage.py @@ -62,7 +62,7 @@ def generate() -> dict[str, Any]: # Splash events # TODO we should make a datamodel for this try: - splash_events = list(upcoming_events[0 : min(3, len(upcoming_events))]) + splash_events = list(upcoming_events[0:min(3, len(upcoming_events))]) splash = EventSerializer(splash_events, many=True).data except IndexError: splash = [] @@ -70,13 +70,11 @@ def generate() -> dict[str, Any]: # Upcoming events try: - elements.append( - carousel( - title_nb='Hva skjer?', - title_en="What's happening?", - events=list(upcoming_events[:10]), - ) - ) + elements.append(carousel( + title_nb='Hva skjer?', + title_en="What's happening?", + events=list(upcoming_events[:10]), + )) except IndexError: pass @@ -91,13 +89,11 @@ def generate() -> dict[str, Any]: # Concerts try: - elements.append( - carousel( - title_nb='Konserter', - title_en='Concerts', - events=list(upcoming_events.filter(category=EventCategory.CONCERT)[:10]), - ) - ) + elements.append(carousel( + title_nb='Konserter', + title_en='Concerts', + events=list(upcoming_events.filter(category=EventCategory.CONCERT)[:10]), + )) except IndexError: pass @@ -112,13 +108,11 @@ def generate() -> dict[str, Any]: # Debates try: - elements.append( - carousel( - title_nb='Debatter', - title_en='Debates', - events=list(upcoming_events.filter(category=EventCategory.DEBATE)[:10]), - ) - ) + elements.append(carousel( + title_nb='Debatter', + title_en='Debates', + events=list(upcoming_events.filter(category=EventCategory.DEBATE)[:10]), + )) except IndexError: pass diff --git a/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py b/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py deleted file mode 100644 index 85e7ff65e..000000000 --- a/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.0 on 2023-12-20 21:31 - -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("samfundet", "0002_alter_notification_options_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="recruitmentadmission", - name="id", - field=models.UUIDField( - default=uuid.uuid4, editable=False, primary_key=True, serialize=False - ), - ), - migrations.AlterField( - model_name="recruitmentadmission", - name="recruiter_status", - field=models.IntegerField( - choices=[ - (0, "Not Set"), - (1, "Called and Accepted"), - (2, "Called and Rejected"), - (3, "Automatic Rejection"), - ], - default=0, - help_text="The status of the admission", - ), - ), - ] diff --git a/backend/samfundet/migrations/0008_alter_recruitmentadmission_id.py b/backend/samfundet/migrations/0008_alter_recruitmentadmission_id.py new file mode 100644 index 000000000..1c3d0d851 --- /dev/null +++ b/backend/samfundet/migrations/0008_alter_recruitmentadmission_id.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0 on 2024-02-08 17:15 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("samfundet", "0007_recruitmentadmission_withdrawn_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="recruitmentadmission", + name="id", + field=models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ] diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index b80d546a0..ea98a162c 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -33,6 +33,7 @@ class Notification(AbstractNotification): + class Meta(AbstractNotification.Meta): abstract = False diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 1e76a5d67..27a530e1f 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -154,7 +154,6 @@ class Interview(CustomBaseModel): notes = models.TextField(help_text='Notes for the interview', null=True, blank=True) - class RecruitmentAdmission(CustomBaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) admission_text = models.TextField(help_text='Admission text for the admission') diff --git a/backend/samfundet/models/tests/test_recruitment.py b/backend/samfundet/models/tests/test_recruitment.py index ca75b3164..e2822e8d0 100644 --- a/backend/samfundet/models/tests/test_recruitment.py +++ b/backend/samfundet/models/tests/test_recruitment.py @@ -44,6 +44,7 @@ def _create_recruitment_with_dt(*, overrides: dict[str, timezone.datetime]) -> R class TestRecruitmentClean: + def test_all_datetimes_is_in_the_future(self, fixture_org): error_msg = 'All times should be in the future' past = timezone.now() - timezone.timedelta(days=2) @@ -72,6 +73,7 @@ def test_reprioritization_deadline_for_applicant_before_reprioritization_deadlin class TestRecruitmentAdmission: + def test_check_withdraw_sets_unwanted(self, fixture_recruitment_admission: RecruitmentAdmission): assert fixture_recruitment_admission.recruiter_status == RecruitmentStatusChoices.NOT_SET assert fixture_recruitment_admission.recruiter_priority == RecruitmentPriorityChoices.NOT_SET diff --git a/backend/samfundet/models/tests/test_reservation.py b/backend/samfundet/models/tests/test_reservation.py index 7543de4ce..3844cd26e 100644 --- a/backend/samfundet/models/tests/test_reservation.py +++ b/backend/samfundet/models/tests/test_reservation.py @@ -6,6 +6,7 @@ class TestReservation: + def test_check_fetches_times( self, fixture_venue: Venue, diff --git a/backend/samfundet/models/utils/fields.py b/backend/samfundet/models/utils/fields.py index aed8075bf..38bd22201 100644 --- a/backend/samfundet/models/utils/fields.py +++ b/backend/samfundet/models/utils/fields.py @@ -9,11 +9,13 @@ class LowerCaseField(models.CharField): + def to_python(self, value: str) -> str: return super().to_python(value.lower()) class PhoneNumberField(models.CharField): + def __init__(self, *args: Any, **kwargs: Any) -> None: kwargs['max_length'] = 15 self.validators = [ diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 116e1b455..5098e4385 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -55,6 +55,7 @@ class TagSerializer(CustomBaseSerializer): + class Meta: model = Tag fields = '__all__' @@ -98,12 +99,14 @@ def get_url(self, image: Image) -> str: class EventCustomTicketSerializer(CustomBaseSerializer): + class Meta: model = EventCustomTicket fields = '__all__' class BilligPriceGroupSerializer(CustomBaseSerializer): + class Meta: model = BilligPriceGroup fields = ['id', 'name', 'can_be_put_on_card', 'membership_needed', 'netsale', 'price'] @@ -164,6 +167,7 @@ def to_representation(self, events: list[Event] | QuerySet[Event]) -> list[str]: class EventSerializer(CustomBaseSerializer): + class Meta: model = Event list_serializer_class = EventListSerializer @@ -195,18 +199,21 @@ def create(self, validated_data: dict) -> Event: class EventGroupSerializer(CustomBaseSerializer): + class Meta: model = EventGroup fields = '__all__' class VenueSerializer(CustomBaseSerializer): + class Meta: model = Venue fields = '__all__' class ClosedPeriodSerializer(CustomBaseSerializer): + class Meta: model = ClosedPeriod fields = '__all__' @@ -306,24 +313,28 @@ def validate(self, attrs: dict) -> dict: class GroupSerializer(serializers.ModelSerializer): + class Meta: model = Group fields = '__all__' class ProfileSerializer(serializers.ModelSerializer): + class Meta: model = Profile fields = ['id', 'nickname'] class UserPreferenceSerializer(serializers.ModelSerializer): + class Meta: model = UserPreference fields = '__all__' class CampusSerializer(serializers.ModelSerializer): + class Meta: model = Campus fields = '__all__' @@ -373,12 +384,14 @@ def get_user_preference(self, user: User) -> dict: # GANGS ### class OrganizationSerializer(CustomBaseSerializer): + class Meta: model = Organization fields = '__all__' class GangSerializer(CustomBaseSerializer): + class Meta: model = Gang fields = '__all__' @@ -393,12 +406,14 @@ class Meta: class InformationPageSerializer(CustomBaseSerializer): + class Meta: model = InformationPage fields = '__all__' class BlogPostSerializer(CustomBaseSerializer): + class Meta: model = BlogPost fields = '__all__' @@ -433,18 +448,21 @@ def create(self, validated_data: dict) -> Event: class TextItemSerializer(serializers.ModelSerializer): + class Meta: model = TextItem fields = '__all__' class InfoboxSerializer(CustomBaseSerializer): + class Meta: model = Infobox fields = '__all__' class KeyValueSerializer(serializers.ModelSerializer): + class Meta: model = KeyValue fields = '__all__' @@ -456,12 +474,14 @@ class Meta: class FoodPreferenceSerializer(CustomBaseSerializer): + class Meta: model = FoodPreference fields = '__all__' class FoodCategorySerializer(CustomBaseSerializer): + class Meta: model = FoodCategory fields = '__all__' @@ -484,18 +504,21 @@ class Meta: class TableSerializer(CustomBaseSerializer): + class Meta: model = Table fields = '__all__' class ReservationSerializer(CustomBaseSerializer): + class Meta: model = Reservation fields = '__all__' class ReservationCheckSerializer(serializers.ModelSerializer): + class Meta: model = Reservation fields = ['guest_count', 'occasion', 'reservation_date'] @@ -516,6 +539,7 @@ class Meta: class RecruitmentSerializer(CustomBaseSerializer): + class Meta: model = Recruitment fields = '__all__' @@ -541,6 +565,7 @@ def get_recruitment_admission_ids(self, obj: User) -> list[int]: class InterviewerSerializer(CustomBaseSerializer): + class Meta: model = User fields = [ @@ -588,6 +613,7 @@ def update(self, instance: RecruitmentPosition, validated_data: dict) -> Recruit self._update_interviewers(recruitment_position=updated_instance, interviewer_objects=interviewer_objects) return updated_instance + class ApplicantInterviewSerializer(serializers.ModelSerializer): class Meta: @@ -622,7 +648,7 @@ class Meta: class RecruitmentAdmissionForApplicantSerializer(serializers.ModelSerializer): interview = ApplicantInterviewSerializer(read_only=True) recruitment_position = RecruitmentPositionForApplicantSerializer(read_only=True) -): + class Meta: model = RecruitmentAdmission fields = [ @@ -653,6 +679,7 @@ def create(self, validated_data: dict) -> RecruitmentAdmission: class OccupiedtimeslotSerializer(serializers.ModelSerializer): + class Meta: model = Occupiedtimeslot fields = '__all__' @@ -667,12 +694,14 @@ class Meta: class InterviewRoomSerializer(CustomBaseSerializer): + class Meta: model = InterviewRoom fields = '__all__' class InterviewSerializer(CustomBaseSerializer): + class Meta: model = Interview fields = '__all__' diff --git a/backend/samfundet/tests/test_signals.py b/backend/samfundet/tests/test_signals.py index c3cacf56b..5c6721304 100644 --- a/backend/samfundet/tests/test_signals.py +++ b/backend/samfundet/tests/test_signals.py @@ -8,6 +8,7 @@ class TestUserSignals: + def test_create_user_preference(self): ### Arrange ### user = User.objects.create_user( @@ -44,6 +45,7 @@ def test_create_profile(self): class TestEditorPermissions: + def test_update_editor_permissions_add( self, fixture_event: Event, diff --git a/backend/samfundet/tests/test_views.py b/backend/samfundet/tests/test_views.py index 69331c193..bfccc42cb 100644 --- a/backend/samfundet/tests/test_views.py +++ b/backend/samfundet/tests/test_views.py @@ -114,6 +114,7 @@ def test_get_groups(fixture_rest_client: APIClient, fixture_user: User): class TestInformationPagesView: + def test_get_informationpage( self, fixture_rest_client: APIClient, @@ -285,6 +286,7 @@ def test_updated_and_created_by(self, fixture_rest_client: APIClient, fixture_us class TestBlogPostView: + def test_get_blogpost( self, fixture_rest_client: APIClient, @@ -380,6 +382,7 @@ def test_put_blogpost( class TestKeyValueView: + def test_anyone_can_retrieve_keyvalues(self, fixture_rest_client: APIClient): ### Arrange ### keyvalue = KeyValue.objects.create(key='FOO', value='bar') @@ -429,6 +432,7 @@ def test_crud_not_possible(self, fixture_rest_client: APIClient, fixture_superus class TestTextItemView: + def test_anyone_can_retrieve_textitems(self, fixture_rest_client: APIClient, fixture_text_item: TextItem): ### Arrange ### url = reverse(routes.samfundet__text_item_detail, kwargs={'pk': fixture_text_item.key}) @@ -474,6 +478,7 @@ def test_crud_not_possible(self, fixture_rest_client: APIClient, fixture_superus class TestAssignGroupView: + def test_assign_group( self, fixture_rest_client: APIClient, diff --git a/backend/samfundet/utils.py b/backend/samfundet/utils.py index a92c778fe..0d57cdfc0 100644 --- a/backend/samfundet/utils.py +++ b/backend/samfundet/utils.py @@ -15,14 +15,9 @@ def event_query(*, query: QueryDict, events: QuerySet[Event] = None) -> QuerySet search = query.get('search', None) if search: events = events.filter( - Q(title_nb__icontains=search) - | Q(title_en__icontains=search) - | Q(description_long_nb__icontains=search) - | Q(description_long_en__icontains=search) - | Q(description_short_en=search) - | Q(description_short_nb=search) - | Q(location__icontains=search) - | Q(event_group__name=search) + Q(title_nb__icontains=search) | Q(title_en__icontains=search) | Q(description_long_nb__icontains=search) | + Q(description_long_en__icontains=search) | Q(description_short_en=search) | Q(description_short_nb=search) | Q(location__icontains=search) | + Q(event_group__name=search) ) event_group = query.get('event_group', None) if event_group: diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index a4184e24a..8248089c9 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -145,14 +145,14 @@ class KeyValueView(ReadOnlyModelViewSet): # Images class ImageView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = ImageSerializer queryset = Image.objects.all().order_by('-pk') # Image tags class TagView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = TagSerializer queryset = Tag.objects.all() @@ -163,7 +163,7 @@ class TagView(ModelViewSet): class EventView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = EventSerializer queryset = Event.objects.all() @@ -196,7 +196,7 @@ def get(self, request: Request) -> Response: class EventGroupView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = EventGroupSerializer queryset = EventGroup.objects.all() @@ -207,14 +207,14 @@ class EventGroupView(ModelViewSet): class VenueView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = VenueSerializer queryset = Venue.objects.all() lookup_field = 'slug' class ClosedPeriodView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = ClosedPeriodSerializer queryset = ClosedPeriod.objects.all() @@ -231,49 +231,49 @@ def get_queryset(self) -> QuerySet: class BookingView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = BookingSerializer queryset = Booking.objects.all() class SaksdokumentView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = SaksdokumentSerializer queryset = Saksdokument.objects.all() class OrganizationView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = OrganizationSerializer queryset = Organization.objects.all() class GangView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = GangSerializer queryset = Gang.objects.all() class GangTypeView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = GangTypeSerializer queryset = GangType.objects.all() class InformationPageView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = InformationPageSerializer queryset = InformationPage.objects.all() class InfoboxView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = InfoboxSerializer queryset = Infobox.objects.all() class BlogPostView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = BlogPostSerializer queryset = BlogPost.objects.all() @@ -284,31 +284,31 @@ class BlogPostView(ModelViewSet): class MenuView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = MenuSerializer queryset = Menu.objects.all() class MenuItemView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = MenuItemSerializer queryset = MenuItem.objects.all() class FoodCategoryView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = FoodCategorySerializer queryset = FoodCategory.objects.all() class FoodPreferenceView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = FoodPreferenceSerializer queryset = FoodPreference.objects.all() class TableView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = TableSerializer queryset = Table.objects.all() @@ -412,7 +412,7 @@ def get(self, request: Request) -> Response: class AllUsersView(ListAPIView): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = UserSerializer queryset = User.objects.all() @@ -428,7 +428,7 @@ def post(self, request: Request) -> Response: class AllGroupsView(ListAPIView): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = GroupSerializer queryset = Group.objects.all() @@ -449,7 +449,7 @@ class UserPreferenceView(ModelViewSet): class ProfileView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) serializer_class = ProfileSerializer queryset = Profile.objects.all() diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss index 7c8a1cc89..5b1f41dd7 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss @@ -24,3 +24,14 @@ $back-button-width: 5rem; .empty_div { width: $back-button-width; } + +.arrows { + &:hover { + filter: brightness(150%); + transform: scale(1.05); + } + &:active { + filter: brightness(200%); + transform: scale(1.10); + } +} diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx index 8174626c9..512ad229b 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx @@ -8,8 +8,9 @@ import { getRecruitmentAdmissionsForApplicant, putRecruitmentAdmission } from '~ import { RecruitmentAdmissionDto } from '~/dto'; import { KEY } from '~/i18n/constants'; import { ROUTES } from '~/routes'; -import { dbT } from '~/utils'; +import { dbT, niceDateTime } from '~/utils'; import styles from './ApplicantApplicationOverviewPage.module.scss'; +import { OccupiedFormModal } from '~/Components/OccupiedForm'; export function ApplicantApplicationOverviewPage() { const { recruitmentID } = useParams(); @@ -17,27 +18,25 @@ export function ApplicantApplicationOverviewPage() { const { t } = useTranslation(); function handleChangePriority(id: number, direction: 'up' | 'down') { - const newAdmissions = [...admissions]; + const newAdmissions = [ + ...admissions.sort(function (a1, a2) { + return a1.applicant_priority - a2.applicant_priority; + }), + ]; const index = newAdmissions.findIndex((admission) => admission.id === id); const directionIncrement = direction === 'up' ? -1 : 1; - if (newAdmissions[index].applicant_priority === 1 && direction === 'up') return; - if (newAdmissions[index].applicant_priority === newAdmissions.length && direction === 'down') return; - const targetIndex = newAdmissions.findIndex( - (admission) => admission.applicant_priority === newAdmissions[index].applicant_priority + directionIncrement, - ); + if (index == 0 && direction === 'up') return; + if (index === newAdmissions.length - 1 && direction === 'down') return; const old_priority = newAdmissions[index].applicant_priority; - const new_priority = newAdmissions[targetIndex].applicant_priority; - - console.log('old priority', old_priority); - console.log('new priority', new_priority); + const new_priority = newAdmissions[index + directionIncrement].applicant_priority; newAdmissions[index].applicant_priority = new_priority; - newAdmissions[targetIndex].applicant_priority = old_priority; + newAdmissions[index + directionIncrement].applicant_priority = old_priority; // TODO: Make this a single API call putRecruitmentAdmission(newAdmissions[index]); - putRecruitmentAdmission(newAdmissions[targetIndex]).then(() => { + putRecruitmentAdmission(newAdmissions[index + directionIncrement]).then(() => { setAdmissions(newAdmissions); }); } @@ -45,8 +44,8 @@ export function ApplicantApplicationOverviewPage() { function upDownArrow(id: number) { return ( <> - handleChangePriority(id, 'up')} /> - handleChangePriority(id, 'down')} /> + handleChangePriority(id, 'up')} /> + handleChangePriority(id, 'down')} /> ); } @@ -74,7 +73,7 @@ export function ApplicantApplicationOverviewPage() { function admissionToTableRow(admission: RecruitmentAdmissionDto) { return [ dbT(admission.recruitment_position, 'name'), - admission.interview.interview_time, + niceDateTime(admission.interview.interview_time), admission.interview.interview_location, admission.applicant_priority, { content: upDownArrow(admission.id) }, @@ -97,6 +96,8 @@ export function ApplicantApplicationOverviewPage() { ) : (

You have not applied to any positions yet

)} + + ); diff --git a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss index f3614acfa..dc736ae8e 100644 --- a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss +++ b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss @@ -20,3 +20,8 @@ border-width: 0; width: 32em; } + +.personalRow { + @include flex-row-center; + gap: 2em; +} diff --git a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx index c0107dc8f..13b927033 100644 --- a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx +++ b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx @@ -1,12 +1,16 @@ import { useEffect, useState } from 'react'; -import { Page, SamfundetLogoSpinner, Video } from '~/Components'; +import { Button, Page, SamfundetLogoSpinner, Video } from '~/Components'; import { getActiveRecruitmentPositions, getGangList } from '~/api'; import { GangTypeDto, RecruitmentPositionDto } from '~/dto'; import { GangTypeContainer } from './Components'; import styles from './RecruitmentPage.module.scss'; import { OccupiedFormModal } from '~/Components/OccupiedForm'; +import { reverse } from '~/named-urls'; +import { useCustomNavigate } from '~/hooks'; +import { ROUTES } from '~/routes'; export function RecruitmentPage() { + const navigate = useCustomNavigate(); const [recruitmentPositions, setRecruitmentPositions] = useState(); const [loading, setLoading] = useState(true); const [gangTypes, setGangs] = useState(); @@ -28,7 +32,22 @@ export function RecruitmentPage() {
- +
+ + +
{loading ? ( ) : ( diff --git a/frontend/src/Pages/index.ts b/frontend/src/Pages/index.ts index 4cad6635f..864abd660 100644 --- a/frontend/src/Pages/index.ts +++ b/frontend/src/Pages/index.ts @@ -14,7 +14,6 @@ export { LoginPage } from './LoginPage'; export { LycheContactPage } from './LycheContactPage'; export { LycheReservationPage } from './LycheReservationPage'; export { LycheAboutPage } from './LycheAboutPage'; -export { LycheContactPage } from './LycheContactPage'; export { LycheHomePage } from './LycheHomePage'; export { LycheMenuPage } from './LycheMenuPage'; export { NotFoundPage } from './NotFoundPage'; diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index dc1c3ede0..9c61bf424 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -172,6 +172,21 @@ export function utcTimestampToLocal(time: string | undefined): string { .replace(' ', 'T'); } +/** + * Converts a UTC timestring from django to + * a finer time + * @param time timestring in django utc format, eg '2028-03-31T02:33:31.835Z' + * @returns timestamp in local format, eg. '2023-04-05T20:15' + */ +export function niceDateTime(time: string | undefined): string | undefined { + const date = new Date(time ?? ''); + if (!isNaN(date.getTime())) { + const dateString = date.toUTCString(); + return dateString.substring(0, dateString.length - 3); + } + return time; +} + /** * Generic query function for DTOs. Returns elements from array matching query. * @param query String query to search with From 691f8903ad5d8e86f0cb3b0cffe8aa18fd684eb8 Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 19:04:41 +0100 Subject: [PATCH 11/20] finalize view own applications --- backend/Pipfile | 1 + backend/Pipfile.lock | 113 +++++++++++------- backend/samfundet/admin.py | 3 - backend/samfundet/conftest.py | 1 - ..._alter_recruitmentadmission_id_and_more.py | 34 ------ .../0008_alter_recruitmentadmission_id.py | 20 ++++ backend/samfundet/models/recruitment.py | 1 - backend/samfundet/serializers.py | 5 +- ...plicantApplicationOverviewPage.module.scss | 11 ++ .../ApplicantApplicationOverviewPage.tsx | 33 ++--- .../RecruitmentPage.module.scss | 5 + .../Pages/RecruitmentPage/RecruitmentPage.tsx | 23 +++- frontend/src/Pages/index.ts | 1 - frontend/src/utils.ts | 15 +++ 14 files changed, 159 insertions(+), 107 deletions(-) delete mode 100644 backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py create mode 100644 backend/samfundet/migrations/0008_alter_recruitmentadmission_id.py diff --git a/backend/Pipfile b/backend/Pipfile index e6bcebdfa..732955009 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -44,6 +44,7 @@ gunicorn = "*" django-admin-autocomplete-filter = "*" django-notifications-hq = "*" psycopg = {extras = ["c"], version = "*"} +ruff = "*" [dev-packages] yapf = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 114bf8e88..18b08a1e7 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0fd219ff24de055c4ba2c99bee2df8d306f3d940e44f045db77bce95791f7ea0" + "sha256": "e3ceaad3e2a939859a970f4044027ff08d71a3e70e0ab569c171c2658edad2a7" }, "pipfile-spec": 6, "requires": { @@ -30,17 +30,15 @@ "sha256:f90578b8a3177f7552f4e1a6e535e84293cd5da421fcce0642d49c0d7bdf8df2" ], "index": "pypi", - "markers": "python_version >= '3.7' and python_version < '4.0'", "version": "==0.6.4" }, "django": { "hashes": [ - "sha256:8c8659665bc6e3a44fefe1ab0a291e5a3fb3979f9a8230be29de975e57e8f854", - "sha256:f47a37a90b9bbe2c8ec360235192c7fddfdc832206fcf618bb849b39256affc1" + "sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4", + "sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080" ], "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==5.0.1" + "version": "==5.0.2" }, "django-admin-autocomplete-filter": { "hashes": [ @@ -56,7 +54,6 @@ "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==4.3.1" }, "django-environ": { @@ -65,7 +62,6 @@ "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be" ], "index": "pypi", - "markers": "python_version >= '3.6' and python_version < '4'", "version": "==0.11.2" }, "django-extensions": { @@ -74,7 +70,6 @@ "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401" ], "index": "pypi", - "markers": "python_version >= '3.6'", "version": "==3.2.3" }, "django-guardian": { @@ -83,7 +78,6 @@ "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0" ], "index": "pypi", - "markers": "python_version >= '3.5'", "version": "==2.4.0" }, "django-model-utils": { @@ -107,7 +101,6 @@ "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08" ], "index": "pypi", - "markers": "python_version >= '3.6'", "version": "==3.14.0" }, "gunicorn": { @@ -116,7 +109,6 @@ "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" ], "index": "pypi", - "markers": "python_version >= '3.5'", "version": "==21.2.0" }, "jsonfield": { @@ -223,7 +215,6 @@ "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==10.2.0" }, "psycopg": { @@ -231,17 +222,17 @@ "c" ], "hashes": [ - "sha256:437e7d7925459f21de570383e2e10542aceb3b9cb972ce957fdd3826ca47edc6", - "sha256:96b7b13af6d5a514118b759a66b2799a8a4aa78675fa6bb0d3f7d52d67eff002" + "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b", + "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e" ], - "markers": "python_version >= '3.7'", - "version": "==3.1.17" + "index": "pypi", + "version": "==3.1.18" }, "psycopg-c": { "hashes": [ - "sha256:5cc4d544d552b8ab92a9e3a9dbe3b4f46ce0a86338654d26387fc076e0c97977" + "sha256:ffff0c4a9c0e0b7aadb1acb7b61eb8f886365dd8ef00120ce14676235846ba73" ], - "version": "==3.1.17" + "version": "==3.1.18" }, "pytz": { "hashes": [ @@ -250,6 +241,29 @@ ], "version": "==2024.1" }, + "ruff": { + "hashes": [ + "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc", + "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105", + "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba", + "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e", + "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1", + "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232", + "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad", + "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35", + "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b", + "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a", + "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec", + "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080", + "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0", + "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02", + "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6", + "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683", + "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855" + ], + "index": "pypi", + "version": "==0.2.1" + }, "sqlparse": { "hashes": [ "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", @@ -279,6 +293,14 @@ "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" ], "version": "==0.9.0" + }, + "tzdata": { + "hashes": [ + "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3", + "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9" + ], + "markers": "sys_platform == 'win32'", + "version": "==2023.4" } }, "develop": { @@ -288,7 +310,6 @@ "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.7.7" }, "certifi": { @@ -395,6 +416,14 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.3.2" }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" + }, "debugpy": { "hashes": [ "sha256:125b9a637e013f9faac0a3d6a82bd17c8b5d2c875fb6b7e2772c5aba6d082332", @@ -417,7 +446,6 @@ "sha256:ef9ab7df0b9a42ed9c878afd3eaaff471fce3fa73df96022e1f5c9f8f8c87ada" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.8.0" }, "idna": { @@ -491,7 +519,6 @@ "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.8.0" }, "mypy-extensions": { @@ -590,7 +617,6 @@ "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==10.2.0" }, "platformdirs": { @@ -623,7 +649,6 @@ "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==8.0.0" }, "pytest-django": { @@ -632,7 +657,6 @@ "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==4.8.0" }, "pyyaml": { @@ -698,7 +722,6 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "rich": { @@ -711,27 +734,26 @@ }, "ruff": { "hashes": [ - "sha256:30ad74687e1f4a9ff8e513b20b82ccadb6bd796fe5697f1e417189c5cde6be3e", - "sha256:3826fb34c144ef1e171b323ed6ae9146ab76d109960addca730756dc19dc7b22", - "sha256:3d3c641f95f435fc6754b05591774a17df41648f0daf3de0d75ad3d9f099ab92", - "sha256:3fbaff1ba9564a2c5943f8f38bc221f04bac687cc7485e45237579fee7ccda79", - "sha256:3ff35433fcf4dff6d610738712152df6b7d92351a1bde8e00bd405b08b3d5759", - "sha256:63856b91837606c673537d2889989733d7dffde553828d3b0f0bacfa6def54be", - "sha256:638ea3294f800d18bae84a492cb5a245c8d29c90d19a91d8e338937a4c27fca0", - "sha256:6d232f99d3ab00094ebaf88e0fb7a8ccacaa54cc7fa3b8993d9627a11e6aed7a", - "sha256:8153a3e4128ed770871c47545f1ae7b055023e0c222ff72a759f5a341ee06483", - "sha256:87057dd2fdde297130ff99553be8549ca38a2965871462a97394c22ed2dfc19d", - "sha256:a7e3818698f8460bd0f8d4322bbe99db8327e9bc2c93c789d3159f5b335f47da", - "sha256:ba918e01cdd21e81b07555564f40d307b0caafa9a7a65742e98ff244f5035c59", - "sha256:bf9faafbdcf4f53917019f2c230766da437d4fd5caecd12ddb68bb6a17d74399", - "sha256:e155147199c2714ff52385b760fe242bb99ea64b240a9ffbd6a5918eb1268843", - "sha256:e8a75a98ae989a27090e9c51f763990ad5bbc92d20626d54e9701c7fe597f399", - "sha256:eceab7d85d09321b4de18b62d38710cf296cb49e98979960a59c6b9307c18cfe", - "sha256:edf23041242c48b0d8295214783ef543847ef29e8226d9f69bf96592dba82a83" + "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc", + "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105", + "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba", + "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e", + "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1", + "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232", + "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad", + "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35", + "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b", + "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a", + "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec", + "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080", + "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0", + "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02", + "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6", + "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683", + "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==0.2.0" + "version": "==0.2.1" }, "stevedore": { "hashes": [ @@ -771,7 +793,6 @@ "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==0.40.2" }, "zipp": { diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index c49b79d8f..69b2cf515 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -587,19 +587,16 @@ def admissions_count(self, obj: RecruitmentPosition) -> int: @admin.register(RecruitmentAdmission) class RecruitmentAdmissionAdmin(CustomBaseAdmin): sortable_by = [ - 'id', 'recruitment_position', 'recruitment', 'user', ] list_display = [ - 'id', 'recruitment_position', 'recruitment', 'user', ] search_fields = [ - 'id', 'recruitment_position', 'recruitment', 'user', diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index 77dc26756..01aff33bd 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -32,7 +32,6 @@ https://docs.pytest.org/en/7.1.x/how-to/fixtures.html """ - TestCase.databases = {'default', 'billig'} diff --git a/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py b/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py deleted file mode 100644 index 85e7ff65e..000000000 --- a/backend/samfundet/migrations/0003_alter_recruitmentadmission_id_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.0 on 2023-12-20 21:31 - -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("samfundet", "0002_alter_notification_options_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="recruitmentadmission", - name="id", - field=models.UUIDField( - default=uuid.uuid4, editable=False, primary_key=True, serialize=False - ), - ), - migrations.AlterField( - model_name="recruitmentadmission", - name="recruiter_status", - field=models.IntegerField( - choices=[ - (0, "Not Set"), - (1, "Called and Accepted"), - (2, "Called and Rejected"), - (3, "Automatic Rejection"), - ], - default=0, - help_text="The status of the admission", - ), - ), - ] diff --git a/backend/samfundet/migrations/0008_alter_recruitmentadmission_id.py b/backend/samfundet/migrations/0008_alter_recruitmentadmission_id.py new file mode 100644 index 000000000..1c3d0d851 --- /dev/null +++ b/backend/samfundet/migrations/0008_alter_recruitmentadmission_id.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0 on 2024-02-08 17:15 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("samfundet", "0007_recruitmentadmission_withdrawn_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="recruitmentadmission", + name="id", + field=models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ] diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 1e76a5d67..27a530e1f 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -154,7 +154,6 @@ class Interview(CustomBaseModel): notes = models.TextField(help_text='Notes for the interview', null=True, blank=True) - class RecruitmentAdmission(CustomBaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) admission_text = models.TextField(help_text='Admission text for the admission') diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 116e1b455..fbb10cac2 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -588,8 +588,8 @@ def update(self, instance: RecruitmentPosition, validated_data: dict) -> Recruit self._update_interviewers(recruitment_position=updated_instance, interviewer_objects=interviewer_objects) return updated_instance -class ApplicantInterviewSerializer(serializers.ModelSerializer): +class ApplicantInterviewSerializer(serializers.ModelSerializer): class Meta: model = Interview fields = [ @@ -600,7 +600,6 @@ class Meta: class RecruitmentPositionForApplicantSerializer(serializers.ModelSerializer): - class Meta: model = RecruitmentPosition fields = [ @@ -622,7 +621,7 @@ class Meta: class RecruitmentAdmissionForApplicantSerializer(serializers.ModelSerializer): interview = ApplicantInterviewSerializer(read_only=True) recruitment_position = RecruitmentPositionForApplicantSerializer(read_only=True) -): + class Meta: model = RecruitmentAdmission fields = [ diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss index 7c8a1cc89..5b1f41dd7 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss @@ -24,3 +24,14 @@ $back-button-width: 5rem; .empty_div { width: $back-button-width; } + +.arrows { + &:hover { + filter: brightness(150%); + transform: scale(1.05); + } + &:active { + filter: brightness(200%); + transform: scale(1.10); + } +} diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx index 8174626c9..512ad229b 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx @@ -8,8 +8,9 @@ import { getRecruitmentAdmissionsForApplicant, putRecruitmentAdmission } from '~ import { RecruitmentAdmissionDto } from '~/dto'; import { KEY } from '~/i18n/constants'; import { ROUTES } from '~/routes'; -import { dbT } from '~/utils'; +import { dbT, niceDateTime } from '~/utils'; import styles from './ApplicantApplicationOverviewPage.module.scss'; +import { OccupiedFormModal } from '~/Components/OccupiedForm'; export function ApplicantApplicationOverviewPage() { const { recruitmentID } = useParams(); @@ -17,27 +18,25 @@ export function ApplicantApplicationOverviewPage() { const { t } = useTranslation(); function handleChangePriority(id: number, direction: 'up' | 'down') { - const newAdmissions = [...admissions]; + const newAdmissions = [ + ...admissions.sort(function (a1, a2) { + return a1.applicant_priority - a2.applicant_priority; + }), + ]; const index = newAdmissions.findIndex((admission) => admission.id === id); const directionIncrement = direction === 'up' ? -1 : 1; - if (newAdmissions[index].applicant_priority === 1 && direction === 'up') return; - if (newAdmissions[index].applicant_priority === newAdmissions.length && direction === 'down') return; - const targetIndex = newAdmissions.findIndex( - (admission) => admission.applicant_priority === newAdmissions[index].applicant_priority + directionIncrement, - ); + if (index == 0 && direction === 'up') return; + if (index === newAdmissions.length - 1 && direction === 'down') return; const old_priority = newAdmissions[index].applicant_priority; - const new_priority = newAdmissions[targetIndex].applicant_priority; - - console.log('old priority', old_priority); - console.log('new priority', new_priority); + const new_priority = newAdmissions[index + directionIncrement].applicant_priority; newAdmissions[index].applicant_priority = new_priority; - newAdmissions[targetIndex].applicant_priority = old_priority; + newAdmissions[index + directionIncrement].applicant_priority = old_priority; // TODO: Make this a single API call putRecruitmentAdmission(newAdmissions[index]); - putRecruitmentAdmission(newAdmissions[targetIndex]).then(() => { + putRecruitmentAdmission(newAdmissions[index + directionIncrement]).then(() => { setAdmissions(newAdmissions); }); } @@ -45,8 +44,8 @@ export function ApplicantApplicationOverviewPage() { function upDownArrow(id: number) { return ( <> - handleChangePriority(id, 'up')} /> - handleChangePriority(id, 'down')} /> + handleChangePriority(id, 'up')} /> + handleChangePriority(id, 'down')} /> ); } @@ -74,7 +73,7 @@ export function ApplicantApplicationOverviewPage() { function admissionToTableRow(admission: RecruitmentAdmissionDto) { return [ dbT(admission.recruitment_position, 'name'), - admission.interview.interview_time, + niceDateTime(admission.interview.interview_time), admission.interview.interview_location, admission.applicant_priority, { content: upDownArrow(admission.id) }, @@ -97,6 +96,8 @@ export function ApplicantApplicationOverviewPage() { ) : (

You have not applied to any positions yet

)} + +
); diff --git a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss index f3614acfa..dc736ae8e 100644 --- a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss +++ b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss @@ -20,3 +20,8 @@ border-width: 0; width: 32em; } + +.personalRow { + @include flex-row-center; + gap: 2em; +} diff --git a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx index c0107dc8f..13b927033 100644 --- a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx +++ b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx @@ -1,12 +1,16 @@ import { useEffect, useState } from 'react'; -import { Page, SamfundetLogoSpinner, Video } from '~/Components'; +import { Button, Page, SamfundetLogoSpinner, Video } from '~/Components'; import { getActiveRecruitmentPositions, getGangList } from '~/api'; import { GangTypeDto, RecruitmentPositionDto } from '~/dto'; import { GangTypeContainer } from './Components'; import styles from './RecruitmentPage.module.scss'; import { OccupiedFormModal } from '~/Components/OccupiedForm'; +import { reverse } from '~/named-urls'; +import { useCustomNavigate } from '~/hooks'; +import { ROUTES } from '~/routes'; export function RecruitmentPage() { + const navigate = useCustomNavigate(); const [recruitmentPositions, setRecruitmentPositions] = useState(); const [loading, setLoading] = useState(true); const [gangTypes, setGangs] = useState(); @@ -28,7 +32,22 @@ export function RecruitmentPage() {
- +
+ + +
{loading ? ( ) : ( diff --git a/frontend/src/Pages/index.ts b/frontend/src/Pages/index.ts index 4cad6635f..864abd660 100644 --- a/frontend/src/Pages/index.ts +++ b/frontend/src/Pages/index.ts @@ -14,7 +14,6 @@ export { LoginPage } from './LoginPage'; export { LycheContactPage } from './LycheContactPage'; export { LycheReservationPage } from './LycheReservationPage'; export { LycheAboutPage } from './LycheAboutPage'; -export { LycheContactPage } from './LycheContactPage'; export { LycheHomePage } from './LycheHomePage'; export { LycheMenuPage } from './LycheMenuPage'; export { NotFoundPage } from './NotFoundPage'; diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index dc1c3ede0..9c61bf424 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -172,6 +172,21 @@ export function utcTimestampToLocal(time: string | undefined): string { .replace(' ', 'T'); } +/** + * Converts a UTC timestring from django to + * a finer time + * @param time timestring in django utc format, eg '2028-03-31T02:33:31.835Z' + * @returns timestamp in local format, eg. '2023-04-05T20:15' + */ +export function niceDateTime(time: string | undefined): string | undefined { + const date = new Date(time ?? ''); + if (!isNaN(date.getTime())) { + const dateString = date.toUTCString(); + return dateString.substring(0, dateString.length - 3); + } + return time; +} + /** * Generic query function for DTOs. Returns elements from array matching query. * @param query String query to search with From 799f9820fbce277d05096eaaa53200169a407f4b Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 19:20:15 +0100 Subject: [PATCH 12/20] ruff --- backend/root/custom_classes/admin_classes.py | 4 +- backend/root/custom_classes/middlewares.py | 1 - .../root/custom_classes/permission_classes.py | 1 - backend/root/db_router.py | 1 - .../root/management/commands/cleanstart.py | 1 - .../management/commands/deletemigrations.py | 1 - .../management/commands/deploymigrations.py | 1 - .../commands/generate_permissions.py | 1 + .../root/management/commands/migratezero.py | 1 - backend/root/management/commands/seed.py | 3 +- .../commands/seed_scripts/events.py | 3 +- .../management/commands/seed_scripts/gangs.py | 32 ++- .../commands/seed_scripts/images.py | 12 +- .../management/commands/seed_scripts/menu.py | 11 +- .../seed_scripts/recruitment_admissions.py | 3 +- .../management/commands/seed_scripts/samf3.py | 4 +- .../commands/seed_scripts/textitems.py | 18 +- .../management/commands/setupmigrations.py | 1 - .../management/commands/setupmigrations2.py | 1 - backend/root/settings/__init__.py | 3 +- backend/root/settings/base.py | 199 +++++++++--------- backend/root/settings/prod.py | 34 ++- backend/root/utils/mixins.py | 4 +- backend/samfundet/conftest.py | 1 + backend/samfundet/homepage/homepage.py | 38 ++-- backend/samfundet/models/general.py | 1 - .../models/tests/test_recruitment.py | 2 - .../models/tests/test_reservation.py | 1 - backend/samfundet/models/utils/fields.py | 2 - backend/samfundet/serializers.py | 29 +-- backend/samfundet/tests/test_signals.py | 2 - backend/samfundet/tests/test_views.py | 7 +- backend/samfundet/utils.py | 11 +- backend/samfundet/views.py | 44 ++-- 34 files changed, 213 insertions(+), 265 deletions(-) diff --git a/backend/root/custom_classes/admin_classes.py b/backend/root/custom_classes/admin_classes.py index 80906cf14..8ba2af927 100644 --- a/backend/root/custom_classes/admin_classes.py +++ b/backend/root/custom_classes/admin_classes.py @@ -54,7 +54,7 @@ def user_link(self, obj: Contact) -> str: def get_admin_url(*, obj: Any) -> str: """https://stackoverflow.com/questions/10420271/django-how-to-get-admin-url-from-model-instance""" info = (obj._meta.app_label, obj._meta.model_name) - admin_url = reverse('admin:%s_%s_change' % info, args=(obj.pk, )) + admin_url = reverse('admin:%s_%s_change' % info, args=(obj.pk,)) return admin_url @@ -258,7 +258,7 @@ def _insert_link(*, field: str, related_links: list[str]) -> str: def autocomplete_filter(**kwargs: Any) -> AutocompleteFilter: """Simple AutocompleteFilter factory.""" - return type('AutocompleteFilter', (AutocompleteFilter, ), kwargs) + return type('AutocompleteFilter', (AutocompleteFilter,), kwargs) class CustomGuardedUserAdmin(CustomGuardedModelAdmin, UserAdmin): diff --git a/backend/root/custom_classes/middlewares.py b/backend/root/custom_classes/middlewares.py index c894b34a7..c208d3118 100644 --- a/backend/root/custom_classes/middlewares.py +++ b/backend/root/custom_classes/middlewares.py @@ -53,7 +53,6 @@ def process_exception(self, request: HttpRequest, exception: Exception) -> None: class ImpersonateUserMiddleware: - def __init__(self, get_response: HttpResponse) -> None: self.get_response = get_response diff --git a/backend/root/custom_classes/permission_classes.py b/backend/root/custom_classes/permission_classes.py index f72892103..70f987f6c 100644 --- a/backend/root/custom_classes/permission_classes.py +++ b/backend/root/custom_classes/permission_classes.py @@ -14,7 +14,6 @@ class SuperUserPermission(BasePermission): - def has_permission(self, request: Request, view: APIView) -> bool: # noqa: PLR0917 user: User = request.user return user.is_active and user.is_superuser diff --git a/backend/root/db_router.py b/backend/root/db_router.py index 4ee9f6db9..9fb9a8268 100644 --- a/backend/root/db_router.py +++ b/backend/root/db_router.py @@ -23,7 +23,6 @@ class SamfundetDatabaseRouter: - def db_for_read(self, model: Type[models.Model], **hints: dict[str, Any]) -> str | None: if model in BILLIG_MODELS: return 'billig' diff --git a/backend/root/management/commands/cleanstart.py b/backend/root/management/commands/cleanstart.py index d48b2014b..b653bff92 100644 --- a/backend/root/management/commands/cleanstart.py +++ b/backend/root/management/commands/cleanstart.py @@ -8,7 +8,6 @@ class Command(BaseCommand): - def add_arguments(self, parser): parser.add_argument( '--noinput', diff --git a/backend/root/management/commands/deletemigrations.py b/backend/root/management/commands/deletemigrations.py index 10f76bbda..86b1e3e85 100644 --- a/backend/root/management/commands/deletemigrations.py +++ b/backend/root/management/commands/deletemigrations.py @@ -12,7 +12,6 @@ class Command(BaseCommand): - def add_arguments(self, parser): parser.add_argument( '--noinput', diff --git a/backend/root/management/commands/deploymigrations.py b/backend/root/management/commands/deploymigrations.py index 4e58d35b4..18ec99279 100644 --- a/backend/root/management/commands/deploymigrations.py +++ b/backend/root/management/commands/deploymigrations.py @@ -8,7 +8,6 @@ class Command(BaseCommand): - def add_arguments(self, parser): parser.add_argument( '--noinput', diff --git a/backend/root/management/commands/generate_permissions.py b/backend/root/management/commands/generate_permissions.py index 55a0ce921..f8ddfbb70 100644 --- a/backend/root/management/commands/generate_permissions.py +++ b/backend/root/management/commands/generate_permissions.py @@ -4,6 +4,7 @@ from django.utils import timezone from django.contrib.auth.models import Permission from django.core.management.base import BaseCommand + """ NOTE: This command cannot run within docker container because the backend has no access to the frontend. Use on host machine. diff --git a/backend/root/management/commands/migratezero.py b/backend/root/management/commands/migratezero.py index 07d401f3b..d404bc231 100644 --- a/backend/root/management/commands/migratezero.py +++ b/backend/root/management/commands/migratezero.py @@ -9,7 +9,6 @@ class Command(BaseCommand): - def add_arguments(self, parser): parser.add_argument( '--noinput', diff --git a/backend/root/management/commands/seed.py b/backend/root/management/commands/seed.py index 4202c7f98..b724bcb62 100755 --- a/backend/root/management/commands/seed.py +++ b/backend/root/management/commands/seed.py @@ -56,8 +56,7 @@ def run_seed_script(*, target: tuple, index: int, count: int): elif type(step) in [int, float]: print_progress(progress=step, prefix=prefix, start_time=start_time) else: - raise Exception(f"Seed script {target[0]} yielded wrong type '{type(step)}', " - 'expected number type or tuple of (number, str)') + raise Exception(f"Seed script {target[0]} yielded wrong type '{type(step)}', " 'expected number type or tuple of (number, str)') # Final output 100%. if isinstance(step, tuple): diff --git a/backend/root/management/commands/seed_scripts/events.py b/backend/root/management/commands/seed_scripts/events.py index 77220be37..10e9176fc 100755 --- a/backend/root/management/commands/seed_scripts/events.py +++ b/backend/root/management/commands/seed_scripts/events.py @@ -150,7 +150,8 @@ def do_seed(): name_nb=f'Billett {i + 1}', name_en=f'Ticket {i + 1}', price=random.randint(50, 200), - ) for i in range(0, random.randint(2, 4)) + ) + for i in range(0, random.randint(2, 4)) ] # Create event(s) diff --git a/backend/root/management/commands/seed_scripts/gangs.py b/backend/root/management/commands/seed_scripts/gangs.py index 0b601ba8c..0810c1644 100755 --- a/backend/root/management/commands/seed_scripts/gangs.py +++ b/backend/root/management/commands/seed_scripts/gangs.py @@ -9,23 +9,21 @@ ('Lørdagskomiteen', 'LØK'), ('Klubbstyret', 'KLST'), ], - 'Drift': - [ - ('Markedsføringsgjengen', 'MG'), - ('Fotogjengen', 'FG'), - ('Diversegjengen', 'DG'), - ('Forsterkerkomiteen', 'FK'), - ('Regi', None), - ('Videokomiteen', 'VK'), - ], - 'Kunstneriske': - [ - ('Studentersamfundets interne teater', 'SIT'), - ('Studentersamfundets Symfoniorkester', 'Symforch'), - ('Strindens promenadeorkester', 'SPO'), - ('Pirum', None), - ('Candiss', None), - ], + 'Drift': [ + ('Markedsføringsgjengen', 'MG'), + ('Fotogjengen', 'FG'), + ('Diversegjengen', 'DG'), + ('Forsterkerkomiteen', 'FK'), + ('Regi', None), + ('Videokomiteen', 'VK'), + ], + 'Kunstneriske': [ + ('Studentersamfundets interne teater', 'SIT'), + ('Studentersamfundets Symfoniorkester', 'Symforch'), + ('Strindens promenadeorkester', 'SPO'), + ('Pirum', None), + ('Candiss', None), + ], 'Styrende': [ ('Finansstyret', 'FS'), ('Styret', None), diff --git a/backend/root/management/commands/seed_scripts/images.py b/backend/root/management/commands/seed_scripts/images.py index 5b148d88c..727ccfbc6 100644 --- a/backend/root/management/commands/seed_scripts/images.py +++ b/backend/root/management/commands/seed_scripts/images.py @@ -40,10 +40,14 @@ def do_seed(): random_image = ImageFile(image_file, name=f'img_{i}') title = words(random.randint(1, 2)) image = Image.objects.create(title=title, image=random_image) - image.tags.set(random.choices( - Tag.objects.all().values_list(flat=True, ), - k=random.randint(1, 4), - )) + image.tags.set( + random.choices( + Tag.objects.all().values_list( + flat=True, + ), + k=random.randint(1, 4), + ) + ) yield int(i / COUNT * 100), 'Creating images' # Remember to close files! diff --git a/backend/root/management/commands/seed_scripts/menu.py b/backend/root/management/commands/seed_scripts/menu.py index 30469418c..a6159a024 100755 --- a/backend/root/management/commands/seed_scripts/menu.py +++ b/backend/root/management/commands/seed_scripts/menu.py @@ -38,10 +38,13 @@ def seed(): FoodPreference.objects.all().delete() # Create food preferences - prefs = [FoodPreference.objects.create( - name_nb=p_name[0], - name_en=p_name[1], - ) for p_name in preferences] + prefs = [ + FoodPreference.objects.create( + name_nb=p_name[0], + name_en=p_name[1], + ) + for p_name in preferences + ] yield 10, f'Created {len(preferences)} food preferences' # Create menu categories diff --git a/backend/root/management/commands/seed_scripts/recruitment_admissions.py b/backend/root/management/commands/seed_scripts/recruitment_admissions.py index ac1ab854a..d777be46d 100644 --- a/backend/root/management/commands/seed_scripts/recruitment_admissions.py +++ b/backend/root/management/commands/seed_scripts/recruitment_admissions.py @@ -27,8 +27,7 @@ def seed(): { 'recruitment_position': position, 'recruitment': position.recruitment, - 'user': users[randint(0, - len(users) - 1)], # random user from all users + 'user': users[randint(0, len(users) - 1)], # random user from all users } ) _admission, created = RecruitmentAdmission.objects.get_or_create(**admission_data) diff --git a/backend/root/management/commands/seed_scripts/samf3.py b/backend/root/management/commands/seed_scripts/samf3.py index 715d226ec..9232d864f 100755 --- a/backend/root/management/commands/seed_scripts/samf3.py +++ b/backend/root/management/commands/seed_scripts/samf3.py @@ -123,7 +123,7 @@ def seed() -> Iterator[tuple[int, str]]: with open(event_path, 'r') as event_file: with open(image_path, 'r') as image_file: events = list(reversed(list(csv.DictReader(event_file)))) - events = events[0:min(max_events, len(events))] + events = events[0 : min(max_events, len(events))] images = list(csv.DictReader(image_file)) event_models = [] @@ -132,7 +132,7 @@ def seed() -> Iterator[tuple[int, str]]: for chunk in range(len(events) // chunk_size): start = chunk * chunk_size - events_in_chunk = events[start:min(start + chunk_size, len(events))] + events_in_chunk = events[start : min(start + chunk_size, len(events))] jobs = [(images, event) for event in events_in_chunk] models = pool.starmap(add_event, jobs) models = [e for e in models if e is not None] diff --git a/backend/root/management/commands/seed_scripts/textitems.py b/backend/root/management/commands/seed_scripts/textitems.py index a9b90e79c..81c7ce8a1 100644 --- a/backend/root/management/commands/seed_scripts/textitems.py +++ b/backend/root/management/commands/seed_scripts/textitems.py @@ -38,23 +38,17 @@ def seed(): 'text_en': 'Do you have any questions or want to get in touch with us? Don"t hesitate to contact us!', }, { - 'key': - 'sulten_reservation_help', - 'text_nb': - """Bord må reserveres minst en dag i forveien. Mat kan forhåndsbestilles slik at dere ikke trenger å vente når dere kommer. + 'key': 'sulten_reservation_help', + 'text_nb': """Bord må reserveres minst en dag i forveien. Mat kan forhåndsbestilles slik at dere ikke trenger å vente når dere kommer. Merk at flertallet av personer må være medlem for å reservere og at alle må være over 20 år etter kl 20:00 i helger.""", - 'text_en': - """Tables must be reserved at least one day in advance. Food can be pre-ordered so you do not have to wait when you arrive. + 'text_en': """Tables must be reserved at least one day in advance. Food can be pre-ordered so you do not have to wait when you arrive. Note that the majority of people must be a member of the Student Society to reserve and that all must be over 20 years after 20:00 on weekends.""", }, { - 'key': - 'sulten_reservation_contact', - 'text_nb': - 'Reservasjonssystemet vårt er fortsatt under utvikling, og vi ber om forbehold om at feil kan forekomme. Klikk her for å bestille via epost: ', - 'text_en': - 'Our reservation system is still under development, and reservation errors may therefore occur. Click here to order via email: ', + 'key': 'sulten_reservation_contact', + 'text_nb': 'Reservasjonssystemet vårt er fortsatt under utvikling, og vi ber om forbehold om at feil kan forekomme. Klikk her for å bestille via epost: ', + 'text_en': 'Our reservation system is still under development, and reservation errors may therefore occur. Click here to order via email: ', }, ] diff --git a/backend/root/management/commands/setupmigrations.py b/backend/root/management/commands/setupmigrations.py index ea3c6242e..29001c5e0 100644 --- a/backend/root/management/commands/setupmigrations.py +++ b/backend/root/management/commands/setupmigrations.py @@ -10,7 +10,6 @@ class Command(BaseCommand): - def handle(self, *args, **options): for app in settings.PROJECT_APPS: try: diff --git a/backend/root/management/commands/setupmigrations2.py b/backend/root/management/commands/setupmigrations2.py index bd1dcc45b..d7016da01 100644 --- a/backend/root/management/commands/setupmigrations2.py +++ b/backend/root/management/commands/setupmigrations2.py @@ -9,7 +9,6 @@ class Command(BaseCommand): - def handle(self, *args, **options): for app in settings.INSTALLED_APPS: appname = app.split('.')[-1] diff --git a/backend/root/settings/__init__.py b/backend/root/settings/__init__.py index e5e20c2d7..7ece708e7 100644 --- a/backend/root/settings/__init__.py +++ b/backend/root/settings/__init__.py @@ -12,8 +12,7 @@ # Raise exception if ENV is invalid (and show possible options). if ENV not in Environment.VALID: ENV_OPTIONS = ''.join([f'\n\t{env}' for env in Environment.VALID]) - raise Exception(f"Environment variable 'ENV' is required to import this module ('{__name__}')." - f'Possible values: {ENV_OPTIONS}') + raise Exception(f"Environment variable 'ENV' is required to import this module ('{__name__}')." f'Possible values: {ENV_OPTIONS}') if ENV == Environment.DEV: from .dev import * # type: ignore[assignment] # noqa: F403 diff --git a/backend/root/settings/base.py b/backend/root/settings/base.py index f613da71f..c1fb44bbf 100644 --- a/backend/root/settings/base.py +++ b/backend/root/settings/base.py @@ -113,16 +113,14 @@ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, - 'OPTIONS': - { - 'context_processors': - [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, }, ] @@ -172,13 +170,12 @@ # 'rest_framework.permissions.IsAuthenticated', 'rest_framework.authentication.SessionAuthentication', ], - 'DEFAULT_PERMISSION_CLASSES': - [ - # 'rest_framework.permissions.IsAuthenticated', - # 'rest_framework.permissions.DjangoObjectPermissions', - 'root.custom_classes.permission_classes.SuperUserPermission', - # 'root.custom_classes.permission_classes.CustomDjangoObjectPermissions', - ], + 'DEFAULT_PERMISSION_CLASSES': [ + # 'rest_framework.permissions.IsAuthenticated', + # 'rest_framework.permissions.DjangoObjectPermissions', + 'root.custom_classes.permission_classes.SuperUserPermission', + # 'root.custom_classes.permission_classes.CustomDjangoObjectPermissions', + ], } ### End: DRF ### @@ -217,97 +214,91 @@ LOGGING = { 'version': 1, 'disable_existing_loggers': False, - 'formatters': - { - 'json': { - # Need to be a callable in order to use init parameters. - '()': lambda: JsonFormatter(indent=4 if ENV == Environment.DEV else None), - }, - 'file': { - 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - }, + 'formatters': { + 'json': { + # Need to be a callable in order to use init parameters. + '()': lambda: JsonFormatter(indent=4 if ENV == Environment.DEV else None), + }, + 'file': { + 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', + }, + }, + 'filters': { + 'request_context_filter': { + '()': RequestContextFilter, + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'null': { + 'class': 'logging.NullHandler', }, - 'filters': - { - 'request_context_filter': { - '()': RequestContextFilter, - }, - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'filters': ['require_debug_true'], }, - 'handlers': - { - 'null': { - 'class': 'logging.NullHandler', - }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'filters': ['require_debug_true'], - }, - 'file': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'formatter': 'json', - 'filename': LOGFILENAME, - 'filters': ['request_context_filter'], - }, - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler', - 'filters': ['require_debug_false'], - }, - 'humio': - { - 'level': 'DEBUG' if ENV == Environment.DEV else 'INFO', - 'formatter': 'json', - 'class': 'logging.StreamHandler', - 'stream': sys.stdout, - 'filters': ['request_context_filter'], - }, - 'sql_file': - { - 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'mode': 'w', - 'filename': SQL_LOG_FILE, # Added to '.gitignore'. - 'filters': ['require_debug_true'], - }, + 'file': { + 'level': 'INFO', + 'class': 'logging.FileHandler', + 'formatter': 'json', + 'filename': LOGFILENAME, + 'filters': ['request_context_filter'], }, - 'loggers': - { - # Default logger. - '': { - 'handlers': ['humio', 'file'], - 'propagate': True, - 'level': 'INFO', - }, - # Catch all from django unless explicitly prevented propagation. - 'django': { - 'handlers': ['console', 'mail_admins'], - 'propagate': True, - 'level': 'DEBUG', - }, - 'django.db.backends': { - 'handlers': ['sql_file'], - 'propagate': False, # Don't pass up to 'django'. - 'level': 'DEBUG', - }, - 'django.server': { - 'handlers': ['console'], - 'propagate': False, # Don't pass up to 'django'. - 'level': 'INFO', - }, - 'django.utils.autoreload': { - 'handlers': ['console'], - 'propagate': False, # Don't pass up to 'django'. - 'level': 'INFO', - }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], }, + 'humio': { + 'level': 'DEBUG' if ENV == Environment.DEV else 'INFO', + 'formatter': 'json', + 'class': 'logging.StreamHandler', + 'stream': sys.stdout, + 'filters': ['request_context_filter'], + }, + 'sql_file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'mode': 'w', + 'filename': SQL_LOG_FILE, # Added to '.gitignore'. + 'filters': ['require_debug_true'], + }, + }, + 'loggers': { + # Default logger. + '': { + 'handlers': ['humio', 'file'], + 'propagate': True, + 'level': 'INFO', + }, + # Catch all from django unless explicitly prevented propagation. + 'django': { + 'handlers': ['console', 'mail_admins'], + 'propagate': True, + 'level': 'DEBUG', + }, + 'django.db.backends': { + 'handlers': ['sql_file'], + 'propagate': False, # Don't pass up to 'django'. + 'level': 'DEBUG', + }, + 'django.server': { + 'handlers': ['console'], + 'propagate': False, # Don't pass up to 'django'. + 'level': 'INFO', + }, + 'django.utils.autoreload': { + 'handlers': ['console'], + 'propagate': False, # Don't pass up to 'django'. + 'level': 'INFO', + }, + }, } # Quick fix for avoiding concurrency issues related to db access diff --git a/backend/root/settings/prod.py b/backend/root/settings/prod.py index 1555e49d1..cd712e696 100644 --- a/backend/root/settings/prod.py +++ b/backend/root/settings/prod.py @@ -27,23 +27,21 @@ DATABASES = { # The default database used for all Samf4 models - 'default': - { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ['DB_NAME'], - 'USER': os.environ['DB_USER'], - 'PASSWORD': os.environ['DB_PASSWORD'], - 'HOST': os.environ['DB_HOST'], - 'PORT': os.environ['DB_PORT'], - }, + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ['DB_NAME'], + 'USER': os.environ['DB_USER'], + 'PASSWORD': os.environ['DB_PASSWORD'], + 'HOST': os.environ['DB_HOST'], + 'PORT': os.environ['DB_PORT'], + }, # The database for the billig system - 'billig': - { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ['BILLIG_DB_NAME'], - 'USER': os.environ['BILLIG_DB_USER'], - 'PASSWORD': os.environ['BILLIG_DB_PASSWORD'], - 'HOST': os.environ['BILLIG_DB_HOST'], - 'PORT': os.environ['BILLIG_DB_PORT'], - }, + 'billig': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ['BILLIG_DB_NAME'], + 'USER': os.environ['BILLIG_DB_USER'], + 'PASSWORD': os.environ['BILLIG_DB_PASSWORD'], + 'HOST': os.environ['BILLIG_DB_HOST'], + 'PORT': os.environ['BILLIG_DB_PORT'], + }, } diff --git a/backend/root/utils/mixins.py b/backend/root/utils/mixins.py index fe52dd818..4ed6c8c4d 100644 --- a/backend/root/utils/mixins.py +++ b/backend/root/utils/mixins.py @@ -140,9 +140,7 @@ def save(self, *args: Any, **kwargs: Any) -> None: else: # Log changes. LOG.info(f'{self} has changed:\n\nold: {dirty_fields_old}\n\n new:{dirty_fields_new}') LOG.info( - f'{self} has changed:\n\n' - f'old: {self.ftm_log_parse(fields=dirty_fields_old)}\n\n' - f'new:{self.ftm_log_parse(fields=dirty_fields_new)}' + f'{self} has changed:\n\n' f'old: {self.ftm_log_parse(fields=dirty_fields_old)}\n\n' f'new:{self.ftm_log_parse(fields=dirty_fields_new)}' ) except Exception as e: # Get all changes. diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index afbed63ac..01aff33bd 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -22,6 +22,7 @@ from samfundet.models.general import Gang, User, Image, Table, Venue, BlogPost, TextItem, Reservation, Organization, InformationPage from samfundet.models.recruitment import Recruitment, RecruitmentPosition, RecruitmentAdmission from samfundet.models.model_choices import EventTicketType, EventAgeRestriction, RecruitmentStatusChoices, RecruitmentPriorityChoices + """ This module contains fixtures available in pytests. These do not need to be imported. diff --git a/backend/samfundet/homepage/homepage.py b/backend/samfundet/homepage/homepage.py index 32ecc5fe5..535d4c485 100644 --- a/backend/samfundet/homepage/homepage.py +++ b/backend/samfundet/homepage/homepage.py @@ -62,7 +62,7 @@ def generate() -> dict[str, Any]: # Splash events # TODO we should make a datamodel for this try: - splash_events = list(upcoming_events[0:min(3, len(upcoming_events))]) + splash_events = list(upcoming_events[0 : min(3, len(upcoming_events))]) splash = EventSerializer(splash_events, many=True).data except IndexError: splash = [] @@ -70,11 +70,13 @@ def generate() -> dict[str, Any]: # Upcoming events try: - elements.append(carousel( - title_nb='Hva skjer?', - title_en="What's happening?", - events=list(upcoming_events[:10]), - )) + elements.append( + carousel( + title_nb='Hva skjer?', + title_en="What's happening?", + events=list(upcoming_events[:10]), + ) + ) except IndexError: pass @@ -89,11 +91,13 @@ def generate() -> dict[str, Any]: # Concerts try: - elements.append(carousel( - title_nb='Konserter', - title_en='Concerts', - events=list(upcoming_events.filter(category=EventCategory.CONCERT)[:10]), - )) + elements.append( + carousel( + title_nb='Konserter', + title_en='Concerts', + events=list(upcoming_events.filter(category=EventCategory.CONCERT)[:10]), + ) + ) except IndexError: pass @@ -108,11 +112,13 @@ def generate() -> dict[str, Any]: # Debates try: - elements.append(carousel( - title_nb='Debatter', - title_en='Debates', - events=list(upcoming_events.filter(category=EventCategory.DEBATE)[:10]), - )) + elements.append( + carousel( + title_nb='Debatter', + title_en='Debates', + events=list(upcoming_events.filter(category=EventCategory.DEBATE)[:10]), + ) + ) except IndexError: pass diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index ea98a162c..b80d546a0 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -33,7 +33,6 @@ class Notification(AbstractNotification): - class Meta(AbstractNotification.Meta): abstract = False diff --git a/backend/samfundet/models/tests/test_recruitment.py b/backend/samfundet/models/tests/test_recruitment.py index e2822e8d0..ca75b3164 100644 --- a/backend/samfundet/models/tests/test_recruitment.py +++ b/backend/samfundet/models/tests/test_recruitment.py @@ -44,7 +44,6 @@ def _create_recruitment_with_dt(*, overrides: dict[str, timezone.datetime]) -> R class TestRecruitmentClean: - def test_all_datetimes_is_in_the_future(self, fixture_org): error_msg = 'All times should be in the future' past = timezone.now() - timezone.timedelta(days=2) @@ -73,7 +72,6 @@ def test_reprioritization_deadline_for_applicant_before_reprioritization_deadlin class TestRecruitmentAdmission: - def test_check_withdraw_sets_unwanted(self, fixture_recruitment_admission: RecruitmentAdmission): assert fixture_recruitment_admission.recruiter_status == RecruitmentStatusChoices.NOT_SET assert fixture_recruitment_admission.recruiter_priority == RecruitmentPriorityChoices.NOT_SET diff --git a/backend/samfundet/models/tests/test_reservation.py b/backend/samfundet/models/tests/test_reservation.py index 3844cd26e..7543de4ce 100644 --- a/backend/samfundet/models/tests/test_reservation.py +++ b/backend/samfundet/models/tests/test_reservation.py @@ -6,7 +6,6 @@ class TestReservation: - def test_check_fetches_times( self, fixture_venue: Venue, diff --git a/backend/samfundet/models/utils/fields.py b/backend/samfundet/models/utils/fields.py index 38bd22201..aed8075bf 100644 --- a/backend/samfundet/models/utils/fields.py +++ b/backend/samfundet/models/utils/fields.py @@ -9,13 +9,11 @@ class LowerCaseField(models.CharField): - def to_python(self, value: str) -> str: return super().to_python(value.lower()) class PhoneNumberField(models.CharField): - def __init__(self, *args: Any, **kwargs: Any) -> None: kwargs['max_length'] = 15 self.validators = [ diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 5dfbe9e61..fbb10cac2 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -55,7 +55,6 @@ class TagSerializer(CustomBaseSerializer): - class Meta: model = Tag fields = '__all__' @@ -99,14 +98,12 @@ def get_url(self, image: Image) -> str: class EventCustomTicketSerializer(CustomBaseSerializer): - class Meta: model = EventCustomTicket fields = '__all__' class BilligPriceGroupSerializer(CustomBaseSerializer): - class Meta: model = BilligPriceGroup fields = ['id', 'name', 'can_be_put_on_card', 'membership_needed', 'netsale', 'price'] @@ -167,7 +164,6 @@ def to_representation(self, events: list[Event] | QuerySet[Event]) -> list[str]: class EventSerializer(CustomBaseSerializer): - class Meta: model = Event list_serializer_class = EventListSerializer @@ -199,21 +195,18 @@ def create(self, validated_data: dict) -> Event: class EventGroupSerializer(CustomBaseSerializer): - class Meta: model = EventGroup fields = '__all__' class VenueSerializer(CustomBaseSerializer): - class Meta: model = Venue fields = '__all__' class ClosedPeriodSerializer(CustomBaseSerializer): - class Meta: model = ClosedPeriod fields = '__all__' @@ -313,28 +306,24 @@ def validate(self, attrs: dict) -> dict: class GroupSerializer(serializers.ModelSerializer): - class Meta: model = Group fields = '__all__' class ProfileSerializer(serializers.ModelSerializer): - class Meta: model = Profile fields = ['id', 'nickname'] class UserPreferenceSerializer(serializers.ModelSerializer): - class Meta: model = UserPreference fields = '__all__' class CampusSerializer(serializers.ModelSerializer): - class Meta: model = Campus fields = '__all__' @@ -384,14 +373,12 @@ def get_user_preference(self, user: User) -> dict: # GANGS ### class OrganizationSerializer(CustomBaseSerializer): - class Meta: model = Organization fields = '__all__' class GangSerializer(CustomBaseSerializer): - class Meta: model = Gang fields = '__all__' @@ -406,14 +393,12 @@ class Meta: class InformationPageSerializer(CustomBaseSerializer): - class Meta: model = InformationPage fields = '__all__' class BlogPostSerializer(CustomBaseSerializer): - class Meta: model = BlogPost fields = '__all__' @@ -448,21 +433,18 @@ def create(self, validated_data: dict) -> Event: class TextItemSerializer(serializers.ModelSerializer): - class Meta: model = TextItem fields = '__all__' class InfoboxSerializer(CustomBaseSerializer): - class Meta: model = Infobox fields = '__all__' class KeyValueSerializer(serializers.ModelSerializer): - class Meta: model = KeyValue fields = '__all__' @@ -474,14 +456,12 @@ class Meta: class FoodPreferenceSerializer(CustomBaseSerializer): - class Meta: model = FoodPreference fields = '__all__' class FoodCategorySerializer(CustomBaseSerializer): - class Meta: model = FoodCategory fields = '__all__' @@ -504,21 +484,18 @@ class Meta: class TableSerializer(CustomBaseSerializer): - class Meta: model = Table fields = '__all__' class ReservationSerializer(CustomBaseSerializer): - class Meta: model = Reservation fields = '__all__' class ReservationCheckSerializer(serializers.ModelSerializer): - class Meta: model = Reservation fields = ['guest_count', 'occasion', 'reservation_date'] @@ -539,7 +516,6 @@ class Meta: class RecruitmentSerializer(CustomBaseSerializer): - class Meta: model = Recruitment fields = '__all__' @@ -565,7 +541,6 @@ def get_recruitment_admission_ids(self, obj: User) -> list[int]: class InterviewerSerializer(CustomBaseSerializer): - class Meta: model = User fields = [ @@ -613,6 +588,7 @@ def update(self, instance: RecruitmentPosition, validated_data: dict) -> Recruit self._update_interviewers(recruitment_position=updated_instance, interviewer_objects=interviewer_objects) return updated_instance + class ApplicantInterviewSerializer(serializers.ModelSerializer): class Meta: model = Interview @@ -676,7 +652,6 @@ def create(self, validated_data: dict) -> RecruitmentAdmission: class OccupiedtimeslotSerializer(serializers.ModelSerializer): - class Meta: model = Occupiedtimeslot fields = '__all__' @@ -691,14 +666,12 @@ class Meta: class InterviewRoomSerializer(CustomBaseSerializer): - class Meta: model = InterviewRoom fields = '__all__' class InterviewSerializer(CustomBaseSerializer): - class Meta: model = Interview fields = '__all__' diff --git a/backend/samfundet/tests/test_signals.py b/backend/samfundet/tests/test_signals.py index 5c6721304..c3cacf56b 100644 --- a/backend/samfundet/tests/test_signals.py +++ b/backend/samfundet/tests/test_signals.py @@ -8,7 +8,6 @@ class TestUserSignals: - def test_create_user_preference(self): ### Arrange ### user = User.objects.create_user( @@ -45,7 +44,6 @@ def test_create_profile(self): class TestEditorPermissions: - def test_update_editor_permissions_add( self, fixture_event: Event, diff --git a/backend/samfundet/tests/test_views.py b/backend/samfundet/tests/test_views.py index bfccc42cb..94f93838e 100644 --- a/backend/samfundet/tests/test_views.py +++ b/backend/samfundet/tests/test_views.py @@ -114,7 +114,6 @@ def test_get_groups(fixture_rest_client: APIClient, fixture_user: User): class TestInformationPagesView: - def test_get_informationpage( self, fixture_rest_client: APIClient, @@ -286,7 +285,6 @@ def test_updated_and_created_by(self, fixture_rest_client: APIClient, fixture_us class TestBlogPostView: - def test_get_blogpost( self, fixture_rest_client: APIClient, @@ -382,7 +380,6 @@ def test_put_blogpost( class TestKeyValueView: - def test_anyone_can_retrieve_keyvalues(self, fixture_rest_client: APIClient): ### Arrange ### keyvalue = KeyValue.objects.create(key='FOO', value='bar') @@ -432,7 +429,6 @@ def test_crud_not_possible(self, fixture_rest_client: APIClient, fixture_superus class TestTextItemView: - def test_anyone_can_retrieve_textitems(self, fixture_rest_client: APIClient, fixture_text_item: TextItem): ### Arrange ### url = reverse(routes.samfundet__text_item_detail, kwargs={'pk': fixture_text_item.key}) @@ -478,7 +474,6 @@ def test_crud_not_possible(self, fixture_rest_client: APIClient, fixture_superus class TestAssignGroupView: - def test_assign_group( self, fixture_rest_client: APIClient, @@ -683,4 +678,4 @@ def test_recruitment_admission_for_applicant( # Assert the returned data based on the logic in the view assert len(response.data) == 1 assert response.data[0]['admission_text'] == fixture_recruitment_admission.admission_text - assert response.data[0]['recruitment_position'] == fixture_recruitment_admission.recruitment_position.id + assert response.data[0]['recruitment_position']['id'] == fixture_recruitment_admission.recruitment_position.id diff --git a/backend/samfundet/utils.py b/backend/samfundet/utils.py index 0d57cdfc0..a92c778fe 100644 --- a/backend/samfundet/utils.py +++ b/backend/samfundet/utils.py @@ -15,9 +15,14 @@ def event_query(*, query: QueryDict, events: QuerySet[Event] = None) -> QuerySet search = query.get('search', None) if search: events = events.filter( - Q(title_nb__icontains=search) | Q(title_en__icontains=search) | Q(description_long_nb__icontains=search) | - Q(description_long_en__icontains=search) | Q(description_short_en=search) | Q(description_short_nb=search) | Q(location__icontains=search) | - Q(event_group__name=search) + Q(title_nb__icontains=search) + | Q(title_en__icontains=search) + | Q(description_long_nb__icontains=search) + | Q(description_long_en__icontains=search) + | Q(description_short_en=search) + | Q(description_short_nb=search) + | Q(location__icontains=search) + | Q(event_group__name=search) ) event_group = query.get('event_group', None) if event_group: diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 8248089c9..a4184e24a 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -145,14 +145,14 @@ class KeyValueView(ReadOnlyModelViewSet): # Images class ImageView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = ImageSerializer queryset = Image.objects.all().order_by('-pk') # Image tags class TagView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = TagSerializer queryset = Tag.objects.all() @@ -163,7 +163,7 @@ class TagView(ModelViewSet): class EventView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = EventSerializer queryset = Event.objects.all() @@ -196,7 +196,7 @@ def get(self, request: Request) -> Response: class EventGroupView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = EventGroupSerializer queryset = EventGroup.objects.all() @@ -207,14 +207,14 @@ class EventGroupView(ModelViewSet): class VenueView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = VenueSerializer queryset = Venue.objects.all() lookup_field = 'slug' class ClosedPeriodView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = ClosedPeriodSerializer queryset = ClosedPeriod.objects.all() @@ -231,49 +231,49 @@ def get_queryset(self) -> QuerySet: class BookingView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = BookingSerializer queryset = Booking.objects.all() class SaksdokumentView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = SaksdokumentSerializer queryset = Saksdokument.objects.all() class OrganizationView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = OrganizationSerializer queryset = Organization.objects.all() class GangView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = GangSerializer queryset = Gang.objects.all() class GangTypeView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = GangTypeSerializer queryset = GangType.objects.all() class InformationPageView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = InformationPageSerializer queryset = InformationPage.objects.all() class InfoboxView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = InfoboxSerializer queryset = Infobox.objects.all() class BlogPostView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = BlogPostSerializer queryset = BlogPost.objects.all() @@ -284,31 +284,31 @@ class BlogPostView(ModelViewSet): class MenuView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = MenuSerializer queryset = Menu.objects.all() class MenuItemView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = MenuItemSerializer queryset = MenuItem.objects.all() class FoodCategoryView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = FoodCategorySerializer queryset = FoodCategory.objects.all() class FoodPreferenceView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = FoodPreferenceSerializer queryset = FoodPreference.objects.all() class TableView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = TableSerializer queryset = Table.objects.all() @@ -412,7 +412,7 @@ def get(self, request: Request) -> Response: class AllUsersView(ListAPIView): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = UserSerializer queryset = User.objects.all() @@ -428,7 +428,7 @@ def post(self, request: Request) -> Response: class AllGroupsView(ListAPIView): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = GroupSerializer queryset = Group.objects.all() @@ -449,7 +449,7 @@ class UserPreferenceView(ModelViewSet): class ProfileView(ModelViewSet): - permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) serializer_class = ProfileSerializer queryset = Profile.objects.all() From 4017879a52c24d560a1f7a8e400979ddf7b140ff Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 19:38:38 +0100 Subject: [PATCH 13/20] mmore ruff --- backend/samfundet/conftest.py | 1 - backend/samfundet/model_choices.py | 15 --------------- backend/samfundet/models/recruitment.py | 4 +--- 3 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 backend/samfundet/model_choices.py diff --git a/backend/samfundet/conftest.py b/backend/samfundet/conftest.py index 01aff33bd..67d609670 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -11,7 +11,6 @@ from django.utils import timezone from django.core.files.images import ImageFile from django.contrib.auth.models import Group -from samfundet.model_choices import RecruitmentPriorityChoices, RecruitmentStatusChoices import root.management.commands.seed_scripts.billig as billig_seed from root.settings import BASE_DIR diff --git a/backend/samfundet/model_choices.py b/backend/samfundet/model_choices.py deleted file mode 100644 index 419f18e06..000000000 --- a/backend/samfundet/model_choices.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import models - - -class RecruitmentPriorityChoices(models.IntegerChoices): - NOT_SET = 0, 'Not Set' - NOT_WANTED = 1, 'Not Wanted' - WANTED = 2, 'Wanted' - RESERVE = 3, 'Reserve' - - -class RecruitmentStatusChoices(models.IntegerChoices): - NOT_SET = 0, 'Not Set' - CALLED_AND_ACCEPTED = 1, 'Called and Accepted' - CALLED_AND_REJECTED = 2, 'Called and Rejected' - AUTOMATIC_REJECTION = 3, 'Automatic Rejection' diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 27a530e1f..ec18bc522 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -184,9 +184,7 @@ def __str__(self) -> str: return f'Admission: {self.user} for {self.recruitment_position} in {self.recruitment}' def save(self, *args: tuple, **kwargs: dict) -> None: - """ - If the admission is saved without an interview, try to find an interview from a shared position. - """ + """If the admission is saved without an interview, try to find an interview from a shared position.""" if not self.applicant_priority: current_applications_count = RecruitmentAdmission.objects.filter(user=self.user).count() # Set the applicant_priority to the number of applications + 1 (for the current application) From 3a356aaf507a6bedb54621b39d274dd6b100ffdc Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 19:44:19 +0100 Subject: [PATCH 14/20] more ruff --- backend/samfundet/models/recruitment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index ec18bc522..039ec50fd 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -4,6 +4,8 @@ from __future__ import annotations import uuid +from .general import Gang, User, Organization + from django.db import models from django.utils import timezone from django.core.exceptions import ValidationError @@ -12,8 +14,6 @@ from samfundet.models.model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices -from .general import Gang, User, Organization - class Recruitment(CustomBaseModel): name_nb = models.CharField(max_length=100, help_text='Name of the recruitment') From 5b334f853a0f1483dcd070aabf6cf4dfea51afa9 Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 19:49:05 +0100 Subject: [PATCH 15/20] mmore ruff :) --- backend/samfundet/models/recruitment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 039ec50fd..eb21ce1d2 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -12,7 +12,7 @@ from root.utils.mixins import CustomBaseModel, FullCleanSaveMixin -from samfundet.models.model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices +from .model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices class Recruitment(CustomBaseModel): From f4d26c3b07e6b5a2812678c0d1bf4bd78b95bfde Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 20:07:47 +0100 Subject: [PATCH 16/20] fix traslat --- .../ApplicantApplicationOverviewPage.tsx | 14 +++++++------- .../src/Pages/RecruitmentPage/RecruitmentPage.tsx | 6 +++++- frontend/src/i18n/constants.ts | 3 +++ frontend/src/i18n/translations.ts | 15 +++++++++++---- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx index 512ad229b..15ed72d32 100644 --- a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx @@ -63,10 +63,10 @@ export function ApplicantApplicationOverviewPage() { }, [admissions]); const tableColumns = [ - { sortable: false, content: 'Recruitment Position' }, - { sortable: false, content: 'Interview Date' }, - { sortable: false, content: 'Interview Location' }, - { sortable: true, content: 'Applicant Priority' }, + { sortable: false, content: t(KEY.recruitment_position) }, + { sortable: false, content: t(KEY.recruitment_interview_time) }, + { sortable: false, content: t(KEY.recruitment_interview_location) }, + { sortable: true, content: t(KEY.recruitment_priority) }, { sortable: false, content: '' }, ]; @@ -87,14 +87,14 @@ export function ApplicantApplicationOverviewPage() { -

My applications

+

{t(KEY.recruitment_my_applications)}

-

All info related to the applications will be anonymized three weeks after the recruitment is over

+

{t(KEY.recruitment_will_be_anonymized)}

{admissions ? (
) : ( -

You have not applied to any positions yet

+

{t(KEY.recruitment_not_applied)}

)} diff --git a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx index 13b927033..05a59f700 100644 --- a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx +++ b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx @@ -8,8 +8,12 @@ import { OccupiedFormModal } from '~/Components/OccupiedForm'; import { reverse } from '~/named-urls'; import { useCustomNavigate } from '~/hooks'; import { ROUTES } from '~/routes'; +import { useTranslation } from 'react-i18next'; +import { KEY } from '~/i18n/constants'; export function RecruitmentPage() { + const { t } = useTranslation(); + const navigate = useCustomNavigate(); const [recruitmentPositions, setRecruitmentPositions] = useState(); const [loading, setLoading] = useState(true); @@ -45,7 +49,7 @@ export function RecruitmentPage() { }); }} > - Se søknader + {t(KEY.recruitment_organization)} {loading ? ( diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 08f659a38..e83c06a53 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -185,6 +185,9 @@ export const KEY = { recruitment_otherpositions: 'KEY.recruitment_otherpositions', recruitment_visible_from: 'recruitment_visible_from', recruitment_administrate: 'recruitment_administrate', + recruitment_my_applications: 'recruitment_my_applications', + recruitment_not_applied: 'recruitment_not_applied', + recruitment_will_be_anonymized: 'recruitment_will_be_anonymized', shown_application_deadline: 'shown_application_deadline', actual_application_deadlin: 'actual_application_deadline', recruitment_number_of_applications: 'recruitment_number_of_applications', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 8b5fd281c..eaed6e559 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -152,6 +152,9 @@ export const nb: Record = { [KEY.recruitment_tags]: 'Tags', [KEY.recruitment_position]: 'Stilling', [KEY.recruitment_applicant]: 'Søker', + [KEY.recruitment_my_applications]: 'Mine søknader', + [KEY.recruitment_not_applied]: 'Du har ikke sendt søknader til noen stillinger ennå', + [KEY.recruitment_will_be_anonymized]: 'All info relatert til dine søknader vil bli slettet 3 uker etter opptaket', [KEY.recruitment_interview_time]: 'Intervjutid', [KEY.recruitment_interview_location]: 'Intervjusted', [KEY.recruitment_interview_notes]: 'Intervju notater', @@ -409,11 +412,15 @@ export const en: Record = { [KEY.recruitment_tags]: 'Tags', [KEY.recruitment_position]: 'Position', [KEY.recruitment_applicant]: 'Applicant', - [KEY.recruitment_interview_time]: 'Intervjutid', - [KEY.recruitment_interview_location]: 'Intervjusted', + [KEY.recruitment_my_applications]: 'My applications', + [KEY.recruitment_not_applied]: 'You have not applied to any positions yet', + [KEY.recruitment_will_be_anonymized]: + 'All info related to the applications will be anonymized three weeks after the recruitment is over', + [KEY.recruitment_interview_time]: 'Interview Time', + [KEY.recruitment_interview_location]: 'Interview Location', [KEY.recruitment_interview_notes]: 'Interview notes', - [KEY.recruitment_priority]: 'Søkers prioritet', - [KEY.recruitment_recruiter_priority]: 'Prioritet', + [KEY.recruitment_priority]: 'Applicants priority', + [KEY.recruitment_recruiter_priority]: 'Priority', [KEY.recruitment_recruiter_status]: 'Status', [KEY.recruitment_duration]: 'Duration', [KEY.recruitment_admission]: 'Admission', From 247a3be3e4f6ce9fcaf3ff8f4e2abeb511b5c7a9 Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 20:10:59 +0100 Subject: [PATCH 17/20] fix --- backend/samfundet/models/recruitment.py | 206 +++++++++++++++++------- 1 file changed, 145 insertions(+), 61 deletions(-) diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index eb21ce1d2..c2c963d69 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -2,6 +2,7 @@ # This file contains models spesific to the recruitment system # from __future__ import annotations + import uuid from .general import Gang, User, Organization @@ -16,80 +17,112 @@ class Recruitment(CustomBaseModel): - name_nb = models.CharField(max_length=100, help_text='Name of the recruitment') - name_en = models.CharField(max_length=100, help_text='Name of the recruitment') - visible_from = models.DateTimeField(help_text='When it becomes visible for applicants') + name_nb = models.CharField(max_length=100, + help_text='Name of the recruitment') + name_en = models.CharField(max_length=100, + help_text='Name of the recruitment') + visible_from = models.DateTimeField( + help_text='When it becomes visible for applicants') actual_application_deadline = models.DateTimeField( - help_text='Last point an application can be sent, typically a bit after the shown deadline to avoid getting a lot of extra mail' + help_text= + 'Last point an application can be sent, typically a bit after the shown deadline to avoid getting a lot of extra mail' ) - shown_application_deadline = models.DateTimeField(help_text='The deadline that is shown to applicants') - reprioritization_deadline_for_applicant = models.DateTimeField(help_text='Before allocation meeting') - reprioritization_deadline_for_groups = models.DateTimeField(help_text='Reprioritization deadline for groups') - organization = models.ForeignKey(to=Organization, on_delete=models.CASCADE, help_text='The organization that is recruiting') + shown_application_deadline = models.DateTimeField( + help_text='The deadline that is shown to applicants') + reprioritization_deadline_for_applicant = models.DateTimeField( + help_text='Before allocation meeting') + reprioritization_deadline_for_groups = models.DateTimeField( + help_text='Reprioritization deadline for groups') + organization = models.ForeignKey( + to=Organization, + on_delete=models.CASCADE, + help_text='The organization that is recruiting') def is_active(self) -> bool: - return self.visible_from < timezone.now() < self.actual_application_deadline + return self.visible_from < timezone.now( + ) < self.actual_application_deadline def clean(self, *args: tuple, **kwargs: dict) -> None: super().clean() - if not all( - [ + if not all([ self.visible_from, self.actual_application_deadline, self.shown_application_deadline, self.reprioritization_deadline_for_applicant, self.reprioritization_deadline_for_groups, - ] - ): + ]): raise ValidationError('Missing datetime') # All times should be in the future. now = timezone.now() - if any( - [ + if any([ self.actual_application_deadline < now, self.shown_application_deadline < now, self.reprioritization_deadline_for_applicant < now, self.reprioritization_deadline_for_groups < now, - ] - ): + ]): raise ValidationError('All times should be in the future') if self.actual_application_deadline < self.visible_from: - raise ValidationError('Visible from should be before application deadline') + raise ValidationError( + 'Visible from should be before application deadline') if self.actual_application_deadline < self.shown_application_deadline: - raise ValidationError('Shown application deadline should be before the actual application deadline') + raise ValidationError( + 'Shown application deadline should be before the actual application deadline' + ) if self.reprioritization_deadline_for_applicant < self.actual_application_deadline: - raise ValidationError('Actual application deadline should be before reprioritization deadline for applicants') + raise ValidationError( + 'Actual application deadline should be before reprioritization deadline for applicants' + ) if self.reprioritization_deadline_for_groups < self.reprioritization_deadline_for_applicant: - raise ValidationError('Reprioritization deadline for applicants should be before reprioritization deadline for groups') + raise ValidationError( + 'Reprioritization deadline for applicants should be before reprioritization deadline for groups' + ) def __str__(self) -> str: return f'Recruitment: {self.name_en} at {self.organization}' class RecruitmentPosition(CustomBaseModel): - name_nb = models.CharField(max_length=100, help_text='Name of the position') - name_en = models.CharField(max_length=100, help_text='Name of the position') - - short_description_nb = models.CharField(max_length=100, help_text='Short description of the position') - short_description_en = models.CharField(max_length=100, help_text='Short description of the position', null=True, blank=True) + name_nb = models.CharField(max_length=100, + help_text='Name of the position') + name_en = models.CharField(max_length=100, + help_text='Name of the position') + + short_description_nb = models.CharField( + max_length=100, help_text='Short description of the position') + short_description_en = models.CharField( + max_length=100, + help_text='Short description of the position', + null=True, + blank=True) - long_description_nb = models.TextField(help_text='Long description of the position') - long_description_en = models.TextField(help_text='Long description of the position', null=True, blank=True) + long_description_nb = models.TextField( + help_text='Long description of the position') + long_description_en = models.TextField( + help_text='Long description of the position', null=True, blank=True) - is_funksjonaer_position = models.BooleanField(help_text='Is this a funksjonær position?') + is_funksjonaer_position = models.BooleanField( + help_text='Is this a funksjonær position?') - default_admission_letter_nb = models.TextField(help_text='Default admission letter for the position') - default_admission_letter_en = models.TextField(help_text='Default admission letter for the position', null=True, blank=True) + default_admission_letter_nb = models.TextField( + help_text='Default admission letter for the position') + default_admission_letter_en = models.TextField( + help_text='Default admission letter for the position', + null=True, + blank=True) - norwegian_applicants_only = models.BooleanField(help_text='Is this position only for Norwegian applicants?', default=False) + norwegian_applicants_only = models.BooleanField( + help_text='Is this position only for Norwegian applicants?', + default=False) - gang = models.ForeignKey(to=Gang, on_delete=models.CASCADE, help_text='The gang that is recruiting') + gang = models.ForeignKey(to=Gang, + on_delete=models.CASCADE, + help_text='The gang that is recruiting') recruitment = models.ForeignKey( Recruitment, on_delete=models.CASCADE, @@ -99,13 +132,21 @@ class RecruitmentPosition(CustomBaseModel): blank=True, ) - shared_interview_positions = models.ManyToManyField('self', symmetrical=True, blank=True, help_text='Positions with shared interview') + shared_interview_positions = models.ManyToManyField( + 'self', + symmetrical=True, + blank=True, + help_text='Positions with shared interview') # TODO: Implement tag functionality tags = models.CharField(max_length=100, help_text='Tags for the position') # TODO: Implement interviewer functionality - interviewers = models.ManyToManyField(to=User, help_text='Interviewers for the position', blank=True, related_name='interviewers') + interviewers = models.ManyToManyField( + to=User, + help_text='Interviewers for the position', + blank=True, + related_name='interviewers') def __str__(self) -> str: return f'Position: {self.name_en} in {self.recruitment}' @@ -121,11 +162,21 @@ def save(self, *args: tuple, **kwargs: dict) -> None: class InterviewRoom(CustomBaseModel): name = models.CharField(max_length=255, help_text='Name of the room') - location = models.CharField(max_length=255, help_text='Physical location, eg. campus') + location = models.CharField(max_length=255, + help_text='Physical location, eg. campus') start_time = models.DateTimeField(help_text='Start time of availability') end_time = models.DateTimeField(help_text='End time of availability') - recruitment = models.ForeignKey(Recruitment, on_delete=models.CASCADE, help_text='The recruitment that is recruiting', related_name='rooms') - gang = models.ForeignKey(to=Gang, on_delete=models.CASCADE, help_text='The gang that booked the room', related_name='rooms', blank=True, null=True) + recruitment = models.ForeignKey( + Recruitment, + on_delete=models.CASCADE, + help_text='The recruitment that is recruiting', + related_name='rooms') + gang = models.ForeignKey(to=Gang, + on_delete=models.CASCADE, + help_text='The gang that booked the room', + related_name='rooms', + blank=True, + null=True) def __str__(self) -> str: return self.name @@ -139,8 +190,13 @@ def clean(self) -> None: class Interview(CustomBaseModel): # User visible fields - interview_time = models.DateTimeField(help_text='The time of the interview', null=True, blank=True) - interview_location = models.CharField(max_length=255, help_text='The location of the interview', null=True, blank=True) + interview_time = models.DateTimeField( + help_text='The time of the interview', null=True, blank=True) + interview_location = models.CharField( + max_length=255, + help_text='The location of the interview', + null=True, + blank=True) # Admin visible fields room = models.ForeignKey( @@ -151,34 +207,52 @@ class Interview(CustomBaseModel): help_text='Room where the interview is held', related_name='interviews', ) - notes = models.TextField(help_text='Notes for the interview', null=True, blank=True) + notes = models.TextField(help_text='Notes for the interview', + null=True, + blank=True) class RecruitmentAdmission(CustomBaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - admission_text = models.TextField(help_text='Admission text for the admission') + admission_text = models.TextField( + help_text='Admission text for the admission') recruitment_position = models.ForeignKey( - RecruitmentPosition, on_delete=models.CASCADE, help_text='The recruitment position that is recruiting', related_name='admissions' - ) - recruitment = models.ForeignKey(Recruitment, on_delete=models.CASCADE, help_text='The recruitment that is recruiting', related_name='admissions') - user = models.ForeignKey(User, on_delete=models.CASCADE, help_text='The user that is applying', related_name='admissions') - applicant_priority = models.IntegerField(help_text='The priority of the admission') + RecruitmentPosition, + on_delete=models.CASCADE, + help_text='The recruitment position that is recruiting', + related_name='admissions') + recruitment = models.ForeignKey( + Recruitment, + on_delete=models.CASCADE, + help_text='The recruitment that is recruiting', + related_name='admissions') + user = models.ForeignKey(User, + on_delete=models.CASCADE, + help_text='The user that is applying', + related_name='admissions') + applicant_priority = models.IntegerField( + help_text='The priority of the admission') created_at = models.DateTimeField(null=True, blank=True, auto_now_add=True) - interview = models.ForeignKey( - Interview, on_delete=models.SET_NULL, null=True, blank=True, help_text='The interview for the admission', related_name='admissions' - ) + interview = models.ForeignKey(Interview, + on_delete=models.SET_NULL, + null=True, + blank=True, + help_text='The interview for the admission', + related_name='admissions') withdrawn = models.BooleanField(default=False, blank=True, null=True) # TODO: Important that the following is not sent along with the rest of the object whenever a user retrieves its admission recruiter_priority = models.IntegerField( - choices=RecruitmentPriorityChoices.choices, default=RecruitmentPriorityChoices.NOT_SET, help_text='The priority of the admission' - ) + choices=RecruitmentPriorityChoices.choices, + default=RecruitmentPriorityChoices.NOT_SET, + help_text='The priority of the admission') recruiter_status = models.IntegerField( - choices=RecruitmentStatusChoices.choices, default=RecruitmentStatusChoices.NOT_SET, help_text='The status of the admission' - ) + choices=RecruitmentStatusChoices.choices, + default=RecruitmentStatusChoices.NOT_SET, + help_text='The status of the admission') def __str__(self) -> str: return f'Admission: {self.user} for {self.recruitment_position} in {self.recruitment}' @@ -186,7 +260,8 @@ def __str__(self) -> str: def save(self, *args: tuple, **kwargs: dict) -> None: """If the admission is saved without an interview, try to find an interview from a shared position.""" if not self.applicant_priority: - current_applications_count = RecruitmentAdmission.objects.filter(user=self.user).count() + current_applications_count = RecruitmentAdmission.objects.filter( + user=self.user).count() # Set the applicant_priority to the number of applications + 1 (for the current application) self.applicant_priority = current_applications_count + 1 """If the admission is saved without an interview, try to find an interview from a shared position.""" @@ -195,10 +270,12 @@ def save(self, *args: tuple, **kwargs: dict) -> None: self.recruiter_status = RecruitmentStatusChoices.AUTOMATIC_REJECTION if not self.interview: # Check if there is already an interview for the same user in shared positions - shared_interview_positions = self.recruitment_position.shared_interview_positions.all() - shared_interview = ( - RecruitmentAdmission.objects.filter(user=self.user, recruitment_position__in=shared_interview_positions).exclude(interview=None).first() + shared_interview_positions = self.recruitment_position.shared_interview_positions.all( ) + shared_interview = (RecruitmentAdmission.objects.filter( + user=self.user, + recruitment_position__in=shared_interview_positions).exclude( + interview=None).first()) if shared_interview: self.interview = shared_interview.interview @@ -220,8 +297,15 @@ class Occupiedtimeslot(FullCleanSaveMixin): related_name='occupied_timeslots', ) # Mostly only used for deletion, and anonymization. - recruitment = models.ForeignKey(Recruitment, on_delete=models.CASCADE, help_text='Occupied timeslots for the users for this recruitment') + recruitment = models.ForeignKey( + Recruitment, + on_delete=models.CASCADE, + help_text='Occupied timeslots for the users for this recruitment') # Start and end time of availability - start_dt = models.DateTimeField(help_text='The time of the interview', null=False, blank=False) - end_dt = models.DateTimeField(help_text='The time of the interview', null=False, blank=False) + start_dt = models.DateTimeField(help_text='The time of the interview', + null=False, + blank=False) + end_dt = models.DateTimeField(help_text='The time of the interview', + null=False, + blank=False) From c1e520152942a16156f0f4155e465d47cadee554 Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 20:14:41 +0100 Subject: [PATCH 18/20] reformat again --- backend/samfundet/models/recruitment.py | 205 +++++++----------------- 1 file changed, 61 insertions(+), 144 deletions(-) diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index c2c963d69..d0bede3f0 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -17,112 +17,80 @@ class Recruitment(CustomBaseModel): - name_nb = models.CharField(max_length=100, - help_text='Name of the recruitment') - name_en = models.CharField(max_length=100, - help_text='Name of the recruitment') - visible_from = models.DateTimeField( - help_text='When it becomes visible for applicants') + name_nb = models.CharField(max_length=100, help_text='Name of the recruitment') + name_en = models.CharField(max_length=100, help_text='Name of the recruitment') + visible_from = models.DateTimeField(help_text='When it becomes visible for applicants') actual_application_deadline = models.DateTimeField( - help_text= - 'Last point an application can be sent, typically a bit after the shown deadline to avoid getting a lot of extra mail' + help_text='Last point an application can be sent, typically a bit after the shown deadline to avoid getting a lot of extra mail' ) - shown_application_deadline = models.DateTimeField( - help_text='The deadline that is shown to applicants') - reprioritization_deadline_for_applicant = models.DateTimeField( - help_text='Before allocation meeting') - reprioritization_deadline_for_groups = models.DateTimeField( - help_text='Reprioritization deadline for groups') - organization = models.ForeignKey( - to=Organization, - on_delete=models.CASCADE, - help_text='The organization that is recruiting') + shown_application_deadline = models.DateTimeField(help_text='The deadline that is shown to applicants') + reprioritization_deadline_for_applicant = models.DateTimeField(help_text='Before allocation meeting') + reprioritization_deadline_for_groups = models.DateTimeField(help_text='Reprioritization deadline for groups') + organization = models.ForeignKey(to=Organization, on_delete=models.CASCADE, help_text='The organization that is recruiting') def is_active(self) -> bool: - return self.visible_from < timezone.now( - ) < self.actual_application_deadline + return self.visible_from < timezone.now() < self.actual_application_deadline def clean(self, *args: tuple, **kwargs: dict) -> None: super().clean() - if not all([ + if not all( + [ self.visible_from, self.actual_application_deadline, self.shown_application_deadline, self.reprioritization_deadline_for_applicant, self.reprioritization_deadline_for_groups, - ]): + ] + ): raise ValidationError('Missing datetime') # All times should be in the future. now = timezone.now() - if any([ + if any( + [ self.actual_application_deadline < now, self.shown_application_deadline < now, self.reprioritization_deadline_for_applicant < now, self.reprioritization_deadline_for_groups < now, - ]): + ] + ): raise ValidationError('All times should be in the future') if self.actual_application_deadline < self.visible_from: - raise ValidationError( - 'Visible from should be before application deadline') + raise ValidationError('Visible from should be before application deadline') if self.actual_application_deadline < self.shown_application_deadline: - raise ValidationError( - 'Shown application deadline should be before the actual application deadline' - ) + raise ValidationError('Shown application deadline should be before the actual application deadline') if self.reprioritization_deadline_for_applicant < self.actual_application_deadline: - raise ValidationError( - 'Actual application deadline should be before reprioritization deadline for applicants' - ) + raise ValidationError('Actual application deadline should be before reprioritization deadline for applicants') if self.reprioritization_deadline_for_groups < self.reprioritization_deadline_for_applicant: - raise ValidationError( - 'Reprioritization deadline for applicants should be before reprioritization deadline for groups' - ) + raise ValidationError('Reprioritization deadline for applicants should be before reprioritization deadline for groups') def __str__(self) -> str: return f'Recruitment: {self.name_en} at {self.organization}' class RecruitmentPosition(CustomBaseModel): - name_nb = models.CharField(max_length=100, - help_text='Name of the position') - name_en = models.CharField(max_length=100, - help_text='Name of the position') - - short_description_nb = models.CharField( - max_length=100, help_text='Short description of the position') - short_description_en = models.CharField( - max_length=100, - help_text='Short description of the position', - null=True, - blank=True) + name_nb = models.CharField(max_length=100, help_text='Name of the position') + name_en = models.CharField(max_length=100, help_text='Name of the position') - long_description_nb = models.TextField( - help_text='Long description of the position') - long_description_en = models.TextField( - help_text='Long description of the position', null=True, blank=True) + short_description_nb = models.CharField(max_length=100, help_text='Short description of the position') + short_description_en = models.CharField(max_length=100, help_text='Short description of the position', null=True, blank=True) - is_funksjonaer_position = models.BooleanField( - help_text='Is this a funksjonær position?') + long_description_nb = models.TextField(help_text='Long description of the position') + long_description_en = models.TextField(help_text='Long description of the position', null=True, blank=True) - default_admission_letter_nb = models.TextField( - help_text='Default admission letter for the position') - default_admission_letter_en = models.TextField( - help_text='Default admission letter for the position', - null=True, - blank=True) + is_funksjonaer_position = models.BooleanField(help_text='Is this a funksjonær position?') + + default_admission_letter_nb = models.TextField(help_text='Default admission letter for the position') + default_admission_letter_en = models.TextField(help_text='Default admission letter for the position', null=True, blank=True) - norwegian_applicants_only = models.BooleanField( - help_text='Is this position only for Norwegian applicants?', - default=False) + norwegian_applicants_only = models.BooleanField(help_text='Is this position only for Norwegian applicants?', default=False) - gang = models.ForeignKey(to=Gang, - on_delete=models.CASCADE, - help_text='The gang that is recruiting') + gang = models.ForeignKey(to=Gang, on_delete=models.CASCADE, help_text='The gang that is recruiting') recruitment = models.ForeignKey( Recruitment, on_delete=models.CASCADE, @@ -132,21 +100,13 @@ class RecruitmentPosition(CustomBaseModel): blank=True, ) - shared_interview_positions = models.ManyToManyField( - 'self', - symmetrical=True, - blank=True, - help_text='Positions with shared interview') + shared_interview_positions = models.ManyToManyField('self', symmetrical=True, blank=True, help_text='Positions with shared interview') # TODO: Implement tag functionality tags = models.CharField(max_length=100, help_text='Tags for the position') # TODO: Implement interviewer functionality - interviewers = models.ManyToManyField( - to=User, - help_text='Interviewers for the position', - blank=True, - related_name='interviewers') + interviewers = models.ManyToManyField(to=User, help_text='Interviewers for the position', blank=True, related_name='interviewers') def __str__(self) -> str: return f'Position: {self.name_en} in {self.recruitment}' @@ -162,21 +122,11 @@ def save(self, *args: tuple, **kwargs: dict) -> None: class InterviewRoom(CustomBaseModel): name = models.CharField(max_length=255, help_text='Name of the room') - location = models.CharField(max_length=255, - help_text='Physical location, eg. campus') + location = models.CharField(max_length=255, help_text='Physical location, eg. campus') start_time = models.DateTimeField(help_text='Start time of availability') end_time = models.DateTimeField(help_text='End time of availability') - recruitment = models.ForeignKey( - Recruitment, - on_delete=models.CASCADE, - help_text='The recruitment that is recruiting', - related_name='rooms') - gang = models.ForeignKey(to=Gang, - on_delete=models.CASCADE, - help_text='The gang that booked the room', - related_name='rooms', - blank=True, - null=True) + recruitment = models.ForeignKey(Recruitment, on_delete=models.CASCADE, help_text='The recruitment that is recruiting', related_name='rooms') + gang = models.ForeignKey(to=Gang, on_delete=models.CASCADE, help_text='The gang that booked the room', related_name='rooms', blank=True, null=True) def __str__(self) -> str: return self.name @@ -190,13 +140,8 @@ def clean(self) -> None: class Interview(CustomBaseModel): # User visible fields - interview_time = models.DateTimeField( - help_text='The time of the interview', null=True, blank=True) - interview_location = models.CharField( - max_length=255, - help_text='The location of the interview', - null=True, - blank=True) + interview_time = models.DateTimeField(help_text='The time of the interview', null=True, blank=True) + interview_location = models.CharField(max_length=255, help_text='The location of the interview', null=True, blank=True) # Admin visible fields room = models.ForeignKey( @@ -207,52 +152,34 @@ class Interview(CustomBaseModel): help_text='Room where the interview is held', related_name='interviews', ) - notes = models.TextField(help_text='Notes for the interview', - null=True, - blank=True) + notes = models.TextField(help_text='Notes for the interview', null=True, blank=True) class RecruitmentAdmission(CustomBaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - admission_text = models.TextField( - help_text='Admission text for the admission') + admission_text = models.TextField(help_text='Admission text for the admission') recruitment_position = models.ForeignKey( - RecruitmentPosition, - on_delete=models.CASCADE, - help_text='The recruitment position that is recruiting', - related_name='admissions') - recruitment = models.ForeignKey( - Recruitment, - on_delete=models.CASCADE, - help_text='The recruitment that is recruiting', - related_name='admissions') - user = models.ForeignKey(User, - on_delete=models.CASCADE, - help_text='The user that is applying', - related_name='admissions') - applicant_priority = models.IntegerField( - help_text='The priority of the admission') + RecruitmentPosition, on_delete=models.CASCADE, help_text='The recruitment position that is recruiting', related_name='admissions' + ) + recruitment = models.ForeignKey(Recruitment, on_delete=models.CASCADE, help_text='The recruitment that is recruiting', related_name='admissions') + user = models.ForeignKey(User, on_delete=models.CASCADE, help_text='The user that is applying', related_name='admissions') + applicant_priority = models.IntegerField(help_text='The priority of the admission') created_at = models.DateTimeField(null=True, blank=True, auto_now_add=True) - interview = models.ForeignKey(Interview, - on_delete=models.SET_NULL, - null=True, - blank=True, - help_text='The interview for the admission', - related_name='admissions') + interview = models.ForeignKey( + Interview, on_delete=models.SET_NULL, null=True, blank=True, help_text='The interview for the admission', related_name='admissions' + ) withdrawn = models.BooleanField(default=False, blank=True, null=True) # TODO: Important that the following is not sent along with the rest of the object whenever a user retrieves its admission recruiter_priority = models.IntegerField( - choices=RecruitmentPriorityChoices.choices, - default=RecruitmentPriorityChoices.NOT_SET, - help_text='The priority of the admission') + choices=RecruitmentPriorityChoices.choices, default=RecruitmentPriorityChoices.NOT_SET, help_text='The priority of the admission' + ) recruiter_status = models.IntegerField( - choices=RecruitmentStatusChoices.choices, - default=RecruitmentStatusChoices.NOT_SET, - help_text='The status of the admission') + choices=RecruitmentStatusChoices.choices, default=RecruitmentStatusChoices.NOT_SET, help_text='The status of the admission' + ) def __str__(self) -> str: return f'Admission: {self.user} for {self.recruitment_position} in {self.recruitment}' @@ -260,8 +187,7 @@ def __str__(self) -> str: def save(self, *args: tuple, **kwargs: dict) -> None: """If the admission is saved without an interview, try to find an interview from a shared position.""" if not self.applicant_priority: - current_applications_count = RecruitmentAdmission.objects.filter( - user=self.user).count() + current_applications_count = RecruitmentAdmission.objects.filter(user=self.user).count() # Set the applicant_priority to the number of applications + 1 (for the current application) self.applicant_priority = current_applications_count + 1 """If the admission is saved without an interview, try to find an interview from a shared position.""" @@ -270,12 +196,10 @@ def save(self, *args: tuple, **kwargs: dict) -> None: self.recruiter_status = RecruitmentStatusChoices.AUTOMATIC_REJECTION if not self.interview: # Check if there is already an interview for the same user in shared positions - shared_interview_positions = self.recruitment_position.shared_interview_positions.all( + shared_interview_positions = self.recruitment_position.shared_interview_positions.all() + shared_interview = ( + RecruitmentAdmission.objects.filter(user=self.user, recruitment_position__in=shared_interview_positions).exclude(interview=None).first() ) - shared_interview = (RecruitmentAdmission.objects.filter( - user=self.user, - recruitment_position__in=shared_interview_positions).exclude( - interview=None).first()) if shared_interview: self.interview = shared_interview.interview @@ -297,15 +221,8 @@ class Occupiedtimeslot(FullCleanSaveMixin): related_name='occupied_timeslots', ) # Mostly only used for deletion, and anonymization. - recruitment = models.ForeignKey( - Recruitment, - on_delete=models.CASCADE, - help_text='Occupied timeslots for the users for this recruitment') + recruitment = models.ForeignKey(Recruitment, on_delete=models.CASCADE, help_text='Occupied timeslots for the users for this recruitment') # Start and end time of availability - start_dt = models.DateTimeField(help_text='The time of the interview', - null=False, - blank=False) - end_dt = models.DateTimeField(help_text='The time of the interview', - null=False, - blank=False) + start_dt = models.DateTimeField(help_text='The time of the interview', null=False, blank=False) + end_dt = models.DateTimeField(help_text='The time of the interview', null=False, blank=False) From 575eba0c450083c5e0e5987612fbfed22509de1c Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 20:51:10 +0100 Subject: [PATCH 19/20] ruff --- backend/samfundet/models/recruitment.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index d0bede3f0..7b7ef6f7a 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -5,14 +5,13 @@ import uuid -from .general import Gang, User, Organization - from django.db import models from django.utils import timezone from django.core.exceptions import ValidationError from root.utils.mixins import CustomBaseModel, FullCleanSaveMixin +from .general import Gang, User, Organization from .model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices From 4b3ce89224b0eed8457df87ff8724f7b0c9e68dc Mon Sep 17 00:00:00 2001 From: magsyg Date: Thu, 8 Feb 2024 21:02:24 +0100 Subject: [PATCH 20/20] fun merge --- backend/Pipfile | 1 - backend/Pipfile.lock | 25 +------------------ .../Pages/RecruitmentPage/RecruitmentPage.tsx | 8 +----- 3 files changed, 2 insertions(+), 32 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index 732955009..e6bcebdfa 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -44,7 +44,6 @@ gunicorn = "*" django-admin-autocomplete-filter = "*" django-notifications-hq = "*" psycopg = {extras = ["c"], version = "*"} -ruff = "*" [dev-packages] yapf = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 18b08a1e7..c063fca08 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e3ceaad3e2a939859a970f4044027ff08d71a3e70e0ab569c171c2658edad2a7" + "sha256": "0fd219ff24de055c4ba2c99bee2df8d306f3d940e44f045db77bce95791f7ea0" }, "pipfile-spec": 6, "requires": { @@ -241,29 +241,6 @@ ], "version": "==2024.1" }, - "ruff": { - "hashes": [ - "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc", - "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105", - "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba", - "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e", - "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1", - "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232", - "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad", - "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35", - "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b", - "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a", - "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec", - "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080", - "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0", - "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02", - "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6", - "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683", - "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855" - ], - "index": "pypi", - "version": "==0.2.1" - }, "sqlparse": { "hashes": [ "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3", diff --git a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx index 2d471fed6..9bd8ac6c3 100644 --- a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx +++ b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx @@ -4,21 +4,15 @@ import { Button, Page, SamfundetLogoSpinner, Video } from '~/Components'; import { getActiveRecruitmentPositions, getGangList } from '~/api'; import { TextItem } from '~/constants'; import { GangTypeDto, RecruitmentPositionDto } from '~/dto'; -import { useTextItem } from '~/hooks'; +import { useTextItem, useCustomNavigate } from '~/hooks'; import { KEY } from '~/i18n/constants'; import { ROUTES } from '~/routes'; import { GangTypeContainer } from './Components'; import styles from './RecruitmentPage.module.scss'; import { OccupiedFormModal } from '~/Components/OccupiedForm'; import { reverse } from '~/named-urls'; -import { useCustomNavigate } from '~/hooks'; -import { ROUTES } from '~/routes'; -import { useTranslation } from 'react-i18next'; -import { KEY } from '~/i18n/constants'; export function RecruitmentPage() { - const { t } = useTranslation(); - const navigate = useCustomNavigate(); const [recruitmentPositions, setRecruitmentPositions] = useState(); const [loading, setLoading] = useState(true);