diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 114bf8e88..c063fca08 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -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": [ @@ -279,6 +270,14 @@ "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" ], "version": "==0.9.0" + }, + "tzdata": { + "hashes": [ + "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3", + "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9" + ], + "markers": "sys_platform == 'win32'", + "version": "==2023.4" } }, "develop": { @@ -288,7 +287,6 @@ "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.7.7" }, "certifi": { @@ -395,6 +393,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 +423,6 @@ "sha256:ef9ab7df0b9a42ed9c878afd3eaaff471fce3fa73df96022e1f5c9f8f8c87ada" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.8.0" }, "idna": { @@ -491,7 +496,6 @@ "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.8.0" }, "mypy-extensions": { @@ -590,7 +594,6 @@ "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==10.2.0" }, "platformdirs": { @@ -623,7 +626,6 @@ "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==8.0.0" }, "pytest-django": { @@ -632,7 +634,6 @@ "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==4.8.0" }, "pyyaml": { @@ -698,7 +699,6 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "rich": { @@ -711,27 +711,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 +770,6 @@ "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==0.40.2" }, "zipp": { diff --git a/backend/root/constants.py b/backend/root/constants.py index a2bff54a4..a2290f974 100644 --- a/backend/root/constants.py +++ b/backend/root/constants.py @@ -44,4 +44,4 @@ class Environment: # Phone number regex -PHONE_NUMBER_REGEX = r'^(\+?\(?\d{3}\)?[-\s\.]?)?\d{3}[-\s\.]?\d{4,6}$' +PHONE_NUMBER_REGEX = r'^\+?\s*(\d\s*){8,15}$' diff --git a/backend/root/management/commands/seed_scripts/recruitment_position.py b/backend/root/management/commands/seed_scripts/recruitment_position.py index 9d8e145db..2be5b7954 100644 --- a/backend/root/management/commands/seed_scripts/recruitment_position.py +++ b/backend/root/management/commands/seed_scripts/recruitment_position.py @@ -28,8 +28,8 @@ def seed(): position_data = POSITION_DATA.copy() position_data.update( { - 'name_nb': f'Stilling {i}', - 'name_en': f'Position {i}', + 'name_nb': f'{gang.abbreviation} stilling {i}', + 'name_en': f'{gang.abbreviation} position {i}', 'gang': gang, 'recruitment': recruitment, } diff --git a/backend/root/management/commands/seed_scripts/textitems.py b/backend/root/management/commands/seed_scripts/textitems.py index 81c7ce8a1..22788957b 100644 --- a/backend/root/management/commands/seed_scripts/textitems.py +++ b/backend/root/management/commands/seed_scripts/textitems.py @@ -37,13 +37,65 @@ def seed(): 'text_nb': 'Har du spørsmål eller ønsker å komme i kontakt med oss? Ikke nøl med å ta kontakt!', 'text_en': 'Do you have any questions or want to get in touch with us? Don"t hesitate to contact us!', }, + { + 'key': 'festivals', + 'text_nb': 'Annenhvert år arrangeres Norges største kulturfestival UKA og verdens største internasjonale tematiske studentfestival under Studentersamfundets paraply.', + 'text_en': "Every other year, Norway's largest cultural festival UKA and the world's largest international thematic student festival are organized under Studentersamfundet's umbrella.", + }, + { + 'key': 'volunteering', + 'text_nb': 'Det meste av arbeid på Studentersamfundet i Trondhjem gjøres gjennom dugnad av studenter. Arbeidet er organisert i enheter som kalles gjenger. Potensielle nye medlemmer må søke den aktuelle gjengen om opptak, og gjengen vurderer hver enkelt søker. Med sine omtrent 1700 frivillige utgjør det indre miljøet i Studentersamfundet en betydelig del av det organiserte fritidstilbudet til studenter i Trondheim.', + 'text_en': 'Most of the work at Samfundet is done on a voluntary basis by students. The work is organized into units called groups. Prospective new members must apply to the appropriate group for admission and the group reviews each applicant. With around 1700 volunteers, the internal environment of Samfundet constitutes a significant part of organized leisure activities for students in Trondheim.', + }, + { + 'key': 'the_society_meeting', + 'text_nb': 'Samfundsmøtet er Studentersamfundets høyeste organ. Her velges leder samt medlemmer til Finansstyret og Rådet. Lederen velger ut sitt eget styre, som utformer den politiske profilen og representer medlemmene og foreningen Samfundet. Finansstyret administrerer forretningsdriften. Rådet kontrollerer at all virksomhet i Samfundet foregår i henhold til norske og interne lover. Samfundet har en daglig leder, økonomiansvarlig, husøkonom, vaktmester og renholdspersonell som er ansatte.', + 'text_en': "The Society Meeting is our highest organ. We elect the leader here, as well as members of the Finance Board and the Council. The leader, elected politically, chooses his or her own Board, which forms the House's political profile and represents all of Samfundet's members. The Finance Board manages the business. The Council ensures that all activities at Samfundet take place according to Norwegian and internal laws. Samfundet has a general manager, financial manager, house-economist, caretaker and cleaning staff who are employees.", + }, + { + 'key': 'about_samfundet', + 'text_nb': 'Studentersamfundet i Trondhjem er en organisasjon for studenter i Trondheim som eies og drives av sine rundt 16 100 medlemmer. Formålsparagrafen vår sier at ”Studentersamfundet skal være det naturlige samlingsstedet for studenter i Trondhjem”. Vårt røde runde huser konserter, ulike kulturarrangementer, utallige barer, en kafé og en restaurant. Mest sagnomsust er Samfundsmøtene, viet til debatt om politikk og aktuelle spørsmål, eller til underholdning og moro. Samfundet har også tre av Trondheims beste konsertscener.', + 'text_en': "Samfundet is an organization for students in Trondheim that is owned and run by its approximately 16100 members. Our mission statement is Samfundet will be the natural meeting place for students in Trondheim. In our red, round building we regularly host concerts and various cultural events, and have countless bars, a café and a restaurant. The Society Meetings are perhaps our most famous events. These meetings serve as a place for debating politics and current events, and but also for entertainment and fun. Samfundet also has three of Trondheim's best music venues.", + }, + { + 'key': 'no_recruitment_text', + 'text_nb': 'Det er for tiden ingen opptak på Samfundet', + 'text_en': 'There are currently no recruitments at Samfundet', + }, + { + 'key': 'no_recruitment_text_0', + 'text_nb': 'Vi har opptak på starten av hvert semester og ønsker at du søker til oss som frivillig!', + 'text_en': 'We have recruitments at the start of each semester and would like you to apply to us as a volunteer!', + }, + { + 'key': 'no_recruitment_text_1', + 'text_nb': 'Studentersamfundet i Trondhjem er Norges største studentersamfund og vi har et tilbud andre byer bare kan drømme om.', + 'text_en': 'Studentersamfundet in Trondhjem is Norways largest student society and we have an offer that other cities can only dream of.', + }, + { + 'key': 'no_recruitment_text_2', + 'text_nb': 'Nesten uansett hvilken studiebakgrunn eller interesser du har, så finnes det en frivillig gjeng som søker nettopp deg!', + 'text_en': 'Almost regardless of your study background or interests, there is a group of volunteers looking for you!', + }, + { + 'key': 'no_recruitment_text_3', + 'text_nb': 'Omtrent 2000 studenter bidrar allerede frivillig! Du kan være med å jobbe med blant annet lyd, lys, teater, snekring, IT, artistbooking, korsang, musikk og mye annet. Du kan lære mye av å jobbe på Samfundet, og du blir garantert kjent med mange andre studenter.', + 'text_en': 'Approximately 2,000 students are already contributing voluntarily! You can help work with, among other things, sound, lighting, theatre, carpentry, IT, artist booking, choir singing, music and much more. You can learn a lot from working at Samfundet, and you are guaranteed to get to know many other students.', + }, + { + 'key': 'no_recruitment_text_4', + 'text_nb': 'For mer informasjon om samfundets gjenger ', + 'text_en': 'For more information about the groups ', + }, + { + 'key': 'no_recruitment_text_5', + 'text_nb': 'Hvis du allerede har søkt kan du logge inn som søker for å prioritere og følge med på dine søknader.', + 'text_en': 'If you have already applied, you can log in as an applicant to prioritize and monitor your applications.', + }, { '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. - 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.""", + '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. 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', 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 55239111d..67d609670 100644 --- a/backend/samfundet/conftest.py +++ b/backend/samfundet/conftest.py @@ -31,7 +31,6 @@ https://docs.pytest.org/en/7.1.x/how-to/fixtures.html """ - TestCase.databases = {'default', 'billig'} 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 936b0f97e..7b7ef6f7a 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -1,18 +1,18 @@ # # This file contains models spesific to the recruitment system # - from __future__ import annotations +import uuid + from django.db import models from django.utils import timezone from django.core.exceptions import ValidationError from root.utils.mixins import CustomBaseModel, FullCleanSaveMixin -from samfundet.models.model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices - from .general import Gang, User, Organization +from .model_choices import RecruitmentStatusChoices, RecruitmentPriorityChoices class Recruitment(CustomBaseModel): @@ -155,6 +155,7 @@ class Interview(CustomBaseModel): 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') recruitment_position = models.ForeignKey( RecruitmentPosition, on_delete=models.CASCADE, help_text='The recruitment position that is recruiting', related_name='admissions' @@ -184,6 +185,11 @@ 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() + # 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.""" if self.withdrawn: self.recruiter_priority = RecruitmentPriorityChoices.NOT_WANTED self.recruiter_status = RecruitmentStatusChoices.AUTOMATIC_REJECTION diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index d5a1f7115..3b53535d9 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -589,12 +589,47 @@ def update(self, instance: RecruitmentPosition, validated_data: dict) -> Recruit return updated_instance +class ApplicantInterviewSerializer(serializers.ModelSerializer): + class Meta: + model = Interview + fields = [ + 'id', + 'interview_time', + 'interview_location', + ] + + +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', + 'interview', 'created_at', 'withdrawn', ] @@ -656,6 +691,7 @@ def update(self, instance: RecruitmentAdmission, validated_data: dict) -> Recrui interview_instance = instance.interview interview_instance.interview_location = interview_data.get('interview_location', interview_instance.interview_location) interview_instance.interview_time = interview_data.get('interview_time', interview_instance.interview_time) + interview_instance.notes = interview_data.get('notes', interview_instance.notes) interview_instance.save() # Update other fields of RecruitmentAdmission instance diff --git a/backend/samfundet/tests/test_views.py b/backend/samfundet/tests/test_views.py index 69331c193..94f93838e 100644 --- a/backend/samfundet/tests/test_views.py +++ b/backend/samfundet/tests/test_views.py @@ -678,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/frontend/src/AppRoutes.tsx b/frontend/src/AppRoutes.tsx index 2becb28a8..0e5f30bb3 100644 --- a/frontend/src/AppRoutes.tsx +++ b/frontend/src/AppRoutes.tsx @@ -3,6 +3,7 @@ import { AboutPage, AdminPage, ApiTestingPage, + ApplicantApplicationOverviewPage, ComponentPage, EventPage, EventsPage, @@ -85,6 +86,8 @@ export function AppRoutes() { } /> } /> } /> + } /> + } /> {/* ADMIN ROUTES diff --git a/frontend/src/Components/MenuItem/MenuItem.module.scss b/frontend/src/Components/MenuItem/MenuItem.module.scss index 03a069961..c3de3f001 100644 --- a/frontend/src/Components/MenuItem/MenuItem.module.scss +++ b/frontend/src/Components/MenuItem/MenuItem.module.scss @@ -7,7 +7,7 @@ max-width: 600px; margin: 0 auto; color: white; - + margin-bottom: 2em; } .item_name { @@ -19,28 +19,23 @@ font-size: 1.25em; margin-bottom: 5px; text-transform: uppercase; - font-family: 'LemonMilkLight', sans-serif; + font-family: $lyche-title-font; } .item_price { - font-size: .75em; + font-size: 0.75em; margin-left: 20px; - margin-top: .25em; - font-family: 'Lora', sans-serif; - color: white; + margin-top: 0.25em; } .item_description { color: dimgray; - font-family: 'Lora', sans-serif; } .item_allergens { color: dimgray; - font-family: 'Lora', sans-serif; } .item_recommendations { color: dimgray; - font-family: 'Lora', sans-serif; } diff --git a/frontend/src/Components/Navbar/Navbar.tsx b/frontend/src/Components/Navbar/Navbar.tsx index 03ee777cc..2e9df7a36 100644 --- a/frontend/src/Components/Navbar/Navbar.tsx +++ b/frontend/src/Components/Navbar/Navbar.tsx @@ -81,7 +81,7 @@ export function Navbar() { className={styles.navbar_dropdown_link} onAfterClick={() => setExpandedDropdown('')} > - {t(KEY.common_about_samfundet)} + {t(KEY.common_general)} setExpandedDropdown('')}> {t(KEY.common_membership)} diff --git a/frontend/src/Components/SultenPage/SultenPage.module.scss b/frontend/src/Components/SultenPage/SultenPage.module.scss index f080559c7..72977d4b5 100644 --- a/frontend/src/Components/SultenPage/SultenPage.module.scss +++ b/frontend/src/Components/SultenPage/SultenPage.module.scss @@ -16,10 +16,11 @@ min-width: 100%; min-height: calc(100vh - 200px); z-index: 10; - padding: calc($sulten-navbar-height / 2) 0 calc($sulten-navbar-height / 2) 0; + padding-top: $sulten-navbar-height; @include for-mobile-down { flex-direction: column; padding: 0%; + padding-top: $sulten-navbar-height; } } diff --git a/frontend/src/Components/Table/Table.tsx b/frontend/src/Components/Table/Table.tsx index 5ba9762e8..123e67411 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 = -1 }: TableProps) { + const [sortColumn, setSortColumn] = useState(defaultSortColumn); const [sortInverse, setSortInverse] = useState(false); function sort(column: number) { diff --git a/frontend/src/Pages/AboutPage/AboutPage.tsx b/frontend/src/Pages/AboutPage/AboutPage.tsx index 70e42be81..4d1ef94da 100644 --- a/frontend/src/Pages/AboutPage/AboutPage.tsx +++ b/frontend/src/Pages/AboutPage/AboutPage.tsx @@ -1,60 +1,54 @@ import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import { Button, Carousel } from '~/Components'; import { Page } from '~/Components/Page'; import { runderode, splash } from '~/assets'; +import { TextItem } from '~/constants'; +import { useTextItem } from '~/hooks'; +import { KEY } from '~/i18n/constants'; import { ROUTES } from '~/routes'; import { backgroundImageFromUrl } from '~/utils'; import styles from './AboutPage.module.scss'; import { VENUES } from './data'; export function AboutPage() { + const { t } = useTranslation(); + return (
Runderode
-

Om Samfundet

-

- Studentersamfundet i Trondhjem er en organisasjon for studenter i Trondheim som eies og drives av sine rundt - 16 100 medlemmer. Formålsparagrafen vår sier at ”Studentersamfundet skal være det naturlige samlingsstedet - for studenter i Trondhjem”. Vårt røde runde huser konserter, ulike kulturarrangementer, utallige barer, en - kafé og en restaurant. Mest sagnomsust er Samfundsmøtene, viet til debatt om politikk og aktuelle spørsmål, - eller til underholdning og moro. Samfundet har også tre av Trondheims beste konsertscener. -

+

{t(KEY.common_about_samfundet)}

+

{useTextItem(TextItem.about_samfundet)}

-

Samfundsmøtet

-

- Samfundsmøtet er Studentersamfundets høyeste organ. Her velges leder samt medlemmer til Finansstyret og Rådet. - Lederen velger ut sitt eget styre, som utformer den politiske profilen og representer medlemmene og foreningen - Samfundet. Finansstyret administrerer forretningsdriften. Rådet kontrollerer at all virksomhet i Samfundet - foregår i henhold til norske og interne lover. Samfundet har en daglig leder, økonomiansvarlig, husøkonom, - vaktmester og renholdspersonell som er ansatte. -

+

{t(KEY.common_the_society_meeting)}

+

{useTextItem(TextItem.the_society_meeting)}

-

Lokaler

+

{t(KEY.common_venues)}

{VENUES.images.map((image, idx) => { @@ -69,29 +63,24 @@ export function AboutPage() {
Splash
-

Frivilligheten

-

- Det meste av arbeid på Studentersamfundet i Trondhjem gjøres gjennom dugnad av studenter. Arbeidet er - organisert i enheter som kalles gjenger. Potensielle nye medlemmer må søke den aktuelle gjengen om opptak, - og gjengen vurderer hver enkelt søker. Med sine omtrent 1700 frivillige utgjør det indre miljøet i - Studentersamfundet en betydelig del av det organiserte fritidstilbudet til studenter i Trondheim. -

+

{t(KEY.common_volunteering)}

+

{useTextItem(TextItem.volunteering)}

@@ -99,10 +88,7 @@ export function AboutPage() {

UKA & ISFiT

-

- Annenhvert år arrangeres Norges største kulturfestival UKA og verdens største internasjonale tematiske - studentfestival under Studentersamfundets paraply. -

+

{useTextItem(TextItem.festivals)}

-

Annen Info

+

{t(KEY.common_more_info)}

diff --git a/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss new file mode 100644 index 000000000..5b1f41dd7 --- /dev/null +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.module.scss @@ -0,0 +1,37 @@ +$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; +} + +.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 new file mode 100644 index 000000000..15ed72d32 --- /dev/null +++ b/frontend/src/Pages/ApplicantApplicationOverviewPage/ApplicantApplicationOverviewPage.tsx @@ -0,0 +1,104 @@ +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, putRecruitmentAdmission } from '~/api'; +import { RecruitmentAdmissionDto } from '~/dto'; +import { KEY } from '~/i18n/constants'; +import { ROUTES } from '~/routes'; +import { dbT, niceDateTime } from '~/utils'; +import styles from './ApplicantApplicationOverviewPage.module.scss'; +import { OccupiedFormModal } from '~/Components/OccupiedForm'; + +export function ApplicantApplicationOverviewPage() { + const { recruitmentID } = useParams(); + const [admissions, setAdmissions] = useState([]); + const { t } = useTranslation(); + + function handleChangePriority(id: number, direction: 'up' | 'down') { + 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 (index == 0 && direction === 'up') return; + if (index === newAdmissions.length - 1 && direction === 'down') return; + + const old_priority = newAdmissions[index].applicant_priority; + const new_priority = newAdmissions[index + directionIncrement].applicant_priority; + + newAdmissions[index].applicant_priority = new_priority; + newAdmissions[index + directionIncrement].applicant_priority = old_priority; + + // TODO: Make this a single API call + putRecruitmentAdmission(newAdmissions[index]); + putRecruitmentAdmission(newAdmissions[index + directionIncrement]).then(() => { + setAdmissions(newAdmissions); + }); + } + + function upDownArrow(id: number) { + return ( + <> + handleChangePriority(id, 'up')} /> + handleChangePriority(id, 'down')} /> + + ); + } + + useEffect(() => { + if (recruitmentID) { + getRecruitmentAdmissionsForApplicant(recruitmentID).then((response) => { + setAdmissions(response.data); + }); + } + }, [recruitmentID]); + + useEffect(() => { + console.log(admissions); + }, [admissions]); + + const tableColumns = [ + { 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: '' }, + ]; + + function admissionToTableRow(admission: RecruitmentAdmissionDto) { + return [ + dbT(admission.recruitment_position, 'name'), + niceDateTime(admission.interview.interview_time), + admission.interview.interview_location, + admission.applicant_priority, + { content: upDownArrow(admission.id) }, + ]; + } + + return ( + +
+
+ +

{t(KEY.recruitment_my_applications)}

+
+
+

{t(KEY.recruitment_will_be_anonymized)}

+ {admissions ? ( +
+ ) : ( +

{t(KEY.recruitment_not_applied)}

+ )} + + +
+
+ ); +} 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/LycheMenuPage/LycheMenuPage.tsx b/frontend/src/Pages/LycheMenuPage/LycheMenuPage.tsx index bcb823ca4..4f5d1853c 100644 --- a/frontend/src/Pages/LycheMenuPage/LycheMenuPage.tsx +++ b/frontend/src/Pages/LycheMenuPage/LycheMenuPage.tsx @@ -1,48 +1,43 @@ +import { LycheFrame } from '~/Components/LycheFrame'; import { MenuItem } from '~/Components/MenuItem'; import { SultenPage } from '~/Components/SultenPage'; export function LycheMenuPage() { return ( - + + -

-

+ - + -

-

- - - -

- - + +
); } diff --git a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx index 8d6a99371..a85f7c010 100644 --- a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx +++ b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx @@ -40,15 +40,19 @@ export function RecruitmentAdmissionFormPage() { }, [recruitmentPosition]); function handleOnSubmit(data: RecruitmentAdmissionDto) { - data.recruitment_position = 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) { diff --git a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss index f3614acfa..6112cf993 100644 --- a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss +++ b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.module.scss @@ -8,10 +8,41 @@ align-items: center; justify-content: center; gap: 2em; + box-sizing: border-box; +} + +.no_recruitment_wrapper { + margin-top: 1em; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.header { + align-content: center; + color: $red-samf; + font-size: 35px; + font-family: sans-serif; +} + +.link { + color: $red-samf; + font-family: sans-serif; +} + +.info { + text-align: left; + box-sizing: inherit; + max-width: 60%; + place-content: center; + font-size: 20px; + font-family: sans-serif; + @include for-desktop-up { padding: 3em 6em 3em 6em; } + } .video { @@ -20,3 +51,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..9bd8ac6c3 100644 --- a/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx +++ b/frontend/src/Pages/RecruitmentPage/RecruitmentPage.tsx @@ -1,15 +1,54 @@ import { useEffect, useState } from 'react'; -import { Page, SamfundetLogoSpinner, Video } from '~/Components'; +import { useTranslation } from 'react-i18next'; +import { Button, Page, SamfundetLogoSpinner, Video } from '~/Components'; import { getActiveRecruitmentPositions, getGangList } from '~/api'; +import { TextItem } from '~/constants'; import { GangTypeDto, RecruitmentPositionDto } from '~/dto'; +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'; export function RecruitmentPage() { + const navigate = useCustomNavigate(); const [recruitmentPositions, setRecruitmentPositions] = useState(); const [loading, setLoading] = useState(true); const [gangTypes, setGangs] = useState(); + const { t } = useTranslation(); + + const noadmissions = ( +
+ ); useEffect(() => { Promise.all([getActiveRecruitmentPositions(), getGangList()]) @@ -28,11 +67,28 @@ export function RecruitmentPage() {
- +
+ + +
{loading ? ( - ) : ( + ) : recruitmentPositions ? ( + ) : ( + noadmissions )}
diff --git a/frontend/src/Pages/index.ts b/frontend/src/Pages/index.ts index 31555ca6f..864abd660 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'; diff --git a/frontend/src/PagesAdmin/InterviewNotesAdminPage/InterviewNotesAdminPage.tsx b/frontend/src/PagesAdmin/InterviewNotesAdminPage/InterviewNotesAdminPage.tsx index 6b8ed1344..6b452bfa9 100644 --- a/frontend/src/PagesAdmin/InterviewNotesAdminPage/InterviewNotesAdminPage.tsx +++ b/frontend/src/PagesAdmin/InterviewNotesAdminPage/InterviewNotesAdminPage.tsx @@ -1,35 +1,78 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; import { Button } from '~/Components'; import { TextAreaField } from '~/Components/TextAreaField/TextAreaField'; +import { getRecruitmentAdmissionsForGang, putRecruitmentAdmissionInterview } from '~/api'; +import { InterviewDto, RecruitmentAdmissionDto } from '~/dto'; import { KEY } from '~/i18n/constants'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './InterviewNotesAdminPage.module.scss'; export function InterviewNotesPage() { - //TODO: interview notes from backend + const recruitmentId = useParams().recruitmentId; + const gangId = useParams().gangId; + const positionId = useParams().positionId; + const interviewId = useParams().interviewId; const [editingMode, setEditingMode] = useState(false); - const [text, setText] = useState('Notater fra intervjuet her...'); //TODO: place the text from the backend here. - const posId = 1; //TODO: get the posId from the backend. + const [recruitmentAdmission, setRecruitmentAdmission] = useState([]); + const [interview, setInterview] = useState(null); + const [disabled, setdisabled] = useState(true); + const [nameUser, setNameUser] = useState(''); const { t } = useTranslation(); - function handleEditSave() { - if (editingMode) { - //TODO: save the text in the textbox and send it to the backend + useEffect(() => { + if (positionId && recruitmentId && gangId && interviewId) { + getRecruitmentAdmissionsForGang(gangId, recruitmentId).then((response) => { + const recruitmentAdmissions = response.data; + const admission = recruitmentAdmissions.filter( + (admission) => + admission.recruitment_position && + admission.recruitment_position.toString() === positionId && + admission.interview.id.toString() === interviewId && + admission.interview.interview_time !== null, + ); + if (admission.length !== 0) { + setdisabled(false); + setRecruitmentAdmission(admission); + setInterview(admission[0].interview); + setNameUser( + admission[0].user.first_name ? admission[0].user.first_name + ' ' + admission[0].user.last_name : '', + ); + } + }); + } + }, [recruitmentId, positionId, gangId, interviewId, t]); + + async function handleEditSave() { + if (editingMode && interview) { + try { + await putRecruitmentAdmissionInterview(interview.id.toString(), interview); + toast.success(t(KEY.common_save_successful)); + } catch (error) { + toast.error(t(KEY.common_something_went_wrong)); + } } setEditingMode(!editingMode); } - //TODO: make handleSave function to save the text in the textbox and send it to the backend - return (
- -
diff --git a/frontend/src/_constants.scss b/frontend/src/_constants.scss index 7260adfb9..0b0e2900a 100644 --- a/frontend/src/_constants.scss +++ b/frontend/src/_constants.scss @@ -81,10 +81,9 @@ $theme-dark: 'theme-dark'; // Must match THEME.DARK $theme-light: 'theme-light'; // Must match THEM.LIGHT //Lyche -$lyche-title-font:'LemonMilkLight', sans-serif; +$lyche-title-font: 'LemonMilkLight', sans-serif; +$lyche-primary-font: 'Lora', sans-serif; //Assets $lyche-menu-divider-left: url('~/assets/lyche/menu-detail-left.png'); $lyche-menu-divider-right: url('~/assets/lyche/menu-detail-right.png'); - - diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 594e518c0..2a5ad1933 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -11,6 +11,7 @@ import { ImageDto, ImagePostDto, InformationPageDto, + InterviewDto, KeyValueDto, MenuDto, MenuItemDto, @@ -688,3 +689,34 @@ 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; +} + +export async function putRecruitmentAdmissionInterview( + interviewId: string, + interview: Partial, +): Promise { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__interview_detail, + urlParams: { pk: interviewId }, + }); + const response = await axios.put(url, interview, { withCredentials: true }); + return response; +} diff --git a/frontend/src/constants/TextItems.ts b/frontend/src/constants/TextItems.ts index ed22c3851..59e36d776 100644 --- a/frontend/src/constants/TextItems.ts +++ b/frontend/src/constants/TextItems.ts @@ -20,5 +20,20 @@ export const TextItem = { sulten_lyche_goal_text: 'sulten_lyche_goal_text', sulten_lyche_about_menu_text: 'sulten_lyche_about_menu_text', sulten_contact_page_text: 'sulten_contact_page_text', + + //About page + festivals: 'festivals', + volunteering: 'volunteering', + the_society_meeting: 'the_society_meeting', + about_samfundet: 'about_samfundet', sulten_contact_page_title: 'sulten_contact_page_title', + + //Recruitment + no_recruitment_text: 'no_recruitment_text', + no_recruitment_text_0: 'no_recruitment_text_0', + no_recruitment_text_1: 'no_recruitment_text_1', + no_recruitment_text_2: 'no_recruitment_text_2', + no_recruitment_text_3: 'no_recruitment_text_3', + no_recruitment_text_4: 'no_recruitment_text_4', + no_recruitment_text_5: 'no_recruitment_text_5', } as const; diff --git a/frontend/src/constants/constants.ts b/frontend/src/constants/constants.ts index 7803b1a6f..3b4af4575 100644 --- a/frontend/src/constants/constants.ts +++ b/frontend/src/constants/constants.ts @@ -35,7 +35,7 @@ export const THEME_KEY = 'data-theme'; // Valid html tag attribute. export const SUPPORT_EMAIL = 'mg-web@samfundet.no'; -export const PHONENUMBER_REGEX = new RegExp('^(\\+?\\(?\\d{3}\\)?[-\\s\\.]?)?\\d{3}[-\\s\\.]?\\d{4,6}$'); +export const PHONENUMBER_REGEX = new RegExp('^\\+?\\s*(\\d\\s*){8,15}$'); /** * Screen sizes, breakpoint (bp). * These values are also in _constants.scss diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 81fa15f52..d6409fd5c 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -332,6 +332,10 @@ export type NotificationDto = { // TODO: There are more fields than this. }; +// ############################################################ +// Recruitment +// ############################################################ + export type RecruitmentDto = { id: string | undefined; name_nb: string; @@ -382,7 +386,7 @@ export type RecruitmentAdmissionDto = { id: number; interview: InterviewDto; admission_text: string; - recruitment_position?: number; + recruitment_position: RecruitmentPositionDto; recruitment: number; user: UserDto; applicant_priority: number; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 08f659a38..ffcbfedff 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -110,15 +110,36 @@ export const KEY = { common_recruitment: 'common_recruitment', common_reservation: 'common_reservation', common_opening_hours: 'common_opening_hours', - common_about_samfundet: 'common_about_samfundet', + common_general: 'common_general', common_long_description: 'common_long_description', common_short_description: 'common_short_description', common_back_to_samfundet: 'common_back_to_samfundet', + common_save_successful: 'common_save_successful', common_delete_successful: 'common_delete_successful', common_update_successful: 'common_update_successful', common_creation_successful: 'common_creation_successful', common_see_in_django_admin: 'common_see_in_django_admin', common_something_went_wrong: 'common_something_went_wrong', + common_click_here: 'common_click_here', + + //About page + common_age_limit: 'common_age_limit', + common_rent_services: 'common_rent_services', + common_press: 'common_press', + common_film_club: 'common_film_club', + common_privacy_policy: 'common_privacy_policy', + common_facilitation: 'common_facilitation', + common_the_groups_at_samfundet: 'commong_the_groups_at_samfundet', + common_volunteering: 'common_volunteering', + common_overview_map: 'common_overview_map', + common_new_building: 'common_new_building', + common_documents: 'common_documents', + common_our_history: 'common_our_history', + common_about_the_organisation: 'common_about_the_organisation', + common_the_society_meeting: 'common_the_society_meeting', + common_tickets: 'common_tickets', + common_contact_information: 'common_contact_information', + common_about_samfundet: 'common_about_samfundet', // Price groups: common_ticket_type: 'common_ticket_type', @@ -185,6 +206,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..b30607d87 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -96,19 +96,40 @@ export const nb: Record = { [KEY.common_description]: 'Beskrivelse', [KEY.common_reservation]: 'Reservasjon', [KEY.common_opening_hours]: 'Åpningstider', - [KEY.common_about_samfundet]: 'Generelt', + [KEY.common_general]: 'Generelt', [KEY.common_long_description]: 'Lang beskrivelse', [KEY.common_short_description]: 'Kort beskrivelse', [KEY.common_back_to_samfundet]: 'Tilbake til samfundet.no', + [KEY.common_save_successful]: 'Lagring var vellykket', [KEY.common_delete_successful]: 'Slettingen var vellykket', [KEY.common_update_successful]: 'Oppdateringen var vellykket', [KEY.common_see_in_django_admin]: 'Se i django admin-panel', [KEY.common_creation_successful]: 'Opprettelsen var vellykket', [KEY.common_something_went_wrong]: 'Noe gikk galt', + [KEY.common_click_here]: 'klikk her', [KEY.common_total]: 'Antall', [KEY.common_guests]: 'Gjester', [KEY.common_occasion]: 'Annledning', + //About page + [KEY.common_age_limit]: 'aldersgrense', + [KEY.common_rent_services]: 'leie og tjenester', + [KEY.common_press]: 'presse', + [KEY.common_film_club]: 'filmklubb', + [KEY.common_privacy_policy]: 'personvern', + [KEY.common_facilitation]: 'tilrettelegging', + [KEY.common_the_groups_at_samfundet]: 'Gjengene på Samfundet', + [KEY.common_volunteering]: 'Frivilligheten', + [KEY.common_overview_map]: 'oversiktskart', + [KEY.common_new_building]: 'nybygg', + [KEY.common_documents]: 'saksdokumenter', + [KEY.common_our_history]: 'Samfundets historie', + [KEY.common_about_the_organisation]: 'Samfundet som organisasjon', + [KEY.common_the_society_meeting]: 'Samfundsmøtet', + [KEY.common_tickets]: 'billetter', + [KEY.common_contact_information]: 'Kontaktinfo', + [KEY.common_about_samfundet]: 'Om Samfundet', + // Price groups: [KEY.common_ticket_type]: 'Billett', [KEY.common_ticket_type_free]: 'Gratis', @@ -152,6 +173,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', @@ -353,19 +377,40 @@ export const en: Record = { [KEY.common_recruitment]: 'Recruitment', [KEY.common_reservation]: 'Reservation', [KEY.common_opening_hours]: 'Opening hours', - [KEY.common_about_samfundet]: 'General', + [KEY.common_general]: 'General', [KEY.common_long_description]: 'Long description', [KEY.common_short_description]: 'Short description', [KEY.common_back_to_samfundet]: 'Back to samfundet.no', + [KEY.common_save_successful]: 'Saving was successful', [KEY.common_delete_successful]: 'Deletion was successful', [KEY.common_update_successful]: 'The update was successful', [KEY.common_see_in_django_admin]: 'See in django admin-panel', [KEY.common_creation_successful]: 'Successfully created', [KEY.common_something_went_wrong]: 'Something went wrong', + [KEY.common_click_here]: 'click here', [KEY.common_total]: 'Total', [KEY.common_guests]: 'Guests', [KEY.common_occasion]: 'Occasion', + //About page + [KEY.common_age_limit]: 'Age limit', + [KEY.common_rent_services]: 'Renting and other services', + [KEY.common_press]: 'Press', + [KEY.common_film_club]: 'Film club', + [KEY.common_privacy_policy]: 'Privacy policy', + [KEY.common_facilitation]: 'Facilitation', + [KEY.common_the_groups_at_samfundet]: 'The groups at Samfundet', + [KEY.common_volunteering]: 'Volunteering', + [KEY.common_overview_map]: 'Overview map', + [KEY.common_new_building]: 'New building', + [KEY.common_documents]: 'Documents', + [KEY.common_our_history]: 'Our history', + [KEY.common_about_the_organisation]: 'About the organisation', + [KEY.common_the_society_meeting]: 'The Society Meeting', + [KEY.common_tickets]: 'tickets', + [KEY.common_contact_information]: 'Contact information', + [KEY.common_about_samfundet]: 'About Samfundet', + // Price groups: [KEY.common_ticket_type]: 'Ticket', [KEY.common_ticket_type_free]: 'Free', @@ -409,11 +454,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', diff --git a/frontend/src/routes/frontend.ts b/frontend/src/routes/frontend.ts index 997262772..7ccab33bc 100644 --- a/frontend/src/routes/frontend.ts +++ b/frontend/src/routes/frontend.ts @@ -14,8 +14,11 @@ 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/', + contact: '/contact', // ==================== // // Sulten // // ==================== // @@ -67,7 +70,7 @@ export const ROUTES_FRONTEND = { admin_recruitment_gang_position_applicants_overview: '/control-panel/recruitment/:recruitmentId/gang/:gangId/position/:positionId', admin_recruitment_gang_position_applicants_interview_notes: - '/control-panel/recruitment/:recruitmentId/gang/:gangId/position/:positionId/notesId', //fix when backend is done + '/control-panel/recruitment/:recruitmentId/gang/:gangId/position/:positionId/interview-notes/:interviewId', admin_sulten_menu: '/control-panel/lyche/menu', // ==================== // // Development // 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