diff --git a/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml index c8ec08da..0d56f627 100644 --- a/.github/workflows/backend-tests.yaml +++ b/.github/workflows/backend-tests.yaml @@ -21,5 +21,7 @@ jobs: python -m pip install --upgrade pip pip install flake8 pip install -r ./backend/requirements.txt + - name: Compile translations + run: django-admin compilemessages - name: Execute tests run: cd backend; python manage.py test diff --git a/backend/api/locale/en/LC_MESSAGES/django.po b/backend/api/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..ac864a0a --- /dev/null +++ b/backend/api/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,148 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-13 23:12+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: helpers/check_folder_structure.py:141 +msgid "zip.errors.invalid_structure.blocked_extension_found" +msgstr "The submitted zip file contains a file with a non-allowed extension." + +#: helpers/check_folder_structure.py:145 helpers/check_folder_structure.py:196 +msgid "zip.success" +msgstr "The submitted zip file succeeds in all checks." + +#: helpers/check_folder_structure.py:148 +msgid "zip.errors.invalid_structure.obligated_extension_not_found" +msgstr "" +"The submitted zip file doesn't have any file with a certain file extension " +"that's obligated." + +#: helpers/check_folder_structure.py:175 +msgid "zip.errors.invalid_structure.directory_not_defined" +msgstr "An obligated directory was not found in the submitted zip file." + +#: helpers/check_folder_structure.py:195 +msgid "zip.errors.invalid_structure.directory_not_found_in_template" +msgstr "" +"There was a directory found in the submitted zip file, which was not asked " +"for." + +#: serializers/course_serializer.py:58 serializers/course_serializer.py:77 +msgid "courses.error.context" +msgstr "The course is not supplied in the context." + +#: serializers/course_serializer.py:64 tests/test_locale.py:28 +#: tests/test_locale.py:38 +msgid "courses.error.students.already_present" +msgstr "The student is already present in the course." + +#: serializers/course_serializer.py:68 serializers/course_serializer.py:87 +msgid "courses.error.students.past_course" +msgstr "The course is from a past year, thus cannot be manipulated." + +#: serializers/course_serializer.py:83 +msgid "courses.error.students.not_present" +msgstr "The student is not present in the course." + +#: serializers/group_serializer.py:47 +msgid "group.errors.score_exceeds_max" +msgstr "The score exceeds the group's max score." + +#: serializers/group_serializer.py:57 serializers/group_serializer.py:87 +msgid "group.error.context" +msgstr "The group is not supplied in the context." + +#: serializers/group_serializer.py:65 serializers/group_serializer.py:99 +msgid "group.errors.locked" +msgstr "The group is currently locked." + +#: serializers/group_serializer.py:69 +msgid "group.errors.full" +msgstr "The group is already full." + +#: serializers/group_serializer.py:73 +msgid "group.errors.not_in_course" +msgstr "The student is not present in the related course." + +#: serializers/group_serializer.py:77 +msgid "group.errors.already_in_group" +msgstr "The student is already in the group." + +#: serializers/group_serializer.py:95 +msgid "group.errors.not_present" +msgstr "The student is currently not in the group." + +#: serializers/project_serializer.py:56 +msgid "project.errors.context" +msgstr "The project is not supplied in the context." + +#: serializers/project_serializer.py:60 +msgid "project.errors.start_date_in_past" +msgstr "The start date of the project lies in the past." + +#: serializers/project_serializer.py:64 +msgid "project.errors.deadline_before_start_date" +msgstr "The deadline of the project lies before the start date of the project." + +#: serializers/project_serializer.py:89 +msgid "project.error.submissions.past_project" +msgstr "The deadline of the project has already passed." + +#: serializers/project_serializer.py:92 +msgid "project.error.submissions.non_visible_project" +msgstr "The project is currently in a non-visible state." + +#: serializers/project_serializer.py:95 +msgid "project.error.submissions.archived_project" +msgstr "The project is archived." + +#: views/course_view.py:58 +msgid "courses.success.assistants.add" +msgstr "The assistant was successfully added to the course." + +#: views/course_view.py:77 +msgid "courses.success.assistants.remove" +msgstr "The assistant was successfully removed from the course." + +#: views/course_view.py:111 +msgid "courses.success.students.add" +msgstr "The student was successfully added to the course." + +#: views/course_view.py:131 +msgid "courses.success.students.remove" +msgstr "The student was successfully removed from the course." + +#: views/course_view.py:186 +msgid "course.success.project.add" +msgstr "The project was successfully added to the course." + +#: views/group_view.py:73 +msgid "group.success.students.add" +msgstr "The student was successfully added to the group." + +#: views/group_view.py:92 +msgid "group.success.students.remove" +msgstr "The student was successfully removed from the group." + +#: views/group_view.py:111 +msgid "group.success.submissions.add" +msgstr "The submission was successfully added to the group." + +#: views/project_view.py:80 +msgid "project.success.groups.created" +msgstr "A group was successfully created for the project." diff --git a/backend/api/locale/nl/LC_MESSAGES/django.po b/backend/api/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 00000000..320b5cb0 --- /dev/null +++ b/backend/api/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,149 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-13 23:07+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: helpers/check_folder_structure.py:141 +msgid "zip.errors.invalid_structure.blocked_extension_found" +msgstr "" +"Bestanden met een verboden extensie zijn gevonden in het ingediende zip-" +"bestand." + +#: helpers/check_folder_structure.py:145 helpers/check_folder_structure.py:196 +msgid "zip.success" +msgstr "Het zip-bestand van de indiening bevat alle benodigde bestanden." + +#: helpers/check_folder_structure.py:148 +msgid "zip.errors.invalid_structure.obligated_extension_not_found" +msgstr "" +"Er is geen enkel bestand met een bepaalde extensie die verplicht is in het " +"ingediende zip-bestand." + +#: helpers/check_folder_structure.py:175 +msgid "zip.errors.invalid_structure.directory_not_defined" +msgstr "Een verplichte map is niet aanwezig in het ingediende zip-bestand." + +#: helpers/check_folder_structure.py:195 +msgid "zip.errors.invalid_structure.directory_not_found_in_template" +msgstr "Het ingediende zip-bestand bevat een map die niet gevraagd is." + +#: serializers/course_serializer.py:58 serializers/course_serializer.py:77 +msgid "courses.error.context" +msgstr "De opleiding is niet meegeleverd als context." + +#: serializers/course_serializer.py:64 tests/test_locale.py:28 +#: tests/test_locale.py:38 +msgid "courses.error.students.already_present" +msgstr "De student bevindt zich al in de opleiding." + +#: serializers/course_serializer.py:68 serializers/course_serializer.py:87 +msgid "courses.error.students.past_course" +msgstr "De opleiding die men probeert te manipuleren is van een vorig jaar." + +#: serializers/course_serializer.py:83 +msgid "courses.error.students.not_present" +msgstr "De student bevindt zich niet in de opleiding." + +#: serializers/group_serializer.py:47 +msgid "group.errors.score_exceeds_max" +msgstr "De score van de groep is groter dan de maximum score." + +#: serializers/group_serializer.py:57 serializers/group_serializer.py:87 +msgid "group.error.context" +msgstr "De groep is niet meegegeven als context waar dat nodig is." + +#: serializers/group_serializer.py:65 serializers/group_serializer.py:99 +msgid "group.errors.locked" +msgstr "De groep is momenteel vergrendeld." + +#: serializers/group_serializer.py:69 +msgid "group.errors.full" +msgstr "De groep is al vol." + +#: serializers/group_serializer.py:73 +msgid "group.errors.not_in_course" +msgstr "" +"De student bevindt zich niet in de opleiding waartoe het project hoort." + +#: serializers/group_serializer.py:77 +msgid "group.errors.already_in_group" +msgstr "De student bevindt zich al in de groep." + +#: serializers/group_serializer.py:95 +msgid "group.errors.not_present" +msgstr "De student bevindt zich niet in de groep." + +#: serializers/project_serializer.py:56 +msgid "project.errors.context" +msgstr "Het project is niet meegegeven als context waar dat nodig is." + +#: serializers/project_serializer.py:60 +msgid "project.errors.start_date_in_past" +msgstr "De startdatum van het project ligt in het verleden." + +#: serializers/project_serializer.py:64 +msgid "project.errors.deadline_before_start_date" +msgstr "De uiterste inleverdatum voor het project ligt voor de startdatum." + +#: serializers/project_serializer.py:89 +msgid "project.error.submissions.past_project" +msgstr "De uiterste inleverdatum voor het project is gepasseerd." + +#: serializers/project_serializer.py:92 +msgid "project.error.submissions.non_visible_project" +msgstr "Het project is niet zichtbaar." + +#: serializers/project_serializer.py:95 +msgid "project.error.submissions.archived_project" +msgstr "Het project is gearchiveerd." + +#: views/course_view.py:58 +msgid "courses.success.assistants.add" +msgstr "De assistent is succesvol toegevoegd aan de opleiding." + +#: views/course_view.py:77 +msgid "courses.success.assistants.remove" +msgstr "De assistent is succesvol verwijderd uit de opleiding." + +#: views/course_view.py:111 +msgid "courses.success.students.add" +msgstr "De student is succesvol toegevoegd aan de opleiding." + +#: views/course_view.py:131 +msgid "courses.success.students.remove" +msgstr "De student is succesvol verwijderd uit de opleiding." + +#: views/course_view.py:186 +msgid "course.success.project.add" +msgstr "Het project is succesvol toegevoegd aan de opleiding." + +#: views/group_view.py:73 +msgid "group.success.students.add" +msgstr "De student is succesvol toegevoegd aan de groep." + +#: views/group_view.py:92 +msgid "group.success.students.remove" +msgstr "De student is succesvol verwijderd uit de groep." + +#: views/group_view.py:111 +msgid "group.success.submissions.add" +msgstr "De indiening is succesvol toegevoegd aan de groep." + +#: views/project_view.py:80 +msgid "project.success.groups.created" +msgstr "De groep is succesvol toegevoegd aan het project." diff --git a/backend/api/models/checks.py b/backend/api/models/checks.py index 0c5e20c3..b27c3bdc 100644 --- a/backend/api/models/checks.py +++ b/backend/api/models/checks.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils.translation import gettext_lazy as _ from api.models.project import Project from api.models.extension import FileExtension @@ -38,6 +39,8 @@ class StructureCheck(models.Model): blank=True ) + # ID check should be generated automatically + class ExtraCheck(models.Model): """Model that represents an extra check for a project. diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index 44c2468c..e7343a86 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -86,12 +86,12 @@ def validate(self, data): # Check if the project's deadline is not passed. if project.deadline_passed(): - raise ValidationError(gettext("project.error.submission.past_project")) + raise ValidationError(gettext("project.error.submissions.past_project")) if not project.is_visible(): - raise ValidationError(gettext("project.error.submission.non_visible_project")) + raise ValidationError(gettext("project.error.submissions.non_visible_project")) if project.is_archived(): - raise ValidationError(gettext("project.error.submission.archived_project")) + raise ValidationError(gettext("project.error.submissions.archived_project")) return data diff --git a/backend/api/tests/test_file_structure.py b/backend/api/tests/test_file_structure.py index d16b2ea2..f29f6a17 100644 --- a/backend/api/tests/test_file_structure.py +++ b/backend/api/tests/test_file_structure.py @@ -212,5 +212,4 @@ def test_your_checking(self): obligated=[fileExtensionTS, fileExtensionTSX], blocked=[]) - succes = (True, 'zip.success') - self.assertEqual(check_zip_file(project=project, dir_path="structures/zip_struct1.zip"), succes) + self.assertTrue(check_zip_file(project=project, dir_path="structures/zip_struct1.zip")[0]) diff --git a/backend/api/tests/test_locale.py b/backend/api/tests/test_locale.py new file mode 100644 index 00000000..43403caa --- /dev/null +++ b/backend/api/tests/test_locale.py @@ -0,0 +1,38 @@ +import json +from django.urls import reverse +from django.utils.translation import activate +from django.utils.translation import gettext as _ +from rest_framework.test import APITestCase + +from api.models.course import Course +from api.models.student import Student +from authentication.models import User + + +class TestLocaleAddAlreadyPresentStudentToCourse(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate(User.get_dummy_admin()) + + course = Course.objects.create(id=1, name="Test Course", academic_startyear=2024) + student = Student.objects.create(id=1, first_name="John", last_name="Doe", email="john.doe@example.com") + + student.courses.add(course) + + def test_default_locale(self): + response = self.client.post(reverse("course-students", args=["1"]), + {"student_id": 1}) + + self.assertEqual(response.status_code, 400) + body = json.loads(response.content.decode('utf-8')) + activate("en") + self.assertEqual(body["non_field_errors"][0], _("courses.error.students.already_present")) + + def test_nl_locale(self): + response = self.client.post(reverse("course-students", args=["1"]), + {"student_id": 1}, + headers={"accept-language": "nl"}) + + self.assertEqual(response.status_code, 400) + body = json.loads(response.content.decode('utf-8')) + activate("nl") + self.assertEqual(body["non_field_errors"][0], _("courses.error.students.already_present")) diff --git a/backend/api/views/course_view.py b/backend/api/views/course_view.py index 6aac3096..991aeff9 100644 --- a/backend/api/views/course_view.py +++ b/backend/api/views/course_view.py @@ -74,7 +74,7 @@ def _remove_assistant(self, request: Request, **_): ) return Response({ - "message": gettext("courses.success.assistants.add") + "message": gettext("courses.success.assistants.remove") }) @action(detail=True, methods=["get"], permission_classes=[IsAdminUser | CourseStudentPermission]) @@ -128,7 +128,7 @@ def _remove_student(self, request: Request, **_): ) return Response({ - "message": gettext("courses.success.students.add") + "message": gettext("courses.success.students.remove") }) @action(detail=True, methods=["get"]) diff --git a/backend/api/views/group_view.py b/backend/api/views/group_view.py index 632bb7ff..ef82db0d 100644 --- a/backend/api/views/group_view.py +++ b/backend/api/views/group_view.py @@ -70,7 +70,7 @@ def _add_student(self, request, **_): ) return Response({ - "message": gettext("group.success.student.add"), + "message": gettext("group.success.students.add"), }) @students.mapping.delete @@ -89,7 +89,7 @@ def _remove_student(self, request, **_): ) return Response({ - "message": gettext("group.success.student.remove"), + "message": gettext("group.success.students.remove"), }) @submissions.mapping.post diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py index 736ed2f0..c931970e 100644 --- a/backend/authentication/tests/test_authentication_serializer.py +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -13,10 +13,10 @@ WRONG_TICKET = "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas5" ID = "1234" -USERNAME = "ddickwd" +USERNAME = "dackers" EMAIL = "dummy@dummy.be" FIRST_NAME = "Dummy" -LAST_NAME = "McDickwad" +LAST_NAME = "Ackers" class UserSerializerModelTests(TestCase): @@ -152,7 +152,7 @@ def test_new_user_activates_user_created_signal(self): be sent when trying to validate the token.""" mock = Mock() - user_created.connect(mock, dispatch_uid="STDsAllAround") + user_created.connect(mock, dispatch_uid="duid") serializer = CASTokenObtainSerializer( data={"token": RefreshToken(), "ticket": TICKET} ) @@ -169,7 +169,7 @@ def test_old_user_does_not_activate_user_created_signal(self): be sent when trying to validate the token.""" mock = Mock() - user_created.connect(mock, dispatch_uid="STDsAllAround") + user_created.connect(mock, dispatch_uid="duid") serializer = CASTokenObtainSerializer( data={"token": RefreshToken(), "ticket": TICKET} ) @@ -186,7 +186,7 @@ def test_login_signal(self): the token, then the user_login signal should be sent. """ mock = Mock() - user_login.connect(mock, dispatch_uid="STDsAllAround") + user_login.connect(mock, dispatch_uid="duid") serializer = CASTokenObtainSerializer( data={"token": RefreshToken(), "ticket": TICKET} ) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 8e7a4155..cb5734c8 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -11,10 +11,10 @@ def setUp(self): """Create a user and generate a token for that user""" self.user = User.objects.create(**{ "id": "1234", - "username": "ddickwd", + "username": "dackers", "email": "dummy@dummy.com", "first_name": "dummy", - "last_name": "McDickwad", + "last_name": "Ackers", }) self.token = f'Bearer {AccessToken().for_user(self.user)}' @@ -54,10 +54,10 @@ class TestLogoutView(APITestCase): def setUp(self): user_data = { "id": "1234", - "username": "ddickwd", + "username": "dackers", "email": "dummy@dummy.com", "first_name": "dummy", - "last_name": "McDickwad", + "last_name": "Ackers", } self.user = User.objects.create(**user_data) diff --git a/backend/ypovoli/handlers.py b/backend/ypovoli/handlers.py new file mode 100644 index 00000000..1a027b7a --- /dev/null +++ b/backend/ypovoli/handlers.py @@ -0,0 +1,14 @@ +from rest_framework.views import exception_handler +from django.utils.translation import gettext_lazy as _ + + +def translate_exception_handler(exc, context): + response = exception_handler(exc, context) + + if response.status_code == 401: + response.data['detail'] = _('Given token not valid for any token type') + + if response.status_code == 404: + response.data['detail'] = _('Not found.') + + return response diff --git a/backend/ypovoli/locale/en/LC_MESSAGES/django.po b/backend/ypovoli/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..b14e202b --- /dev/null +++ b/backend/ypovoli/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-12 17:05+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: settings.py:115 +msgid "languages.en" +msgstr "English" + +#: settings.py:115 +msgid "languages.nl" +msgstr "Dutch" diff --git a/backend/ypovoli/locale/nl/LC_MESSAGES/django.po b/backend/ypovoli/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 00000000..513be77b --- /dev/null +++ b/backend/ypovoli/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-12 17:05+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: settings.py:115 +msgid "languages.en" +msgstr "Engels" + +#: settings.py:115 +msgid "languages.nl" +msgstr "Nederlands" diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index aa3665fc..21e59fd6 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/5.0/ref/settings/ """ +from django.utils.translation import gettext_lazy as _ import os from datetime import timedelta from os import environ @@ -116,6 +117,7 @@ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True +LANGUAGES = [("en", _("languages.en")), ("nl", _("languages.nl"))] USE_L10N = False USE_TZ = True