From e0b5b9b9fbda225da0e13f9e4a22904f6dac7896 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 16:45:01 +0100 Subject: [PATCH 01/24] chore: first test file added #27 --- .../tests/test_authentication.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/authentication/tests/test_authentication.py diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py new file mode 100644 index 00000000..0d66972e --- /dev/null +++ b/backend/authentication/tests/test_authentication.py @@ -0,0 +1,31 @@ +import cas_client +from django.test import TestCase +from unittest.mock import patch + +from ..serializers import CASTokenObtainSerializer, UserSerializer + + +def service_validate( + self, + ticket=None, + service_url=None, + headers=None,): + response = {} + if ticket != "1": + response.error = "This is an error" + else: + response.data = { + "ugentID": 1234, + "uid": 4321, + "mail": "dummy@dummy.be", + "givenname": "Dummy", + "surname": "McDickwad", + "faculty": "Sciences", + "lastenrolled": "" + } + + +class SerializersTests(TestCase): + @patch.object(cas_client.CASClient, 'perform_service_validate', service_validate) + def test_invalid_ticket_generates_error(self): + pass \ No newline at end of file From e35232e49aa3e2bbfaba344561a7133d5f1f8aca Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 16:45:01 +0100 Subject: [PATCH 02/24] chore: first test file added #27 --- .../tests/test_authentication.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/authentication/tests/test_authentication.py diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py new file mode 100644 index 00000000..0d66972e --- /dev/null +++ b/backend/authentication/tests/test_authentication.py @@ -0,0 +1,31 @@ +import cas_client +from django.test import TestCase +from unittest.mock import patch + +from ..serializers import CASTokenObtainSerializer, UserSerializer + + +def service_validate( + self, + ticket=None, + service_url=None, + headers=None,): + response = {} + if ticket != "1": + response.error = "This is an error" + else: + response.data = { + "ugentID": 1234, + "uid": 4321, + "mail": "dummy@dummy.be", + "givenname": "Dummy", + "surname": "McDickwad", + "faculty": "Sciences", + "lastenrolled": "" + } + + +class SerializersTests(TestCase): + @patch.object(cas_client.CASClient, 'perform_service_validate', service_validate) + def test_invalid_ticket_generates_error(self): + pass \ No newline at end of file From a177535c2ddf4669d98d60f03b0c025444146702 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 18:07:53 +0100 Subject: [PATCH 03/24] test: first UserSerializer test + base CASTokenObtain class #27 --- .../tests/test_authentication.py | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py index 0d66972e..2820f9f5 100644 --- a/backend/authentication/tests/test_authentication.py +++ b/backend/authentication/tests/test_authentication.py @@ -1,31 +1,58 @@ import cas_client from django.test import TestCase +from rest_framework.serializers import ValidationError from unittest.mock import patch from ..serializers import CASTokenObtainSerializer, UserSerializer -def service_validate( - self, - ticket=None, - service_url=None, - headers=None,): - response = {} - if ticket != "1": - response.error = "This is an error" - else: - response.data = { - "ugentID": 1234, - "uid": 4321, - "mail": "dummy@dummy.be", - "givenname": "Dummy", - "surname": "McDickwad", - "faculty": "Sciences", - "lastenrolled": "" - } +def customize_data(ugent_id, uid, mail): + + def service_validate( + self, + ticket=None, + service_url=None, + headers=None,): + response = {} + if ticket != "1": + response.error = "This is an error" + else: + response.data = { + "ugentID": ugent_id, + "uid": uid, + "mail": mail, + "givenname": "Dummy", + "surname": "McDickwad", + "faculty": "Sciences", + "lastenrolled": "2021-05-21", + "lastlogin": "", + "createtime": "" + } + return response + + return service_validate + + +class UserSerializerModelTests(TestCase): + def test_non_string_id_makes_user_serializer_invalid(self): + user = UserSerializer(data={ + "id": 1234 + }) + self.assertFalse(user.is_valid()) class SerializersTests(TestCase): - @patch.object(cas_client.CASClient, 'perform_service_validate', service_validate) + class CASTokenObtain: + def __init__(self, ticket): + self.token = "ABCD" + self.ticket = ticket + + @patch.object(cas_client.CASClient, + 'perform_service_validate', + customize_data("1234", "ddickwd", "dummy@dummy.be")) def test_invalid_ticket_generates_error(self): - pass \ No newline at end of file + """When the wrong ticket is provided, a ValidationError should be raised.""" + # I have set "1" as the correct ticket here + obtain = self.CASTokenObtain("2") + serializer = CASTokenObtainSerializer(obtain) + self.assertRaises(ValidationError, lambda: serializer.validate(serializer.ticket)) From 438ce8afbb9d78cb9e15f2dd0c930cb83b17f2e6 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 19:01:50 +0100 Subject: [PATCH 04/24] test: add more UserSerializerModel tests #27 --- .../tests/test_authentication.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py index 2820f9f5..b687c0d4 100644 --- a/backend/authentication/tests/test_authentication.py +++ b/backend/authentication/tests/test_authentication.py @@ -35,11 +35,43 @@ def service_validate( class UserSerializerModelTests(TestCase): def test_non_string_id_makes_user_serializer_invalid(self): + """ + The is_valid() method of a UserSerializer whose supplied User's ID is not a string + should return False. + """ user = UserSerializer(data={ "id": 1234 }) self.assertFalse(user.is_valid()) + def test_non_string_username_makes_user_serializer_invalid(self): + """ + The is_valid() method of a UserSerializer whose supplied User's username is not a string + should return False. + """ + user = UserSerializer(data={ + "username": 10 + }) + self.assertFalse(user.is_valid()) + + def test_invalid_email_makes_user_serializer_invalid(self): + """ + The is_valid() method of a UserSerializer whose supplied User's email is not + formatted as an email address should return False. + """ + user = UserSerializer(data={ + "email": "dummy" + }) + user2 = UserSerializer(data={ + "email": "dummy@dummy" + }) + user3 = UserSerializer(data={ + "email": 21 + }) + self.assertFalse(user.is_valid()) + self.assertFalse(user2.is_valid()) + self.assertFalse(user3.is_valid()) + class SerializersTests(TestCase): class CASTokenObtain: From fd9ce378d3a363961a122cccf3d081cefc39aa4a Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 16:45:01 +0100 Subject: [PATCH 05/24] chore: first test file added #27 --- .../tests/test_authentication.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/authentication/tests/test_authentication.py diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py new file mode 100644 index 00000000..0d66972e --- /dev/null +++ b/backend/authentication/tests/test_authentication.py @@ -0,0 +1,31 @@ +import cas_client +from django.test import TestCase +from unittest.mock import patch + +from ..serializers import CASTokenObtainSerializer, UserSerializer + + +def service_validate( + self, + ticket=None, + service_url=None, + headers=None,): + response = {} + if ticket != "1": + response.error = "This is an error" + else: + response.data = { + "ugentID": 1234, + "uid": 4321, + "mail": "dummy@dummy.be", + "givenname": "Dummy", + "surname": "McDickwad", + "faculty": "Sciences", + "lastenrolled": "" + } + + +class SerializersTests(TestCase): + @patch.object(cas_client.CASClient, 'perform_service_validate', service_validate) + def test_invalid_ticket_generates_error(self): + pass \ No newline at end of file From 05034e48b90831bdf9c59709e6232b998aaac26c Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 18:07:53 +0100 Subject: [PATCH 06/24] test: first UserSerializer test + base CASTokenObtain class #27 --- .../tests/test_authentication.py | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py index 0d66972e..2820f9f5 100644 --- a/backend/authentication/tests/test_authentication.py +++ b/backend/authentication/tests/test_authentication.py @@ -1,31 +1,58 @@ import cas_client from django.test import TestCase +from rest_framework.serializers import ValidationError from unittest.mock import patch from ..serializers import CASTokenObtainSerializer, UserSerializer -def service_validate( - self, - ticket=None, - service_url=None, - headers=None,): - response = {} - if ticket != "1": - response.error = "This is an error" - else: - response.data = { - "ugentID": 1234, - "uid": 4321, - "mail": "dummy@dummy.be", - "givenname": "Dummy", - "surname": "McDickwad", - "faculty": "Sciences", - "lastenrolled": "" - } +def customize_data(ugent_id, uid, mail): + + def service_validate( + self, + ticket=None, + service_url=None, + headers=None,): + response = {} + if ticket != "1": + response.error = "This is an error" + else: + response.data = { + "ugentID": ugent_id, + "uid": uid, + "mail": mail, + "givenname": "Dummy", + "surname": "McDickwad", + "faculty": "Sciences", + "lastenrolled": "2021-05-21", + "lastlogin": "", + "createtime": "" + } + return response + + return service_validate + + +class UserSerializerModelTests(TestCase): + def test_non_string_id_makes_user_serializer_invalid(self): + user = UserSerializer(data={ + "id": 1234 + }) + self.assertFalse(user.is_valid()) class SerializersTests(TestCase): - @patch.object(cas_client.CASClient, 'perform_service_validate', service_validate) + class CASTokenObtain: + def __init__(self, ticket): + self.token = "ABCD" + self.ticket = ticket + + @patch.object(cas_client.CASClient, + 'perform_service_validate', + customize_data("1234", "ddickwd", "dummy@dummy.be")) def test_invalid_ticket_generates_error(self): - pass \ No newline at end of file + """When the wrong ticket is provided, a ValidationError should be raised.""" + # I have set "1" as the correct ticket here + obtain = self.CASTokenObtain("2") + serializer = CASTokenObtainSerializer(obtain) + self.assertRaises(ValidationError, lambda: serializer.validate(serializer.ticket)) From 2dcc767d9a5a63758a2fa2f7c0dd872d7b2fba39 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 19:01:50 +0100 Subject: [PATCH 07/24] test: add more UserSerializerModel tests #27 --- .../tests/test_authentication.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py index 2820f9f5..b687c0d4 100644 --- a/backend/authentication/tests/test_authentication.py +++ b/backend/authentication/tests/test_authentication.py @@ -35,11 +35,43 @@ def service_validate( class UserSerializerModelTests(TestCase): def test_non_string_id_makes_user_serializer_invalid(self): + """ + The is_valid() method of a UserSerializer whose supplied User's ID is not a string + should return False. + """ user = UserSerializer(data={ "id": 1234 }) self.assertFalse(user.is_valid()) + def test_non_string_username_makes_user_serializer_invalid(self): + """ + The is_valid() method of a UserSerializer whose supplied User's username is not a string + should return False. + """ + user = UserSerializer(data={ + "username": 10 + }) + self.assertFalse(user.is_valid()) + + def test_invalid_email_makes_user_serializer_invalid(self): + """ + The is_valid() method of a UserSerializer whose supplied User's email is not + formatted as an email address should return False. + """ + user = UserSerializer(data={ + "email": "dummy" + }) + user2 = UserSerializer(data={ + "email": "dummy@dummy" + }) + user3 = UserSerializer(data={ + "email": 21 + }) + self.assertFalse(user.is_valid()) + self.assertFalse(user2.is_valid()) + self.assertFalse(user3.is_valid()) + class SerializersTests(TestCase): class CASTokenObtain: From 6772e9537af7a0b0f1578251994a859cf9bfb3b0 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Thu, 29 Feb 2024 21:50:34 +0100 Subject: [PATCH 08/24] chore: mock CAS ticket --- backend/api/migrations/0002_populate.py | 156 ------------------ backend/authentication/models.py | 1 - backend/authentication/serializers.py | 9 +- .../tests/test_authentication.py | 24 +-- backend/ypovoli/settings.py | 2 +- 5 files changed, 13 insertions(+), 179 deletions(-) delete mode 100644 backend/api/migrations/0002_populate.py diff --git a/backend/api/migrations/0002_populate.py b/backend/api/migrations/0002_populate.py deleted file mode 100644 index a289fd86..00000000 --- a/backend/api/migrations/0002_populate.py +++ /dev/null @@ -1,156 +0,0 @@ -from django.db import migrations, transaction -from api.models.teacher import Teacher -from api.models.student import Student -from api.models.course import Course -from api.models.assistant import Assistant -from api.models.project import Project -from api.models.group import Group -from authentication.models import Faculty -# from datetime import date - - -def populate_db(apps, schema_editor): - with transaction.atomic(): - # Faculteit Letteren en Wijsbegeerte - Faculty.objects.create(name="Letteren_Wijsbegeerte") - # Faculteit Recht en Criminologie - Faculty.objects.create(name="Recht_Criminologie") - # Faculteit Wetenschappen - f_wet = Faculty.objects.create(name="Wetenschappen") - # Faculteit Geneeskunde en Gezondheidswetenschappen - f_genGez = Faculty.objects.create( - name="Geneeskunde_Gezondheidswetenschappen" - ) - # Faculteit Ingenieurswetenschappen en Architectuur - Faculty.objects.create(name="Ingenieurswetenschappen_Architectuur") - # Faculteit Economie en Bedrijfskunde - Faculty.objects.create(name="Economie_Bedrijfskunde") - # Faculteit Diergeneeskunde - Faculty.objects.create(name="Diergeneeskunde") - # Faculteit Psychologie en Pedagogische Wetenschappen - f_psyPeda = Faculty.objects.create( - name="Psychologie_PedagogischeWetenschappen" - ) - # Faculteit Bio-ingenieurswetenschappen - Faculty.objects.create(name="Bio-ingenieurswetenschappen") - # Faculteit Farmaceutische Wetenschappen - Faculty.objects.create(name="Farmaceutische_Wetenschappen") - # Faculteit Politieke en Sociale Wetenschappen - Faculty.objects.create(name="Politieke_Sociale_Wetenschappen") - - teacher1 = Teacher.objects.create( - id=123, - first_name="Tom", - last_name="Boonen", - email="Tom.Boonen@gmail.be", - username="tboonen", - create_time="2023-01-01T00:00:00Z", - ) - - teacher1.faculty.add(f_psyPeda) - - assistant1 = Assistant.objects.create( - id=235, - first_name="Bart", - last_name="Simpson", - username="bsimpson", - email="Bart.Simpson@gmail.be", - create_time="2023-01-01T00:00:00Z", - ) - - assistant1.faculty.add(f_wet) - - assistant2 = Assistant.objects.create( - id=236, - first_name="Kim", - last_name="Clijsters", - username="kclijster", - email="Kim.Clijsters@gmail.be", - create_time="2023-01-01T00:00:00Z", - ) - - assistant2.faculty.add(f_psyPeda) - - teacher2 = Teacher.objects.create( - id=124, - first_name="Peter", - last_name="Sagan", - username="psagan", - email="Peter.Sagan@gmail.com", - create_time="2023-01-01T00:00:00Z", - ) - - teacher2.faculty.add(f_psyPeda) - - student1 = Student.objects.create( - id=1, - first_name="John", - last_name="Doe", - username="jdoe", - email="John.Doe@hotmail.com", - create_time="2023-01-01T00:00:00Z", - ) - - student1.faculty.add(f_wet) - - student2 = Student.objects.create( - id=2, - first_name="Bartje", - last_name="Verhaege", - username="bverhae", - email="Bartje.Verhaege@gmail.com", - create_time="2023-01-01T00:00:00Z", - ) - - student2.faculty.add(f_genGez) - - course = Course.objects.create( - name="Math", - academic_startyear=2023, - description="Math course", - ) - - course2 = Course.objects.create( - name="Sel2", - academic_startyear=2023, - description="Software course", - ) - - project1 = Project.objects.create( - id=123456, - name="sel2", - description="make a project", - visible=True, - archived=False, - # Set the start date as 26th February 2024 - start_date="2024-02-26T00:00:00+00:00", - # Set the deadline as 27th February 2024 - deadline="2024-02-27T00:00:00+00:00", - course=course2 - ) - - group1 = Group.objects.create( - project=project1, - ) - - group1.students.add(student1) - group1.students.add(student2) - - teacher1.courses.add(course) - teacher2.courses.add(course) - student1.courses.add(course) - teacher2.courses.add(course2) - - course.assistants.add(assistant1) - course2.assistants.add(assistant2) - - -class Migration(migrations.Migration): - dependencies = [ - ("api", "0001_initial"), - ("authentication", "0001_initial"), - ] - - operations = [ - migrations.RunPython(populate_db), - ] diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 7bef9b4c..2ba5bb76 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -55,7 +55,6 @@ class User(AbstractBaseUser): """Model settings""" USERNAME_FIELD = "username" EMAIL_FIELD = "email" - REQUIRED_FIELDS = [] class Faculty(models.Model): """This model represents a faculty.""" diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 9c7e8173..86551495 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -10,6 +10,8 @@ class CASTokenObtainSerializer(Serializer): """Serializer for CAS ticket validation This serializer takes the CAS ticket and tries to validate it. Upon successful validation, create a new user if it doesn't exist. + + /auth/token """ token = RefreshToken ticket = CharField(required=True, min_length=49, max_length=49) @@ -75,12 +77,7 @@ class UserSerializer(ModelSerializer): class Meta: model = User - fields = [ - 'id', 'username', 'email', - 'first_name', 'last_name', - 'faculty', - 'last_enrolled', 'last_login', 'create_time' - ] + fields = '__all__' def get_or_create(self, validated_data: dict) -> User: """Create or fetch the user based on the validated data.""" diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py index b687c0d4..8049b7f1 100644 --- a/backend/authentication/tests/test_authentication.py +++ b/backend/authentication/tests/test_authentication.py @@ -1,9 +1,7 @@ -import cas_client from django.test import TestCase -from rest_framework.serializers import ValidationError from unittest.mock import patch - -from ..serializers import CASTokenObtainSerializer, UserSerializer +from authentication.cas.client import client +from authentication.serializers import CASTokenObtainSerializer, UserSerializer def customize_data(ugent_id, uid, mail): @@ -14,7 +12,7 @@ def service_validate( service_url=None, headers=None,): response = {} - if ticket != "1": + if ticket != "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas6": response.error = "This is an error" else: response.data = { @@ -74,17 +72,13 @@ def test_invalid_email_makes_user_serializer_invalid(self): class SerializersTests(TestCase): - class CASTokenObtain: - def __init__(self, ticket): - self.token = "ABCD" - self.ticket = ticket - - @patch.object(cas_client.CASClient, + @patch.object(client, 'perform_service_validate', customize_data("1234", "ddickwd", "dummy@dummy.be")) def test_invalid_ticket_generates_error(self): """When the wrong ticket is provided, a ValidationError should be raised.""" - # I have set "1" as the correct ticket here - obtain = self.CASTokenObtain("2") - serializer = CASTokenObtainSerializer(obtain) - self.assertRaises(ValidationError, lambda: serializer.validate(serializer.ticket)) + # I have set "1" as the correct ticket hereµ + serializer = CASTokenObtainSerializer(data={ + 'token': 'qslmdfjklmqsdfjklmqsjdkf' + }) + self.assertFalse(serializer.is_valid()) diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index c912fa45..0cc3c354 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -73,7 +73,7 @@ "ACCESS_TOKEN_LIFETIME": timedelta(days=365), "REFRESH_TOKEN_LIFETIME": timedelta(days=1), "UPDATE_LAST_LOGIN": True, - "TOKEN_OBTAIN_SERIALIZER": "authentication.serializers.CASTokenObtainSerializer", + "TOKEN_OBTAIN_SERIALIZER": "authentication.serializers.CASTokenObtainSerializer" } AUTH_USER_MODEL = "authentication.User" From 4b77d4b850e1fd718dd081165e889eece38bf849 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 00:19:36 +0100 Subject: [PATCH 09/24] test: removing unneeded tests + eliminating all other opportunities for failure in tests #27 --- .../tests/test_authentication.py | 122 +++++++++++------- 1 file changed, 72 insertions(+), 50 deletions(-) diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py index 8049b7f1..8af073d2 100644 --- a/backend/authentication/tests/test_authentication.py +++ b/backend/authentication/tests/test_authentication.py @@ -1,56 +1,21 @@ +from cas_client import CASClient from django.test import TestCase +from rest_framework_simplejwt.tokens import RefreshToken from unittest.mock import patch -from authentication.cas.client import client from authentication.serializers import CASTokenObtainSerializer, UserSerializer -def customize_data(ugent_id, uid, mail): - - def service_validate( - self, - ticket=None, - service_url=None, - headers=None,): - response = {} - if ticket != "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas6": - response.error = "This is an error" - else: - response.data = { - "ugentID": ugent_id, - "uid": uid, - "mail": mail, - "givenname": "Dummy", - "surname": "McDickwad", - "faculty": "Sciences", - "lastenrolled": "2021-05-21", - "lastlogin": "", - "createtime": "" - } - return response +TICKET = "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas6" +WRONG_TICKET = "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas5" - return service_validate +ID = "1234" +USERNAME = "ddickwd" +EMAIL = "dummy@dummy.be" +FIRST_NAME = "Dummy" +LAST_NAME = "McDickwad" class UserSerializerModelTests(TestCase): - def test_non_string_id_makes_user_serializer_invalid(self): - """ - The is_valid() method of a UserSerializer whose supplied User's ID is not a string - should return False. - """ - user = UserSerializer(data={ - "id": 1234 - }) - self.assertFalse(user.is_valid()) - - def test_non_string_username_makes_user_serializer_invalid(self): - """ - The is_valid() method of a UserSerializer whose supplied User's username is not a string - should return False. - """ - user = UserSerializer(data={ - "username": 10 - }) - self.assertFalse(user.is_valid()) def test_invalid_email_makes_user_serializer_invalid(self): """ @@ -58,27 +23,84 @@ def test_invalid_email_makes_user_serializer_invalid(self): formatted as an email address should return False. """ user = UserSerializer(data={ - "email": "dummy" + 'id': ID, + 'username': USERNAME, + 'email': 'dummy', + 'first_name': FIRST_NAME, + 'last_name': LAST_NAME, }) user2 = UserSerializer(data={ - "email": "dummy@dummy" + 'id': ID, + 'username': USERNAME, + 'email': "dummy@dummy", + 'first_name': FIRST_NAME, + 'last_name': LAST_NAME, }) user3 = UserSerializer(data={ - "email": 21 + 'id': ID, + 'username': USERNAME, + 'email': 21, + 'first_name': FIRST_NAME, + 'last_name': LAST_NAME, }) self.assertFalse(user.is_valid()) self.assertFalse(user2.is_valid()) self.assertFalse(user3.is_valid()) + def test_valid_id_and_username_and_email_makes_valid_serializer(self): + user = UserSerializer(data={ + 'id': ID, + 'username': USERNAME, + 'email': EMAIL, + 'first_name': FIRST_NAME, + 'last_name': LAST_NAME, + }) + self.assertTrue(user.is_valid()) + + +def customize_data(ugent_id, uid, mail): + + class Response: + __slots__ = ('error', 'data') + + def __init__(self): + self.error = None + self.data = {} + + def service_validate( + self, + ticket=None, + service_url=None, + headers=None,): + response = Response() + if ticket != TICKET: + response.error = "This is an error" + else: + response.data['attributes'] = { + 'ugentID': ugent_id, + 'uid': uid, + 'mail': mail, + 'givenname': FIRST_NAME, + 'surname': LAST_NAME, + 'faculty': "Sciences", + 'lastenrolled': "2023 - 2024", + 'lastlogin': "", + 'createtime': "" + } + return response + + return service_validate + class SerializersTests(TestCase): - @patch.object(client, + @patch.object(CASClient, 'perform_service_validate', - customize_data("1234", "ddickwd", "dummy@dummy.be")) + customize_data(ID, USERNAME, EMAIL)) def test_invalid_ticket_generates_error(self): """When the wrong ticket is provided, a ValidationError should be raised.""" # I have set "1" as the correct ticket hereµ serializer = CASTokenObtainSerializer(data={ - 'token': 'qslmdfjklmqsdfjklmqsjdkf' + 'token': RefreshToken(), + 'ticket': WRONG_TICKET }) self.assertFalse(serializer.is_valid()) From 62cd8b2b061b15e92ba16557376fd6631227e702 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 00:25:05 +0100 Subject: [PATCH 10/24] test: valid_id_and_username_and_email to valid_email #27 --- backend/authentication/tests/test_authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py index 8af073d2..72dc80bc 100644 --- a/backend/authentication/tests/test_authentication.py +++ b/backend/authentication/tests/test_authentication.py @@ -47,7 +47,7 @@ def test_invalid_email_makes_user_serializer_invalid(self): self.assertFalse(user2.is_valid()) self.assertFalse(user3.is_valid()) - def test_valid_id_and_username_and_email_makes_valid_serializer(self): + def test_valid_email_makes_valid_serializer(self): user = UserSerializer(data={ 'id': ID, 'username': USERNAME, From 12cd8326b59cb6f4455dff860dceeba488a136ca Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 02:13:02 +0100 Subject: [PATCH 11/24] test: fully added tests for authentication/serializers.py #27 --- .../tests/test_authentication.py | 106 ---------- .../tests/test_authentication_serializer.py | 188 ++++++++++++++++++ 2 files changed, 188 insertions(+), 106 deletions(-) delete mode 100644 backend/authentication/tests/test_authentication.py create mode 100644 backend/authentication/tests/test_authentication_serializer.py diff --git a/backend/authentication/tests/test_authentication.py b/backend/authentication/tests/test_authentication.py deleted file mode 100644 index 72dc80bc..00000000 --- a/backend/authentication/tests/test_authentication.py +++ /dev/null @@ -1,106 +0,0 @@ -from cas_client import CASClient -from django.test import TestCase -from rest_framework_simplejwt.tokens import RefreshToken -from unittest.mock import patch -from authentication.serializers import CASTokenObtainSerializer, UserSerializer - - -TICKET = "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas6" -WRONG_TICKET = "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas5" - -ID = "1234" -USERNAME = "ddickwd" -EMAIL = "dummy@dummy.be" -FIRST_NAME = "Dummy" -LAST_NAME = "McDickwad" - - -class UserSerializerModelTests(TestCase): - - def test_invalid_email_makes_user_serializer_invalid(self): - """ - The is_valid() method of a UserSerializer whose supplied User's email is not - formatted as an email address should return False. - """ - user = UserSerializer(data={ - 'id': ID, - 'username': USERNAME, - 'email': 'dummy', - 'first_name': FIRST_NAME, - 'last_name': LAST_NAME, - }) - user2 = UserSerializer(data={ - 'id': ID, - 'username': USERNAME, - 'email': "dummy@dummy", - 'first_name': FIRST_NAME, - 'last_name': LAST_NAME, - }) - user3 = UserSerializer(data={ - 'id': ID, - 'username': USERNAME, - 'email': 21, - 'first_name': FIRST_NAME, - 'last_name': LAST_NAME, - }) - self.assertFalse(user.is_valid()) - self.assertFalse(user2.is_valid()) - self.assertFalse(user3.is_valid()) - - def test_valid_email_makes_valid_serializer(self): - user = UserSerializer(data={ - 'id': ID, - 'username': USERNAME, - 'email': EMAIL, - 'first_name': FIRST_NAME, - 'last_name': LAST_NAME, - }) - self.assertTrue(user.is_valid()) - - -def customize_data(ugent_id, uid, mail): - - class Response: - __slots__ = ('error', 'data') - - def __init__(self): - self.error = None - self.data = {} - - def service_validate( - self, - ticket=None, - service_url=None, - headers=None,): - response = Response() - if ticket != TICKET: - response.error = "This is an error" - else: - response.data['attributes'] = { - 'ugentID': ugent_id, - 'uid': uid, - 'mail': mail, - 'givenname': FIRST_NAME, - 'surname': LAST_NAME, - 'faculty': "Sciences", - 'lastenrolled': "2023 - 2024", - 'lastlogin': "", - 'createtime': "" - } - return response - - return service_validate - - -class SerializersTests(TestCase): - @patch.object(CASClient, - 'perform_service_validate', - customize_data(ID, USERNAME, EMAIL)) - def test_invalid_ticket_generates_error(self): - """When the wrong ticket is provided, a ValidationError should be raised.""" - # I have set "1" as the correct ticket hereµ - serializer = CASTokenObtainSerializer(data={ - 'token': RefreshToken(), - 'ticket': WRONG_TICKET - }) - self.assertFalse(serializer.is_valid()) diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py new file mode 100644 index 00000000..c2d233fa --- /dev/null +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -0,0 +1,188 @@ +from cas_client import CASClient +from django.test import TestCase +from rest_framework_simplejwt.tokens import RefreshToken +from unittest.mock import patch, Mock +from authentication.serializers import CASTokenObtainSerializer, UserSerializer +from authentication.signals import user_created, user_login + + +TICKET = 'ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas6' +WRONG_TICKET = 'ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas5' + +ID = '1234' +USERNAME = 'ddickwd' +EMAIL = 'dummy@dummy.be' +FIRST_NAME = 'Dummy' +LAST_NAME = 'McDickwad' + + +class UserSerializerModelTests(TestCase): + + def test_invalid_email_makes_user_serializer_invalid(self): + """ + The is_valid() method of a UserSerializer whose supplied User's email is not + formatted as an email address should return False. + """ + user = UserSerializer(data={ + 'id': ID, + 'username': USERNAME, + 'email': 'dummy', + 'first_name': FIRST_NAME, + 'last_name': LAST_NAME, + }) + user2 = UserSerializer(data={ + 'id': ID, + 'username': USERNAME, + 'email': 'dummy@dummy', + 'first_name': FIRST_NAME, + 'last_name': LAST_NAME, + }) + user3 = UserSerializer(data={ + 'id': ID, + 'username': USERNAME, + 'email': 21, + 'first_name': FIRST_NAME, + 'last_name': LAST_NAME, + }) + self.assertFalse(user.is_valid()) + self.assertFalse(user2.is_valid()) + self.assertFalse(user3.is_valid()) + + def test_valid_email_makes_valid_serializer(self): + user = UserSerializer(data={ + 'id': ID, + 'username': USERNAME, + 'email': EMAIL, + 'first_name': FIRST_NAME, + 'last_name': LAST_NAME, + }) + self.assertTrue(user.is_valid()) + + +def customize_data(ugent_id, uid, mail): + + class Response: + __slots__ = ('error', 'data') + + def __init__(self): + self.error = None + self.data = {} + + def service_validate( + self, + ticket=None, + service_url=None, + headers=None,): + response = Response() + if ticket != TICKET: + response.error = 'This is an error' + else: + response.data['attributes'] = { + 'ugentID': ugent_id, + 'uid': uid, + 'mail': mail, + 'givenname': FIRST_NAME, + 'surname': LAST_NAME, + 'faculty': 'Sciences', + 'lastenrolled': '2023 - 2024', + 'lastlogin': '', + 'createtime': '' + } + return response + + return service_validate + + +class SerializersTests(TestCase): + def test_wrong_length_ticket_generates_error(self): + """ + When the provided ticket has the wrong length, a ValidationError should be raised + when validating the serializer. + """ + serializer = CASTokenObtainSerializer(data={ + 'token': RefreshToken(), + 'ticket': 'ST' + }) + self.assertFalse(serializer.is_valid()) + + @patch.object(CASClient, + 'perform_service_validate', + customize_data(ID, USERNAME, EMAIL)) + def test_wrong_ticket_generates_error(self): + """ + When the wrong ticket is provided, a ValidationError should be raised when trying to validate + the serializer. + """ + serializer = CASTokenObtainSerializer(data={ + 'token': RefreshToken(), + 'ticket': WRONG_TICKET + }) + self.assertFalse(serializer.is_valid()) + + @patch.object(CASClient, + 'perform_service_validate', + customize_data(ID, USERNAME, "dummy@dummy")) + def test_wrong_user_arguments_generate_error(self): + """ + If the user arguments returned by CAS are not valid, then a ValidationError + should be raised when validating the serializer. + """ + serializer = CASTokenObtainSerializer(data={ + 'token': RefreshToken(), + 'ticket': TICKET + }) + self.assertFalse(serializer.is_valid()) + + @patch.object(CASClient, + 'perform_service_validate', + customize_data(ID, USERNAME, EMAIL)) + def test_new_user_activates_user_created_signal(self): + """ + If the authenticated user is new to the app, then the user_created signal should + be sent when trying to validate the token.""" + + mock = Mock() + user_created.connect(mock, dispatch_uid="STDsAllAround") + serializer = CASTokenObtainSerializer(data={ + 'token': RefreshToken(), + 'ticket': TICKET + }) + # this next line triggers the retrieval of User information and logs in the user + serializer.is_valid() + self.assertEquals(mock.call_count, 1) + + @patch.object(CASClient, + 'perform_service_validate', + customize_data(ID, USERNAME, EMAIL)) + def test_old_user_does_not_activate_user_created_signal(self): + """ + If the authenticated user is new to the app, then the user_created signal should + be sent when trying to validate the token.""" + + mock = Mock() + user_created.connect(mock, dispatch_uid="STDsAllAround") + serializer = CASTokenObtainSerializer(data={ + 'token': RefreshToken(), + 'ticket': TICKET + }) + # this next line triggers the retrieval of User information and logs in the user + serializer.is_valid() + self.assertEquals(mock.call_count, 0) + + @patch.object(CASClient, + 'perform_service_validate', + customize_data(ID, USERNAME, EMAIL)) + def test_login_signal(self): + """ + When the token is correct and all user data is correct, while trying to validate + the token, then the user_login signal should be sent. + """ + mock = Mock() + user_login.connect(mock, dispatch_uid="STDsAllAround") + serializer = CASTokenObtainSerializer(data={ + 'token': RefreshToken(), + 'ticket': TICKET + }) + # this next line triggers the retrieval of User information and logs in the user + serializer.is_valid() + self.assertEquals(mock.call_count, 1) From 5d30a336df11546ad196466e0dd29a21183f1050 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 15:04:43 +0100 Subject: [PATCH 12/24] test: fully added tests for authentication/views directory #27 --- .../tests/test_authentication_views.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 backend/authentication/tests/test_authentication_views.py diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py new file mode 100644 index 00000000..72421b4d --- /dev/null +++ b/backend/authentication/tests/test_authentication_views.py @@ -0,0 +1,94 @@ +from django.core.serializers import serialize +import json +from rest_framework.request import Request +from rest_framework.reverse import reverse +from rest_framework.test import APIRequestFactory, APITestCase +from rest_framework_simplejwt.tokens import AccessToken +from unittest.mock import patch +from authentication.models import User +from authentication.serializers import UserSerializer +from ypovoli import settings + +USER_DATA = { + 'id': '1234', + 'username': 'ddickwd', + 'email': 'dummy@dummy.com', + 'first_name': 'dummy', + 'last_name': 'McDickwad', +} + + +class TestWhomAmIView(APITestCase): + def setUp(self): + """Create a user and generate a token for that user""" + self.user = User.objects.create(**USER_DATA) + access_token = AccessToken().for_user(self.user) + self.token = f'Bearer {access_token}' + + def test_who_am_i_view_get_returns_user_if_existing_and_authenticated(self): + """ + WhoAmIView should return the User info when requested if User + exists in database and token is supplied. + """ + self.client.credentials(HTTP_AUTHORIZATION=self.token) + response = self.client.get(reverse('auth.whoami')) + self.assertJSONEqual(response.content.decode('utf-8'), UserSerializer(self.user).data) + + def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(self): + """ + WhoAmIView should return that the user was not found if + authenticated user was deleted from the database. + """ + self.user.delete() + self.client.credentials(HTTP_AUTHORIZATION=self.token) + response = self.client.get(reverse('auth.whoami')) + self.assertJSONNotEqual(response.content, UserSerializer(self.user).data) + content = json.loads(response.content.decode('utf-8')) + self.assertEqual(content['detail'], 'User not found') + + def test_who_am_i_view_returns_401_when_not_authenticated(self): + """WhoAmIView should return a 401 status code when the user is not authenticated""" + response = self.client.get(reverse('auth.whoami')) + self.assertEqual(response.status_code, 401) + + +class TestLogoutView(APITestCase): + def test_logout_view_authenticated_logout_url(self): + """LogoutView should return a logout url redirect if authenticated user sends a post request.""" + self.user = User.objects.create(**USER_DATA) + access_token = AccessToken().for_user(self.user) + self.token = f'Bearer {access_token}' + self.client.credentials(HTTP_AUTHORIZATION=self.token) + response = self.client.post(reverse('auth.logout')) + self.assertEqual(response.status_code, 302) + url = '{server_url}/logout?service={service_url}'.format( + server_url=settings.CAS_ENDPOINT, + service_url=settings.API_ENDPOINT + ) + self.assertEqual(response['Location'], url) + + def test_logout_view_not_authenticated_logout_url(self): + """LogoutView should return a 401 error when trying to access it while not authenticated.""" + response = self.client.post(reverse('auth.logout')) + self.assertEqual(response.status_code, 401) + +class TestLoginView(APITestCase): + def test_login_view_returns_login_url(self): + """LoginView should return a login url redirect if a post request is sent.""" + response = self.client.get(reverse('auth.login')) + self.assertEqual(response.status_code, 302) + url = '{server_url}/login?service={service_url}'.format( + server_url=settings.CAS_ENDPOINT, + service_url=settings.CAS_RESPONSE + ) + self.assertEqual(response['Location'], url) + +class TestTokenEchoView(APITestCase): + def test_token_echo_echoes_token(self): + """TokenEchoView should echo the User's current token""" + ticket = 'This is a ticket.' + response = self.client.get(reverse('auth.echo'), data={'ticket': ticket}) + content = response.rendered_content.decode('utf-8').strip('"') + self.assertEqual(content, ticket) + + From 102f1ceadd01b63a9b2b9465faa6d2bf68a1d740 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 16:49:26 +0100 Subject: [PATCH 13/24] test: factoring out CASClient class from patch.object decorator #27 --- .../tests/test_authentication_serializer.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py index c2d233fa..05a65630 100644 --- a/backend/authentication/tests/test_authentication_serializer.py +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -1,7 +1,7 @@ -from cas_client import CASClient from django.test import TestCase from rest_framework_simplejwt.tokens import RefreshToken from unittest.mock import patch, Mock +from authentication.cas.client import client from authentication.serializers import CASTokenObtainSerializer, UserSerializer from authentication.signals import user_created, user_login @@ -69,7 +69,6 @@ def __init__(self): self.data = {} def service_validate( - self, ticket=None, service_url=None, headers=None,): @@ -105,7 +104,7 @@ def test_wrong_length_ticket_generates_error(self): }) self.assertFalse(serializer.is_valid()) - @patch.object(CASClient, + @patch.object(client, 'perform_service_validate', customize_data(ID, USERNAME, EMAIL)) def test_wrong_ticket_generates_error(self): @@ -119,7 +118,7 @@ def test_wrong_ticket_generates_error(self): }) self.assertFalse(serializer.is_valid()) - @patch.object(CASClient, + @patch.object(client, 'perform_service_validate', customize_data(ID, USERNAME, "dummy@dummy")) def test_wrong_user_arguments_generate_error(self): @@ -133,7 +132,7 @@ def test_wrong_user_arguments_generate_error(self): }) self.assertFalse(serializer.is_valid()) - @patch.object(CASClient, + @patch.object(client, 'perform_service_validate', customize_data(ID, USERNAME, EMAIL)) def test_new_user_activates_user_created_signal(self): @@ -151,7 +150,7 @@ def test_new_user_activates_user_created_signal(self): serializer.is_valid() self.assertEquals(mock.call_count, 1) - @patch.object(CASClient, + @patch.object(client, 'perform_service_validate', customize_data(ID, USERNAME, EMAIL)) def test_old_user_does_not_activate_user_created_signal(self): @@ -169,7 +168,7 @@ def test_old_user_does_not_activate_user_created_signal(self): serializer.is_valid() self.assertEquals(mock.call_count, 0) - @patch.object(CASClient, + @patch.object(client, 'perform_service_validate', customize_data(ID, USERNAME, EMAIL)) def test_login_signal(self): From da964d45a290952291b81eb0bb618c2ff22bf360 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Fri, 1 Mar 2024 16:04:09 +0100 Subject: [PATCH 14/24] chore: merge with development --- backend/authentication/fixtures/users.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/authentication/fixtures/users.yaml b/backend/authentication/fixtures/users.yaml index 40922b7d..3e6a5801 100644 --- a/backend/authentication/fixtures/users.yaml +++ b/backend/authentication/fixtures/users.yaml @@ -6,7 +6,7 @@ email: John.Doe@hotmail.com first_name: John last_name: Doe - last_enrolled: 1 + last_enrolled: 2023 create_time: 2024-02-29 20:35:45.690556+00:00 faculties: - Wetenschappen @@ -18,7 +18,7 @@ email: Tom.Boonen@gmail.be first_name: Tom last_name: Boonen - last_enrolled: 1 + last_enrolled: 2023 create_time: 2024-02-29 20:35:45.686541+00:00 faculties: - Psychologie_PedagogischeWetenschappen @@ -30,7 +30,7 @@ email: Peter.Sagan@gmail.com first_name: Peter last_name: Sagan - last_enrolled: 1 + last_enrolled: 2023 create_time: 2024-02-29 20:35:45.689543+00:00 faculties: - Psychologie_PedagogischeWetenschappen @@ -42,7 +42,7 @@ email: Bartje.Verhaege@gmail.com first_name: Bartje last_name: Verhaege - last_enrolled: 1 + last_enrolled: 2023 create_time: 2024-02-29 20:35:45.691565+00:00 faculties: - Geneeskunde_Gezondheidswetenschappen @@ -54,7 +54,7 @@ email: Bart.Simpson@gmail.be first_name: Bart last_name: Simpson - last_enrolled: 1 + last_enrolled: 2023 create_time: 2024-02-29 20:35:45.687541+00:00 faculties: - Wetenschappen @@ -66,7 +66,7 @@ email: Kim.Clijsters@gmail.be first_name: Kim last_name: Clijsters - last_enrolled: 1 + last_enrolled: 2023 create_time: 2024-02-29 20:35:45.688545+00:00 faculties: - Psychologie_PedagogischeWetenschappen From 9c582f13100a3ba23503802ac016292a43ef45bf Mon Sep 17 00:00:00 2001 From: EwoutV Date: Fri, 1 Mar 2024 16:45:18 +0100 Subject: [PATCH 15/24] testing: (wip) improving testing logic --- backend/.coverage | Bin 0 -> 53248 bytes .../tests/test_authentication_serializer.py | 17 +++++--- .../tests/test_authentication_views.py | 38 +++++++++--------- backend/authentication/views/auth.py | 2 +- 4 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 backend/.coverage diff --git a/backend/.coverage b/backend/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..7362567266f8b365d756e208dc63d12e97dfd129 GIT binary patch literal 53248 zcmeI4UyK_^9mjX=_1f$8dVF`J<`gAasnE;m{y0ZlRc%zrHMu6a1k%#9MQVhzw)gHf zwY`_!_5DFTA-9zxfp~x?s#cU2=o?a1@sNNDRi#4nK-CBSN>KF$DiD+ksDuIyznR@V z+exlen@ClX@5=V<&dz-2H=p^0dMzt3Zb4ht8vDOEH+y`}JU)Bh(K$1WGk51r+NP$=S<`b@ z&1Ks&7o4VTI_-v2w*sfVXa-AmoVw%N4L;FP98GmR;k3xl_o_~Vas~DxNi2I#%ktLE z$L;k=ksvB9u-Aexf<$a*vCUJMyQA#mre`nMp53n7erUyRr!k%%f9k9GY5L)@BXsF4?&jgthhDRwZ!FrqdSHZ2L~& zxNXy3v+JF}Zd|c~lN?&S5xoA|kk(0Fp_5_GsH{FZz06n086}T0FOYFVhxmDi4jVk+*+vW;w*{knBbH>Pdi-x&6ZRd2y-fa9y zvzK#HRHN-qn!FO^0iV{@^4%k{L|$pH`KO!oeXDA90#_VYsqw2b^igko+9jVk zEgXg8sn(pp-`vp#oMtn63)kW2xAq&}O9q{U_o8OgbQeUI;knJQambtd`%EQ+gJ+qTKwut)H5iN7-gmEbYGE^x_2 z@X^g*wt^+|a@%<{89tU#%X_zNWjHsXI>SHJryffQlh2F_`Yjst`8z#eGLPw*e8gU# zTCtimnk-w6M_rc2;BW>odBTQ8iLIL3355UhL*br$?&;YZ3iOU+_LOQ|B&Y8Efb07X zO{;v`^6!ab&9NoR=cAB6X4YJ{XzsHpP<* zALfr!r;Ga+?X+4}yAjP-d>Rf_Y3%2nla~_?B9ze_qfY5l7rjI?7LCsG5i8c8UM417 zwPiKJ?#>&fPjPk0zDzEhjSg6W9XKu9iE!ous*Rd@TK8ZopfsI>SHt zDmll$%5`_i+497M-0ei2F&t;AK27Q^D>geiObs~6B`16Sa*PIC`nJ>WC!Ad$4t^Mx zzz4Um1#2ONI92prCqK>?_%gd7(F+?0fB*=900@8p2!H?xfB*=900@A&wfB*=900@8p2!H?xfB*=900>;41PZD24i^8!V-J(l+so090PdQ; zWBT?y)hfl_k=Q%zU)N^^VKoSV00@8p2!H?xfB*=900@8p2!KFLppf1n$5#PT`Sf-z zx(LAU|BvJ+BzBIiu{(x8l%)F)H*74$r5*=^h*=t&v1pD~`R|6S{whgods{h+9eq?JJQ^CXr6Z zYw{{R!AM~vkwVK^^yqOV^fZFXo;`a$Q}yf?J&~ZgVAVU#V7!S2 z5U>A74k_J{WLksk|KWp5cj#(_qV@mKtkTUS(}~vqr46OKkxZd))suAn&yFkIiEB}M z|N6g}yu~kD{~ITi?%reu{{QuVA-OyC7S()Fr6+f%f%SjhQo3{3y0$%2$xj`~=>S3k3Et%jY>;KFFrK=}X>aG8k{YqC$CbGHy zPbVL5qV<1@s$wL&3ekra&;P^B4gw$m0w4eaAOHd&00JNY0w4eaH#C8?oR$oJ|6gYR zk?4gD1V8`;KmY_l00ck)1V8`;KmY_l;07d+P8%A(|3A#=_y7N9@3FVpZ`qsdb@mE7 z%bsE1WE<=htFedKJln_aq(s<200ck)1V8`;KmY_l00ck)1VG>>AdnqaWUVy(2U#1U zolgvXFm=mcawXawF1`HAy&B_@?9e~{ChJAo{bcdm=Py3{Mp`xKplHZL1qwe^_;dRw zIa4Z4O22#g{b8M=Zqr}8>sLoFy!7IG&z=3#30cka!>p{TJXB>h!^4cMDm+x~DW)mhnLhWT|BJid zc;=<7n&R;(FC#0OR+Rbu|6%sD#4fOZvcIxFvbWf8=o-Lz_A2`^`yu-Qdx3qIt_6IH zeVr0v0|5{K0T2KI5C8!X009sH0T2KI5J*gbuiWL~VG#@siJ(*x0b?R47DZqfA}AC@ zpz9*Y=S84tBFN=Lkj;uf6&D5MOhz0jiU`ta5u{QIT>udG|D{r5(~%JbKmY_l00ck) z1V8`;KmY_l00cnbIv~LB|6~1s9W)e71OX5L0T2KI5C8!X009sH0T2Lzt02Jt|Bv None: + self.request = RequestFactory() + def test_invalid_email_makes_user_serializer_invalid(self): """ The is_valid() method of a UserSerializer whose supplied User's email is not @@ -29,21 +36,21 @@ def test_invalid_email_makes_user_serializer_invalid(self): 'email': 'dummy', 'first_name': FIRST_NAME, 'last_name': LAST_NAME, - }) + }, context={'context': self.request}) user2 = UserSerializer(data={ 'id': ID, 'username': USERNAME, 'email': 'dummy@dummy', 'first_name': FIRST_NAME, 'last_name': LAST_NAME, - }) + }, context={'context': self.request}) user3 = UserSerializer(data={ 'id': ID, 'username': USERNAME, 'email': 21, 'first_name': FIRST_NAME, 'last_name': LAST_NAME, - }) + }, context={'context': self.request}) self.assertFalse(user.is_valid()) self.assertFalse(user2.is_valid()) self.assertFalse(user3.is_valid()) @@ -55,7 +62,7 @@ def test_valid_email_makes_valid_serializer(self): 'email': EMAIL, 'first_name': FIRST_NAME, 'last_name': LAST_NAME, - }) + }, context={'context': self.request}) self.assertTrue(user.is_valid()) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 72421b4d..0d2820a4 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -1,29 +1,27 @@ -from django.core.serializers import serialize import json -from rest_framework.request import Request + +from rest_framework.test import APIRequestFactory + from rest_framework.reverse import reverse -from rest_framework.test import APIRequestFactory, APITestCase +from rest_framework.test import APITestCase from rest_framework_simplejwt.tokens import AccessToken -from unittest.mock import patch + from authentication.models import User from authentication.serializers import UserSerializer from ypovoli import settings -USER_DATA = { - 'id': '1234', - 'username': 'ddickwd', - 'email': 'dummy@dummy.com', - 'first_name': 'dummy', - 'last_name': 'McDickwad', -} - - class TestWhomAmIView(APITestCase): def setUp(self): """Create a user and generate a token for that user""" - self.user = User.objects.create(**USER_DATA) - access_token = AccessToken().for_user(self.user) - self.token = f'Bearer {access_token}' + self.user = User.objects.create(**{ + 'id': '1234', + 'username': 'ddickwd', + 'email': 'dummy@dummy.com', + 'first_name': 'dummy', + 'last_name': 'McDickwad', + }) + self.serialized_user = UserSerializer(self.user).data + self.token = f'Bearer {AccessToken().for_user(self.user)}' def test_who_am_i_view_get_returns_user_if_existing_and_authenticated(self): """ @@ -31,8 +29,9 @@ def test_who_am_i_view_get_returns_user_if_existing_and_authenticated(self): exists in database and token is supplied. """ self.client.credentials(HTTP_AUTHORIZATION=self.token) + response = self.client.get(reverse('auth.whoami')) - self.assertJSONEqual(response.content.decode('utf-8'), UserSerializer(self.user).data) + self.assertJSONEqual(response.content.decode('utf-8'), self.serialized_user) def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(self): """ @@ -41,8 +40,11 @@ def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(s """ self.user.delete() self.client.credentials(HTTP_AUTHORIZATION=self.token) + response = self.client.get(reverse('auth.whoami')) - self.assertJSONNotEqual(response.content, UserSerializer(self.user).data) + serializer = UserSerializer(self.user, context={'request': self.request}) + self.assertJSONNotEqual(response.content, serializer.initial_data) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(content['detail'], 'User not found') diff --git a/backend/authentication/views/auth.py b/backend/authentication/views/auth.py index 20730e50..ebd2529b 100644 --- a/backend/authentication/views/auth.py +++ b/backend/authentication/views/auth.py @@ -12,7 +12,7 @@ class WhoAmIView(APIView): def get(self, request: Request) -> Response: """Get the user account data for the current user""" - return Response(UserSerializer(request.user).data) + return Response(UserSerializer(request.user, context={'request': request}).data) class LogoutView(APIView): permission_classes = [IsAuthenticated] From 905354156d7ab81e1526374786a9642503188ec0 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 20:31:34 +0100 Subject: [PATCH 16/24] test: update WhoAmI test to only compare id for correctness #27 --- .../tests/test_authentication_views.py | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 72421b4d..ca302006 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -1,27 +1,22 @@ -from django.core.serializers import serialize import json -from rest_framework.request import Request from rest_framework.reverse import reverse -from rest_framework.test import APIRequestFactory, APITestCase +from rest_framework.test import APITestCase from rest_framework_simplejwt.tokens import AccessToken -from unittest.mock import patch from authentication.models import User -from authentication.serializers import UserSerializer from ypovoli import settings -USER_DATA = { - 'id': '1234', - 'username': 'ddickwd', - 'email': 'dummy@dummy.com', - 'first_name': 'dummy', - 'last_name': 'McDickwad', -} - class TestWhomAmIView(APITestCase): def setUp(self): """Create a user and generate a token for that user""" - self.user = User.objects.create(**USER_DATA) + user_data = { + 'id': '1234', + 'username': 'ddickwd', + 'email': 'dummy@dummy.com', + 'first_name': 'dummy', + 'last_name': 'McDickwad', + } + self.user = User.objects.create(**user_data) access_token = AccessToken().for_user(self.user) self.token = f'Bearer {access_token}' @@ -32,7 +27,9 @@ def test_who_am_i_view_get_returns_user_if_existing_and_authenticated(self): """ self.client.credentials(HTTP_AUTHORIZATION=self.token) response = self.client.get(reverse('auth.whoami')) - self.assertJSONEqual(response.content.decode('utf-8'), UserSerializer(self.user).data) + self.assertEqual(response.status_code, 200) + content = json.loads(response.content.decode('utf-8')) + self.assertEqual(content['id'], self.user.id) def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(self): """ @@ -42,7 +39,7 @@ def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(s self.user.delete() self.client.credentials(HTTP_AUTHORIZATION=self.token) response = self.client.get(reverse('auth.whoami')) - self.assertJSONNotEqual(response.content, UserSerializer(self.user).data) + self.assertTrue(response.status_code, 200) content = json.loads(response.content.decode('utf-8')) self.assertEqual(content['detail'], 'User not found') @@ -53,9 +50,18 @@ def test_who_am_i_view_returns_401_when_not_authenticated(self): class TestLogoutView(APITestCase): + def setUp(self): + user_data = { + 'id': '1234', + 'username': 'ddickwd', + 'email': 'dummy@dummy.com', + 'first_name': 'dummy', + 'last_name': 'McDickwad', + } + self.user = User.objects.create(**user_data) + def test_logout_view_authenticated_logout_url(self): """LogoutView should return a logout url redirect if authenticated user sends a post request.""" - self.user = User.objects.create(**USER_DATA) access_token = AccessToken().for_user(self.user) self.token = f'Bearer {access_token}' self.client.credentials(HTTP_AUTHORIZATION=self.token) @@ -72,6 +78,7 @@ def test_logout_view_not_authenticated_logout_url(self): response = self.client.post(reverse('auth.logout')) self.assertEqual(response.status_code, 401) + class TestLoginView(APITestCase): def test_login_view_returns_login_url(self): """LoginView should return a login url redirect if a post request is sent.""" @@ -83,6 +90,7 @@ def test_login_view_returns_login_url(self): ) self.assertEqual(response['Location'], url) + class TestTokenEchoView(APITestCase): def test_token_echo_echoes_token(self): """TokenEchoView should echo the User's current token""" From d39c71bf869a4387c7e41a44d0b5eafb1f86e2c4 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 22:45:41 +0100 Subject: [PATCH 17/24] test: fix mistake of previous merge #27 --- backend/authentication/tests/test_authentication_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index f1ee2588..cfb54cc2 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -64,7 +64,6 @@ def setUp(self): def test_logout_view_authenticated_logout_url(self): """LogoutView should return a logout url redirect if authenticated user sends a post request.""" - self.user = User.objects.create(**USER_DATA) access_token = AccessToken().for_user(self.user) self.token = f'Bearer {access_token}' self.client.credentials(HTTP_AUTHORIZATION=self.token) From 4adaa072817355ff1390315a6c426e72c6830589 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Sat, 2 Mar 2024 10:17:00 +0100 Subject: [PATCH 18/24] test: add docs to function, remove unnecessary context argument and restyle to flake8 style #27 --- .../tests/test_authentication_serializer.py | 16 ++++++++-------- .../tests/test_authentication_views.py | 2 -- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py index a4c8dc2d..1e60698b 100644 --- a/backend/authentication/tests/test_authentication_serializer.py +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -1,5 +1,4 @@ from django.test import TestCase -from django.test.client import RequestFactory from unittest.mock import patch, Mock @@ -22,9 +21,6 @@ class UserSerializerModelTests(TestCase): - def setUp(self) -> None: - self.request = RequestFactory() - def test_invalid_email_makes_user_serializer_invalid(self): """ The is_valid() method of a UserSerializer whose supplied User's email is not @@ -36,33 +32,37 @@ def test_invalid_email_makes_user_serializer_invalid(self): 'email': 'dummy', 'first_name': FIRST_NAME, 'last_name': LAST_NAME, - }, context={'context': self.request}) + }) user2 = UserSerializer(data={ 'id': ID, 'username': USERNAME, 'email': 'dummy@dummy', 'first_name': FIRST_NAME, 'last_name': LAST_NAME, - }, context={'context': self.request}) + }) user3 = UserSerializer(data={ 'id': ID, 'username': USERNAME, 'email': 21, 'first_name': FIRST_NAME, 'last_name': LAST_NAME, - }, context={'context': self.request}) + }) self.assertFalse(user.is_valid()) self.assertFalse(user2.is_valid()) self.assertFalse(user3.is_valid()) def test_valid_email_makes_valid_serializer(self): + """ + When the serializer is provided with a valid email, the serializer becomes valid, + thus the is_valid() method returns True. + """ user = UserSerializer(data={ 'id': ID, 'username': USERNAME, 'email': EMAIL, 'first_name': FIRST_NAME, 'last_name': LAST_NAME, - }, context={'context': self.request}) + }) self.assertTrue(user.is_valid()) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index cfb54cc2..6f1754fc 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -100,5 +100,3 @@ def test_token_echo_echoes_token(self): response = self.client.get(reverse('auth.echo'), data={'ticket': ticket}) content = response.rendered_content.decode('utf-8').strip('"') self.assertEqual(content, ticket) - - From e5b1db6da9106d1d1fab9176ae7d55d23a9121c0 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Sat, 2 Mar 2024 12:03:54 +0100 Subject: [PATCH 19/24] chore: replace auto_now=True with auto_now_add=True for User create_time field #35 --- backend/authentication/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/authentication/models.py b/backend/authentication/models.py index c7fae247..0bf42d22 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -49,7 +49,7 @@ class User(AbstractBaseUser): ) create_time = DateTimeField( - auto_now=True + auto_now_add=True ) """Model settings""" From 637c4fbc107af20949d92083ecfac1529ea48e2c Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Sat, 2 Mar 2024 18:24:42 +0100 Subject: [PATCH 20/24] chore: init swagger --- backend/ypovoli/settings.py | 21 +++++++++++++++++++++ backend/ypovoli/urls.py | 17 +++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index c912fa45..7552d92e 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -36,6 +36,7 @@ "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", + 'django.contrib.staticfiles', # Third party "rest_framework_swagger", # Swagger @@ -109,3 +110,23 @@ USE_I18N = True USE_L10N = False USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ +STATIC_URL = 'static/' + +TEMPLATES = [ + { + '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', + ], + }, + }, +] diff --git a/backend/ypovoli/urls.py b/backend/ypovoli/urls.py index 6f4771a1..f4cf1d40 100644 --- a/backend/ypovoli/urls.py +++ b/backend/ypovoli/urls.py @@ -15,6 +15,18 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.urls import path, include +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="YpoVoli API", + default_version='v1',), + public=True, + permission_classes=(permissions.AllowAny,), +) + urlpatterns = [ # Base API endpoints. @@ -22,4 +34,9 @@ # Authentication endpoints. path("auth/", include("authentication.urls")), path("notifications/", include("notifications.urls")), + # Swagger documentation. + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), + name='schema-swagger-ui'), + path('swagger/', schema_view.without_ui(cache_timeout=0), + name='schema-json'), ] From c27bdba2538f8e8a2e801e7d22d771e9153d079b Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Sat, 2 Mar 2024 18:51:59 +0100 Subject: [PATCH 21/24] fix: typo --- backend/ypovoli/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/ypovoli/urls.py b/backend/ypovoli/urls.py index f4cf1d40..cb541b25 100644 --- a/backend/ypovoli/urls.py +++ b/backend/ypovoli/urls.py @@ -21,7 +21,7 @@ schema_view = get_schema_view( openapi.Info( - title="YpoVoli API", + title="Ypovoli API", default_version='v1',), public=True, permission_classes=(permissions.AllowAny,), From 055306cde20eb92fa019e501ed4675fac0a5885e Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Sat, 2 Mar 2024 19:23:05 +0100 Subject: [PATCH 22/24] test: fix that test expects the whoami page for a deleted/nonexistent user to be 404 #27 --- backend/authentication/tests/test_authentication_views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 6f1754fc..9f1359f1 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -41,9 +41,7 @@ def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(s self.client.credentials(HTTP_AUTHORIZATION=self.token) response = self.client.get(reverse('auth.whoami')) - self.assertTrue(response.status_code, 200) - content = json.loads(response.content.decode('utf-8')) - self.assertEqual(content['detail'], 'User not found') + self.assertTrue(response.status_code, 404) def test_who_am_i_view_returns_401_when_not_authenticated(self): """WhoAmIView should return a 401 status code when the user is not authenticated""" From 43550c6d69c4faf843b7466302e9dbec6b43edcb Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Sat, 2 Mar 2024 19:52:36 +0100 Subject: [PATCH 23/24] test: from 404 to 405 from last commit #27 --- backend/authentication/tests/test_authentication_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 9f1359f1..4c2cb756 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -41,7 +41,7 @@ def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(s self.client.credentials(HTTP_AUTHORIZATION=self.token) response = self.client.get(reverse('auth.whoami')) - self.assertTrue(response.status_code, 404) + self.assertEqual(response.status_code, 405) def test_who_am_i_view_returns_401_when_not_authenticated(self): """WhoAmIView should return a 401 status code when the user is not authenticated""" From 8057562d71abb7b6ab43acd6a216ee7b771ddd1a Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 4 Mar 2024 16:33:13 +0100 Subject: [PATCH 24/24] chore: format code --- backend/.flake8 | 14 + backend/api/apps.py | 4 +- backend/api/migrations/0001_initial.py | 297 ++++++++++++++---- backend/api/models/assistant.py | 4 +- backend/api/models/checks.py | 13 +- backend/api/models/course.py | 22 +- backend/api/models/group.py | 11 +- backend/api/models/project.py | 30 +- backend/api/models/student.py | 11 +- backend/api/models/submission.py | 24 +- backend/api/models/teacher.py | 4 +- backend/api/serializers/admin_serializer.py | 16 +- .../api/serializers/assistant_serializer.py | 19 +- backend/api/serializers/checks_serializer.py | 11 +- backend/api/serializers/course_serializer.py | 26 +- backend/api/serializers/faculty_serializer.py | 4 +- backend/api/serializers/group_serializer.py | 10 +- backend/api/serializers/project_serializer.py | 22 +- backend/api/serializers/student_serializer.py | 22 +- .../api/serializers/submision_serializer.py | 11 +- backend/api/serializers/teacher_serializer.py | 19 +- backend/api/signals.py | 1 - backend/api/tests/test_admin.py | 42 +-- backend/api/tests/test_assistant.py | 100 +++--- backend/api/tests/test_checks.py | 70 ++--- backend/api/tests/test_course.py | 161 ++++------ backend/api/tests/test_group.py | 145 +++------ backend/api/tests/test_project.py | 215 +++++++------ backend/api/tests/test_student.py | 100 +++--- backend/api/tests/test_submision.py | 140 +++------ backend/api/tests/test_teacher.py | 100 +++--- backend/api/urls.py | 59 +--- backend/api/views/assistant_view.py | 10 +- backend/api/views/checks_view.py | 4 +- backend/api/views/course_view.py | 36 ++- backend/api/views/group_view.py | 9 +- backend/api/views/student_view.py | 18 +- backend/api/views/submision_view.py | 3 +- backend/api/views/teacher_view.py | 9 +- backend/authentication/apps.py | 4 +- backend/authentication/cas/client.py | 4 +- .../authentication/migrations/0001_initial.py | 54 ++-- ...culty_user_remove_user_faculty_and_more.py | 19 +- .../migrations/0003_alter_user_create_time.py | 17 + backend/authentication/models.py | 64 ++-- backend/authentication/serializers.py | 70 ++--- backend/authentication/services/users.py | 56 ++-- backend/authentication/signals.py | 2 +- .../tests/test_authentication_serializer.py | 183 +++++------ .../tests/test_authentication_views.py | 66 ++-- backend/authentication/urls.py | 26 +- backend/authentication/views/auth.py | 8 +- backend/authentication/views/users.py | 3 +- backend/checks/apps.py | 4 +- backend/manage.py | 4 +- backend/notifications/apps.py | 6 +- .../notifications/migrations/0001_initial.py | 47 ++- backend/ypovoli/asgi.py | 2 +- backend/ypovoli/settings.py | 26 +- backend/ypovoli/urls.py | 15 +- backend/ypovoli/wsgi.py | 2 +- 61 files changed, 1229 insertions(+), 1269 deletions(-) create mode 100644 backend/.flake8 create mode 100644 backend/authentication/migrations/0003_alter_user_create_time.py diff --git a/backend/.flake8 b/backend/.flake8 new file mode 100644 index 00000000..b92dcdfc --- /dev/null +++ b/backend/.flake8 @@ -0,0 +1,14 @@ +[flake8] + +# Ignore unused imports +ignore = F401 + +max-line-length = 119 + +max-complexity = 10 + +exclude = .git, + __pycache__, + .venv, + venv, + migrations \ No newline at end of file diff --git a/backend/api/apps.py b/backend/api/apps.py index a13c8f06..55a607c6 100644 --- a/backend/api/apps.py +++ b/backend/api/apps.py @@ -2,8 +2,8 @@ class ApiConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'api' + default_auto_field = "django.db.models.BigAutoField" + name = "api" def ready(self): from authentication.signals import user_created diff --git a/backend/api/migrations/0001_initial.py b/backend/api/migrations/0001_initial.py index bb01d604..6b842090 100644 --- a/backend/api/migrations/0001_initial.py +++ b/backend/api/migrations/0001_initial.py @@ -7,125 +7,300 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('authentication', '0001_initial'), + ("authentication", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Admin', + name="Admin", fields=[ - ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ( + "user_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('authentication.user',), + bases=("authentication.user",), ), migrations.CreateModel( - name='FileExtension', + name="FileExtension", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('extension', models.CharField(max_length=10, unique=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("extension", models.CharField(max_length=10, unique=True)), ], ), migrations.CreateModel( - name='Course', + name="Course", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('academic_startyear', models.IntegerField()), - ('description', models.TextField(blank=True, null=True)), - ('parent_course', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_course', to='api.course')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("academic_startyear", models.IntegerField()), + ("description", models.TextField(blank=True, null=True)), + ( + "parent_course", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="child_course", + to="api.course", + ), + ), ], ), migrations.CreateModel( - name='Assistant', + name="Assistant", fields=[ - ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), - ('courses', models.ManyToManyField(blank=True, related_name='assistants', to='api.course')), + ( + "user_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "courses", + models.ManyToManyField( + blank=True, related_name="assistants", to="api.course" + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('authentication.user',), + bases=("authentication.user",), ), migrations.CreateModel( - name='Checks', + name="Checks", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('dockerfile', models.FileField(blank=True, null=True, upload_to='')), - ('allowed_file_extensions', models.ManyToManyField(blank=True, related_name='checks_allowed', to='api.fileextension')), - ('forbidden_file_extensions', models.ManyToManyField(blank=True, related_name='checks_forbidden', to='api.fileextension')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("dockerfile", models.FileField(blank=True, null=True, upload_to="")), + ( + "allowed_file_extensions", + models.ManyToManyField( + blank=True, + related_name="checks_allowed", + to="api.fileextension", + ), + ), + ( + "forbidden_file_extensions", + models.ManyToManyField( + blank=True, + related_name="checks_forbidden", + to="api.fileextension", + ), + ), ], ), migrations.CreateModel( - name='Project', + name="Project", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.TextField(blank=True, null=True)), - ('visible', models.BooleanField(default=True)), - ('archived', models.BooleanField(default=False)), - ('start_date', models.DateTimeField(blank=True, default=datetime.datetime.now)), - ('deadline', models.DateTimeField()), - ('checks', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.checks')), - ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='api.course')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.TextField(blank=True, null=True)), + ("visible", models.BooleanField(default=True)), + ("archived", models.BooleanField(default=False)), + ( + "start_date", + models.DateTimeField(blank=True, default=datetime.datetime.now), + ), + ("deadline", models.DateTimeField()), + ( + "checks", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="api.checks", + ), + ), + ( + "course", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="projects", + to="api.course", + ), + ), ], ), migrations.CreateModel( - name='Student', + name="Student", fields=[ - ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), - ('student_id', models.CharField(max_length=8, null=True, unique=True)), - ('courses', models.ManyToManyField(blank=True, related_name='students', to='api.course')), + ( + "user_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), + ("student_id", models.CharField(max_length=8, null=True, unique=True)), + ( + "courses", + models.ManyToManyField( + blank=True, related_name="students", to="api.course" + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('authentication.user',), + bases=("authentication.user",), ), migrations.CreateModel( - name='Group', + name="Group", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('score', models.FloatField(blank=True, null=True)), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='api.project')), - ('students', models.ManyToManyField(related_name='groups', to='api.student')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("score", models.FloatField(blank=True, null=True)), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="groups", + to="api.project", + ), + ), + ( + "students", + models.ManyToManyField(related_name="groups", to="api.student"), + ), ], ), migrations.CreateModel( - name='Submission', + name="Submission", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('submission_number', models.PositiveIntegerField()), - ('submission_time', models.DateTimeField(auto_now_add=True)), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='api.group')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("submission_number", models.PositiveIntegerField()), + ("submission_time", models.DateTimeField(auto_now_add=True)), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submissions", + to="api.group", + ), + ), ], options={ - 'unique_together': {('group', 'submission_number')}, + "unique_together": {("group", "submission_number")}, }, ), migrations.CreateModel( - name='SubmissionFile', + name="SubmissionFile", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('file', models.FileField(upload_to='')), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='api.submission')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("file", models.FileField(upload_to="")), + ( + "submission", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to="api.submission", + ), + ), ], ), migrations.CreateModel( - name='Teacher', + name="Teacher", fields=[ - ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), - ('courses', models.ManyToManyField(blank=True, related_name='teachers', to='api.course')), + ( + "user_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "courses", + models.ManyToManyField( + blank=True, related_name="teachers", to="api.course" + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('authentication.user',), + bases=("authentication.user",), ), ] diff --git a/backend/api/models/assistant.py b/backend/api/models/assistant.py index a761e8a5..4c6d9f19 100644 --- a/backend/api/models/assistant.py +++ b/backend/api/models/assistant.py @@ -13,6 +13,6 @@ class Assistant(User): courses = models.ManyToManyField( Course, # Allows us to access the assistants from the course - related_name='assistants', - blank=True + related_name="assistants", + blank=True, ) diff --git a/backend/api/models/checks.py b/backend/api/models/checks.py index db11e6e9..ef0595ba 100644 --- a/backend/api/models/checks.py +++ b/backend/api/models/checks.py @@ -17,21 +17,14 @@ class Checks(models.Model): # ID check should be generated automatically - dockerfile = models.FileField( - blank=True, - null=True - ) + dockerfile = models.FileField(blank=True, null=True) # Link to the file extensions that are allowed allowed_file_extensions = models.ManyToManyField( - FileExtension, - related_name='checks_allowed', - blank=True + FileExtension, related_name="checks_allowed", blank=True ) # Link to the file extensions that are forbidden forbidden_file_extensions = models.ManyToManyField( - FileExtension, - related_name='checks_forbidden', - blank=True + FileExtension, related_name="checks_forbidden", blank=True ) diff --git a/backend/api/models/course.py b/backend/api/models/course.py index f0804caf..f4ec41f2 100644 --- a/backend/api/models/course.py +++ b/backend/api/models/course.py @@ -7,33 +7,23 @@ class Course(models.Model): # ID of the course should automatically be generated - name = models.CharField( - max_length=100, - blank=False, - null=False - ) + name = models.CharField(max_length=100, blank=False, null=False) # Begin year of the academic year - academic_startyear = models.IntegerField( - blank=False, - null=False - ) + academic_startyear = models.IntegerField(blank=False, null=False) - description = models.TextField( - blank=True, - null=True - ) + description = models.TextField(blank=True, null=True) # OneToOneField is used to represent a one-to-one relationship # with the course of the previous academic year parent_course = models.OneToOneField( - 'self', + "self", # If the old course is deleted, the child course should remain on_delete=models.SET_NULL, # Allows us to access the child course from the parent course - related_name='child_course', + related_name="child_course", blank=True, - null=True + null=True, ) def __str__(self) -> str: diff --git a/backend/api/models/group.py b/backend/api/models/group.py index b820bcc3..b963aa89 100644 --- a/backend/api/models/group.py +++ b/backend/api/models/group.py @@ -13,21 +13,18 @@ class Group(models.Model): # If the project is deleted, the group should be deleted as well on_delete=models.CASCADE, # This is how we can access groups from a project - related_name='groups', + related_name="groups", blank=False, - null=False + null=False, ) # Students that are part of the group students = models.ManyToManyField( Student, # This is how we can access groups from a student - related_name='groups', + related_name="groups", blank=False, ) # Score of the group - score = models.FloatField( - blank=True, - null=True - ) + score = models.FloatField(blank=True, null=True) diff --git a/backend/api/models/project.py b/backend/api/models/project.py index 4c142be1..ec16ed77 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -10,26 +10,15 @@ class Project(models.Model): # ID should be generated automatically - name = models.CharField( - max_length=100, - blank=False, - null=False - ) + name = models.CharField(max_length=100, blank=False, null=False) - description = models.TextField( - blank=True, - null=True - ) + description = models.TextField(blank=True, null=True) # Project already visible to students - visible = models.BooleanField( - default=True - ) + visible = models.BooleanField(default=True) # Project archived - archived = models.BooleanField( - default=False - ) + archived = models.BooleanField(default=False) start_date = models.DateTimeField( # The default value is the current date and time @@ -37,10 +26,7 @@ class Project(models.Model): blank=True, ) - deadline = models.DateTimeField( - blank=False, - null=False - ) + deadline = models.DateTimeField(blank=False, null=False) # Check entity that is linked to the project checks = models.ForeignKey( @@ -48,7 +34,7 @@ class Project(models.Model): # If the checks are deleted, the project should remain on_delete=models.SET_NULL, blank=True, - null=True + null=True, ) # Course that the project belongs to @@ -56,9 +42,9 @@ class Project(models.Model): Course, # If the course is deleted, the project should be deleted as well on_delete=models.CASCADE, - related_name='projects', + related_name="projects", blank=False, - null=False + null=False, ) def deadline_approaching_in(self, days=7): diff --git a/backend/api/models/student.py b/backend/api/models/student.py index a11fe0f8..c619d924 100644 --- a/backend/api/models/student.py +++ b/backend/api/models/student.py @@ -8,17 +8,14 @@ class Student(User): It extends the User model from the authentication app with student-specific attributes. """ + # The student's Ghent University ID - student_id = models.CharField( - max_length=8, - null=True, - unique=True - ) + student_id = models.CharField(max_length=8, null=True, unique=True) # All the courses the student is enrolled in courses = models.ManyToManyField( Course, # Allows us to access the students from the course - related_name='students', - blank=True + related_name="students", + blank=True, ) diff --git a/backend/api/models/submission.py b/backend/api/models/submission.py index a94c9360..8f41018c 100644 --- a/backend/api/models/submission.py +++ b/backend/api/models/submission.py @@ -11,25 +11,20 @@ class Submission(models.Model): Group, # If the group is deleted, the submission should be deleted as well on_delete=models.CASCADE, - related_name='submissions', + related_name="submissions", blank=False, - null=False + null=False, ) # Multiple submissions can be made by a group - submission_number = models.PositiveIntegerField( - blank=False, - null=False - ) + submission_number = models.PositiveIntegerField(blank=False, null=False) # Automatically set the submission time to the current time - submission_time = models.DateTimeField( - auto_now_add=True - ) + submission_time = models.DateTimeField(auto_now_add=True) class Meta: # A group can only have one submission with a specific number - unique_together = ('group', 'submission_number') + unique_together = ("group", "submission_number") class SubmissionFile(models.Model): @@ -41,13 +36,10 @@ class SubmissionFile(models.Model): Submission, # If the submission is deleted, the file should be deleted as well on_delete=models.CASCADE, - related_name='files', + related_name="files", blank=False, - null=False + null=False, ) # TODO - Set the right place to save the file - file = models.FileField( - blank=False, - null=False - ) + file = models.FileField(blank=False, null=False) diff --git a/backend/api/models/teacher.py b/backend/api/models/teacher.py index e2e6260e..89f3d471 100644 --- a/backend/api/models/teacher.py +++ b/backend/api/models/teacher.py @@ -13,6 +13,6 @@ class Teacher(User): courses = models.ManyToManyField( Course, # Allows us to access the teachers from the course - related_name='teachers', - blank=True + related_name="teachers", + blank=True, ) diff --git a/backend/api/serializers/admin_serializer.py b/backend/api/serializers/admin_serializer.py index c7749f87..7b060e69 100644 --- a/backend/api/serializers/admin_serializer.py +++ b/backend/api/serializers/admin_serializer.py @@ -3,16 +3,18 @@ class AdminSerializer(serializers.ModelSerializer): - faculties = serializers.HyperlinkedRelatedField( - many=True, - read_only=True, - view_name='faculty-detail' + many=True, read_only=True, view_name="faculty-detail" ) class Meta: model = Admin fields = [ - 'id', 'first_name', 'last_name', 'email', - 'faculties', 'last_enrolled', 'create_time' - ] + "id", + "first_name", + "last_name", + "email", + "faculties", + "last_enrolled", + "create_time", + ] diff --git a/backend/api/serializers/assistant_serializer.py b/backend/api/serializers/assistant_serializer.py index 540f9f32..16e26206 100644 --- a/backend/api/serializers/assistant_serializer.py +++ b/backend/api/serializers/assistant_serializer.py @@ -3,21 +3,24 @@ class AssistantSerializer(serializers.ModelSerializer): - courses = serializers.HyperlinkedIdentityField( - view_name='assistant-courses', + view_name="assistant-courses", read_only=True, ) faculties = serializers.HyperlinkedRelatedField( - many=True, - read_only=True, - view_name='faculty-detail' + many=True, read_only=True, view_name="faculty-detail" ) class Meta: model = Assistant fields = [ - 'id', 'first_name', 'last_name', 'email', - 'faculties', 'last_enrolled', 'create_time', 'courses' - ] + "id", + "first_name", + "last_name", + "email", + "faculties", + "last_enrolled", + "create_time", + "courses", + ] diff --git a/backend/api/serializers/checks_serializer.py b/backend/api/serializers/checks_serializer.py index dc312e4b..01254ec0 100644 --- a/backend/api/serializers/checks_serializer.py +++ b/backend/api/serializers/checks_serializer.py @@ -5,16 +5,19 @@ class FileExtensionSerializer(serializers.ModelSerializer): class Meta: model = FileExtension - fields = ['extension'] + fields = ["extension"] class ChecksSerializer(serializers.ModelSerializer): - allowed_file_extensions = FileExtensionSerializer(many=True) forbidden_file_extensions = FileExtensionSerializer(many=True) class Meta: model = Checks - fields = ['id', 'dockerfile', 'allowed_file_extensions', - 'forbidden_file_extensions'] + fields = [ + "id", + "dockerfile", + "allowed_file_extensions", + "forbidden_file_extensions", + ] diff --git a/backend/api/serializers/course_serializer.py b/backend/api/serializers/course_serializer.py index 3ff5e1c7..4d6edede 100644 --- a/backend/api/serializers/course_serializer.py +++ b/backend/api/serializers/course_serializer.py @@ -3,36 +3,40 @@ class CourseSerializer(serializers.ModelSerializer): - teachers = serializers.HyperlinkedIdentityField( - view_name='course-teachers', + view_name="course-teachers", read_only=True, ) assistants = serializers.HyperlinkedIdentityField( - view_name='course-assistants', + view_name="course-assistants", read_only=True, ) students = serializers.HyperlinkedIdentityField( - view_name='course-students', + view_name="course-students", read_only=True, ) projects = serializers.HyperlinkedIdentityField( - view_name='course-projects', + view_name="course-projects", read_only=True, ) parent_course = serializers.HyperlinkedRelatedField( - many=False, - read_only=True, - view_name='course-detail' + many=False, read_only=True, view_name="course-detail" ) class Meta: model = Course fields = [ - 'id', 'name', 'academic_startyear', 'description', - 'parent_course', 'teachers', 'assistants', 'students', 'projects' - ] + "id", + "name", + "academic_startyear", + "description", + "parent_course", + "teachers", + "assistants", + "students", + "projects", + ] diff --git a/backend/api/serializers/faculty_serializer.py b/backend/api/serializers/faculty_serializer.py index 9c22e1ce..eab4a48d 100644 --- a/backend/api/serializers/faculty_serializer.py +++ b/backend/api/serializers/faculty_serializer.py @@ -5,6 +5,4 @@ class facultySerializer(serializers.ModelSerializer): class Meta: model = Faculty - fields = [ - 'name' - ] + fields = ["name"] diff --git a/backend/api/serializers/group_serializer.py b/backend/api/serializers/group_serializer.py index 9a9e1604..d3b0ecfa 100644 --- a/backend/api/serializers/group_serializer.py +++ b/backend/api/serializers/group_serializer.py @@ -4,16 +4,14 @@ class GroupSerializer(serializers.ModelSerializer): project = serializers.HyperlinkedRelatedField( - many=False, - read_only=True, - view_name='project-detail' + many=False, read_only=True, view_name="project-detail" ) students = serializers.HyperlinkedIdentityField( - view_name='group-students', - read_only=True, + view_name="group-students", + read_only=True, ) class Meta: model = Group - fields = ['id', 'project', 'students', 'score'] + fields = ["id", "project", "students", "score"] diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index a04058f0..c0e36a9a 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -3,22 +3,24 @@ class ProjectSerializer(serializers.ModelSerializer): - course = serializers.HyperlinkedRelatedField( - many=False, - read_only=True, - view_name='course-detail' + many=False, read_only=True, view_name="course-detail" ) checks = serializers.HyperlinkedRelatedField( - many=False, - read_only=True, - view_name='check-detail' + many=False, read_only=True, view_name="check-detail" ) class Meta: model = Project fields = [ - 'id', 'name', 'description', 'visible', 'archived', - 'start_date', 'deadline', 'checks', 'course' - ] + "id", + "name", + "description", + "visible", + "archived", + "start_date", + "deadline", + "checks", + "course", + ] diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index 3ea93733..fddbd8d2 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -3,26 +3,30 @@ class StudentSerializer(serializers.ModelSerializer): - courses = serializers.HyperlinkedIdentityField( - view_name='student-courses', + view_name="student-courses", read_only=True, ) groups = serializers.HyperlinkedIdentityField( - view_name='student-groups', + view_name="student-groups", read_only=True, ) faculties = serializers.HyperlinkedRelatedField( - many=True, - read_only=True, - view_name='faculty-detail' + many=True, read_only=True, view_name="faculty-detail" ) class Meta: model = Student fields = [ - 'id', 'first_name', 'last_name', 'email', 'faculties', - 'last_enrolled', 'create_time', 'courses', 'groups' - ] + "id", + "first_name", + "last_name", + "email", + "faculties", + "last_enrolled", + "create_time", + "courses", + "groups", + ] diff --git a/backend/api/serializers/submision_serializer.py b/backend/api/serializers/submision_serializer.py index 95d26dbf..da7458b8 100644 --- a/backend/api/serializers/submision_serializer.py +++ b/backend/api/serializers/submision_serializer.py @@ -5,21 +5,16 @@ class SubmissionFileSerializer(serializers.ModelSerializer): class Meta: model = SubmissionFile - fields = ['file'] + fields = ["file"] class SubmissionSerializer(serializers.ModelSerializer): - group = serializers.HyperlinkedRelatedField( - many=False, - read_only=True, - view_name='group-detail' + many=False, read_only=True, view_name="group-detail" ) files = SubmissionFileSerializer(many=True, read_only=True) class Meta: model = Submission - fields = ['id', 'group', 'submission_number', 'submission_time', - 'files' - ] + fields = ["id", "group", "submission_number", "submission_time", "files"] diff --git a/backend/api/serializers/teacher_serializer.py b/backend/api/serializers/teacher_serializer.py index 8fbb98e9..fcfa35e1 100644 --- a/backend/api/serializers/teacher_serializer.py +++ b/backend/api/serializers/teacher_serializer.py @@ -3,21 +3,24 @@ class TeacherSerializer(serializers.ModelSerializer): - courses = serializers.HyperlinkedIdentityField( - view_name='teacher-courses', + view_name="teacher-courses", read_only=True, ) faculties = serializers.HyperlinkedRelatedField( - many=True, - read_only=True, - view_name='faculty-detail' + many=True, read_only=True, view_name="faculty-detail" ) class Meta: model = Teacher fields = [ - 'id', 'first_name', 'last_name', 'email', - 'faculties', 'last_enrolled', 'create_time', 'courses' - ] + "id", + "first_name", + "last_name", + "email", + "faculties", + "last_enrolled", + "create_time", + "courses", + ] diff --git a/backend/api/signals.py b/backend/api/signals.py index 8b50a749..85f94211 100644 --- a/backend/api/signals.py +++ b/backend/api/signals.py @@ -3,7 +3,6 @@ def user_creation(user: User, attributes: dict, **kwargs): - """Upon user creation, auto-populate additional properties""" student_id = attributes.get("ugentStudentID") diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index 292e1f21..d6d44888 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -11,9 +11,7 @@ def create_faculty(name): """ Create a Faculty with the given arguments.""" - return Faculty.objects.create( - name=name - ) + return Faculty.objects.create(name=name) def create_admin(id, first_name, last_name, email, faculty=None): @@ -28,7 +26,7 @@ def create_admin(id, first_name, last_name, email, faculty=None): last_name=last_name, username=username, email=email, - create_time=timezone.now() + create_time=timezone.now(), ) else: admin = Admin.objects.create( @@ -64,10 +62,7 @@ def test_admin_exists(self): Able to retrieve a single admin after creating it. """ admin = create_admin( - id=3, - first_name="John", - last_name="Doe", - email="john.doe@example.com" + id=3, first_name="John", last_name="Doe", email="john.doe@example.com" ) # Make a GET request to retrieve the admin @@ -98,17 +93,11 @@ def test_multiple_admins(self): """ # Create multiple admins admin1 = create_admin( - id=1, - first_name="Johny", - last_name="Doeg", - email="john.doe@example.com" - ) + id=1, first_name="Johny", last_name="Doeg", email="john.doe@example.com" + ) admin2 = create_admin( - id=2, - first_name="Jane", - last_name="Doe", - email="jane.doe@example.com" - ) + id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + ) # Make a GET request to retrieve the admins response = self.client.get(reverse("admin-list"), follow=True) @@ -143,15 +132,13 @@ def test_admin_detail_view(self): """ # Create an admin for testing with the name "Bob Peeters" admin = create_admin( - id=5, - first_name="Bob", - last_name="Peeters", - email="bob.peeters@example.com" - ) + id=5, first_name="Bob", last_name="Peeters", email="bob.peeters@example.com" + ) # Make a GET request to retrieve the admin details response = self.client.get( - reverse("admin-detail", args=[str(admin.id)]), follow=True) + reverse("admin-detail", args=[str(admin.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -179,12 +166,13 @@ def test_admin_faculty(self): first_name="Bob", last_name="Peeters", email="bob.peeters@example.com", - faculty=[faculty] - ) + faculty=[faculty], + ) # Make a GET request to retrieve the admin details response = self.client.get( - reverse("admin-detail", args=[str(admin.id)]), follow=True) + reverse("admin-detail", args=[str(admin.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index 4e8af6a9..15d53aa1 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -8,8 +8,7 @@ from authentication.models import Faculty -def create_course(name, academic_startyear, description=None, - parent_course=None): +def create_course(name, academic_startyear, description=None, parent_course=None): """ Create a Course with the given arguments. """ @@ -17,37 +16,28 @@ def create_course(name, academic_startyear, description=None, name=name, academic_startyear=academic_startyear, description=description, - parent_course=parent_course + parent_course=parent_course, ) def create_faculty(name): """Create a Faculty with the given arguments.""" - return Faculty.objects.create( - name=name - ) + return Faculty.objects.create(name=name) -def create_assistant( - id, - first_name, - last_name, - email, - faculty=None, - courses=None - ): +def create_assistant(id, first_name, last_name, email, faculty=None, courses=None): """ Create a assistant with the given arguments. """ username = f"{first_name}_{last_name}" assistant = Assistant.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now() - ) + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) if faculty is not None: for fac in faculty: @@ -80,10 +70,7 @@ def test_assistant_exists(self): Able to retrieve a single assistant after creating it. """ assistant = create_assistant( - id=3, - first_name="John", - last_name="Doe", - email="john.doe@example.com" + id=3, first_name="John", last_name="Doe", email="john.doe@example.com" ) # Make a GET request to retrieve the assistant @@ -105,8 +92,7 @@ def test_assistant_exists(self): # match the created assistant retrieved_assistant = content_json[0] self.assertEqual(int(retrieved_assistant["id"]), assistant.id) - self.assertEqual( - retrieved_assistant["first_name"], assistant.first_name) + self.assertEqual(retrieved_assistant["first_name"], assistant.first_name) self.assertEqual(retrieved_assistant["last_name"], assistant.last_name) self.assertEqual(retrieved_assistant["email"], assistant.email) @@ -116,17 +102,11 @@ def test_multiple_assistant(self): """ # Create multiple assistant assistant1 = create_assistant( - id=1, - first_name="Johny", - last_name="Doeg", - email="john.doe@example.com" - ) + id=1, first_name="Johny", last_name="Doeg", email="john.doe@example.com" + ) assistant2 = create_assistant( - id=2, - first_name="Jane", - last_name="Doe", - email="jane.doe@example.com" - ) + id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + ) # Make a GET request to retrieve the assistant response = self.client.get(reverse("assistant-list"), follow=True) @@ -147,17 +127,13 @@ def test_multiple_assistant(self): # assistant match the created assistant retrieved_assistant1, retrieved_assistant2 = content_json self.assertEqual(int(retrieved_assistant1["id"]), assistant1.id) - self.assertEqual( - retrieved_assistant1["first_name"], assistant1.first_name) - self.assertEqual( - retrieved_assistant1["last_name"], assistant1.last_name) + self.assertEqual(retrieved_assistant1["first_name"], assistant1.first_name) + self.assertEqual(retrieved_assistant1["last_name"], assistant1.last_name) self.assertEqual(retrieved_assistant1["email"], assistant1.email) self.assertEqual(int(retrieved_assistant2["id"]), assistant2.id) - self.assertEqual( - retrieved_assistant2["first_name"], assistant2.first_name) - self.assertEqual( - retrieved_assistant2["last_name"], assistant2.last_name) + self.assertEqual(retrieved_assistant2["first_name"], assistant2.first_name) + self.assertEqual(retrieved_assistant2["last_name"], assistant2.last_name) self.assertEqual(retrieved_assistant2["email"], assistant2.email) def test_assistant_detail_view(self): @@ -166,15 +142,13 @@ def test_assistant_detail_view(self): """ # Create an assistant for testing with the name "Bob Peeters" assistant = create_assistant( - id=5, - first_name="Bob", - last_name="Peeters", - email="bob.peeters@example.com" - ) + id=5, first_name="Bob", last_name="Peeters", email="bob.peeters@example.com" + ) # Make a GET request to retrieve the assistant details response = self.client.get( - reverse("assistant-detail", args=[str(assistant.id)]), follow=True) + reverse("assistant-detail", args=[str(assistant.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -203,12 +177,13 @@ def test_assistant_faculty(self): first_name="Bob", last_name="Peeters", email="bob.peeters@example.com", - faculty=[faculty] - ) + faculty=[faculty], + ) # Make a GET request to retrieve the assistant details response = self.client.get( - reverse("assistant-detail", args=[str(assistant.id)]), follow=True) + reverse("assistant-detail", args=[str(assistant.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -246,12 +221,12 @@ def test_assistant_courses(self): course1 = create_course( name="Introduction to Computer Science", academic_startyear=2022, - description="An introductory course on computer science." + description="An introductory course on computer science.", ) course2 = create_course( name="Intermediate to Computer Science", academic_startyear=2023, - description="An second course on computer science." + description="An second course on computer science.", ) assistant = create_assistant( @@ -259,12 +234,13 @@ def test_assistant_courses(self): first_name="Bob", last_name="Peeters", email="bob.peeters@example.com", - courses=[course1, course2] - ) + courses=[course1, course2], + ) # Make a GET request to retrieve the assistant details response = self.client.get( - reverse("assistant-detail", args=[str(assistant.id)]), follow=True) + reverse("assistant-detail", args=[str(assistant.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -299,13 +275,11 @@ def test_assistant_courses(self): content = content_json[0] self.assertEqual(int(content["id"]), course1.id) self.assertEqual(content["name"], course1.name) - self.assertEqual( - int(content["academic_startyear"]), course1.academic_startyear) + self.assertEqual(int(content["academic_startyear"]), course1.academic_startyear) self.assertEqual(content["description"], course1.description) content = content_json[1] self.assertEqual(int(content["id"]), course2.id) self.assertEqual(content["name"], course2.name) - self.assertEqual( - int(content["academic_startyear"]), course2.academic_startyear) + self.assertEqual(int(content["academic_startyear"]), course2.academic_startyear) self.assertEqual(content["description"], course2.description) diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index e4247956..f7273144 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -10,10 +10,7 @@ def create_fileExtension(id, extension): """ Create a FileExtension with the given arguments. """ - return FileExtension.objects.create( - id=id, - extension=extension - ) + return FileExtension.objects.create(id=id, extension=extension) def create_checks(id, allowed_file_extensions, forbidden_file_extensions): @@ -34,8 +31,7 @@ def test_no_fileExtension(self): """ able to retrieve no FileExtension before publishing it. """ - response_root = self.client.get( - reverse("fileExtension-list"), follow=True) + response_root = self.client.get(reverse("fileExtension-list"), follow=True) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") @@ -48,10 +44,7 @@ def test_fileExtension_exists(self): """ Able to retrieve a single fileExtension after creating it. """ - fileExtension = create_fileExtension( - id=5, - extension="pdf" - ) + fileExtension = create_fileExtension(id=5, extension="pdf") # Make a GET request to retrieve the fileExtension response = self.client.get(reverse("fileExtension-list"), follow=True) @@ -71,22 +64,15 @@ def test_fileExtension_exists(self): # Assert the details of the retrieved fileExtension # match the created fileExtension retrieved_fileExtension = content_json[0] - self.assertEqual( - retrieved_fileExtension["extension"], fileExtension.extension) + self.assertEqual(retrieved_fileExtension["extension"], fileExtension.extension) def test_multiple_fileExtension(self): """ Able to retrieve multiple fileExtension after creating them. """ # Create multiple fileExtension - fileExtension1 = create_fileExtension( - id=1, - extension="jpg" - ) - fileExtension2 = create_fileExtension( - id=2, - extension="png" - ) + fileExtension1 = create_fileExtension(id=1, extension="jpg") + fileExtension2 = create_fileExtension(id=2, extension="png") # Make a GET request to retrieve the fileExtension response = self.client.get(reverse("fileExtension-list"), follow=True) @@ -107,27 +93,24 @@ def test_multiple_fileExtension(self): # match the created fileExtension retrieved_fileExtension1, retrieved_fileExtension2 = content_json self.assertEqual( - retrieved_fileExtension1["extension"], fileExtension1.extension) + retrieved_fileExtension1["extension"], fileExtension1.extension + ) self.assertEqual( - retrieved_fileExtension2["extension"], fileExtension2.extension) + retrieved_fileExtension2["extension"], fileExtension2.extension + ) def test_fileExtension_detail_view(self): """ Able to retrieve details of a single fileExtension. """ # Create an fileExtension for testing. - fileExtension = create_fileExtension( - id=3, - extension="zip" - ) + fileExtension = create_fileExtension(id=3, extension="zip") # Make a GET request to retrieve the fileExtension details response = self.client.get( - reverse( - "fileExtension-detail", - args=[str(fileExtension.id)]), - follow=True) + reverse("fileExtension-detail", args=[str(fileExtension.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -148,8 +131,7 @@ def test_no_checks(self): """ Able to retrieve no Checks before publishing it. """ - response_root = self.client.get( - reverse("check-list"), follow=True) + response_root = self.client.get(reverse("check-list"), follow=True) self.assertEqual(response_root.status_code, 200) self.assertEqual(response_root.accepted_media_type, "application/json") content_json = json.loads(response_root.content.decode("utf-8")) @@ -167,8 +149,8 @@ def test_checks_exists(self): checks = create_checks( id=5, allowed_file_extensions=[fileExtension1, fileExtension4], - forbidden_file_extensions=[fileExtension2, fileExtension3] - ) + forbidden_file_extensions=[fileExtension2, fileExtension3], + ) # Make a GET request to retrieve the Checks response = self.client.get(reverse("check-list"), follow=True) @@ -189,23 +171,25 @@ def test_checks_exists(self): # Assert the file extensions of the retrieved # Checks match the created file extensions - retrieved_allowed_file_extensions = retrieved_checks[ - "allowed_file_extensions"] + retrieved_allowed_file_extensions = retrieved_checks["allowed_file_extensions"] self.assertEqual(len(retrieved_allowed_file_extensions), 2) self.assertEqual( - retrieved_allowed_file_extensions[0]["extension"], - fileExtension1.extension) + retrieved_allowed_file_extensions[0]["extension"], fileExtension1.extension + ) self.assertEqual( - retrieved_allowed_file_extensions[1]["extension"], - fileExtension4.extension) + retrieved_allowed_file_extensions[1]["extension"], fileExtension4.extension + ) retrieved_forbidden_file_extensions = retrieved_checks[ - "forbidden_file_extensions"] + "forbidden_file_extensions" + ] self.assertEqual(len(retrieved_forbidden_file_extensions), 2) self.assertEqual( retrieved_forbidden_file_extensions[0]["extension"], - fileExtension2.extension) + fileExtension2.extension, + ) self.assertEqual( retrieved_forbidden_file_extensions[1]["extension"], - fileExtension3.extension) + fileExtension3.extension, + ) diff --git a/backend/api/tests/test_course.py b/backend/api/tests/test_course.py index df05eb90..97de9259 100644 --- a/backend/api/tests/test_course.py +++ b/backend/api/tests/test_course.py @@ -10,14 +10,7 @@ from ..models.project import Project -def create_project( - name, - description, - visible, - archived, - days, - course -): +def create_project(name, description, visible, archived, days, course): """Create a Project with the given arguments.""" deadline = timezone.now() + timezone.timedelta(days=days) @@ -27,75 +20,59 @@ def create_project( visible=visible, archived=archived, deadline=deadline, - course=course + course=course, ) -def create_student( - id, - first_name, - last_name, - email - ): +def create_student(id, first_name, last_name, email): """ Create a student with the given arguments. """ username = f"{first_name}_{last_name}" student = Student.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now() - ) + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) return student -def create_assistant( - id, - first_name, - last_name, - email - ): +def create_assistant(id, first_name, last_name, email): """ Create a assistant with the given arguments. """ username = f"{first_name}_{last_name}" assistant = Assistant.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now() - ) + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) return assistant -def create_teacher( - id, - first_name, - last_name, - email - ): +def create_teacher(id, first_name, last_name, email): """ Create a teacher with the given arguments. """ username = f"{first_name}_{last_name}" teacher = Teacher.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now() - ) + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) return teacher -def create_course(name, academic_startyear, description=None, - parent_course=None): +def create_course(name, academic_startyear, description=None, parent_course=None): """ Create a Course with the given arguments. """ @@ -103,7 +80,7 @@ def create_course(name, academic_startyear, description=None, name=name, academic_startyear=academic_startyear, description=description, - parent_course=parent_course + parent_course=parent_course, ) @@ -125,7 +102,7 @@ def test_course_exists(self): course = create_course( name="Introduction to Computer Science", academic_startyear=2022, - description="An introductory course on computer science." + description="An introductory course on computer science.", ) response = self.client.get(reverse("course-list"), follow=True) @@ -140,7 +117,8 @@ def test_course_exists(self): retrieved_course = content_json[0] self.assertEqual(retrieved_course["name"], course.name) self.assertEqual( - retrieved_course["academic_startyear"], course.academic_startyear) + retrieved_course["academic_startyear"], course.academic_startyear + ) self.assertEqual(retrieved_course["description"], course.description) def test_multiple_courses(self): @@ -150,12 +128,12 @@ def test_multiple_courses(self): course1 = create_course( name="Mathematics 101", academic_startyear=2022, - description="A basic mathematics course." + description="A basic mathematics course.", ) course2 = create_course( name="Physics 101", academic_startyear=2022, - description="An introductory physics course." + description="An introductory physics course.", ) response = self.client.get(reverse("course-list"), follow=True) @@ -170,14 +148,14 @@ def test_multiple_courses(self): retrieved_course1, retrieved_course2 = content_json self.assertEqual(retrieved_course1["name"], course1.name) self.assertEqual( - retrieved_course1["academic_startyear"], - course1.academic_startyear) + retrieved_course1["academic_startyear"], course1.academic_startyear + ) self.assertEqual(retrieved_course1["description"], course1.description) self.assertEqual(retrieved_course2["name"], course2.name) self.assertEqual( - retrieved_course2["academic_startyear"], - course2.academic_startyear) + retrieved_course2["academic_startyear"], course2.academic_startyear + ) self.assertEqual(retrieved_course2["description"], course2.description) def test_course_detail_view(self): @@ -187,11 +165,12 @@ def test_course_detail_view(self): course = create_course( name="Chemistry 101", academic_startyear=2022, - description="An introductory chemistry course." + description="An introductory chemistry course.", ) response = self.client.get( - reverse("course-detail", args=[str(course.id)]), follow=True) + reverse("course-detail", args=[str(course.id)]), follow=True + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.accepted_media_type, "application/json") @@ -199,8 +178,7 @@ def test_course_detail_view(self): content_json = json.loads(response.content.decode("utf-8")) self.assertEqual(content_json["name"], course.name) - self.assertEqual( - content_json["academic_startyear"], course.academic_startyear) + self.assertEqual(content_json["academic_startyear"], course.academic_startyear) self.assertEqual(content_json["description"], course.description) def test_course_teachers(self): @@ -211,26 +189,24 @@ def test_course_teachers(self): id=5, first_name="Simon", last_name="Mignolet", - email="simon.mignolet@ugent.be" + email="simon.mignolet@ugent.be", ) teacher2 = create_teacher( - id=6, - first_name="Ronny", - last_name="Deila", - email="ronny.deila@brugge.be" + id=6, first_name="Ronny", last_name="Deila", email="ronny.deila@brugge.be" ) course = create_course( name="Chemistry 101", academic_startyear=2022, - description="An introductory chemistry course." + description="An introductory chemistry course.", ) course.teachers.add(teacher1) course.teachers.add(teacher2) response = self.client.get( - reverse("course-detail", args=[str(course.id)]), follow=True) + reverse("course-detail", args=[str(course.id)]), follow=True + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.accepted_media_type, "application/json") @@ -238,8 +214,7 @@ def test_course_teachers(self): content_json = json.loads(response.content.decode("utf-8")) self.assertEqual(content_json["name"], course.name) - self.assertEqual( - content_json["academic_startyear"], course.academic_startyear) + self.assertEqual(content_json["academic_startyear"], course.academic_startyear) self.assertEqual(content_json["description"], course.description) response = self.client.get(content_json["teachers"], follow=True) @@ -276,26 +251,24 @@ def test_course_assistant(self): id=5, first_name="Simon", last_name="Mignolet", - email="simon.mignolet@ugent.be" + email="simon.mignolet@ugent.be", ) assistant2 = create_assistant( - id=6, - first_name="Ronny", - last_name="Deila", - email="ronny.deila@brugge.be" + id=6, first_name="Ronny", last_name="Deila", email="ronny.deila@brugge.be" ) course = create_course( name="Chemistry 101", academic_startyear=2022, - description="An introductory chemistry course." + description="An introductory chemistry course.", ) course.assistants.add(assistant1) course.assistants.add(assistant2) response = self.client.get( - reverse("course-detail", args=[str(course.id)]), follow=True) + reverse("course-detail", args=[str(course.id)]), follow=True + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.accepted_media_type, "application/json") @@ -303,8 +276,7 @@ def test_course_assistant(self): content_json = json.loads(response.content.decode("utf-8")) self.assertEqual(content_json["name"], course.name) - self.assertEqual( - content_json["academic_startyear"], course.academic_startyear) + self.assertEqual(content_json["academic_startyear"], course.academic_startyear) self.assertEqual(content_json["description"], course.description) response = self.client.get(content_json["assistants"], follow=True) @@ -341,26 +313,24 @@ def test_course_student(self): id=5, first_name="Simon", last_name="Mignolet", - email="simon.mignolet@ugent.be" + email="simon.mignolet@ugent.be", ) student2 = create_student( - id=6, - first_name="Ronny", - last_name="Deila", - email="ronny.deila@brugge.be" + id=6, first_name="Ronny", last_name="Deila", email="ronny.deila@brugge.be" ) course = create_course( name="Chemistry 101", academic_startyear=2022, - description="An introductory chemistry course." + description="An introductory chemistry course.", ) course.students.add(student1) course.students.add(student2) response = self.client.get( - reverse("course-detail", args=[str(course.id)]), follow=True) + reverse("course-detail", args=[str(course.id)]), follow=True + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.accepted_media_type, "application/json") @@ -368,8 +338,7 @@ def test_course_student(self): content_json = json.loads(response.content.decode("utf-8")) self.assertEqual(content_json["name"], course.name) - self.assertEqual( - content_json["academic_startyear"], course.academic_startyear) + self.assertEqual(content_json["academic_startyear"], course.academic_startyear) self.assertEqual(content_json["description"], course.description) response = self.client.get(content_json["students"], follow=True) @@ -405,7 +374,7 @@ def test_course_project(self): course = create_course( name="Chemistry 101", academic_startyear=2022, - description="An introductory chemistry course." + description="An introductory chemistry course.", ) project1 = create_project( @@ -414,7 +383,7 @@ def test_course_project(self): visible=True, archived=False, days=50, - course=course + course=course, ) project2 = create_project( @@ -423,11 +392,12 @@ def test_course_project(self): visible=True, archived=False, days=50, - course=course + course=course, ) response = self.client.get( - reverse("course-detail", args=[str(course.id)]), follow=True) + reverse("course-detail", args=[str(course.id)]), follow=True + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.accepted_media_type, "application/json") @@ -435,8 +405,7 @@ def test_course_project(self): content_json = json.loads(response.content.decode("utf-8")) self.assertEqual(content_json["name"], course.name) - self.assertEqual( - content_json["academic_startyear"], course.academic_startyear) + self.assertEqual(content_json["academic_startyear"], course.academic_startyear) self.assertEqual(content_json["description"], course.description) response = self.client.get(content_json["projects"], follow=True) diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index e0320a44..dafbc1a8 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -9,8 +9,7 @@ from ..models.course import Course -def create_course(name, academic_startyear, description=None, - parent_course=None): +def create_course(name, academic_startyear, description=None, parent_course=None): """ Create a Course with the given arguments. """ @@ -18,7 +17,7 @@ def create_course(name, academic_startyear, description=None, name=name, academic_startyear=academic_startyear, description=description, - parent_course=parent_course + parent_course=parent_course, ) @@ -26,10 +25,7 @@ def create_project(name, description, days, course): """Create a Project with the given arguments.""" deadline = timezone.now() + timedelta(days=days) return Project.objects.create( - name=name, - description=description, - deadline=deadline, - course=course + name=name, description=description, deadline=deadline, course=course ) @@ -41,7 +37,7 @@ def create_student(id, first_name, last_name, email): first_name=first_name, last_name=last_name, username=username, - email=email + email=email, ) @@ -61,23 +57,14 @@ def test_no_groups(self): def test_group_exists(self): """Able to retrieve a single group after creating it.""" - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", - description="Description 1", - days=7, - course=course - ) + name="Project 1", description="Description 1", days=7, course=course + ) student = create_student( - id=1, - first_name="John", - last_name="Doe", - email="john.doe@example.com" + id=1, first_name="John", last_name="Doe", email="john.doe@example.com" ) group = create_group(project=project, score=10) @@ -91,8 +78,8 @@ def test_group_exists(self): retrieved_group = content_json[0] expected_project_url = "http://testserver" + reverse( - "project-detail", args=[str(project.id)] - ) + "project-detail", args=[str(project.id)] + ) self.assertEqual(retrieved_group["project"], expected_project_url) self.assertEqual(int(retrieved_group["id"]), group.id) @@ -100,34 +87,21 @@ def test_group_exists(self): def test_multiple_groups(self): """Able to retrieve multiple groups after creating them.""" - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project1 = create_project( - name="Project 1", - description="Description 1", - days=7, course=course - ) + name="Project 1", description="Description 1", days=7, course=course + ) project2 = create_project( - name="Project 2", - description="Description 2", - days=7, course=course - ) + name="Project 2", description="Description 2", days=7, course=course + ) student1 = create_student( - id=2, - first_name="Bart", - last_name="Rex", - email="bart.rex@example.com" - ) + id=2, first_name="Bart", last_name="Rex", email="bart.rex@example.com" + ) student2 = create_student( - id=3, - first_name="Jane", - last_name="Doe", - email="jane.doe@example.com" - ) + id=3, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + ) group1 = create_group(project=project1, score=10) group1.students.add(student1) @@ -143,9 +117,11 @@ def test_multiple_groups(self): retrieved_group1, retrieved_group2 = content_json expected_project_url1 = "http://testserver" + reverse( - "project-detail", args=[str(project1.id)]) + "project-detail", args=[str(project1.id)] + ) expected_project_url2 = "http://testserver" + reverse( - "project-detail", args=[str(project2.id)]) + "project-detail", args=[str(project2.id)] + ) self.assertEqual(retrieved_group1["project"], expected_project_url1) self.assertEqual(int(retrieved_group1["id"]), group1.id) @@ -157,35 +133,29 @@ def test_multiple_groups(self): def test_group_detail_view(self): """Able to retrieve details of a single group.""" - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", - description="Description 1", - days=7, course=course - ) + name="Project 1", description="Description 1", days=7, course=course + ) student = create_student( - id=5, - first_name="John", - last_name="Doe", - - email="john.doe@example.com") + id=5, first_name="John", last_name="Doe", email="john.doe@example.com" + ) group = create_group(project=project, score=10) group.students.add(student) response = self.client.get( - reverse("group-detail", args=[str(group.id)]), follow=True) + reverse("group-detail", args=[str(group.id)]), follow=True + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.accepted_media_type, "application/json") content_json = json.loads(response.content.decode("utf-8")) expected_project_url = "http://testserver" + reverse( - "project-detail", args=[str(project.id)]) + "project-detail", args=[str(project.id)] + ) self.assertEqual(int(content_json["id"]), group.id) self.assertEqual(content_json["project"], expected_project_url) @@ -193,28 +163,21 @@ def test_group_detail_view(self): def test_group_project(self): """Able to retrieve details of a single group.""" - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", - description="Description 1", - days=7, course=course - ) + name="Project 1", description="Description 1", days=7, course=course + ) student = create_student( - id=5, - first_name="John", - last_name="Doe", - - email="john.doe@example.com") + id=5, first_name="John", last_name="Doe", email="john.doe@example.com" + ) group = create_group(project=project, score=10) group.students.add(student) response = self.client.get( - reverse("group-detail", args=[str(group.id)]), follow=True) + reverse("group-detail", args=[str(group.id)]), follow=True + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.accepted_media_type, "application/json") @@ -235,8 +198,8 @@ def test_group_project(self): content_json = json.loads(response.content.decode("utf-8")) expected_course_url = "http://testserver" + reverse( - "course-detail", args=[str(course.id)] - ) + "course-detail", args=[str(course.id)] + ) self.assertEqual(content_json["name"], project.name) self.assertEqual(content_json["description"], project.description) @@ -246,34 +209,26 @@ def test_group_project(self): def test_group_students(self): """Able to retrieve students details of a group.""" - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", - description="Description 1", - days=7, course=course - ) + name="Project 1", description="Description 1", days=7, course=course + ) student1 = create_student( - id=5, - first_name="John", - last_name="Doe", - email="john.doe@example.com") + id=5, first_name="John", last_name="Doe", email="john.doe@example.com" + ) student2 = create_student( - id=6, - first_name="kom", - last_name="mor_up", - email="kom.mor_up@example.com") + id=6, first_name="kom", last_name="mor_up", email="kom.mor_up@example.com" + ) group = create_group(project=project, score=10) group.students.add(student1) group.students.add(student2) response = self.client.get( - reverse("group-detail", args=[str(group.id)]), follow=True) + reverse("group-detail", args=[str(group.id)]), follow=True + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.accepted_media_type, "application/json") diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index b4e4058d..72f3fbef 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -12,23 +12,20 @@ def create_course(id, name, academic_startyear): Create a Course with the given arguments. """ return Course.objects.create( - id=id, name=name, academic_startyear=academic_startyear) + id=id, name=name, academic_startyear=academic_startyear + ) def create_fileExtension(id, extension): """ Create a FileExtension with the given arguments. """ - return FileExtension.objects.create( - id=id, - extension=extension - ) + return FileExtension.objects.create(id=id, extension=extension) def create_checks( - id=None, - allowed_file_extensions=None, - forbidden_file_extensions=None): + id=None, allowed_file_extensions=None, forbidden_file_extensions=None +): """Create a Checks with the given arguments.""" if id is None and allowed_file_extensions is None: # extra if to make line shorter @@ -46,15 +43,7 @@ def create_checks( return check -def create_project( - name, - description, - visible, - archived, - days, - checks, - course -): +def create_project(name, description, visible, archived, days, checks, course): """Create a Project with the given arguments.""" deadline = timezone.now() + timezone.timedelta(days=days) @@ -65,7 +54,7 @@ def create_project( archived=archived, deadline=deadline, checks=checks, - course=course + course=course, ) @@ -74,12 +63,16 @@ def test_toggle_visible(self): """ toggle the visible state of a project. """ - course = create_course( - id=3, name="test course", academic_startyear=2024) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() past_project = create_project( - name="test", description="descr", visible=True, archived=False, - days=-10, checks=checks, course=course + name="test", + description="descr", + visible=True, + archived=False, + days=-10, + checks=checks, + course=course, ) self.assertIs(past_project.visible, True) past_project.toggle_visible() @@ -91,12 +84,16 @@ def test_toggle_archived(self): """ toggle the archived state of a project. """ - course = create_course( - id=3, name="test course", academic_startyear=2024) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() past_project = create_project( - name="test", description="descr", visible=True, archived=True, - days=-10, checks=checks, course=course + name="test", + description="descr", + visible=True, + archived=True, + days=-10, + checks=checks, + course=course, ) self.assertIs(past_project.archived, True) @@ -110,12 +107,16 @@ def test_deadline_approaching_in_with_past_Project(self): deadline_approaching_in() returns False for Projects whose Deadline is in the past. """ - course = create_course( - id=3, name="test course", academic_startyear=2024) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() past_project = create_project( - name="test", description="descr", visible=True, archived=False, - days=-10, checks=checks, course=course + name="test", + description="descr", + visible=True, + archived=False, + days=-10, + checks=checks, + course=course, ) self.assertIs(past_project.deadline_approaching_in(), False) @@ -124,12 +125,16 @@ def test_deadline_approaching_in_with_future_Project_within_time(self): deadline_approaching_in() returns True for Projects whose Deadline is in the timerange given. """ - course = create_course( - id=3, name="test course", academic_startyear=2024) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() future_project = create_project( - name="test", description="descr", visible=True, archived=False, - days=6, checks=checks, course=course + name="test", + description="descr", + visible=True, + archived=False, + days=6, + checks=checks, + course=course, ) self.assertIs(future_project.deadline_approaching_in(days=7), True) @@ -138,12 +143,16 @@ def test_deadline_approaching_in_with_future_Project_not_within_time(self): deadline_approaching_in() returns False for Projects whose Deadline is out of the timerange given. """ - course = create_course( - id=3, name="test course", academic_startyear=2024) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() future_project = create_project( - name="test", description="descr", visible=True, archived=False, - days=8, checks=checks, course=course + name="test", + description="descr", + visible=True, + archived=False, + days=8, + checks=checks, + course=course, ) self.assertIs(future_project.deadline_approaching_in(days=7), False) @@ -152,12 +161,16 @@ def test_deadline_passed_with_future_Project(self): deadline_passed() returns False for Projects whose Deadline is not passed. """ - course = create_course( - id=3, name="test course", academic_startyear=2024) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() future_project = create_project( - name="test", description="descr", visible=True, archived=False, - days=1, checks=checks, course=course + name="test", + description="descr", + visible=True, + archived=False, + days=1, + checks=checks, + course=course, ) self.assertIs(future_project.deadline_passed(), False) @@ -166,12 +179,16 @@ def test_deadline_passed_with_past_Project(self): deadline_passed() returns True for Projects whose Deadline is passed. """ - course = create_course( - id=3, name="test course", academic_startyear=2024) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() past_project = create_project( - name="test", description="descr", visible=True, archived=False, - days=-1, checks=checks, course=course + name="test", + description="descr", + visible=True, + archived=False, + days=-1, + checks=checks, + course=course, ) self.assertIs(past_project.deadline_passed(), True) @@ -188,11 +205,7 @@ def test_project_exists(self): Able to retrieve a single project after creating it. """ - course = create_course( - id=3, - name="test course", - academic_startyear=2024 - ) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() project = create_project( name="test project", @@ -201,8 +214,8 @@ def test_project_exists(self): archived=False, days=7, checks=checks, - course=course - ) + course=course, + ) response = self.client.get(reverse("project-list"), follow=True) @@ -216,12 +229,12 @@ def test_project_exists(self): retrieved_project = content_json[0] expected_checks_url = "http://testserver" + reverse( - "check-detail", args=[str(checks.id)] - ) + "check-detail", args=[str(checks.id)] + ) expected_course_url = "http://testserver" + reverse( - "course-detail", args=[str(course.id)] - ) + "course-detail", args=[str(course.id)] + ) self.assertEqual(retrieved_project["name"], project.name) self.assertEqual(retrieved_project["description"], project.description) @@ -234,11 +247,7 @@ def test_multiple_project(self): """ Able to retrieve multiple projects after creating it. """ - course = create_course( - id=3, - name="test course", - academic_startyear=2024 - ) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() project = create_project( name="test project", @@ -247,8 +256,8 @@ def test_multiple_project(self): archived=False, days=7, checks=checks, - course=course - ) + course=course, + ) project2 = create_project( name="test project2", @@ -257,8 +266,8 @@ def test_multiple_project(self): archived=False, days=7, checks=checks, - course=course - ) + course=course, + ) response = self.client.get(reverse("project-list"), follow=True) @@ -272,12 +281,12 @@ def test_multiple_project(self): retrieved_project = content_json[0] expected_checks_url = "http://testserver" + reverse( - "check-detail", args=[str(checks.id)] - ) + "check-detail", args=[str(checks.id)] + ) expected_course_url = "http://testserver" + reverse( - "course-detail", args=[str(course.id)] - ) + "course-detail", args=[str(course.id)] + ) self.assertEqual(retrieved_project["name"], project.name) self.assertEqual(retrieved_project["description"], project.description) @@ -289,16 +298,15 @@ def test_multiple_project(self): retrieved_project = content_json[1] expected_checks_url = "http://testserver" + reverse( - "check-detail", args=[str(checks.id)] - ) + "check-detail", args=[str(checks.id)] + ) expected_course_url = "http://testserver" + reverse( - "course-detail", args=[str(course.id)] - ) + "course-detail", args=[str(course.id)] + ) self.assertEqual(retrieved_project["name"], project2.name) - self.assertEqual( - retrieved_project["description"], project2.description) + self.assertEqual(retrieved_project["description"], project2.description) self.assertEqual(retrieved_project["visible"], project2.visible) self.assertEqual(retrieved_project["archived"], project2.archived) self.assertEqual(retrieved_project["checks"], expected_checks_url) @@ -309,11 +317,7 @@ def test_project_course(self): Able to retrieve a course of a project after creating it. """ - course = create_course( - id=3, - name="test course", - academic_startyear=2024 - ) + course = create_course(id=3, name="test course", academic_startyear=2024) checks = create_checks() project = create_project( name="test project", @@ -322,8 +326,8 @@ def test_project_course(self): archived=False, days=7, checks=checks, - course=course - ) + course=course, + ) response = self.client.get(reverse("project-list"), follow=True) @@ -337,8 +341,8 @@ def test_project_course(self): retrieved_project = content_json[0] expected_checks_url = "http://testserver" + reverse( - "check-detail", args=[str(checks.id)] - ) + "check-detail", args=[str(checks.id)] + ) self.assertEqual(retrieved_project["name"], project.name) self.assertEqual(retrieved_project["description"], project.description) @@ -358,8 +362,7 @@ def test_project_course(self): content_json = json.loads(response.content.decode("utf-8")) self.assertEqual(content_json["name"], course.name) - self.assertEqual( - content_json["academic_startyear"], course.academic_startyear) + self.assertEqual(content_json["academic_startyear"], course.academic_startyear) self.assertEqual(content_json["description"], course.description) def test_project_checks(self): @@ -367,11 +370,7 @@ def test_project_checks(self): Able to retrieve a check of a project after creating it. """ - course = create_course( - id=3, - name="test course", - academic_startyear=2024 - ) + course = create_course(id=3, name="test course", academic_startyear=2024) fileExtension1 = create_fileExtension(id=1, extension="jpg") fileExtension2 = create_fileExtension(id=2, extension="png") fileExtension3 = create_fileExtension(id=3, extension="tar") @@ -379,8 +378,8 @@ def test_project_checks(self): checks = create_checks( id=5, allowed_file_extensions=[fileExtension1, fileExtension4], - forbidden_file_extensions=[fileExtension2, fileExtension3] - ) + forbidden_file_extensions=[fileExtension2, fileExtension3], + ) project = create_project( name="test project", description="test description", @@ -388,8 +387,8 @@ def test_project_checks(self): archived=False, days=7, checks=checks, - course=course - ) + course=course, + ) response = self.client.get(reverse("project-list"), follow=True) @@ -403,8 +402,8 @@ def test_project_checks(self): retrieved_project = content_json[0] expected_course_url = "http://testserver" + reverse( - "course-detail", args=[str(course.id)] - ) + "course-detail", args=[str(course.id)] + ) self.assertEqual(retrieved_project["name"], project.name) self.assertEqual(retrieved_project["description"], project.description) @@ -427,23 +426,23 @@ def test_project_checks(self): # Assert the file extensions of the retrieved # Checks match the created file extensions - retrieved_allowed_file_extensions = content_json[ - "allowed_file_extensions"] + retrieved_allowed_file_extensions = content_json["allowed_file_extensions"] self.assertEqual(len(retrieved_allowed_file_extensions), 2) self.assertEqual( - retrieved_allowed_file_extensions[0]["extension"], - fileExtension1.extension) + retrieved_allowed_file_extensions[0]["extension"], fileExtension1.extension + ) self.assertEqual( - retrieved_allowed_file_extensions[1]["extension"], - fileExtension4.extension) + retrieved_allowed_file_extensions[1]["extension"], fileExtension4.extension + ) - retrieved_forbidden_file_extensions = content_json[ - "forbidden_file_extensions"] + retrieved_forbidden_file_extensions = content_json["forbidden_file_extensions"] self.assertEqual(len(retrieved_forbidden_file_extensions), 2) self.assertEqual( retrieved_forbidden_file_extensions[0]["extension"], - fileExtension2.extension) + fileExtension2.extension, + ) self.assertEqual( retrieved_forbidden_file_extensions[1]["extension"], - fileExtension3.extension) + fileExtension3.extension, + ) diff --git a/backend/api/tests/test_student.py b/backend/api/tests/test_student.py index df03625d..c43a89e7 100644 --- a/backend/api/tests/test_student.py +++ b/backend/api/tests/test_student.py @@ -8,8 +8,7 @@ from authentication.models import Faculty -def create_course(name, academic_startyear, description=None, - parent_course=None): +def create_course(name, academic_startyear, description=None, parent_course=None): """ Create a Course with the given arguments. """ @@ -17,37 +16,28 @@ def create_course(name, academic_startyear, description=None, name=name, academic_startyear=academic_startyear, description=description, - parent_course=parent_course + parent_course=parent_course, ) def create_faculty(name): """Create a Faculty with the given arguments.""" - return Faculty.objects.create( - name=name - ) + return Faculty.objects.create(name=name) -def create_student( - id, - first_name, - last_name, - email, - faculty=None, - courses=None - ): +def create_student(id, first_name, last_name, email, faculty=None, courses=None): """ Create a student with the given arguments. """ username = f"{first_name}_{last_name}" student = Student.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now() - ) + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) if faculty is not None: for fac in faculty: @@ -80,10 +70,7 @@ def test_student_exists(self): Able to retrieve a single student after creating it. """ student = create_student( - id=3, - first_name="John", - last_name="Doe", - email="john.doe@example.com" + id=3, first_name="John", last_name="Doe", email="john.doe@example.com" ) # Make a GET request to retrieve the student @@ -104,8 +91,7 @@ def test_student_exists(self): # Assert the details of the retrieved student match the created student retrieved_student = content_json[0] self.assertEqual(int(retrieved_student["id"]), student.id) - self.assertEqual( - retrieved_student["first_name"], student.first_name) + self.assertEqual(retrieved_student["first_name"], student.first_name) self.assertEqual(retrieved_student["last_name"], student.last_name) self.assertEqual(retrieved_student["email"], student.email) @@ -115,17 +101,11 @@ def test_multiple_students(self): """ # Create multiple assistant student1 = create_student( - id=1, - first_name="Johny", - last_name="Doeg", - email="john.doe@example.com" - ) + id=1, first_name="Johny", last_name="Doeg", email="john.doe@example.com" + ) student2 = create_student( - id=2, - first_name="Jane", - last_name="Doe", - email="jane.doe@example.com" - ) + id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + ) # Make a GET request to retrieve the student response = self.client.get(reverse("student-list"), follow=True) @@ -146,17 +126,13 @@ def test_multiple_students(self): # match the created students retrieved_student1, retrieved_student2 = content_json self.assertEqual(int(retrieved_student1["id"]), student1.id) - self.assertEqual( - retrieved_student1["first_name"], student1.first_name) - self.assertEqual( - retrieved_student1["last_name"], student1.last_name) + self.assertEqual(retrieved_student1["first_name"], student1.first_name) + self.assertEqual(retrieved_student1["last_name"], student1.last_name) self.assertEqual(retrieved_student1["email"], student1.email) self.assertEqual(int(retrieved_student2["id"]), student2.id) - self.assertEqual( - retrieved_student2["first_name"], student2.first_name) - self.assertEqual( - retrieved_student2["last_name"], student2.last_name) + self.assertEqual(retrieved_student2["first_name"], student2.first_name) + self.assertEqual(retrieved_student2["last_name"], student2.last_name) self.assertEqual(retrieved_student2["email"], student2.email) def test_student_detail_view(self): @@ -165,15 +141,13 @@ def test_student_detail_view(self): """ # Create an student for testing with the name "Bob Peeters" student = create_student( - id=5, - first_name="Bob", - last_name="Peeters", - email="bob.peeters@example.com" - ) + id=5, first_name="Bob", last_name="Peeters", email="bob.peeters@example.com" + ) # Make a GET request to retrieve the student details response = self.client.get( - reverse("student-detail", args=[str(student.id)]), follow=True) + reverse("student-detail", args=[str(student.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -201,12 +175,13 @@ def test_student_faculty(self): first_name="Bob", last_name="Peeters", email="bob.peeters@example.com", - faculty=[faculty] - ) + faculty=[faculty], + ) # Make a GET request to retrieve the student details response = self.client.get( - reverse("student-detail", args=[str(student.id)]), follow=True) + reverse("student-detail", args=[str(student.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -244,12 +219,12 @@ def test_student_courses(self): course1 = create_course( name="Introduction to Computer Science", academic_startyear=2022, - description="An introductory course on computer science." + description="An introductory course on computer science.", ) course2 = create_course( name="Intermediate to Computer Science", academic_startyear=2023, - description="An second course on computer science." + description="An second course on computer science.", ) student = create_student( @@ -257,12 +232,13 @@ def test_student_courses(self): first_name="Bob", last_name="Peeters", email="bob.peeters@example.com", - courses=[course1, course2] - ) + courses=[course1, course2], + ) # Make a GET request to retrieve the student details response = self.client.get( - reverse("student-detail", args=[str(student.id)]), follow=True) + reverse("student-detail", args=[str(student.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -297,13 +273,11 @@ def test_student_courses(self): content = content_json[0] self.assertEqual(int(content["id"]), course1.id) self.assertEqual(content["name"], course1.name) - self.assertEqual( - int(content["academic_startyear"]), course1.academic_startyear) + self.assertEqual(int(content["academic_startyear"]), course1.academic_startyear) self.assertEqual(content["description"], course1.description) content = content_json[1] self.assertEqual(int(content["id"]), course2.id) self.assertEqual(content["name"], course2.name) - self.assertEqual( - int(content["academic_startyear"]), course2.academic_startyear) + self.assertEqual(int(content["academic_startyear"]), course2.academic_startyear) self.assertEqual(content["description"], course2.description) diff --git a/backend/api/tests/test_submision.py b/backend/api/tests/test_submision.py index 119415ef..51b571ea 100644 --- a/backend/api/tests/test_submision.py +++ b/backend/api/tests/test_submision.py @@ -9,8 +9,7 @@ from ..models.course import Course -def create_course(name, academic_startyear, description=None, - parent_course=None): +def create_course(name, academic_startyear, description=None, parent_course=None): """ Create a Course with the given arguments. """ @@ -18,7 +17,7 @@ def create_course(name, academic_startyear, description=None, name=name, academic_startyear=academic_startyear, description=description, - parent_course=parent_course + parent_course=parent_course, ) @@ -26,10 +25,7 @@ def create_project(name, description, days, course): """Create a Project with the given arguments.""" deadline = timezone.now() + timedelta(days=days) return Project.objects.create( - name=name, - description=description, - deadline=deadline, - course=course + name=name, description=description, deadline=deadline, course=course ) @@ -41,18 +37,13 @@ def create_group(project, score): def create_submission(group, submission_number): """Create an Submission with the given arguments.""" return Submission.objects.create( - group=group, - submission_number=submission_number, - submission_time=timezone.now() + group=group, submission_number=submission_number, submission_time=timezone.now() ) def create_submissionFile(submission, file): """Create an SubmissionFile with the given arguments.""" - return SubmissionFile.objects.create( - submission=submission, - file=file - ) + return SubmissionFile.objects.create(submission=submission, file=file) class SubmissionModelTests(TestCase): @@ -61,8 +52,7 @@ def test_no_submission(self): able to retrieve no submission before publishing it. """ - response_root = self.client.get( - reverse("submission-list"), follow=True) + response_root = self.client.get(reverse("submission-list"), follow=True) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") @@ -75,20 +65,12 @@ def test_submission_exists(self): """ Able to retrieve a single submission after creating it. """ - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", - description="Description 1", - days=7, - course=course - ) - group = create_group(project=project, score=10) - submission = create_submission( - group=group, submission_number=1 + name="Project 1", description="Description 1", days=7, course=course ) + group = create_group(project=project, score=10) + submission = create_submission(group=group, submission_number=1) # Make a GET request to retrieve the submission response = self.client.get(reverse("submission-list"), follow=True) @@ -109,37 +91,26 @@ def test_submission_exists(self): # match the created submission retrieved_submission = content_json[0] expected_group_url = "http://testserver" + reverse( - "group-detail", args=[str(group.id)] - ) + "group-detail", args=[str(group.id)] + ) self.assertEqual(int(retrieved_submission["id"]), submission.id) self.assertEqual( - int(retrieved_submission["submission_number"]), - submission.submission_number - ) + int(retrieved_submission["submission_number"]), submission.submission_number + ) self.assertEqual(retrieved_submission["group"], expected_group_url) def test_multiple_submission_exists(self): """ Able to retrieve multiple submissions after creating them. """ - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", - description="Description 1", - days=7, - course=course - ) - group = create_group(project=project, score=10) - submission1 = create_submission( - group=group, submission_number=1 + name="Project 1", description="Description 1", days=7, course=course ) + group = create_group(project=project, score=10) + submission1 = create_submission(group=group, submission_number=1) - submission2 = create_submission( - group=group, submission_number=2 - ) + submission2 = create_submission(group=group, submission_number=2) # Make a GET request to retrieve the submission response = self.client.get(reverse("submission-list"), follow=True) @@ -160,49 +131,41 @@ def test_multiple_submission_exists(self): # match the created submission retrieved_submission = content_json[0] expected_group_url = "http://testserver" + reverse( - "group-detail", args=[str(group.id)] - ) + "group-detail", args=[str(group.id)] + ) self.assertEqual(int(retrieved_submission["id"]), submission1.id) self.assertEqual( int(retrieved_submission["submission_number"]), - submission1.submission_number - ) + submission1.submission_number, + ) self.assertEqual(retrieved_submission["group"], expected_group_url) retrieved_submission = content_json[1] expected_group_url = "http://testserver" + reverse( - "group-detail", args=[str(group.id)] - ) + "group-detail", args=[str(group.id)] + ) self.assertEqual(int(retrieved_submission["id"]), submission2.id) self.assertEqual( int(retrieved_submission["submission_number"]), - submission2.submission_number - ) + submission2.submission_number, + ) self.assertEqual(retrieved_submission["group"], expected_group_url) def test_submission_detail_view(self): """ Able to retrieve details of a single submission. """ - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", - description="Description 1", - days=7, - course=course - ) - group = create_group(project=project, score=10) - submission = create_submission( - group=group, submission_number=1 + name="Project 1", description="Description 1", days=7, course=course ) + group = create_group(project=project, score=10) + submission = create_submission(group=group, submission_number=1) # Make a GET request to retrieve the submission response = self.client.get( - reverse("submission-detail", args=[str(submission.id)]), - follow=True) + reverse("submission-detail", args=[str(submission.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -217,38 +180,29 @@ def test_submission_detail_view(self): # match the created submission retrieved_submission = content_json expected_group_url = "http://testserver" + reverse( - "group-detail", args=[str(group.id)] - ) + "group-detail", args=[str(group.id)] + ) self.assertEqual(int(retrieved_submission["id"]), submission.id) self.assertEqual( - int(retrieved_submission["submission_number"]), - submission.submission_number - ) + int(retrieved_submission["submission_number"]), submission.submission_number + ) self.assertEqual(retrieved_submission["group"], expected_group_url) def test_submission_group(self): """ Able to retrieve group of a single submission. """ - course = create_course( - name="sel2", - academic_startyear=2023 - ) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", - description="Description 1", - days=7, - course=course - ) - group = create_group(project=project, score=10) - submission = create_submission( - group=group, submission_number=1 + name="Project 1", description="Description 1", days=7, course=course ) + group = create_group(project=project, score=10) + submission = create_submission(group=group, submission_number=1) # Make a GET request to retrieve the submission response = self.client.get( - reverse("submission-detail", args=[str(submission.id)]), - follow=True) + reverse("submission-detail", args=[str(submission.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -264,9 +218,8 @@ def test_submission_group(self): retrieved_submission = content_json self.assertEqual(int(retrieved_submission["id"]), submission.id) self.assertEqual( - int(retrieved_submission["submission_number"]), - submission.submission_number - ) + int(retrieved_submission["submission_number"]), submission.submission_number + ) response = self.client.get(content_json["group"], follow=True) @@ -280,7 +233,8 @@ def test_submission_group(self): content_json = json.loads(response.content.decode("utf-8")) expected_project_url = "http://testserver" + reverse( - "project-detail", args=[str(project.id)]) + "project-detail", args=[str(project.id)] + ) self.assertEqual(int(content_json["id"]), group.id) self.assertEqual(content_json["project"], expected_project_url) diff --git a/backend/api/tests/test_teacher.py b/backend/api/tests/test_teacher.py index 2e3d8c6e..dc07da70 100644 --- a/backend/api/tests/test_teacher.py +++ b/backend/api/tests/test_teacher.py @@ -8,8 +8,7 @@ from authentication.models import Faculty -def create_course(name, academic_startyear, description=None, - parent_course=None): +def create_course(name, academic_startyear, description=None, parent_course=None): """ Create a Course with the given arguments. """ @@ -17,37 +16,28 @@ def create_course(name, academic_startyear, description=None, name=name, academic_startyear=academic_startyear, description=description, - parent_course=parent_course + parent_course=parent_course, ) def create_faculty(name): """Create a Faculty with the given arguments.""" - return Faculty.objects.create( - name=name - ) + return Faculty.objects.create(name=name) -def create_teacher( - id, - first_name, - last_name, - email, - faculty=None, - courses=None - ): +def create_teacher(id, first_name, last_name, email, faculty=None, courses=None): """ Create a teacher with the given arguments. """ username = f"{first_name}_{last_name}" teacher = Teacher.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now() - ) + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) if faculty is not None: for fac in faculty: @@ -80,10 +70,7 @@ def test_teacher_exists(self): Able to retrieve a single teacher after creating it. """ teacher = create_teacher( - id=3, - first_name="John", - last_name="Doe", - email="john.doe@example.com" + id=3, first_name="John", last_name="Doe", email="john.doe@example.com" ) # Make a GET request to retrieve the teacher @@ -104,8 +91,7 @@ def test_teacher_exists(self): # Assert the details of the retrieved teacher match the created teacher retrieved_teacher = content_json[0] self.assertEqual(int(retrieved_teacher["id"]), teacher.id) - self.assertEqual( - retrieved_teacher["first_name"], teacher.first_name) + self.assertEqual(retrieved_teacher["first_name"], teacher.first_name) self.assertEqual(retrieved_teacher["last_name"], teacher.last_name) self.assertEqual(retrieved_teacher["email"], teacher.email) @@ -115,17 +101,11 @@ def test_multiple_teachers(self): """ # Create multiple assistant teacher1 = create_teacher( - id=1, - first_name="Johny", - last_name="Doeg", - email="john.doe@example.com" - ) + id=1, first_name="Johny", last_name="Doeg", email="john.doe@example.com" + ) teacher2 = create_teacher( - id=2, - first_name="Jane", - last_name="Doe", - email="jane.doe@example.com" - ) + id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + ) # Make a GET request to retrieve the teacher response = self.client.get(reverse("teacher-list"), follow=True) @@ -145,17 +125,13 @@ def test_multiple_teachers(self): # Assert the details of the retrieved teacher match the created teacher retrieved_teacher1, retrieved_teacher2 = content_json self.assertEqual(int(retrieved_teacher1["id"]), teacher1.id) - self.assertEqual( - retrieved_teacher1["first_name"], teacher1.first_name) - self.assertEqual( - retrieved_teacher1["last_name"], teacher1.last_name) + self.assertEqual(retrieved_teacher1["first_name"], teacher1.first_name) + self.assertEqual(retrieved_teacher1["last_name"], teacher1.last_name) self.assertEqual(retrieved_teacher1["email"], teacher1.email) self.assertEqual(int(retrieved_teacher2["id"]), teacher2.id) - self.assertEqual( - retrieved_teacher2["first_name"], teacher2.first_name) - self.assertEqual( - retrieved_teacher2["last_name"], teacher2.last_name) + self.assertEqual(retrieved_teacher2["first_name"], teacher2.first_name) + self.assertEqual(retrieved_teacher2["last_name"], teacher2.last_name) self.assertEqual(retrieved_teacher2["email"], teacher2.email) def test_teacher_detail_view(self): @@ -164,15 +140,13 @@ def test_teacher_detail_view(self): """ # Create an teacher for testing with the name "Bob Peeters" teacher = create_teacher( - id=5, - first_name="Bob", - last_name="Peeters", - email="bob.peeters@example.com" - ) + id=5, first_name="Bob", last_name="Peeters", email="bob.peeters@example.com" + ) # Make a GET request to retrieve the teacher details response = self.client.get( - reverse("teacher-detail", args=[str(teacher.id)]), follow=True) + reverse("teacher-detail", args=[str(teacher.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -200,12 +174,13 @@ def test_teacher_faculty(self): first_name="Bob", last_name="Peeters", email="bob.peeters@example.com", - faculty=[faculty] - ) + faculty=[faculty], + ) # Make a GET request to retrieve the teacher details response = self.client.get( - reverse("teacher-detail", args=[str(teacher.id)]), follow=True) + reverse("teacher-detail", args=[str(teacher.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -243,12 +218,12 @@ def test_teacher_courses(self): course1 = create_course( name="Introduction to Computer Science", academic_startyear=2022, - description="An introductory course on computer science." + description="An introductory course on computer science.", ) course2 = create_course( name="Intermediate to Computer Science", academic_startyear=2023, - description="An second course on computer science." + description="An second course on computer science.", ) teacher = create_teacher( @@ -256,12 +231,13 @@ def test_teacher_courses(self): first_name="Bob", last_name="Peeters", email="bob.peeters@example.com", - courses=[course1, course2] - ) + courses=[course1, course2], + ) # Make a GET request to retrieve the teacher details response = self.client.get( - reverse("teacher-detail", args=[str(teacher.id)]), follow=True) + reverse("teacher-detail", args=[str(teacher.id)]), follow=True + ) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -296,13 +272,11 @@ def test_teacher_courses(self): content = content_json[0] self.assertEqual(int(content["id"]), course1.id) self.assertEqual(content["name"], course1.name) - self.assertEqual( - int(content["academic_startyear"]), course1.academic_startyear) + self.assertEqual(int(content["academic_startyear"]), course1.academic_startyear) self.assertEqual(content["description"], course1.description) content = content_json[1] self.assertEqual(int(content["id"]), course2.id) self.assertEqual(content["name"], course2.name) - self.assertEqual( - int(content["academic_startyear"]), course2.academic_startyear) + self.assertEqual(int(content["academic_startyear"]), course2.academic_startyear) self.assertEqual(content["description"], course2.description) diff --git a/backend/api/urls.py b/backend/api/urls.py index c95373cc..450301ca 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -12,51 +12,20 @@ from rest_framework.routers import DefaultRouter router = DefaultRouter() -router.register( - r'teachers', - teacher_view.TeacherViewSet, - basename='teacher') -router.register( - r'admins', - admin_view.AdminViewSet, - basename='admin') -router.register( - r'assistants', - assistant_view.AssistantViewSet, - basename='assistant') -router.register( - r'students', - student_view.StudentViewSet, - basename='student') -router.register( - r'projects', - project_view.ProjectViewSet, - basename='project') -router.register( - r'groups', - group_view.GroupViewSet, - basename='group') -router.register( - r'courses', - course_view.CourseViewSet, - basename='course') -router.register( - r'submissions', - submision_view.SubmissionViewSet, - basename='submission') -router.register( - r'checks', - checks_view.ChecksViewSet, - basename='check') -router.register( - r'fileExtensions', - checks_view.FileExtensionViewSet, - basename='fileExtension') -router.register( - r'faculties', - faculty_view.facultyViewSet, - basename='faculty') +router.register(r"teachers", teacher_view.TeacherViewSet, basename="teacher") +router.register(r"admins", admin_view.AdminViewSet, basename="admin") +router.register(r"assistants", assistant_view.AssistantViewSet, basename="assistant") +router.register(r"students", student_view.StudentViewSet, basename="student") +router.register(r"projects", project_view.ProjectViewSet, basename="project") +router.register(r"groups", group_view.GroupViewSet, basename="group") +router.register(r"courses", course_view.CourseViewSet, basename="course") +router.register(r"submissions", submision_view.SubmissionViewSet, basename="submission") +router.register(r"checks", checks_view.ChecksViewSet, basename="check") +router.register( + r"fileExtensions", checks_view.FileExtensionViewSet, basename="fileExtension" +) +router.register(r"faculties", faculty_view.facultyViewSet, basename="faculty") urlpatterns = [ - path('', include(router.urls)), + path("", include(router.urls)), ] diff --git a/backend/api/views/assistant_view.py b/backend/api/views/assistant_view.py index 3d7d8ba8..ea75fc8b 100644 --- a/backend/api/views/assistant_view.py +++ b/backend/api/views/assistant_view.py @@ -10,7 +10,7 @@ class AssistantViewSet(viewsets.ModelViewSet): queryset = Assistant.objects.all() serializer_class = AssistantSerializer - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def courses(self, request, pk=None): """Returns a list of courses for the given assistant""" @@ -20,11 +20,13 @@ def courses(self, request, pk=None): # Serialize the course objects serializer = CourseSerializer( - courses, many=True, context={'request': request} + courses, many=True, context={"request": request} ) return Response(serializer.data) except Assistant.DoesNotExist: # Invalid assistant ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Assistant not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, + data={"message": "Assistant not found"}, + ) diff --git a/backend/api/views/checks_view.py b/backend/api/views/checks_view.py index 9d136f23..654eb1f1 100644 --- a/backend/api/views/checks_view.py +++ b/backend/api/views/checks_view.py @@ -1,8 +1,6 @@ from rest_framework import viewsets from ..models.checks import Checks, FileExtension -from ..serializers.checks_serializer import ( - ChecksSerializer, FileExtensionSerializer -) +from ..serializers.checks_serializer import ChecksSerializer, FileExtensionSerializer class ChecksViewSet(viewsets.ModelViewSet): diff --git a/backend/api/views/course_view.py b/backend/api/views/course_view.py index 85012452..54b1fcf2 100644 --- a/backend/api/views/course_view.py +++ b/backend/api/views/course_view.py @@ -13,7 +13,7 @@ class CourseViewSet(viewsets.ModelViewSet): queryset = Course.objects.all() serializer_class = CourseSerializer - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def teachers(self, request, pk=None): """Returns a list of teachers for the given course""" @@ -23,16 +23,17 @@ def teachers(self, request, pk=None): # Serialize the teacher objects serializer = TeacherSerializer( - teachers, many=True, context={'request': request} + teachers, many=True, context={"request": request} ) return Response(serializer.data) except Course.DoesNotExist: # Invalid course ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Course not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"} + ) - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def assistants(self, request, pk=None): """Returns a list of assistants for the given course""" @@ -42,16 +43,17 @@ def assistants(self, request, pk=None): # Serialize the assistant objects serializer = AssistantSerializer( - assistants, many=True, context={'request': request} + assistants, many=True, context={"request": request} ) return Response(serializer.data) except Course.DoesNotExist: # Invalid course ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Course not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"} + ) - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def students(self, request, pk=None): """Returns a list of students for the given course""" @@ -61,16 +63,17 @@ def students(self, request, pk=None): # Serialize the student objects serializer = StudentSerializer( - students, many=True, context={'request': request} + students, many=True, context={"request": request} ) return Response(serializer.data) except Course.DoesNotExist: # Invalid course ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Course not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"} + ) - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def projects(self, request, pk=None): """Returns a list of projects for the given course""" @@ -80,11 +83,12 @@ def projects(self, request, pk=None): # Serialize the project objects serializer = ProjectSerializer( - projects, many=True, context={'request': request} + projects, many=True, context={"request": request} ) return Response(serializer.data) except Course.DoesNotExist: # Invalid course ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Course not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"} + ) diff --git a/backend/api/views/group_view.py b/backend/api/views/group_view.py index 809fb893..0402f198 100644 --- a/backend/api/views/group_view.py +++ b/backend/api/views/group_view.py @@ -10,7 +10,7 @@ class GroupViewSet(viewsets.ModelViewSet): queryset = Group.objects.all() serializer_class = GroupSerializer - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def students(self, request, pk=None): """Returns a list of students for the given group""" @@ -20,11 +20,12 @@ def students(self, request, pk=None): # Serialize the student objects serializer = StudentSerializer( - students, many=True, context={'request': request} + students, many=True, context={"request": request} ) return Response(serializer.data) except Group.DoesNotExist: # Invalid group ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Group not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Group not found"} + ) diff --git a/backend/api/views/student_view.py b/backend/api/views/student_view.py index b5f87e82..4fe6f92c 100644 --- a/backend/api/views/student_view.py +++ b/backend/api/views/student_view.py @@ -11,7 +11,7 @@ class StudentViewSet(viewsets.ModelViewSet): queryset = Student.objects.all() serializer_class = StudentSerializer - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def courses(self, request, pk=None): """Returns a list of courses for the given student""" @@ -21,16 +21,17 @@ def courses(self, request, pk=None): # Serialize the course objects serializer = CourseSerializer( - courses, many=True, context={'request': request} + courses, many=True, context={"request": request} ) return Response(serializer.data) except Student.DoesNotExist: # Invalid student ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Student not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Student not found"} + ) - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def groups(self, request, pk=None): """Returns a list of groups for the given student""" @@ -40,11 +41,12 @@ def groups(self, request, pk=None): # Serialize the group objects serializer = GroupSerializer( - groups, many=True, context={'request': request} + groups, many=True, context={"request": request} ) return Response(serializer.data) except Student.DoesNotExist: # Invalid student ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Student not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Student not found"} + ) diff --git a/backend/api/views/submision_view.py b/backend/api/views/submision_view.py index 72c95e45..8e0de7ad 100644 --- a/backend/api/views/submision_view.py +++ b/backend/api/views/submision_view.py @@ -1,7 +1,8 @@ from rest_framework import viewsets from ..models.submission import Submission, SubmissionFile from ..serializers.submision_serializer import ( - SubmissionSerializer, SubmissionFileSerializer + SubmissionSerializer, + SubmissionFileSerializer, ) diff --git a/backend/api/views/teacher_view.py b/backend/api/views/teacher_view.py index 9f26361b..49038133 100644 --- a/backend/api/views/teacher_view.py +++ b/backend/api/views/teacher_view.py @@ -10,7 +10,7 @@ class TeacherViewSet(viewsets.ModelViewSet): queryset = Teacher.objects.all() serializer_class = TeacherSerializer - @action(detail=True, methods=['get']) + @action(detail=True, methods=["get"]) def courses(self, request, pk=None): """Returns a list of courses for the given teacher""" @@ -20,11 +20,12 @@ def courses(self, request, pk=None): # Serialize the course objects serializer = CourseSerializer( - courses, many=True, context={'request': request} + courses, many=True, context={"request": request} ) return Response(serializer.data) except Teacher.DoesNotExist: # Invalid teacher ID - return Response(status=status.HTTP_404_NOT_FOUND, - data={"message": "Teacher not found"}) + return Response( + status=status.HTTP_404_NOT_FOUND, data={"message": "Teacher not found"} + ) diff --git a/backend/authentication/apps.py b/backend/authentication/apps.py index 8bab8df0..c65f1d28 100644 --- a/backend/authentication/apps.py +++ b/backend/authentication/apps.py @@ -2,5 +2,5 @@ class AuthenticationConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'authentication' + default_auto_field = "django.db.models.BigAutoField" + name = "authentication" diff --git a/backend/authentication/cas/client.py b/backend/authentication/cas/client.py index e9133f75..8388c978 100644 --- a/backend/authentication/cas/client.py +++ b/backend/authentication/cas/client.py @@ -2,7 +2,5 @@ from ypovoli import settings client = CASClient( - server_url=settings.CAS_ENDPOINT, - service_url=settings.CAS_RESPONSE, - auth_prefix='' + server_url=settings.CAS_ENDPOINT, service_url=settings.CAS_RESPONSE, auth_prefix="" ) diff --git a/backend/authentication/migrations/0001_initial.py b/backend/authentication/migrations/0001_initial.py index a86e2591..8895c9c4 100644 --- a/backend/authentication/migrations/0001_initial.py +++ b/backend/authentication/migrations/0001_initial.py @@ -5,39 +5,55 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('id', models.CharField(max_length=12, primary_key=True, serialize=False)), - ('username', models.CharField(max_length=12, unique=True)), - ('email', models.EmailField(max_length=254, unique=True)), - ('first_name', models.CharField(max_length=50)), - ('last_name', models.CharField(max_length=50)), - ('last_enrolled', models.IntegerField(default=1, null=True)), - ('create_time', models.DateTimeField(auto_now=True)), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "id", + models.CharField(max_length=12, primary_key=True, serialize=False), + ), + ("username", models.CharField(max_length=12, unique=True)), + ("email", models.EmailField(max_length=254, unique=True)), + ("first_name", models.CharField(max_length=50)), + ("last_name", models.CharField(max_length=50)), + ("last_enrolled", models.IntegerField(default=1, null=True)), + ("create_time", models.DateTimeField(auto_now=True)), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='Faculty', + name="Faculty", fields=[ - ('name', models.CharField(max_length=50, primary_key=True, serialize=False)), - ('user', models.ManyToManyField(blank=True, related_name='users', to=settings.AUTH_USER_MODEL)), + ( + "name", + models.CharField(max_length=50, primary_key=True, serialize=False), + ), + ( + "user", + models.ManyToManyField( + blank=True, related_name="users", to=settings.AUTH_USER_MODEL + ), + ), ], ), migrations.AddField( - model_name='user', - name='faculty', - field=models.ManyToManyField(blank=True, related_name='faculties', to='authentication.faculty'), + model_name="user", + name="faculty", + field=models.ManyToManyField( + blank=True, related_name="faculties", to="authentication.faculty" + ), ), ] diff --git a/backend/authentication/migrations/0002_remove_faculty_user_remove_user_faculty_and_more.py b/backend/authentication/migrations/0002_remove_faculty_user_remove_user_faculty_and_more.py index 42ce1147..6323b90c 100644 --- a/backend/authentication/migrations/0002_remove_faculty_user_remove_user_faculty_and_more.py +++ b/backend/authentication/migrations/0002_remove_faculty_user_remove_user_faculty_and_more.py @@ -4,23 +4,24 @@ class Migration(migrations.Migration): - dependencies = [ - ('authentication', '0001_initial'), + ("authentication", "0001_initial"), ] operations = [ migrations.RemoveField( - model_name='faculty', - name='user', + model_name="faculty", + name="user", ), migrations.RemoveField( - model_name='user', - name='faculty', + model_name="user", + name="faculty", ), migrations.AddField( - model_name='user', - name='faculties', - field=models.ManyToManyField(blank=True, related_name='users', to='authentication.faculty'), + model_name="user", + name="faculties", + field=models.ManyToManyField( + blank=True, related_name="users", to="authentication.faculty" + ), ), ] diff --git a/backend/authentication/migrations/0003_alter_user_create_time.py b/backend/authentication/migrations/0003_alter_user_create_time.py new file mode 100644 index 00000000..f65d50cf --- /dev/null +++ b/backend/authentication/migrations/0003_alter_user_create_time.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2024-03-04 15:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("authentication", "0002_remove_faculty_user_remove_user_faculty_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="create_time", + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 0bf42d22..a9cce277 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -4,53 +4,30 @@ from django.db.models import CharField, EmailField, IntegerField, DateTimeField from django.contrib.auth.models import AbstractBaseUser + class User(AbstractBaseUser): """This model represents a single authenticatable user. It extends the built-in Django user model with CAS-specific attributes. """ """Model fields""" - password = None # We don't use passwords for our user model. - - id = CharField( - max_length=12, - primary_key=True - ) - - username = CharField( - max_length=12, - unique=True - ) - - email = EmailField( - null=False, - unique=True - ) - - first_name = CharField( - max_length=50, - null=False - ) - - last_name = CharField( - max_length=50, - null=False - ) - - faculties = models.ManyToManyField( - 'Faculty', - related_name='users', - blank=True - ) - - last_enrolled = IntegerField( - default=datetime.MINYEAR, - null=True - ) - - create_time = DateTimeField( - auto_now_add=True - ) + password = None # We don't use passwords for our user model. + + id = CharField(max_length=12, primary_key=True) + + username = CharField(max_length=12, unique=True) + + email = EmailField(null=False, unique=True) + + first_name = CharField(max_length=50, null=False) + + last_name = CharField(max_length=50, null=False) + + faculties = models.ManyToManyField("Faculty", related_name="users", blank=True) + + last_enrolled = IntegerField(default=datetime.MINYEAR, null=True) + + create_time = DateTimeField(auto_now_add=True) """Model settings""" USERNAME_FIELD = "username" @@ -61,7 +38,4 @@ class Faculty(models.Model): """This model represents a faculty.""" """Model fields""" - name = CharField( - max_length=50, - primary_key=True - ) + name = CharField(max_length=50, primary_key=True) diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index cd1d2f97..1ec9b9a0 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -1,7 +1,12 @@ from django.contrib.auth.models import update_last_login from rest_framework.serializers import ( - CharField, EmailField, ModelSerializer, ValidationError, - Serializer, HyperlinkedIdentityField, HyperlinkedRelatedField + CharField, + EmailField, + ModelSerializer, + ValidationError, + Serializer, + HyperlinkedIdentityField, + HyperlinkedRelatedField, ) from rest_framework_simplejwt.tokens import RefreshToken, AccessToken from rest_framework_simplejwt.settings import api_settings @@ -9,87 +14,82 @@ from authentication.models import User from authentication.cas.client import client + class CASTokenObtainSerializer(Serializer): """Serializer for CAS ticket validation This serializer takes the CAS ticket and tries to validate it. Upon successful validation, create a new user if it doesn't exist. """ + token = RefreshToken ticket = CharField(required=True, min_length=49, max_length=49) def validate(self, data): """Validate a ticket using CAS client""" - response = client.perform_service_validate( - ticket=data['ticket'] - ) + response = client.perform_service_validate(ticket=data["ticket"]) if response.error: raise ValidationError(response.error) # Validation success: create user if it doesn't exist yet. - attributes = response.data.get('attributes', dict) - - if attributes.get('lastenrolled'): - attributes['lastenrolled'] = int(attributes.get('lastenrolled').split()[0]) - - user = UserSerializer(data={ - 'id': attributes.get('ugentID'), - 'username': attributes.get('uid'), - 'email': attributes.get('mail'), - 'first_name': attributes.get('givenname'), - 'last_name': attributes.get('surname'), - 'last_enrolled': attributes.get('lastenrolled') - }) + attributes = response.data.get("attributes", dict) + + if attributes.get("lastenrolled"): + attributes["lastenrolled"] = int(attributes.get("lastenrolled").split()[0]) + + user = UserSerializer( + data={ + "id": attributes.get("ugentID"), + "username": attributes.get("uid"), + "email": attributes.get("mail"), + "first_name": attributes.get("givenname"), + "last_name": attributes.get("surname"), + "last_enrolled": attributes.get("lastenrolled"), + } + ) if not user.is_valid(): raise ValidationError(user.errors) - user, created = user.get_or_create( - user.validated_data - ) + user, created = user.get_or_create(user.validated_data) # Update the user's last login. if api_settings.UPDATE_LAST_LOGIN: update_last_login(self, user) - user_login.send(sender=self, - user=user - ) + user_login.send(sender=self, user=user) # Send signal upon creation. if created: - user_created.send(sender=self, - attributes=attributes, - user=user - ) + user_created.send(sender=self, attributes=attributes, user=user) return { - 'access': str(AccessToken.for_user(user)), - 'refresh': str(RefreshToken.for_user(user)) + "access": str(AccessToken.for_user(user)), + "refresh": str(RefreshToken.for_user(user)), } + class UserSerializer(ModelSerializer): """Serializer for the user model This serializer validates the user fields for creation and updating. """ + id = CharField() username = CharField() email = EmailField() faculties = HyperlinkedRelatedField( - many=True, - read_only=True, - view_name='faculty-detail' + many=True, read_only=True, view_name="faculty-detail" ) notifications = HyperlinkedIdentityField( - view_name='notification-detail', + view_name="notification-detail", read_only=True, ) class Meta: model = User - fields = '__all__' + fields = "__all__" def get_or_create(self, validated_data: dict) -> User: """Create or fetch the user based on the validated data.""" diff --git a/backend/authentication/services/users.py b/backend/authentication/services/users.py index 0b621fc5..dcafcdf7 100644 --- a/backend/authentication/services/users.py +++ b/backend/authentication/services/users.py @@ -1,54 +1,70 @@ from authentication.models import User + def exists(user_id: str) -> bool: """Check if a user exists""" - return User.objects.filter(id = user_id).exists() + return User.objects.filter(id=user_id).exists() + -def get_by_id(user_id: str) -> User|None: +def get_by_id(user_id: str) -> User | None: """Get a user by its user id""" return User.objects.filter(id=user_id).first() + def get_by_username(username: str) -> User: """Get a user by its username""" return User.objects.filter(username=username).first() + def create( - user_id: str, username: str, email: str, - first_name: str, last_name: str, + user_id: str, + username: str, + email: str, + first_name: str, + last_name: str, faculty: str = None, last_enrolled: str = None, - student_id: str = None + student_id: str = None, ) -> User: """Create a new user Note: this does not assign specific user classes. This should be handled by consumers of this package. """ return User.objects.create( - id = user_id, - student_id = student_id, - username = username, - email = email, - first_name = first_name, - last_name = last_name, - faculty = faculty, - last_enrolled = last_enrolled + id=user_id, + student_id=student_id, + username=username, + email=email, + first_name=first_name, + last_name=last_name, + faculty=faculty, + last_enrolled=last_enrolled, ) + def get_or_create( - user_id: str, username: str, email: str, - first_name: str, last_name: str, + user_id: str, + username: str, + email: str, + first_name: str, + last_name: str, faculty: str = None, last_enrolled: str = None, - student_id: str = None + student_id: str = None, ) -> User: """Get a user by ID, or create if it doesn't exist""" user = get_by_id(user_id) if user is None: return create( - user_id, username, email, - first_name, last_name, - faculty, last_enrolled, student_id + user_id, + username, + email, + first_name, + last_name, + faculty, + last_enrolled, + student_id, ) - return user \ No newline at end of file + return user diff --git a/backend/authentication/signals.py b/backend/authentication/signals.py index e584b6bf..40c89941 100644 --- a/backend/authentication/signals.py +++ b/backend/authentication/signals.py @@ -2,4 +2,4 @@ user_created = Signal() user_login = Signal() -user_logout = Signal() \ No newline at end of file +user_logout = Signal() diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py index 1e60698b..736ed2f0 100644 --- a/backend/authentication/tests/test_authentication_serializer.py +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -9,44 +9,49 @@ from authentication.signals import user_created, user_login -TICKET = 'ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas6' -WRONG_TICKET = 'ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas5' +TICKET = "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas6" +WRONG_TICKET = "ST-da8e1747f248a54a5f078e3905b88a9767f11d7aedcas5" -ID = '1234' -USERNAME = 'ddickwd' -EMAIL = 'dummy@dummy.be' -FIRST_NAME = 'Dummy' -LAST_NAME = 'McDickwad' +ID = "1234" +USERNAME = "ddickwd" +EMAIL = "dummy@dummy.be" +FIRST_NAME = "Dummy" +LAST_NAME = "McDickwad" class UserSerializerModelTests(TestCase): - def test_invalid_email_makes_user_serializer_invalid(self): """ The is_valid() method of a UserSerializer whose supplied User's email is not formatted as an email address should return False. """ - user = UserSerializer(data={ - 'id': ID, - 'username': USERNAME, - 'email': 'dummy', - 'first_name': FIRST_NAME, - 'last_name': LAST_NAME, - }) - user2 = UserSerializer(data={ - 'id': ID, - 'username': USERNAME, - 'email': 'dummy@dummy', - 'first_name': FIRST_NAME, - 'last_name': LAST_NAME, - }) - user3 = UserSerializer(data={ - 'id': ID, - 'username': USERNAME, - 'email': 21, - 'first_name': FIRST_NAME, - 'last_name': LAST_NAME, - }) + user = UserSerializer( + data={ + "id": ID, + "username": USERNAME, + "email": "dummy", + "first_name": FIRST_NAME, + "last_name": LAST_NAME, + } + ) + user2 = UserSerializer( + data={ + "id": ID, + "username": USERNAME, + "email": "dummy@dummy", + "first_name": FIRST_NAME, + "last_name": LAST_NAME, + } + ) + user3 = UserSerializer( + data={ + "id": ID, + "username": USERNAME, + "email": 21, + "first_name": FIRST_NAME, + "last_name": LAST_NAME, + } + ) self.assertFalse(user.is_valid()) self.assertFalse(user2.is_valid()) self.assertFalse(user3.is_valid()) @@ -56,43 +61,45 @@ def test_valid_email_makes_valid_serializer(self): When the serializer is provided with a valid email, the serializer becomes valid, thus the is_valid() method returns True. """ - user = UserSerializer(data={ - 'id': ID, - 'username': USERNAME, - 'email': EMAIL, - 'first_name': FIRST_NAME, - 'last_name': LAST_NAME, - }) + user = UserSerializer( + data={ + "id": ID, + "username": USERNAME, + "email": EMAIL, + "first_name": FIRST_NAME, + "last_name": LAST_NAME, + } + ) self.assertTrue(user.is_valid()) def customize_data(ugent_id, uid, mail): - class Response: - __slots__ = ('error', 'data') + __slots__ = ("error", "data") def __init__(self): self.error = None self.data = {} def service_validate( - ticket=None, - service_url=None, - headers=None,): + ticket=None, + service_url=None, + headers=None, + ): response = Response() if ticket != TICKET: - response.error = 'This is an error' + response.error = "This is an error" else: - response.data['attributes'] = { - 'ugentID': ugent_id, - 'uid': uid, - 'mail': mail, - 'givenname': FIRST_NAME, - 'surname': LAST_NAME, - 'faculty': 'Sciences', - 'lastenrolled': '2023 - 2024', - 'lastlogin': '', - 'createtime': '' + response.data["attributes"] = { + "ugentID": ugent_id, + "uid": uid, + "mail": mail, + "givenname": FIRST_NAME, + "surname": LAST_NAME, + "faculty": "Sciences", + "lastenrolled": "2023 - 2024", + "lastlogin": "", + "createtime": "", } return response @@ -105,43 +112,40 @@ def test_wrong_length_ticket_generates_error(self): When the provided ticket has the wrong length, a ValidationError should be raised when validating the serializer. """ - serializer = CASTokenObtainSerializer(data={ - 'token': RefreshToken(), - 'ticket': 'ST' - }) + serializer = CASTokenObtainSerializer( + data={"token": RefreshToken(), "ticket": "ST"} + ) self.assertFalse(serializer.is_valid()) - @patch.object(client, - 'perform_service_validate', - customize_data(ID, USERNAME, EMAIL)) + @patch.object( + client, "perform_service_validate", customize_data(ID, USERNAME, EMAIL) + ) def test_wrong_ticket_generates_error(self): """ When the wrong ticket is provided, a ValidationError should be raised when trying to validate the serializer. """ - serializer = CASTokenObtainSerializer(data={ - 'token': RefreshToken(), - 'ticket': WRONG_TICKET - }) + serializer = CASTokenObtainSerializer( + data={"token": RefreshToken(), "ticket": WRONG_TICKET} + ) self.assertFalse(serializer.is_valid()) - @patch.object(client, - 'perform_service_validate', - customize_data(ID, USERNAME, "dummy@dummy")) + @patch.object( + client, "perform_service_validate", customize_data(ID, USERNAME, "dummy@dummy") + ) def test_wrong_user_arguments_generate_error(self): """ If the user arguments returned by CAS are not valid, then a ValidationError should be raised when validating the serializer. """ - serializer = CASTokenObtainSerializer(data={ - 'token': RefreshToken(), - 'ticket': TICKET - }) + serializer = CASTokenObtainSerializer( + data={"token": RefreshToken(), "ticket": TICKET} + ) self.assertFalse(serializer.is_valid()) - @patch.object(client, - 'perform_service_validate', - customize_data(ID, USERNAME, EMAIL)) + @patch.object( + client, "perform_service_validate", customize_data(ID, USERNAME, EMAIL) + ) def test_new_user_activates_user_created_signal(self): """ If the authenticated user is new to the app, then the user_created signal should @@ -149,17 +153,16 @@ def test_new_user_activates_user_created_signal(self): mock = Mock() user_created.connect(mock, dispatch_uid="STDsAllAround") - serializer = CASTokenObtainSerializer(data={ - 'token': RefreshToken(), - 'ticket': TICKET - }) + serializer = CASTokenObtainSerializer( + data={"token": RefreshToken(), "ticket": TICKET} + ) # this next line triggers the retrieval of User information and logs in the user serializer.is_valid() self.assertEquals(mock.call_count, 1) - @patch.object(client, - 'perform_service_validate', - customize_data(ID, USERNAME, EMAIL)) + @patch.object( + client, "perform_service_validate", customize_data(ID, USERNAME, EMAIL) + ) def test_old_user_does_not_activate_user_created_signal(self): """ If the authenticated user is new to the app, then the user_created signal should @@ -167,17 +170,16 @@ def test_old_user_does_not_activate_user_created_signal(self): mock = Mock() user_created.connect(mock, dispatch_uid="STDsAllAround") - serializer = CASTokenObtainSerializer(data={ - 'token': RefreshToken(), - 'ticket': TICKET - }) + serializer = CASTokenObtainSerializer( + data={"token": RefreshToken(), "ticket": TICKET} + ) # this next line triggers the retrieval of User information and logs in the user serializer.is_valid() self.assertEquals(mock.call_count, 0) - @patch.object(client, - 'perform_service_validate', - customize_data(ID, USERNAME, EMAIL)) + @patch.object( + client, "perform_service_validate", customize_data(ID, USERNAME, EMAIL) + ) def test_login_signal(self): """ When the token is correct and all user data is correct, while trying to validate @@ -185,10 +187,9 @@ def test_login_signal(self): """ mock = Mock() user_login.connect(mock, dispatch_uid="STDsAllAround") - serializer = CASTokenObtainSerializer(data={ - 'token': RefreshToken(), - 'ticket': TICKET - }) + serializer = CASTokenObtainSerializer( + data={"token": RefreshToken(), "ticket": TICKET} + ) # this next line triggers the retrieval of User information and logs in the user serializer.is_valid() self.assertEquals(mock.call_count, 1) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 4c2cb756..ce1ad7e5 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -10,15 +10,15 @@ class TestWhomAmIView(APITestCase): def setUp(self): """Create a user and generate a token for that user""" user_data = { - 'id': '1234', - 'username': 'ddickwd', - 'email': 'dummy@dummy.com', - 'first_name': 'dummy', - 'last_name': 'McDickwad', + "id": "1234", + "username": "ddickwd", + "email": "dummy@dummy.com", + "first_name": "dummy", + "last_name": "McDickwad", } self.user = User.objects.create(**user_data) access_token = AccessToken().for_user(self.user) - self.token = f'Bearer {access_token}' + self.token = f"Bearer {access_token}" def test_who_am_i_view_get_returns_user_if_existing_and_authenticated(self): """ @@ -27,12 +27,14 @@ def test_who_am_i_view_get_returns_user_if_existing_and_authenticated(self): """ self.client.credentials(HTTP_AUTHORIZATION=self.token) - response = self.client.get(reverse('auth.whoami')) + response = self.client.get(reverse("auth.whoami")) self.assertEqual(response.status_code, 200) - content = json.loads(response.content.decode('utf-8')) - self.assertEqual(content['id'], self.user.id) + content = json.loads(response.content.decode("utf-8")) + self.assertEqual(content["id"], self.user.id) - def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(self): + def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated( + self, + ): """ WhoAmIView should return that the user was not found if authenticated user was deleted from the database. @@ -40,61 +42,59 @@ def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated(s self.user.delete() self.client.credentials(HTTP_AUTHORIZATION=self.token) - response = self.client.get(reverse('auth.whoami')) - self.assertEqual(response.status_code, 405) + response = self.client.get(reverse("auth.whoami")) + self.assertEqual(response.status_code, 401) def test_who_am_i_view_returns_401_when_not_authenticated(self): """WhoAmIView should return a 401 status code when the user is not authenticated""" - response = self.client.get(reverse('auth.whoami')) + response = self.client.get(reverse("auth.whoami")) self.assertEqual(response.status_code, 401) class TestLogoutView(APITestCase): def setUp(self): user_data = { - 'id': '1234', - 'username': 'ddickwd', - 'email': 'dummy@dummy.com', - 'first_name': 'dummy', - 'last_name': 'McDickwad', + "id": "1234", + "username": "ddickwd", + "email": "dummy@dummy.com", + "first_name": "dummy", + "last_name": "McDickwad", } self.user = User.objects.create(**user_data) def test_logout_view_authenticated_logout_url(self): """LogoutView should return a logout url redirect if authenticated user sends a post request.""" access_token = AccessToken().for_user(self.user) - self.token = f'Bearer {access_token}' + self.token = f"Bearer {access_token}" self.client.credentials(HTTP_AUTHORIZATION=self.token) - response = self.client.post(reverse('auth.logout')) + response = self.client.post(reverse("auth.logout")) self.assertEqual(response.status_code, 302) - url = '{server_url}/logout?service={service_url}'.format( - server_url=settings.CAS_ENDPOINT, - service_url=settings.API_ENDPOINT + url = "{server_url}/logout?service={service_url}".format( + server_url=settings.CAS_ENDPOINT, service_url=settings.API_ENDPOINT ) - self.assertEqual(response['Location'], url) + self.assertEqual(response["Location"], url) def test_logout_view_not_authenticated_logout_url(self): """LogoutView should return a 401 error when trying to access it while not authenticated.""" - response = self.client.post(reverse('auth.logout')) + response = self.client.post(reverse("auth.logout")) self.assertEqual(response.status_code, 401) class TestLoginView(APITestCase): def test_login_view_returns_login_url(self): """LoginView should return a login url redirect if a post request is sent.""" - response = self.client.get(reverse('auth.login')) + response = self.client.get(reverse("auth.login")) self.assertEqual(response.status_code, 302) - url = '{server_url}/login?service={service_url}'.format( - server_url=settings.CAS_ENDPOINT, - service_url=settings.CAS_RESPONSE + url = "{server_url}/login?service={service_url}".format( + server_url=settings.CAS_ENDPOINT, service_url=settings.CAS_RESPONSE ) - self.assertEqual(response['Location'], url) + self.assertEqual(response["Location"], url) class TestTokenEchoView(APITestCase): def test_token_echo_echoes_token(self): """TokenEchoView should echo the User's current token""" - ticket = 'This is a ticket.' - response = self.client.get(reverse('auth.echo'), data={'ticket': ticket}) - content = response.rendered_content.decode('utf-8').strip('"') + ticket = "This is a ticket." + response = self.client.get(reverse("auth.echo"), data={"ticket": ticket}) + content = response.rendered_content.decode("utf-8").strip('"') self.assertEqual(content, ticket) diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 4bdc45cd..4e53f3d0 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -1,22 +1,26 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenVerifyView, +) from authentication.views.auth import WhoAmIView, LoginView, LogoutView, TokenEchoView from authentication.views.users import UsersView router = DefaultRouter() -router.register('users', UsersView, basename='user') +router.register("users", UsersView, basename="user") urlpatterns = [ # USER endpoints. - path('', include(router.urls)), + path("", include(router.urls)), # AUTH endpoints. - path('login', LoginView.as_view(), name='auth.login'), - path('logout', LogoutView.as_view(), name='auth.logout'), - path('whoami', WhoAmIView.as_view(), name='auth.whoami'), - path('echo', TokenEchoView.as_view(), name='auth.echo'), + path("login", LoginView.as_view(), name="auth.login"), + path("logout", LogoutView.as_view(), name="auth.logout"), + path("whoami", WhoAmIView.as_view(), name="auth.whoami"), + path("echo", TokenEchoView.as_view(), name="auth.echo"), # TOKEN endpoints. - path('token', TokenObtainPairView.as_view(), name='auth.token'), - path('token/refresh', TokenRefreshView.as_view(), name='auth.token.refresh'), - path('token/verify', TokenVerifyView.as_view(), name='auth.token.verify') -] \ No newline at end of file + path("token", TokenObtainPairView.as_view(), name="auth.token"), + path("token/refresh", TokenRefreshView.as_view(), name="auth.token.refresh"), + path("token/verify", TokenVerifyView.as_view(), name="auth.token.verify"), +] diff --git a/backend/authentication/views/auth.py b/backend/authentication/views/auth.py index ebd2529b..fea0ded0 100644 --- a/backend/authentication/views/auth.py +++ b/backend/authentication/views/auth.py @@ -7,12 +7,14 @@ from authentication.cas.client import client from ypovoli import settings + class WhoAmIView(APIView): permission_classes = [IsAuthenticated] def get(self, request: Request) -> Response: """Get the user account data for the current user""" - return Response(UserSerializer(request.user, context={'request': request}).data) + return Response(UserSerializer(request.user, context={"request": request}).data) + class LogoutView(APIView): permission_classes = [IsAuthenticated] @@ -21,11 +23,13 @@ def post(self, request: Request) -> Response: """Attempt to log out. Redirect to our single CAS endpoint.""" return redirect(client.get_logout_url(service_url=settings.API_ENDPOINT)) + class LoginView(APIView): def get(self, request: Request): """Attempt to log in. Redirect to our single CAS endpoint.""" return redirect(client.get_login_url()) + class TokenEchoView(APIView): def get(self, request: Request) -> Response: - return Response(request.query_params.get('ticket')) \ No newline at end of file + return Response(request.query_params.get("ticket")) diff --git a/backend/authentication/views/users.py b/backend/authentication/views/users.py index 4c6c4b2b..cea6e4a9 100644 --- a/backend/authentication/views/users.py +++ b/backend/authentication/views/users.py @@ -3,6 +3,7 @@ from authentication.models import User from authentication.serializers import UserSerializer + class UsersView(ListModelMixin, RetrieveModelMixin, GenericViewSet): queryset = User.objects.all() - serializer_class = UserSerializer \ No newline at end of file + serializer_class = UserSerializer diff --git a/backend/checks/apps.py b/backend/checks/apps.py index 28a74284..5fa5cda6 100644 --- a/backend/checks/apps.py +++ b/backend/checks/apps.py @@ -2,5 +2,5 @@ class ChecksConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'checks' + default_auto_field = "django.db.models.BigAutoField" + name = "checks" diff --git a/backend/manage.py b/backend/manage.py index f2b51f89..75478bbb 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ypovoli.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ypovoli.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/backend/notifications/apps.py b/backend/notifications/apps.py index e5db3f92..e81be476 100644 --- a/backend/notifications/apps.py +++ b/backend/notifications/apps.py @@ -2,11 +2,11 @@ class NotificationsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'notifications' + default_auto_field = "django.db.models.BigAutoField" + name = "notifications" # TODO: Allow is_sent to be adjusted # TODO: Signals to send notifications # TODO: Send emails -# TODO: Think about the required api endpoints \ No newline at end of file +# TODO: Think about the required api endpoints diff --git a/backend/notifications/migrations/0001_initial.py b/backend/notifications/migrations/0001_initial.py index 5565733d..c0a67c04 100644 --- a/backend/notifications/migrations/0001_initial.py +++ b/backend/notifications/migrations/0001_initial.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -15,23 +14,45 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='NotificationTemplate', + name="NotificationTemplate", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('title_key', models.CharField(max_length=255)), - ('description_key', models.CharField(max_length=511)), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("title_key", models.CharField(max_length=255)), + ("description_key", models.CharField(max_length=511)), ], ), migrations.CreateModel( - name='Notification', + name="Notification", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('arguments', models.JSONField(default=dict)), - ('is_read', models.BooleanField(default=False)), - ('is_sent', models.BooleanField(default=False)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('template_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='notifications.notificationtemplate')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("arguments", models.JSONField(default=dict)), + ("is_read", models.BooleanField(default=False)), + ("is_sent", models.BooleanField(default=False)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "template_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="notifications.notificationtemplate", + ), + ), ], ), ] diff --git a/backend/ypovoli/asgi.py b/backend/ypovoli/asgi.py index 70cd7d09..ac8466f7 100644 --- a/backend/ypovoli/asgi.py +++ b/backend/ypovoli/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ypovoli.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ypovoli.settings") application = get_asgi_application() diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index 3c966165..6663866c 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -36,14 +36,12 @@ "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", - 'django.contrib.staticfiles', - + "django.contrib.staticfiles", # Third party "rest_framework_swagger", # Swagger "rest_framework", # Django rest framework "drf_yasg", # Yet Another Swagger generator "sslserver", # Used for local SSL support (needed by CAS) - # First party "authentication", # Ypovoli authentication "api", # Ypovoli logic of the base application @@ -74,7 +72,7 @@ "ACCESS_TOKEN_LIFETIME": timedelta(days=365), "REFRESH_TOKEN_LIFETIME": timedelta(days=1), "UPDATE_LAST_LOGIN": True, - "TOKEN_OBTAIN_SERIALIZER": "authentication.serializers.CASTokenObtainSerializer" + "TOKEN_OBTAIN_SERIALIZER": "authentication.serializers.CASTokenObtainSerializer", } AUTH_USER_MODEL = "authentication.User" @@ -113,19 +111,19 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" TEMPLATES = [ { - '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', + "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", ], }, }, diff --git a/backend/ypovoli/urls.py b/backend/ypovoli/urls.py index cb541b25..4dc43b32 100644 --- a/backend/ypovoli/urls.py +++ b/backend/ypovoli/urls.py @@ -22,7 +22,8 @@ schema_view = get_schema_view( openapi.Info( title="Ypovoli API", - default_version='v1',), + default_version="v1", + ), public=True, permission_classes=(permissions.AllowAny,), ) @@ -35,8 +36,12 @@ path("auth/", include("authentication.urls")), path("notifications/", include("notifications.urls")), # Swagger documentation. - path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), - name='schema-swagger-ui'), - path('swagger/', schema_view.without_ui(cache_timeout=0), - name='schema-json'), + path( + "swagger/", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + path( + "swagger/", schema_view.without_ui(cache_timeout=0), name="schema-json" + ), ] diff --git a/backend/ypovoli/wsgi.py b/backend/ypovoli/wsgi.py index c617cd31..0495fc95 100644 --- a/backend/ypovoli/wsgi.py +++ b/backend/ypovoli/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ypovoli.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ypovoli.settings") application = get_wsgi_application()