From 20b3a719a52450750bc3c470f3bdfa53d858269c Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Thu, 29 Feb 2024 14:13:37 +0100 Subject: [PATCH 01/86] chore: start fixtures api + faculty by auto generating --- backend/api/fixtures/admins.yaml | 1 + backend/api/fixtures/assistants.yaml | 10 +++++ backend/api/fixtures/checks.yaml | 1 + backend/api/fixtures/courses.yaml | 14 ++++++ backend/api/fixtures/file_extensions.yaml | 1 + backend/api/fixtures/groups.yaml | 8 ++++ backend/api/fixtures/projects.yaml | 11 +++++ backend/api/fixtures/students.yaml | 11 +++++ backend/api/fixtures/submissions.yaml | 1 + backend/api/fixtures/teachers.yaml | 11 +++++ .../authentication/fixtures/faculties.yaml | 44 +++++++++++++++++++ 11 files changed, 113 insertions(+) create mode 100644 backend/api/fixtures/admins.yaml create mode 100644 backend/api/fixtures/assistants.yaml create mode 100644 backend/api/fixtures/checks.yaml create mode 100644 backend/api/fixtures/courses.yaml create mode 100644 backend/api/fixtures/file_extensions.yaml create mode 100644 backend/api/fixtures/groups.yaml create mode 100644 backend/api/fixtures/projects.yaml create mode 100644 backend/api/fixtures/students.yaml create mode 100644 backend/api/fixtures/submissions.yaml create mode 100644 backend/api/fixtures/teachers.yaml create mode 100644 backend/authentication/fixtures/faculties.yaml diff --git a/backend/api/fixtures/admins.yaml b/backend/api/fixtures/admins.yaml new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/backend/api/fixtures/admins.yaml @@ -0,0 +1 @@ +[] diff --git a/backend/api/fixtures/assistants.yaml b/backend/api/fixtures/assistants.yaml new file mode 100644 index 00000000..aa7d31c5 --- /dev/null +++ b/backend/api/fixtures/assistants.yaml @@ -0,0 +1,10 @@ +- model: api.assistant + pk: '235' + fields: + courses: + - 1 +- model: api.assistant + pk: '236' + fields: + courses: + - 2 diff --git a/backend/api/fixtures/checks.yaml b/backend/api/fixtures/checks.yaml new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/backend/api/fixtures/checks.yaml @@ -0,0 +1 @@ +[] diff --git a/backend/api/fixtures/courses.yaml b/backend/api/fixtures/courses.yaml new file mode 100644 index 00000000..154f0e0b --- /dev/null +++ b/backend/api/fixtures/courses.yaml @@ -0,0 +1,14 @@ +- model: api.course + pk: 1 + fields: + name: Math + academic_startyear: 2023 + description: Math course + parent_course: null +- model: api.course + pk: 2 + fields: + name: Sel2 + academic_startyear: 2023 + description: Software course + parent_course: null diff --git a/backend/api/fixtures/file_extensions.yaml b/backend/api/fixtures/file_extensions.yaml new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/backend/api/fixtures/file_extensions.yaml @@ -0,0 +1 @@ +[] diff --git a/backend/api/fixtures/groups.yaml b/backend/api/fixtures/groups.yaml new file mode 100644 index 00000000..b8adc460 --- /dev/null +++ b/backend/api/fixtures/groups.yaml @@ -0,0 +1,8 @@ +- model: api.group + pk: 1 + fields: + project: 123456 + score: null + students: + - '1' + - '2' diff --git a/backend/api/fixtures/projects.yaml b/backend/api/fixtures/projects.yaml new file mode 100644 index 00000000..2e42bd30 --- /dev/null +++ b/backend/api/fixtures/projects.yaml @@ -0,0 +1,11 @@ +- model: api.project + pk: 123456 + fields: + name: sel2 + description: make a project + visible: true + archived: false + start_date: 2024-02-26 00:00:00+00:00 + deadline: 2024-02-27 00:00:00+00:00 + checks: null + course: 2 diff --git a/backend/api/fixtures/students.yaml b/backend/api/fixtures/students.yaml new file mode 100644 index 00000000..ac61cbd7 --- /dev/null +++ b/backend/api/fixtures/students.yaml @@ -0,0 +1,11 @@ +- model: api.student + pk: '1' + fields: + student_id: null + courses: + - 1 +- model: api.student + pk: '2' + fields: + student_id: null + courses: [] diff --git a/backend/api/fixtures/submissions.yaml b/backend/api/fixtures/submissions.yaml new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/backend/api/fixtures/submissions.yaml @@ -0,0 +1 @@ +[] diff --git a/backend/api/fixtures/teachers.yaml b/backend/api/fixtures/teachers.yaml new file mode 100644 index 00000000..44b883f2 --- /dev/null +++ b/backend/api/fixtures/teachers.yaml @@ -0,0 +1,11 @@ +- model: api.teacher + pk: '123' + fields: + courses: + - 1 +- model: api.teacher + pk: '124' + fields: + courses: + - 1 + - 2 diff --git a/backend/authentication/fixtures/faculties.yaml b/backend/authentication/fixtures/faculties.yaml new file mode 100644 index 00000000..c46fc982 --- /dev/null +++ b/backend/authentication/fixtures/faculties.yaml @@ -0,0 +1,44 @@ +- model: authentication.faculty + pk: Bio-ingenieurswetenschappen + fields: + user: [] +- model: authentication.faculty + pk: Diergeneeskunde + fields: + user: [] +- model: authentication.faculty + pk: Economie_Bedrijfskunde + fields: + user: [] +- model: authentication.faculty + pk: Farmaceutische_Wetenschappen + fields: + user: [] +- model: authentication.faculty + pk: Geneeskunde_Gezondheidswetenschappen + fields: + user: [] +- model: authentication.faculty + pk: Ingenieurswetenschappen_Architectuur + fields: + user: [] +- model: authentication.faculty + pk: Letteren_Wijsbegeerte + fields: + user: [] +- model: authentication.faculty + pk: Politieke_Sociale_Wetenschappen + fields: + user: [] +- model: authentication.faculty + pk: Psychologie_PedagogischeWetenschappen + fields: + user: [] +- model: authentication.faculty + pk: Recht_Criminologie + fields: + user: [] +- model: authentication.faculty + pk: Wetenschappen + fields: + user: [] From 9156c4e64584ac0cd71104c831c1d932111d24e9 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Thu, 29 Feb 2024 14:21:25 +0100 Subject: [PATCH 02/86] fix: faculty model --- ...culty_user_remove_user_faculty_and_more.py | 26 +++++++++++++++++++ backend/authentication/models.py | 15 ++++------- 2 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 backend/authentication/migrations/0002_remove_faculty_user_remove_user_faculty_and_more.py 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 new file mode 100644 index 00000000..42ce1147 --- /dev/null +++ b/backend/authentication/migrations/0002_remove_faculty_user_remove_user_faculty_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.2 on 2024-02-29 13:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='faculty', + name='user', + ), + migrations.RemoveField( + model_name='user', + name='faculty', + ), + migrations.AddField( + model_name='user', + name='faculties', + field=models.ManyToManyField(blank=True, related_name='users', to='authentication.faculty'), + ), + ] diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 7bef9b4c..224e05f1 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -37,15 +37,15 @@ class User(AbstractBaseUser): null=False ) - faculty = models.ManyToManyField( + faculties = models.ManyToManyField( 'Faculty', - related_name='faculties', + related_name='users', blank=True ) last_enrolled = IntegerField( - default = datetime.MINYEAR, - null = True + default=datetime.MINYEAR, + null=True ) create_time = DateTimeField( @@ -57,6 +57,7 @@ class User(AbstractBaseUser): EMAIL_FIELD = "email" REQUIRED_FIELDS = [] + class Faculty(models.Model): """This model represents a faculty.""" @@ -65,9 +66,3 @@ class Faculty(models.Model): max_length=50, primary_key=True ) - - user = models.ManyToManyField( - 'User', - related_name='users', - blank=True - ) From e0b5b9b9fbda225da0e13f9e4a22904f6dac7896 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 16:45:01 +0100 Subject: [PATCH 03/86] 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 0d2cf4d3404bb4f4c06108a192b3537bc28e4dbb Mon Sep 17 00:00:00 2001 From: EwoutV Date: Thu, 29 Feb 2024 18:10:28 +0100 Subject: [PATCH 04/86] fix: requirements.txt sync --- backend/requirements.txt | 47 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index f0eec8cc..9233f72d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,9 +1,48 @@ +asgiref==3.7.2 +astroid==3.1.0 +cachetools==5.3.2 +cas-client==1.0.0 +certifi==2024.2.2 +cffi==1.16.0 +chardet==5.2.0 +charset-normalizer==3.3.2 +colorama==0.4.6 +coreapi==2.3.3 +coreschema==0.0.4 +cryptography==42.0.5 +dill==0.3.8 +distlib==0.3.8 Django==5.0.2 +django-rest-knox==4.2.0 +django-rest-swagger==2.2.0 django-sslserver==0.22 djangorestframework==3.14.0 -django-rest-swagger==2.2.0 +djangorestframework-simplejwt==5.3.1 drf-yasg==1.21.7 +filelock==3.13.1 +flake8==7.0.0 +flake8-for-pycharm==0.4.1 +idna==3.6 +inflection==0.5.1 +isort==5.13.2 +itypes==1.2.0 +Jinja2==3.1.3 +MarkupSafe==2.1.5 +mccabe==0.7.0 +openapi-codec==1.3.2 +PyJWT==2.8.0 +pylint==3.1.0 +pyproject-api==1.6.1 +pytz==2024.1 +PyYAML==6.0.1 requests==2.31.0 -cas-client==1.0.0 -psycopg2-binary==2.9.9 -djangorestframework-simplejwt==5.3.1 \ No newline at end of file +simplejson==3.19.2 +six==1.16.0 +sqlparse==0.4.4 +tomli==2.0.1 +tomlkit==0.12.4 +tox==4.13.0 +typing_extensions==4.9.0 +uritemplate==4.1.1 +urllib3==2.2.1 +virtualenv==20.25.1 \ No newline at end of file From 9fdffe7f15d0d73d7441338eea9be839c817d25e Mon Sep 17 00:00:00 2001 From: EwoutV Date: Thu, 29 Feb 2024 18:12:10 +0100 Subject: [PATCH 05/86] Revert "fix: requirements.txt sync" This reverts commit 0d2cf4d3404bb4f4c06108a192b3537bc28e4dbb. --- backend/requirements.txt | 47 ++++------------------------------------ 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 9233f72d..f0eec8cc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,48 +1,9 @@ -asgiref==3.7.2 -astroid==3.1.0 -cachetools==5.3.2 -cas-client==1.0.0 -certifi==2024.2.2 -cffi==1.16.0 -chardet==5.2.0 -charset-normalizer==3.3.2 -colorama==0.4.6 -coreapi==2.3.3 -coreschema==0.0.4 -cryptography==42.0.5 -dill==0.3.8 -distlib==0.3.8 Django==5.0.2 -django-rest-knox==4.2.0 -django-rest-swagger==2.2.0 django-sslserver==0.22 djangorestframework==3.14.0 -djangorestframework-simplejwt==5.3.1 +django-rest-swagger==2.2.0 drf-yasg==1.21.7 -filelock==3.13.1 -flake8==7.0.0 -flake8-for-pycharm==0.4.1 -idna==3.6 -inflection==0.5.1 -isort==5.13.2 -itypes==1.2.0 -Jinja2==3.1.3 -MarkupSafe==2.1.5 -mccabe==0.7.0 -openapi-codec==1.3.2 -PyJWT==2.8.0 -pylint==3.1.0 -pyproject-api==1.6.1 -pytz==2024.1 -PyYAML==6.0.1 requests==2.31.0 -simplejson==3.19.2 -six==1.16.0 -sqlparse==0.4.4 -tomli==2.0.1 -tomlkit==0.12.4 -tox==4.13.0 -typing_extensions==4.9.0 -uritemplate==4.1.1 -urllib3==2.2.1 -virtualenv==20.25.1 \ No newline at end of file +cas-client==1.0.0 +psycopg2-binary==2.9.9 +djangorestframework-simplejwt==5.3.1 \ 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 06/86] 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 07/86] 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 08/86] 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 ac0ff10bf209a5e3d4abe3616b0a4df71a40a376 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Thu, 29 Feb 2024 20:36:07 +0100 Subject: [PATCH 09/86] hotfix: import errors in Python 3.11 --- backend/api/apps.py | 6 +++++- backend/api/signals.py | 9 ++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/api/apps.py b/backend/api/apps.py index 60def201..6cd16758 100644 --- a/backend/api/apps.py +++ b/backend/api/apps.py @@ -5,4 +5,8 @@ class ApiConfig(AppConfig): name = 'api' def ready(self): - from . import signals \ No newline at end of file + # Only here we can import from apps. + from authentication.signals import user_created + from api.signals import user_creation + + user_created.connect(user_creation) \ No newline at end of file diff --git a/backend/api/signals.py b/backend/api/signals.py index 7cfd15b2..767bff1e 100644 --- a/backend/api/signals.py +++ b/backend/api/signals.py @@ -1,12 +1,11 @@ -from django.dispatch import receiver -from authentication.signals import user_created from authentication.models import User -from api.models.student import Student -@receiver(user_created) def user_creation(user: User, attributes: dict, **kwargs): + # With Python 3.11, we need to import Student here. + from api.models.student import Student + """Upon user creation, auto-populate additional properties""" student_id = attributes.get("ugentStudentID") if student_id: - Student(user_ptr=user,student_id=student_id).save_base(raw=True) \ No newline at end of file + Student(user_ptr=user, student_id=student_id).save_base(raw=True) \ No newline at end of file From b5b79fa6182a19d9dac7151640496f4ac892a283 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Thu, 29 Feb 2024 20:41:19 +0100 Subject: [PATCH 10/86] fix: update faculty --- backend/api/migrations/0002_populate.py | 13 +++++++------ backend/api/serializers/admin_serializer.py | 9 ++++++++- backend/api/serializers/assistant_serializer.py | 2 +- backend/api/serializers/student_serializer.py | 2 +- backend/api/serializers/teacher_serializer.py | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/backend/api/migrations/0002_populate.py b/backend/api/migrations/0002_populate.py index a289fd86..1bc31c0d 100644 --- a/backend/api/migrations/0002_populate.py +++ b/backend/api/migrations/0002_populate.py @@ -47,7 +47,7 @@ def populate_db(apps, schema_editor): create_time="2023-01-01T00:00:00Z", ) - teacher1.faculty.add(f_psyPeda) + teacher1.faculties.add(f_psyPeda) assistant1 = Assistant.objects.create( id=235, @@ -58,7 +58,7 @@ def populate_db(apps, schema_editor): create_time="2023-01-01T00:00:00Z", ) - assistant1.faculty.add(f_wet) + assistant1.faculties.add(f_wet) assistant2 = Assistant.objects.create( id=236, @@ -69,7 +69,7 @@ def populate_db(apps, schema_editor): create_time="2023-01-01T00:00:00Z", ) - assistant2.faculty.add(f_psyPeda) + assistant2.faculties.add(f_psyPeda) teacher2 = Teacher.objects.create( id=124, @@ -80,7 +80,7 @@ def populate_db(apps, schema_editor): create_time="2023-01-01T00:00:00Z", ) - teacher2.faculty.add(f_psyPeda) + teacher2.faculties.add(f_psyPeda) student1 = Student.objects.create( id=1, @@ -91,7 +91,7 @@ def populate_db(apps, schema_editor): create_time="2023-01-01T00:00:00Z", ) - student1.faculty.add(f_wet) + student1.faculties.add(f_wet) student2 = Student.objects.create( id=2, @@ -102,7 +102,7 @@ def populate_db(apps, schema_editor): create_time="2023-01-01T00:00:00Z", ) - student2.faculty.add(f_genGez) + student2.faculties.add(f_genGez) course = Course.objects.create( name="Math", @@ -149,6 +149,7 @@ class Migration(migrations.Migration): dependencies = [ ("api", "0001_initial"), ("authentication", "0001_initial"), + ("authentication", "0002_remove_faculty_user_remove_user_faculty_and_more") ] operations = [ diff --git a/backend/api/serializers/admin_serializer.py b/backend/api/serializers/admin_serializer.py index 3cd2404c..a00348e7 100644 --- a/backend/api/serializers/admin_serializer.py +++ b/backend/api/serializers/admin_serializer.py @@ -3,9 +3,16 @@ class AdminSerializer(serializers.ModelSerializer): + + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + class Meta: model = Admin fields = [ 'id', 'first_name', 'last_name', 'email', - 'faculty', 'last_enrolled', 'create_time' + 'faculties', 'last_enrolled', 'create_time' ] diff --git a/backend/api/serializers/assistant_serializer.py b/backend/api/serializers/assistant_serializer.py index 378f5322..210f7404 100644 --- a/backend/api/serializers/assistant_serializer.py +++ b/backend/api/serializers/assistant_serializer.py @@ -13,5 +13,5 @@ class Meta: model = Assistant fields = [ 'id', 'first_name', 'last_name', 'email', - 'faculty', 'last_enrolled', 'create_time', 'courses' + 'faculties', 'last_enrolled', 'create_time', 'courses' ] diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index 4e5b794b..214d637b 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -17,6 +17,6 @@ class StudentSerializer(serializers.ModelSerializer): class Meta: model = Student fields = [ - 'id', 'first_name', 'last_name', 'email', 'faculty', + 'id', 'first_name', 'last_name', 'email', 'faculties', 'last_enrolled', 'create_time', 'courses', 'groups' ] diff --git a/backend/api/serializers/teacher_serializer.py b/backend/api/serializers/teacher_serializer.py index e24b0ed0..c35bea68 100644 --- a/backend/api/serializers/teacher_serializer.py +++ b/backend/api/serializers/teacher_serializer.py @@ -13,5 +13,5 @@ class Meta: model = Teacher fields = [ 'id', 'first_name', 'last_name', 'email', - 'faculty', 'last_enrolled', 'create_time', 'courses' + 'faculties', 'last_enrolled', 'create_time', 'courses' ] From fd9ce378d3a363961a122cccf3d081cefc39aa4a Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Thu, 29 Feb 2024 16:45:01 +0100 Subject: [PATCH 11/86] 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 12/86] 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 13/86] 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 460476475915c338f95404b36686610aa0dfdd2d Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Thu, 29 Feb 2024 21:44:34 +0100 Subject: [PATCH 14/86] chore: refactor to fixtures finish --- backend/api/fixtures/admins.yaml | 1 - backend/api/fixtures/checks.yaml | 1 - backend/api/fixtures/file_extensions.yaml | 1 - backend/api/fixtures/submissions.yaml | 1 - backend/api/migrations/0002_populate.py | 157 ------------------ .../authentication/fixtures/faculties.yaml | 33 ++-- backend/authentication/fixtures/users.yaml | 72 ++++++++ 7 files changed, 83 insertions(+), 183 deletions(-) delete mode 100644 backend/api/fixtures/admins.yaml delete mode 100644 backend/api/fixtures/checks.yaml delete mode 100644 backend/api/fixtures/file_extensions.yaml delete mode 100644 backend/api/fixtures/submissions.yaml delete mode 100644 backend/api/migrations/0002_populate.py create mode 100644 backend/authentication/fixtures/users.yaml diff --git a/backend/api/fixtures/admins.yaml b/backend/api/fixtures/admins.yaml deleted file mode 100644 index fe51488c..00000000 --- a/backend/api/fixtures/admins.yaml +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/backend/api/fixtures/checks.yaml b/backend/api/fixtures/checks.yaml deleted file mode 100644 index fe51488c..00000000 --- a/backend/api/fixtures/checks.yaml +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/backend/api/fixtures/file_extensions.yaml b/backend/api/fixtures/file_extensions.yaml deleted file mode 100644 index fe51488c..00000000 --- a/backend/api/fixtures/file_extensions.yaml +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/backend/api/fixtures/submissions.yaml b/backend/api/fixtures/submissions.yaml deleted file mode 100644 index fe51488c..00000000 --- a/backend/api/fixtures/submissions.yaml +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/backend/api/migrations/0002_populate.py b/backend/api/migrations/0002_populate.py deleted file mode 100644 index 1bc31c0d..00000000 --- a/backend/api/migrations/0002_populate.py +++ /dev/null @@ -1,157 +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.faculties.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.faculties.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.faculties.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.faculties.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.faculties.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.faculties.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"), - ("authentication", "0002_remove_faculty_user_remove_user_faculty_and_more") - ] - - operations = [ - migrations.RunPython(populate_db), - ] diff --git a/backend/authentication/fixtures/faculties.yaml b/backend/authentication/fixtures/faculties.yaml index c46fc982..e13fd9e5 100644 --- a/backend/authentication/fixtures/faculties.yaml +++ b/backend/authentication/fixtures/faculties.yaml @@ -1,44 +1,33 @@ - model: authentication.faculty pk: Bio-ingenieurswetenschappen - fields: - user: [] + fields: {} - model: authentication.faculty pk: Diergeneeskunde - fields: - user: [] + fields: {} - model: authentication.faculty pk: Economie_Bedrijfskunde - fields: - user: [] + fields: {} - model: authentication.faculty pk: Farmaceutische_Wetenschappen - fields: - user: [] + fields: {} - model: authentication.faculty pk: Geneeskunde_Gezondheidswetenschappen - fields: - user: [] + fields: {} - model: authentication.faculty pk: Ingenieurswetenschappen_Architectuur - fields: - user: [] + fields: {} - model: authentication.faculty pk: Letteren_Wijsbegeerte - fields: - user: [] + fields: {} - model: authentication.faculty pk: Politieke_Sociale_Wetenschappen - fields: - user: [] + fields: {} - model: authentication.faculty pk: Psychologie_PedagogischeWetenschappen - fields: - user: [] + fields: {} - model: authentication.faculty pk: Recht_Criminologie - fields: - user: [] + fields: {} - model: authentication.faculty pk: Wetenschappen - fields: - user: [] + fields: {} diff --git a/backend/authentication/fixtures/users.yaml b/backend/authentication/fixtures/users.yaml new file mode 100644 index 00000000..40922b7d --- /dev/null +++ b/backend/authentication/fixtures/users.yaml @@ -0,0 +1,72 @@ +- model: authentication.user + pk: '1' + fields: + last_login: null + username: jdoe + email: John.Doe@hotmail.com + first_name: John + last_name: Doe + last_enrolled: 1 + create_time: 2024-02-29 20:35:45.690556+00:00 + faculties: + - Wetenschappen +- model: authentication.user + pk: '123' + fields: + last_login: null + username: tboonen + email: Tom.Boonen@gmail.be + first_name: Tom + last_name: Boonen + last_enrolled: 1 + create_time: 2024-02-29 20:35:45.686541+00:00 + faculties: + - Psychologie_PedagogischeWetenschappen +- model: authentication.user + pk: '124' + fields: + last_login: null + username: psagan + email: Peter.Sagan@gmail.com + first_name: Peter + last_name: Sagan + last_enrolled: 1 + create_time: 2024-02-29 20:35:45.689543+00:00 + faculties: + - Psychologie_PedagogischeWetenschappen +- model: authentication.user + pk: '2' + fields: + last_login: null + username: bverhae + email: Bartje.Verhaege@gmail.com + first_name: Bartje + last_name: Verhaege + last_enrolled: 1 + create_time: 2024-02-29 20:35:45.691565+00:00 + faculties: + - Geneeskunde_Gezondheidswetenschappen +- model: authentication.user + pk: '235' + fields: + last_login: null + username: bsimpson + email: Bart.Simpson@gmail.be + first_name: Bart + last_name: Simpson + last_enrolled: 1 + create_time: 2024-02-29 20:35:45.687541+00:00 + faculties: + - Wetenschappen +- model: authentication.user + pk: '236' + fields: + last_login: null + username: kclijster + email: Kim.Clijsters@gmail.be + first_name: Kim + last_name: Clijsters + last_enrolled: 1 + create_time: 2024-02-29 20:35:45.688545+00:00 + faculties: + - Psychologie_PedagogischeWetenschappen From a5f5ed93048c51e77a7db623e3fbb8b15998792c Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Thu, 29 Feb 2024 21:50:27 +0100 Subject: [PATCH 15/86] chore: update admin --- backend/api/serializers/admin_serializer.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/api/serializers/admin_serializer.py b/backend/api/serializers/admin_serializer.py index a00348e7..2468e4ba 100644 --- a/backend/api/serializers/admin_serializer.py +++ b/backend/api/serializers/admin_serializer.py @@ -4,11 +4,6 @@ class AdminSerializer(serializers.ModelSerializer): - faculties = serializers.HyperlinkedRelatedField( - many=True, - read_only=True, - view_name='faculty-detail' - ) class Meta: model = Admin From 6772e9537af7a0b0f1578251994a859cf9bfb3b0 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Thu, 29 Feb 2024 21:50:34 +0100 Subject: [PATCH 16/86] 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 29e1ed09401d83a0315fa5ac0da423742fe2bd58 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Thu, 29 Feb 2024 22:56:38 +0100 Subject: [PATCH 17/86] chore: init tests --- backend/api/tests/test_admin.py | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 backend/api/tests/test_admin.py diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py new file mode 100644 index 00000000..6c635311 --- /dev/null +++ b/backend/api/tests/test_admin.py @@ -0,0 +1,40 @@ +import datetime +import json + +from django.test import TestCase +from django.utils import timezone +from django.urls import reverse + +from ..models.admin import Admin + + +def create_admin(id, first_name, last_name, username, email): + """ + Create a Admin with the given arguments. + """ + return Admin.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) + + +class AdminModelTests(TestCase): + def test_no_admins(self): + """ + able to retrieve no admin before publishing it. + """ + + # print(reverse("api:admin.index")) + response_root = self.client.get("https://localhost:8080/admins") + # print(response.content) + self.assertEqual(response_root.status_code, 301) + # Assert that the response is JSON + self.assertEqual(response_root.accepted_media_type, "application/json") + # Parse the JSON content from the response + content_json = json.loads(response_root.content.decode("utf-8")) + # Assert that the parsed JSON is an empty list + self.assertEqual(content_json, []) From d47783f1404871bdecc2501d44a9ed0f086810ba Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Thu, 29 Feb 2024 23:41:33 +0100 Subject: [PATCH 18/86] chore: test admin --- backend/api/tests/test_admin.py | 118 +++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 11 deletions(-) diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index 6c635311..56517cb2 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -8,18 +8,19 @@ from ..models.admin import Admin -def create_admin(id, first_name, last_name, username, email): +def create_admin(id, first_name, last_name, email): """ Create a Admin with the given arguments. """ + username = f"{first_name}_{last_name}" return Admin.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(), + ) class AdminModelTests(TestCase): @@ -28,13 +29,108 @@ def test_no_admins(self): able to retrieve no admin before publishing it. """ - # print(reverse("api:admin.index")) - response_root = self.client.get("https://localhost:8080/admins") + response_root = self.client.get("/admins", follow=True) # print(response.content) - self.assertEqual(response_root.status_code, 301) + self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") # Parse the JSON content from the response content_json = json.loads(response_root.content.decode("utf-8")) # Assert that the parsed JSON is an empty list self.assertEqual(content_json, []) + + 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" + ) + + # Make a GET request to retrieve the admin + response = self.client.get("/admins", follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one admin + self.assertEqual(len(content_json), 1) + + # Assert the details of the retrieved admin match the created admin + retrieved_admin = content_json[0] + self.assertEqual(int(retrieved_admin["id"]), admin.id) + self.assertEqual(retrieved_admin["first_name"], admin.first_name) + self.assertEqual(retrieved_admin["last_name"], admin.last_name) + self.assertEqual(retrieved_admin["email"], admin.email) + + def test_multiple_admins(self): + """ + Able to retrieve multiple admins after creating them. + """ + # Create multiple admins + admin1 = create_admin(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") + + # Make a GET request to retrieve the admins + response = self.client.get("/admins", follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple admins + self.assertEqual(len(content_json), 2) + + # Assert the details of the retrieved admins match the created admins + retrieved_admin1, retrieved_admin2 = content_json + self.assertEqual(int(retrieved_admin1["id"]), admin1.id) + self.assertEqual(retrieved_admin1["first_name"], admin1.first_name) + self.assertEqual(retrieved_admin1["last_name"], admin1.last_name) + self.assertEqual(retrieved_admin1["email"], admin1.email) + + self.assertEqual(int(retrieved_admin2["id"]), admin2.id) + self.assertEqual(retrieved_admin2["first_name"], admin2.first_name) + self.assertEqual(retrieved_admin2["last_name"], admin2.last_name) + self.assertEqual(retrieved_admin2["email"], admin2.email) + +""" + def test_admin_detail_view(self): + + # Able to retrieve details of a single admin. + + # 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") + + # Make a GET request to retrieve the admin details + response = self.client.get(reverse("/admins/"+str(admin.id), args=[admin.id]), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved admin match the created admin + self.assertEqual(int(content_json["id"]), admin.id) + self.assertEqual(content_json["first_name"], admin.first_name) + self.assertEqual(content_json["last_name"], admin.last_name) + self.assertEqual(content_json["email"], admin.email) + self.assertEqual(content_json["username"], admin.username) +""" From 4b77d4b850e1fd718dd081165e889eece38bf849 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 00:19:36 +0100 Subject: [PATCH 19/86] 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 20/86] 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 21/86] 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 22/86] 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 4e7d131f4372da41ed4de6823eae83a6a3ffdfae Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Fri, 1 Mar 2024 15:17:48 +0100 Subject: [PATCH 23/86] chore: user has field notifications + faculties list --- backend/api/serializers/admin_serializer.py | 5 ++++ .../api/serializers/assistant_serializer.py | 6 ++++ backend/api/serializers/student_serializer.py | 6 ++++ backend/api/serializers/teacher_serializer.py | 6 ++++ backend/authentication/serializers.py | 28 +++++++++++++++---- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/backend/api/serializers/admin_serializer.py b/backend/api/serializers/admin_serializer.py index 2468e4ba..c7749f87 100644 --- a/backend/api/serializers/admin_serializer.py +++ b/backend/api/serializers/admin_serializer.py @@ -4,6 +4,11 @@ class AdminSerializer(serializers.ModelSerializer): + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) class Meta: model = Admin diff --git a/backend/api/serializers/assistant_serializer.py b/backend/api/serializers/assistant_serializer.py index 210f7404..540f9f32 100644 --- a/backend/api/serializers/assistant_serializer.py +++ b/backend/api/serializers/assistant_serializer.py @@ -9,6 +9,12 @@ class AssistantSerializer(serializers.ModelSerializer): read_only=True, ) + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + class Meta: model = Assistant fields = [ diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index 214d637b..3ea93733 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -14,6 +14,12 @@ class StudentSerializer(serializers.ModelSerializer): read_only=True, ) + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + class Meta: model = Student fields = [ diff --git a/backend/api/serializers/teacher_serializer.py b/backend/api/serializers/teacher_serializer.py index c35bea68..8fbb98e9 100644 --- a/backend/api/serializers/teacher_serializer.py +++ b/backend/api/serializers/teacher_serializer.py @@ -9,6 +9,12 @@ class TeacherSerializer(serializers.ModelSerializer): read_only=True, ) + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + class Meta: model = Teacher fields = [ diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 9c7e8173..f33e6c91 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -1,5 +1,8 @@ from django.contrib.auth.models import update_last_login -from rest_framework.serializers import CharField, EmailField, ModelSerializer, ValidationError, Serializer +from rest_framework.serializers import ( + CharField, EmailField, ModelSerializer, ValidationError, + Serializer, HyperlinkedIdentityField, HyperlinkedRelatedField +) from rest_framework_simplejwt.tokens import RefreshToken, AccessToken from rest_framework_simplejwt.settings import api_settings from authentication.signals import user_created, user_login @@ -65,23 +68,36 @@ def validate(self, data): '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() - + email = EmailField() + + faculties = HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + + notifications = HyperlinkedIdentityField( + view_name='notification-detail', + read_only=True, + ) + class Meta: model = User fields = [ 'id', 'username', 'email', 'first_name', 'last_name', - 'faculty', - 'last_enrolled', 'last_login', 'create_time' + 'faculties', + 'last_enrolled', 'last_login', 'create_time', + 'notifications' ] def get_or_create(self, validated_data: dict) -> User: """Create or fetch the user based on the validated data.""" - return User.objects.get_or_create(**validated_data) \ No newline at end of file + return User.objects.get_or_create(**validated_data) From 3b63c2fccfee464b1a3d2a19b0f125d5bf99f268 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 15:20:25 +0100 Subject: [PATCH 24/86] chore: add admin tests --- backend/api/tests/test_admin.py | 44 +++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index 56517cb2..eaddd911 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -1,4 +1,3 @@ -import datetime import json from django.test import TestCase @@ -29,7 +28,7 @@ def test_no_admins(self): able to retrieve no admin before publishing it. """ - response_root = self.client.get("/admins", follow=True) + response_root = self.client.get(reverse("admin-list"), follow=True) # print(response.content) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON @@ -51,17 +50,17 @@ def test_admin_exists(self): ) # Make a GET request to retrieve the admin - response = self.client.get("/admins", follow=True) + response = self.client.get(reverse("admin-list"), follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) # Assert that the response is JSON self.assertEqual(response.accepted_media_type, "application/json") - + # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - + # Assert that the parsed JSON is a list with one admin self.assertEqual(len(content_json), 1) @@ -77,11 +76,21 @@ def test_multiple_admins(self): Able to retrieve multiple admins after creating them. """ # Create multiple admins - admin1 = create_admin(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") + admin1 = create_admin( + 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" + ) # Make a GET request to retrieve the admins - response = self.client.get("/admins", follow=True) + response = self.client.get(reverse("admin-list"), follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -107,16 +116,21 @@ def test_multiple_admins(self): self.assertEqual(retrieved_admin2["last_name"], admin2.last_name) self.assertEqual(retrieved_admin2["email"], admin2.email) -""" def test_admin_detail_view(self): - - # Able to retrieve details of a single admin. - + """ + Able to retrieve details of a single admin. + """ # 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") + admin = create_admin( + 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("/admins/"+str(admin.id), args=[admin.id]), follow=True) + response = self.client.get( + reverse("admin-detail", args=[str(admin.id)]), follow=True) # Check if the response was successful self.assertEqual(response.status_code, 200) @@ -132,5 +146,3 @@ def test_admin_detail_view(self): self.assertEqual(content_json["first_name"], admin.first_name) self.assertEqual(content_json["last_name"], admin.last_name) self.assertEqual(content_json["email"], admin.email) - self.assertEqual(content_json["username"], admin.username) -""" From d814a86602f92d48b11ddf61d478ad8f79c48a29 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 16:07:31 +0100 Subject: [PATCH 25/86] chore: add fileExtensions tests --- backend/api/tests/test_assistant.py | 155 ++++++++++++++++++++++++++++ backend/api/tests/test_checks.py | 148 ++++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 backend/api/tests/test_assistant.py create mode 100644 backend/api/tests/test_checks.py diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py new file mode 100644 index 00000000..c2ef3b5e --- /dev/null +++ b/backend/api/tests/test_assistant.py @@ -0,0 +1,155 @@ +import json + +from django.test import TestCase +from django.utils import timezone +from django.urls import reverse + +#from ..models.assistant import Assistant + +""" +def create_assistant(id, first_name, last_name, email): + + # Create a Assistant with the given arguments. + + username = f"{first_name}_{last_name}" + return Assistant.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) +""" + + +""" class AssistantModelTests(TestCase): + def test_no_assistant(self): + + # able to retrieve no assistant before publishing it. + + + response_root = self.client.get(reverse("assistant-list"), follow=True) + # print(response.content) + self.assertEqual(response_root.status_code, 200) + # Assert that the response is JSON + self.assertEqual(response_root.accepted_media_type, "application/json") + # Parse the JSON content from the response + content_json = json.loads(response_root.content.decode("utf-8")) + # Assert that the parsed JSON is an empty list + self.assertEqual(content_json, []) + + 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" + ) + + # Make a GET request to retrieve the assistant + response = self.client.get(reverse("assistant-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one admin + self.assertEqual(len(content_json), 1) + + # Assert the details of the retrieved admin match the created admin + 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["last_name"], assistant.last_name) + self.assertEqual(retrieved_assistant["email"], assistant.email) + + def test_multiple_assistant(self): + + # Able to retrieve multiple assistant after creating them. + + # Create multiple assistant + assistant1 = create_assistant( + 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" + ) + + # Make a GET request to retrieve the assistant + response = self.client.get(reverse("assistant-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple admins + self.assertEqual(len(content_json), 2) + + # Assert the details of the retrieved admins match the created admins + 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["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["email"], assistant2.email) + + def test_assistant_detail_view(self): + + # Able to retrieve details of a single assistant. + + # Create an admin for testing with the name "Bob Peeters" + assistant = create_assistant( + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved admin match the created admin + self.assertEqual(int(content_json["id"]), assistant.id) + self.assertEqual(content_json["first_name"], assistant.first_name) + self.assertEqual(content_json["last_name"], assistant.last_name) + self.assertEqual(content_json["email"], assistant.email) + """ \ No newline at end of file diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py new file mode 100644 index 00000000..47a65e13 --- /dev/null +++ b/backend/api/tests/test_checks.py @@ -0,0 +1,148 @@ +import json + +from django.test import TestCase +from django.urls import reverse + +from ..models.checks import FileExtension + + +def create_fileExtension(id, extension): + """ + Create a FileExtension with the given arguments. + """ + return FileExtension.objects.create( + id=id, + extension=extension + ) + +""" +def create_checks(id, allowed_file_extensions, forbidden_file_extensions): + + # Create a Checks with the given arguments. + + return Checks.objects.create( + id=id, + allowed_file_extensions=allowed_file_extensions, + forbidden_file_extensions=forbidden_file_extensions + ) +""" + + +class FileExtensionModelTests(TestCase): + def test_no_fileExtension(self): + """ + able to retrieve no FileExtension before publishing it. + """ + + response_root = self.client.get(reverse("fileExtension-list"), follow=True) + # print(response.content) + self.assertEqual(response_root.status_code, 200) + # Assert that the response is JSON + self.assertEqual(response_root.accepted_media_type, "application/json") + # Parse the JSON content from the response + content_json = json.loads(response_root.content.decode("utf-8")) + # Assert that the parsed JSON is an empty list + self.assertEqual(content_json, []) + + def test_fileExtension_exists(self): + """ + Able to retrieve a single fileExtension after creating it. + """ + fileExtension = create_fileExtension( + id=5, + extension=".pdf" + ) + + # Make a GET request to retrieve the fileExtension + response = self.client.get(reverse("fileExtension-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one admin + self.assertEqual(len(content_json), 1) + + # Assert the details of the retrieved admin match the created admin + retrieved_fileExtension = content_json[0] + self.assertEqual( + int(retrieved_fileExtension["id"]), fileExtension.id) + 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" + ) + + # Make a GET request to retrieve the fileExtension + response = self.client.get(reverse("fileExtension-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple admins + self.assertEqual(len(content_json), 2) + + # Assert the details of the retrieved admins match the created admins + retrieved_fileExtension1, retrieved_fileExtension2 = content_json + self.assertEqual( + int(retrieved_fileExtension1["id"]), fileExtension1.id) + self.assertEqual( + retrieved_fileExtension1["extension"], fileExtension1.extension) + + self.assertEqual( + int(retrieved_fileExtension2["id"]), fileExtension2.id) + self.assertEqual( + retrieved_fileExtension2["extension"], fileExtension2.extension) + + def test_fileExtension_detail_view(self): + """ + Able to retrieve details of a single fileExtension. + """ + # Create an fileExtension for testing with the name "Bob Peeters" + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved fileExtension + # match the created fileExtension + self.assertEqual(int(content_json["id"]), fileExtension.id) + self.assertEqual(content_json["extension"], fileExtension.extension) From 81135349ce109019f65fba71986bebb32a9ab719 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 16:49:56 +0100 Subject: [PATCH 26/86] chore: checks tests --- backend/api/apps.py | 3 +- backend/api/serializers/checks_serializer.py | 6 +- backend/api/signals.py | 3 +- backend/api/tests/test_assistant.py | 293 +++++++++---------- backend/api/tests/test_checks.py | 28 +- 5 files changed, 169 insertions(+), 164 deletions(-) diff --git a/backend/api/apps.py b/backend/api/apps.py index 6cd16758..5e092e01 100644 --- a/backend/api/apps.py +++ b/backend/api/apps.py @@ -1,5 +1,6 @@ from django.apps import AppConfig + class ApiConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'api' @@ -9,4 +10,4 @@ def ready(self): from authentication.signals import user_created from api.signals import user_creation - user_created.connect(user_creation) \ No newline at end of file + user_created.connect(user_creation) diff --git a/backend/api/serializers/checks_serializer.py b/backend/api/serializers/checks_serializer.py index 51bbfd84..fd1aa13b 100644 --- a/backend/api/serializers/checks_serializer.py +++ b/backend/api/serializers/checks_serializer.py @@ -5,13 +5,13 @@ class ChecksSerializer(serializers.ModelSerializer): class Meta: model = Checks - fields = ['id', 'dockerfile'] + fields = ['id', 'dockerfile', + 'allowed_file_extensions', 'forbidden_file_extensions'] class FileExtensionSerializer(serializers.ModelSerializer): class Meta: model = FileExtension fields = [ - 'id', 'extension', - 'allowed_file_extensions', 'forbidden_file_extensions' + 'id', 'extension' ] diff --git a/backend/api/signals.py b/backend/api/signals.py index 767bff1e..4c1e73a8 100644 --- a/backend/api/signals.py +++ b/backend/api/signals.py @@ -1,5 +1,6 @@ from authentication.models import User + def user_creation(user: User, attributes: dict, **kwargs): # With Python 3.11, we need to import Student here. from api.models.student import Student @@ -8,4 +9,4 @@ def user_creation(user: User, attributes: dict, **kwargs): student_id = attributes.get("ugentStudentID") if student_id: - Student(user_ptr=user, student_id=student_id).save_base(raw=True) \ No newline at end of file + Student(user_ptr=user, student_id=student_id).save_base(raw=True) diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index c2ef3b5e..a316e574 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -4,152 +4,149 @@ from django.utils import timezone from django.urls import reverse -#from ..models.assistant import Assistant -""" -def create_assistant(id, first_name, last_name, email): - - # Create a Assistant with the given arguments. - - username = f"{first_name}_{last_name}" - return Assistant.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), - ) -""" - - -""" class AssistantModelTests(TestCase): - def test_no_assistant(self): - - # able to retrieve no assistant before publishing it. - - - response_root = self.client.get(reverse("assistant-list"), follow=True) - # print(response.content) - self.assertEqual(response_root.status_code, 200) - # Assert that the response is JSON - self.assertEqual(response_root.accepted_media_type, "application/json") - # Parse the JSON content from the response - content_json = json.loads(response_root.content.decode("utf-8")) - # Assert that the parsed JSON is an empty list - self.assertEqual(content_json, []) - - 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" - ) - - # Make a GET request to retrieve the assistant - response = self.client.get(reverse("assistant-list"), follow=True) - - # Check if the response was successful - self.assertEqual(response.status_code, 200) - - # Assert that the response is JSON - self.assertEqual(response.accepted_media_type, "application/json") - - # Parse the JSON content from the response - content_json = json.loads(response.content.decode("utf-8")) - - # Assert that the parsed JSON is a list with one admin - self.assertEqual(len(content_json), 1) - - # Assert the details of the retrieved admin match the created admin - 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["last_name"], assistant.last_name) - self.assertEqual(retrieved_assistant["email"], assistant.email) - - def test_multiple_assistant(self): - - # Able to retrieve multiple assistant after creating them. - - # Create multiple assistant - assistant1 = create_assistant( - 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" - ) - - # Make a GET request to retrieve the assistant - response = self.client.get(reverse("assistant-list"), follow=True) - - # Check if the response was successful - self.assertEqual(response.status_code, 200) - - # Assert that the response is JSON - self.assertEqual(response.accepted_media_type, "application/json") - - # Parse the JSON content from the response - content_json = json.loads(response.content.decode("utf-8")) - - # Assert that the parsed JSON is a list with multiple admins - self.assertEqual(len(content_json), 2) - - # Assert the details of the retrieved admins match the created admins - 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["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["email"], assistant2.email) - - def test_assistant_detail_view(self): - - # Able to retrieve details of a single assistant. - - # Create an admin for testing with the name "Bob Peeters" - assistant = create_assistant( - 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) - - # Check if the response was successful - self.assertEqual(response.status_code, 200) - - # Assert that the response is JSON - self.assertEqual(response.accepted_media_type, "application/json") - - # Parse the JSON content from the response - content_json = json.loads(response.content.decode("utf-8")) - - # Assert the details of the retrieved admin match the created admin - self.assertEqual(int(content_json["id"]), assistant.id) - self.assertEqual(content_json["first_name"], assistant.first_name) - self.assertEqual(content_json["last_name"], assistant.last_name) - self.assertEqual(content_json["email"], assistant.email) - """ \ No newline at end of file +# from ..models.assistant import Assistant + + +# def create_assistant(id, first_name, last_name, email): +# # Create a Assistant with the given arguments. +# username = f"{first_name}_{last_name}" +# return Assistant.objects.create( +# id=id, +# first_name=first_name, +# last_name=last_name, +# username=username, +# email=email, +# create_time=timezone.now(), +# ) + + +# class AssistantModelTests(TestCase): +# def test_no_assistant(self): +# """ +# able to retrieve no assistant before publishing it. +# """ + +# response_root = self.client.get(reverse("assistant-list"), follow=True) +# # print(response.content) +# self.assertEqual(response_root.status_code, 200) +# # Assert that the response is JSON +# self.assertEqual(response_root.accepted_media_type, "application/json") +# # Parse the JSON content from the response +# content_json = json.loads(response_root.content.decode("utf-8")) +# # Assert that the parsed JSON is an empty list +# self.assertEqual(content_json, []) + +# 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" +# ) + +# # Make a GET request to retrieve the assistant +# response = self.client.get(reverse("assistant-list"), follow=True) + +# # Check if the response was successful +# self.assertEqual(response.status_code, 200) + +# # Assert that the response is JSON +# self.assertEqual(response.accepted_media_type, "application/json") + +# # Parse the JSON content from the response +# content_json = json.loads(response.content.decode("utf-8")) + +# # Assert that the parsed JSON is a list with one admin +# self.assertEqual(len(content_json), 1) + +# # Assert the details of the retrieved admin match the created admin +# 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["last_name"], assistant.last_name) +# self.assertEqual(retrieved_assistant["email"], assistant.email) + +# def test_multiple_assistant(self): +# """ +# Able to retrieve multiple assistant after creating them. +# """ +# # Create multiple assistant +# assistant1 = create_assistant( +# 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" +# ) + +# # Make a GET request to retrieve the assistant +# response = self.client.get(reverse("assistant-list"), follow=True) + +# # Check if the response was successful +# self.assertEqual(response.status_code, 200) + +# # Assert that the response is JSON +# self.assertEqual(response.accepted_media_type, "application/json") + +# # Parse the JSON content from the response +# content_json = json.loads(response.content.decode("utf-8")) + +# # Assert that the parsed JSON is a list with multiple admins +# self.assertEqual(len(content_json), 2) + +# # Assert the details of the retrieved admins match the created admins +# 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["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["email"], assistant2.email) + +# def test_assistant_detail_view(self): +# """ +# Able to retrieve details of a single assistant. +# """ +# # Create an admin for testing with the name "Bob Peeters" +# assistant = create_assistant( +# 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) + +# # Check if the response was successful +# self.assertEqual(response.status_code, 200) + +# # Assert that the response is JSON +# self.assertEqual(response.accepted_media_type, "application/json") + +# # Parse the JSON content from the response +# content_json = json.loads(response.content.decode("utf-8")) + +# # Assert the details of the retrieved admin match the created admin +# self.assertEqual(int(content_json["id"]), assistant.id) +# self.assertEqual(content_json["first_name"], assistant.first_name) +# self.assertEqual(content_json["last_name"], assistant.last_name) +# self.assertEqual(content_json["email"], assistant.email) diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index 47a65e13..8b359ff8 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.urls import reverse -from ..models.checks import FileExtension +from ..models.checks import FileExtension, Checks def create_fileExtension(id, extension): @@ -15,17 +15,22 @@ def create_fileExtension(id, extension): extension=extension ) -""" + def create_checks(id, allowed_file_extensions, forbidden_file_extensions): # Create a Checks with the given arguments. - return Checks.objects.create( + check = Checks.objects.create( id=id, - allowed_file_extensions=allowed_file_extensions, - forbidden_file_extensions=forbidden_file_extensions ) -""" + + for ext in allowed_file_extensions: + check.allowed_file_extensions.add(ext) + + for ext in forbidden_file_extensions: + check.forbidden_file_extensions.add(ext) + + return check class FileExtensionModelTests(TestCase): @@ -34,7 +39,8 @@ 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) # print(response.content) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON @@ -50,7 +56,7 @@ def test_fileExtension_exists(self): """ fileExtension = create_fileExtension( id=5, - extension=".pdf" + extension="pdf" ) # Make a GET request to retrieve the fileExtension @@ -82,11 +88,11 @@ def test_multiple_fileExtension(self): # Create multiple fileExtension fileExtension1 = create_fileExtension( id=1, - extension=".jpg" + extension="jpg" ) fileExtension2 = create_fileExtension( id=2, - extension=".png" + extension="png" ) # Make a GET request to retrieve the fileExtension @@ -123,7 +129,7 @@ def test_fileExtension_detail_view(self): # Create an fileExtension for testing with the name "Bob Peeters" fileExtension = create_fileExtension( id=3, - extension=".zip" + extension="zip" ) # Make a GET request to retrieve the fileExtension details From 04dd9c88fa49fbb5557c4a1582f85196b32c9261 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Fri, 1 Mar 2024 15:56:10 +0100 Subject: [PATCH 27/86] fix: model relationships now with direct imports --- backend/api/apps.py | 1 - backend/api/models/assistant.py | 5 +- backend/api/models/group.py | 6 +- backend/api/models/project.py | 10 +- backend/api/models/student.py | 5 +- backend/api/models/submission.py | 46 ++--- backend/api/models/teacher.py | 5 +- backend/api/signals.py | 3 +- backend/api/tests/test_assistant.py | 292 ++++++++++++++-------------- 9 files changed, 188 insertions(+), 185 deletions(-) diff --git a/backend/api/apps.py b/backend/api/apps.py index 5e092e01..a13c8f06 100644 --- a/backend/api/apps.py +++ b/backend/api/apps.py @@ -6,7 +6,6 @@ class ApiConfig(AppConfig): name = 'api' def ready(self): - # Only here we can import from apps. from authentication.signals import user_created from api.signals import user_creation diff --git a/backend/api/models/assistant.py b/backend/api/models/assistant.py index 3489a776..a761e8a5 100644 --- a/backend/api/models/assistant.py +++ b/backend/api/models/assistant.py @@ -1,5 +1,6 @@ -from authentication.models import User from django.db import models +from authentication.models import User +from api.models.course import Course class Assistant(User): @@ -10,7 +11,7 @@ class Assistant(User): # All the courses the assistant is assisting in courses = models.ManyToManyField( - 'Course', + Course, # Allows us to access the assistants from the course related_name='assistants', blank=True diff --git a/backend/api/models/group.py b/backend/api/models/group.py index 3a8158c2..b820bcc3 100644 --- a/backend/api/models/group.py +++ b/backend/api/models/group.py @@ -1,4 +1,6 @@ from django.db import models +from api.models.project import Project +from api.models.student import Student class Group(models.Model): @@ -7,7 +9,7 @@ class Group(models.Model): # ID should be generated automatically project = models.ForeignKey( - 'Project', + Project, # 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 @@ -18,7 +20,7 @@ class Group(models.Model): # Students that are part of the group students = models.ManyToManyField( - 'Student', + Student, # This is how we can access groups from a student related_name='groups', blank=False, diff --git a/backend/api/models/project.py b/backend/api/models/project.py index a83ffe89..29e4264a 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -1,5 +1,7 @@ -import datetime +from datetime import datetime from django.db import models +from api.models.checks import Checks +from api.models.course import Course class Project(models.Model): @@ -30,7 +32,7 @@ class Project(models.Model): start_date = models.DateTimeField( # The default value is the current date and time - default=datetime.datetime.now, + default=datetime.now, blank=True, ) @@ -41,7 +43,7 @@ class Project(models.Model): # Check entity that is linked to the project checks = models.ForeignKey( - 'Checks', + Checks, # If the checks are deleted, the project should remain on_delete=models.SET_NULL, blank=True, @@ -50,7 +52,7 @@ class Project(models.Model): # Course that the project belongs to course = models.ForeignKey( - 'Course', + Course, # If the course is deleted, the project should be deleted as well on_delete=models.CASCADE, related_name='projects', diff --git a/backend/api/models/student.py b/backend/api/models/student.py index 8eec4be0..a11fe0f8 100644 --- a/backend/api/models/student.py +++ b/backend/api/models/student.py @@ -1,5 +1,6 @@ -from authentication.models import User from django.db import models +from authentication.models import User +from api.models.course import Course class Student(User): @@ -16,7 +17,7 @@ class Student(User): # All the courses the student is enrolled in courses = models.ManyToManyField( - 'Course', + Course, # Allows us to access the students from the course related_name='students', blank=True diff --git a/backend/api/models/submission.py b/backend/api/models/submission.py index 7adac13a..54a00133 100644 --- a/backend/api/models/submission.py +++ b/backend/api/models/submission.py @@ -1,26 +1,5 @@ from django.db import models - - -class SubmissionFile(models.Model): - """Model for a file that is part of a submission.""" - - # File ID should be generated automatically - - submission = models.ForeignKey( - 'Submission', - # If the submission is deleted, the file should be deleted as well - on_delete=models.CASCADE, - related_name='files', - blank=False, - null=False - ) - - # TODO - Set the right place to save the file - file = models.FileField( - blank=False, - null=False - ) - +from api.models.group import Group class Submission(models.Model): """Model for submission of a project by a group of students.""" @@ -28,7 +7,7 @@ class Submission(models.Model): # Submission ID should be generated automatically group = models.ForeignKey( - 'Group', + Group, # If the group is deleted, the submission should be deleted as well on_delete=models.CASCADE, related_name='submissions', @@ -50,3 +29,24 @@ class Submission(models.Model): class Meta: # A group can only have one submission with a specific number unique_together = ('group', 'submission_number') + + +class SubmissionFile(models.Model): + """Model for a file that is part of a submission.""" + + # File ID should be generated automatically + + submission = models.ForeignKey( + Submission, + # If the submission is deleted, the file should be deleted as well + on_delete=models.CASCADE, + related_name='files', + blank=False, + null=False + ) + + # TODO - Set the right place to save the file + file = models.FileField( + blank=False, + null=False + ) \ No newline at end of file diff --git a/backend/api/models/teacher.py b/backend/api/models/teacher.py index 37bb264b..e2e6260e 100644 --- a/backend/api/models/teacher.py +++ b/backend/api/models/teacher.py @@ -1,5 +1,6 @@ -from authentication.models import User from django.db import models +from api.models.course import Course +from authentication.models import User class Teacher(User): @@ -10,7 +11,7 @@ class Teacher(User): # All the courses the teacher is teaching courses = models.ManyToManyField( - 'Course', + Course, # Allows us to access the teachers from the course related_name='teachers', blank=True diff --git a/backend/api/signals.py b/backend/api/signals.py index 4c1e73a8..8b50a749 100644 --- a/backend/api/signals.py +++ b/backend/api/signals.py @@ -1,9 +1,8 @@ from authentication.models import User +from api.models.student import Student def user_creation(user: User, attributes: dict, **kwargs): - # With Python 3.11, we need to import Student here. - from api.models.student import Student """Upon user creation, auto-populate additional properties""" student_id = attributes.get("ugentStudentID") diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index a316e574..381e1a6a 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -3,150 +3,148 @@ from django.test import TestCase from django.utils import timezone from django.urls import reverse - - -# from ..models.assistant import Assistant - - -# def create_assistant(id, first_name, last_name, email): -# # Create a Assistant with the given arguments. -# username = f"{first_name}_{last_name}" -# return Assistant.objects.create( -# id=id, -# first_name=first_name, -# last_name=last_name, -# username=username, -# email=email, -# create_time=timezone.now(), -# ) - - -# class AssistantModelTests(TestCase): -# def test_no_assistant(self): -# """ -# able to retrieve no assistant before publishing it. -# """ - -# response_root = self.client.get(reverse("assistant-list"), follow=True) -# # print(response.content) -# self.assertEqual(response_root.status_code, 200) -# # Assert that the response is JSON -# self.assertEqual(response_root.accepted_media_type, "application/json") -# # Parse the JSON content from the response -# content_json = json.loads(response_root.content.decode("utf-8")) -# # Assert that the parsed JSON is an empty list -# self.assertEqual(content_json, []) - -# 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" -# ) - -# # Make a GET request to retrieve the assistant -# response = self.client.get(reverse("assistant-list"), follow=True) - -# # Check if the response was successful -# self.assertEqual(response.status_code, 200) - -# # Assert that the response is JSON -# self.assertEqual(response.accepted_media_type, "application/json") - -# # Parse the JSON content from the response -# content_json = json.loads(response.content.decode("utf-8")) - -# # Assert that the parsed JSON is a list with one admin -# self.assertEqual(len(content_json), 1) - -# # Assert the details of the retrieved admin match the created admin -# 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["last_name"], assistant.last_name) -# self.assertEqual(retrieved_assistant["email"], assistant.email) - -# def test_multiple_assistant(self): -# """ -# Able to retrieve multiple assistant after creating them. -# """ -# # Create multiple assistant -# assistant1 = create_assistant( -# 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" -# ) - -# # Make a GET request to retrieve the assistant -# response = self.client.get(reverse("assistant-list"), follow=True) - -# # Check if the response was successful -# self.assertEqual(response.status_code, 200) - -# # Assert that the response is JSON -# self.assertEqual(response.accepted_media_type, "application/json") - -# # Parse the JSON content from the response -# content_json = json.loads(response.content.decode("utf-8")) - -# # Assert that the parsed JSON is a list with multiple admins -# self.assertEqual(len(content_json), 2) - -# # Assert the details of the retrieved admins match the created admins -# 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["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["email"], assistant2.email) - -# def test_assistant_detail_view(self): -# """ -# Able to retrieve details of a single assistant. -# """ -# # Create an admin for testing with the name "Bob Peeters" -# assistant = create_assistant( -# 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) - -# # Check if the response was successful -# self.assertEqual(response.status_code, 200) - -# # Assert that the response is JSON -# self.assertEqual(response.accepted_media_type, "application/json") - -# # Parse the JSON content from the response -# content_json = json.loads(response.content.decode("utf-8")) - -# # Assert the details of the retrieved admin match the created admin -# self.assertEqual(int(content_json["id"]), assistant.id) -# self.assertEqual(content_json["first_name"], assistant.first_name) -# self.assertEqual(content_json["last_name"], assistant.last_name) -# self.assertEqual(content_json["email"], assistant.email) +from api.models.assistant import Assistant + + +def create_assistant(id, first_name, last_name, email): + # Create an Assistant with the given arguments. + username = f"{first_name}_{last_name}" + return Assistant.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) + + +class AssistantModelTests(TestCase): + def test_no_assistant(self): + """ + able to retrieve no assistant before publishing it. + """ + + response_root = self.client.get(reverse("assistant-list"), follow=True) + # print(response.content) + self.assertEqual(response_root.status_code, 200) + # Assert that the response is JSON + self.assertEqual(response_root.accepted_media_type, "application/json") + # Parse the JSON content from the response + content_json = json.loads(response_root.content.decode("utf-8")) + # Assert that the parsed JSON is an empty list + self.assertEqual(content_json, []) + + 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" + ) + + # Make a GET request to retrieve the assistant + response = self.client.get(reverse("assistant-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one admin + self.assertEqual(len(content_json), 1) + + # Assert the details of the retrieved admin match the created admin + 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["last_name"], assistant.last_name) + self.assertEqual(retrieved_assistant["email"], assistant.email) + + def test_multiple_assistant(self): + """ + Able to retrieve multiple assistant after creating them. + """ + # Create multiple assistant + assistant1 = create_assistant( + 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" + ) + + # Make a GET request to retrieve the assistant + response = self.client.get(reverse("assistant-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple admins + self.assertEqual(len(content_json), 2) + + # Assert the details of the retrieved admins match the created admins + 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["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["email"], assistant2.email) + + def test_assistant_detail_view(self): + """ + Able to retrieve details of a single assistant. + """ + # Create an admin for testing with the name "Bob Peeters" + assistant = create_assistant( + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved admin match the created admin + self.assertEqual(int(content_json["id"]), assistant.id) + self.assertEqual(content_json["first_name"], assistant.first_name) + self.assertEqual(content_json["last_name"], assistant.last_name) + self.assertEqual(content_json["email"], assistant.email) From 102f1ceadd01b63a9b2b9465faa6d2bf68a1d740 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 16:49:26 +0100 Subject: [PATCH 28/86] 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 29/86] 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 30/86] 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 f920fbbe0c9b56a755402587ba7f1b6c81a077cd Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 20:07:15 +0100 Subject: [PATCH 31/86] chore: add checks tests and fix admin tests --- backend/api/serializers/admin_serializer.py | 2 - backend/api/serializers/checks_serializer.py | 3 + backend/api/tests/test_checks.py | 64 ++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/backend/api/serializers/admin_serializer.py b/backend/api/serializers/admin_serializer.py index 2468e4ba..2603278b 100644 --- a/backend/api/serializers/admin_serializer.py +++ b/backend/api/serializers/admin_serializer.py @@ -3,8 +3,6 @@ class AdminSerializer(serializers.ModelSerializer): - - class Meta: model = Admin fields = [ diff --git a/backend/api/serializers/checks_serializer.py b/backend/api/serializers/checks_serializer.py index fd1aa13b..d6f45b75 100644 --- a/backend/api/serializers/checks_serializer.py +++ b/backend/api/serializers/checks_serializer.py @@ -3,6 +3,9 @@ class ChecksSerializer(serializers.ModelSerializer): + allowed_file_extensions = serializers.StringRelatedField(many=True) + forbidden_file_extensions = serializers.StringRelatedField(many=True) + class Meta: model = Checks fields = ['id', 'dockerfile', diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index 8b359ff8..2d00e8b6 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -152,3 +152,67 @@ def test_fileExtension_detail_view(self): # match the created fileExtension self.assertEqual(int(content_json["id"]), fileExtension.id) self.assertEqual(content_json["extension"], fileExtension.extension) + + +class ChecksModelTests(TestCase): + def test_no_checks(self): + """ + Able to retrieve no Checks before publishing it. + """ + 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")) + self.assertEqual(content_json, []) + + def test_checks_exists(self): + """ + Able to retrieve a single Checks after creating it. + """ + # Create a Checks instance with some file extensions + fileExtension1 = create_fileExtension(id=1, extension="jpg") + fileExtension2 = create_fileExtension(id=2, extension="png") + fileExtension3 = create_fileExtension(id=3, extension="tar") + fileExtension4 = create_fileExtension(id=4, extension="wfp") + checks = create_checks( + id=5, + allowed_file_extensions=[fileExtension1, fileExtension4], + forbidden_file_extensions=[fileExtension2, fileExtension3] + ) + + # Make a GET request to retrieve the Checks + response = self.client.get(reverse("check-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one Checks + self.assertEqual(len(content_json), 1) + + # Assert the details of the retrieved Checks match the created Checks + retrieved_checks = content_json[0] + self.assertEqual(int(retrieved_checks["id"]), checks.id) + + # Assert the file extensions of the retrieved + # Checks match the created 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], fileExtension1.extension) + self.assertEqual( + retrieved_allowed_file_extensions[1], fileExtension4.extension) + + retrieved_forbidden_file_extensions = retrieved_checks[ + "forbidden_file_extensions"] + self.assertEqual(len(retrieved_forbidden_file_extensions), 2) + self.assertEqual( + retrieved_forbidden_file_extensions[0], fileExtension2.extension) + self.assertEqual( + retrieved_forbidden_file_extensions[1], fileExtension3.extension) From acaca7f7c5c54890306615ac42f8808105b07e04 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 20:22:51 +0100 Subject: [PATCH 32/86] chore: add course tests --- backend/api/tests/test_course.py | 115 +++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 backend/api/tests/test_course.py diff --git a/backend/api/tests/test_course.py b/backend/api/tests/test_course.py new file mode 100644 index 00000000..21ff40b7 --- /dev/null +++ b/backend/api/tests/test_course.py @@ -0,0 +1,115 @@ +import json + +from django.test import TestCase +from django.urls import reverse +from ..models.course import Course + + +def create_course(name, academic_startyear, description=None, + parent_course=None): + """ + Create a Course with the given arguments. + """ + return Course.objects.create( + name=name, + academic_startyear=academic_startyear, + description=description, + parent_course=parent_course + ) + + +class CourseModelTests(TestCase): + def test_no_courses(self): + """ + Able to retrieve no courses before publishing any. + """ + response_root = self.client.get(reverse("course-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")) + self.assertEqual(content_json, []) + + def test_course_exists(self): + """ + Able to retrieve a single course after creating it. + """ + course = create_course( + name="Introduction to Computer Science", + academic_startyear=2022, + description="An introductory course on computer science." + ) + + response = self.client.get(reverse("course-list"), 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")) + + self.assertEqual(len(content_json), 1) + + retrieved_course = content_json[0] + self.assertEqual(retrieved_course["name"], course.name) + self.assertEqual( + retrieved_course["academic_startyear"], course.academic_startyear) + self.assertEqual(retrieved_course["description"], course.description) + + def test_multiple_courses(self): + """ + Able to retrieve multiple courses after creating them. + """ + course1 = create_course( + name="Mathematics 101", + academic_startyear=2022, + description="A basic mathematics course." + ) + course2 = create_course( + name="Physics 101", + academic_startyear=2022, + description="An introductory physics course." + ) + + response = self.client.get(reverse("course-list"), 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")) + + self.assertEqual(len(content_json), 2) + + retrieved_course1, retrieved_course2 = content_json + self.assertEqual(retrieved_course1["name"], course1.name) + self.assertEqual( + 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) + self.assertEqual(retrieved_course2["description"], course2.description) + + def test_course_detail_view(self): + """ + Able to retrieve details of a single course. + """ + course = create_course( + name="Chemistry 101", + academic_startyear=2022, + description="An introductory chemistry course." + ) + + response = self.client.get( + reverse("course-detail", args=[str(course.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")) + + self.assertEqual(content_json["name"], course.name) + self.assertEqual( + content_json["academic_startyear"], course.academic_startyear) + self.assertEqual(content_json["description"], course.description) From 905354156d7ab81e1526374786a9642503188ec0 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 20:31:34 +0100 Subject: [PATCH 33/86] 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 54d1eaf64076eaf91e36eddc0f0312bffa10d390 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 21:54:33 +0100 Subject: [PATCH 34/86] chore: add group tests --- backend/api/tests/test_group.py | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 backend/api/tests/test_group.py diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py new file mode 100644 index 00000000..e676fce7 --- /dev/null +++ b/backend/api/tests/test_group.py @@ -0,0 +1,156 @@ +import json +from datetime import timedelta +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone +from ..models.project import Project +from ..models.student import Student +from ..models.group import Group +from ..models.course import Course + + +def create_course(name, academic_startyear, description=None, + parent_course=None): + """ + Create a Course with the given arguments. + """ + return Course.objects.create( + name=name, + academic_startyear=academic_startyear, + description=description, + parent_course=parent_course + ) + + +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 + ) + + +def create_student(id, first_name, last_name, email): + """Create a Student with the given arguments.""" + username = f"{first_name}_{last_name}" + return Student.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email + ) + + +class GroupModelTests(TestCase): + def test_no_groups(self): + """Able to retrieve no groups before creating any.""" + response = self.client.get(reverse("group-list"), 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")) + self.assertEqual(content_json, []) + + def test_group_exists(self): + """Able to retrieve a single group after creating it.""" + course = create_course( + name="sel2", + academic_startyear=2023 + ) + + project = create_project( + 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" + ) + + group = Group.objects.create(project=project, score=80) + group.students.add(student) + + response = self.client.get(reverse("group-list"), 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")) + self.assertEqual(len(content_json), 1) + + retrieved_group = content_json[0] + expected_project_url = "http://testserver" + reverse( + "project-detail", args=[str(project.id)] + ) + + self.assertEqual(retrieved_group["project"], expected_project_url) + self.assertEqual(int(retrieved_group["id"]), group.id) + self.assertEqual(retrieved_group["score"], group.score) + + def test_multiple_groups(self): + """Able to retrieve multiple groups after creating them.""" + course = create_course( + name="sel2", + academic_startyear=2023 + ) + + project1 = create_project(name="Project 1", description="Description 1", days=7, course=course) + project2 = create_project(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") + student2 = create_student(id=3, first_name="Jane", last_name="Doe", email="jane.doe@example.com") + + group1 = Group.objects.create(project=project1, score=80) + group1.students.add(student1) + + group2 = Group.objects.create(project=project2, score=90) + group2.students.add(student1, student2) + + response = self.client.get(reverse("group-list"), 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")) + self.assertEqual(len(content_json), 2) + + retrieved_group1, retrieved_group2 = content_json + expected_project_url1 = "http://testserver" + reverse("project-detail", args=[str(project1.id)]) + expected_project_url2 = "http://testserver" + reverse("project-detail", args=[str(project2.id)]) + + self.assertEqual(retrieved_group1["project"], expected_project_url1) + self.assertEqual(int(retrieved_group1["id"]), group1.id) + self.assertEqual(retrieved_group1["score"], group1.score) + + self.assertEqual(retrieved_group2["project"], expected_project_url2) + self.assertEqual(int(retrieved_group2["id"]), group2.id) + self.assertEqual(retrieved_group2["score"], group2.score) + + def test_group_detail_view(self): + """Able to retrieve details of a single group.""" + course = create_course( + name="sel2", + academic_startyear=2023 + ) + + project = create_project(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") + + group = Group.objects.create(project=project, score=80) + group.students.add(student) + + response = self.client.get(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)]) + + self.assertEqual(int(content_json["id"]), group.id) + self.assertEqual(content_json["project"], expected_project_url) + self.assertEqual(content_json["score"], group.score) From d39c71bf869a4387c7e41a44d0b5eafb1f86e2c4 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Fri, 1 Mar 2024 22:45:41 +0100 Subject: [PATCH 35/86] 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 115ae17fe846099fbdb6e61c1faee549eeb1561d Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 23:08:01 +0100 Subject: [PATCH 36/86] chore: add student test --- backend/api/tests/test_group.py | 51 +++++++-- backend/api/tests/test_project.py | 172 ++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 backend/api/tests/test_project.py diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index e676fce7..5734f50c 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -100,11 +100,29 @@ def test_multiple_groups(self): academic_startyear=2023 ) - project1 = create_project(name="Project 1", description="Description 1", days=7, course=course) - project2 = create_project(name="Project 2", description="Description 2", days=7, course=course) + project1 = create_project( + name="Project 1", + description="Description 1", + days=7, course=course + ) + project2 = create_project( + 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") - student2 = create_student(id=3, first_name="Jane", last_name="Doe", email="jane.doe@example.com") + student1 = create_student( + 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" + ) group1 = Group.objects.create(project=project1, score=80) group1.students.add(student1) @@ -119,8 +137,10 @@ def test_multiple_groups(self): self.assertEqual(len(content_json), 2) retrieved_group1, retrieved_group2 = content_json - expected_project_url1 = "http://testserver" + reverse("project-detail", args=[str(project1.id)]) - expected_project_url2 = "http://testserver" + reverse("project-detail", args=[str(project2.id)]) + expected_project_url1 = "http://testserver" + reverse( + "project-detail", args=[str(project1.id)]) + expected_project_url2 = "http://testserver" + reverse( + "project-detail", args=[str(project2.id)]) self.assertEqual(retrieved_group1["project"], expected_project_url1) self.assertEqual(int(retrieved_group1["id"]), group1.id) @@ -137,19 +157,30 @@ def test_group_detail_view(self): academic_startyear=2023 ) - project = create_project(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") + project = create_project( + 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") group = Group.objects.create(project=project, score=80) group.students.add(student) - response = self.client.get(reverse("group-detail", args=[str(group.id)]), follow=True) + response = self.client.get( + 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)]) + expected_project_url = "http://testserver" + reverse( + "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_project.py b/backend/api/tests/test_project.py new file mode 100644 index 00000000..16a7d081 --- /dev/null +++ b/backend/api/tests/test_project.py @@ -0,0 +1,172 @@ +import json +from django.test import TestCase +from django.utils import timezone +from django.urls import reverse +from ..models.project import Project +from ..models.course import Course +from ..models.checks import Checks + + +def create_course(id, name, academic_startyear): + return Course.objects.create( + id=id, name=name, academic_startyear=academic_startyear) + + +def create_checks(): + return Checks.objects.create() + + +def create_project( + name, + description, + visible, + archived, + days, + checks, + course +): + deadline = timezone.now() + timezone.timedelta(days=days) + + return Project.objects.create( + name=name, + description=description, + visible=visible, + archived=archived, + deadline=deadline, + checks=checks, + course=course + ) + + +class ProjectModelTests(TestCase): + def test_no_projects(self): + """Able to retrieve no projects before creating any.""" + response = self.client.get(reverse("group-list"), 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")) + self.assertEqual(content_json, []) + + def test_project_exists(self): + """ + Able to retrieve a single project after creating it. + """ + course = create_course( + id=1, + name="test course", + academic_startyear=2024 + ) + checks = create_checks() + + project = create_project( + name="test project", + description="test description", + visible=True, + archived=False, + days=7, + checks=checks, + course=course + ) + + response = self.client.get(reverse("project-list"), 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")) + + self.assertEqual(len(content_json), 1) + + retrieved_project = content_json[0] + + expected_checks_url = "http://testserver" + reverse( + "check-detail", args=[str(checks.id)] + ) + + expected_course_url = "http://testserver" + reverse( + "course-detail", args=[str(course.id)] + ) + + self.assertEqual(retrieved_project["name"], project.name) + self.assertEqual(retrieved_project["description"], project.description) + self.assertEqual(retrieved_project["visible"], project.visible) + self.assertEqual(retrieved_project["archived"], project.archived) + # self.assertEqual(retrieved_project["checks"], expected_checks_url) + self.assertEqual(retrieved_project["course"], expected_course_url) + + def test_multiple_project(self): + """ + Able to retrieve multiple projects after creating it. + """ + course = create_course( + id=1, + name="test course", + academic_startyear=2024 + ) + + checks = create_checks() + + project = create_project( + name="test project", + description="test description", + visible=True, + archived=False, + days=7, + checks=checks, + course=course + ) + + project2 = create_project( + name="test project2", + description="test description2", + visible=True, + archived=False, + days=7, + checks=checks, + course=course + ) + + response = self.client.get(reverse("project-list"), 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")) + + self.assertEqual(len(content_json), 2) + + retrieved_project = content_json[0] + + expected_checks_url = "http://testserver" + reverse( + "check-detail", args=[str(checks.id)] + ) + + expected_course_url = "http://testserver" + reverse( + "course-detail", args=[str(course.id)] + ) + + self.assertEqual(retrieved_project["name"], project.name) + self.assertEqual(retrieved_project["description"], project.description) + self.assertEqual(retrieved_project["visible"], project.visible) + self.assertEqual(retrieved_project["archived"], project.archived) + # TODO + # self.assertEqual(retrieved_project["checks"], expected_checks_url) + self.assertEqual(retrieved_project["course"], expected_course_url) + + retrieved_project = content_json[1] + + expected_checks_url = "http://testserver" + reverse( + "check-detail", args=[str(checks.id)] + ) + + expected_course_url = "http://testserver" + reverse( + "course-detail", args=[str(course.id)] + ) + + self.assertEqual(retrieved_project["name"], project2.name) + self.assertEqual(retrieved_project["description"], project2.description) + self.assertEqual(retrieved_project["visible"], project2.visible) + self.assertEqual(retrieved_project["archived"], project2.archived) + # TODO + # self.assertEqual(retrieved_project["checks"], expected_checks_url) + self.assertEqual(retrieved_project["course"], expected_course_url) From 16f282a912176d6aa36fc8d730fef24c6606adb9 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 23:22:49 +0100 Subject: [PATCH 37/86] chore: add student tests --- backend/api/tests/test_student.py | 150 ++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 backend/api/tests/test_student.py diff --git a/backend/api/tests/test_student.py b/backend/api/tests/test_student.py new file mode 100644 index 00000000..57542435 --- /dev/null +++ b/backend/api/tests/test_student.py @@ -0,0 +1,150 @@ +import json + +from django.test import TestCase +from django.utils import timezone +from django.urls import reverse +from ..models.student import Student + + +def create_student(id, first_name, last_name, email): + # Create an Assistant with the given arguments. + username = f"{first_name}_{last_name}" + return Student.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) + + +class StudentModelTests(TestCase): + def test_no_student(self): + """ + able to retrieve no student before publishing it. + """ + + response_root = self.client.get(reverse("student-list"), follow=True) + # print(response.content) + self.assertEqual(response_root.status_code, 200) + # Assert that the response is JSON + self.assertEqual(response_root.accepted_media_type, "application/json") + # Parse the JSON content from the response + content_json = json.loads(response_root.content.decode("utf-8")) + # Assert that the parsed JSON is an empty list + self.assertEqual(content_json, []) + + 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" + ) + + # Make a GET request to retrieve the student + response = self.client.get(reverse("student-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one admin + self.assertEqual(len(content_json), 1) + + # Assert the details of the retrieved admin match the created admin + 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["last_name"], student.last_name) + self.assertEqual(retrieved_student["email"], student.email) + + def test_multiple_students(self): + """ + Able to retrieve multiple students after creating them. + """ + # Create multiple assistant + student1 = create_student( + 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" + ) + + # Make a GET request to retrieve the student + response = self.client.get(reverse("student-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple students + self.assertEqual(len(content_json), 2) + + # Assert the details of the retrieved admins 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["email"], student1.email) + + 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["email"], student1.email) + + def test_student_detail_view(self): + """ + Able to retrieve details of a single student. + """ + # Create an admin for testing with the name "Bob Peeters" + student = create_student( + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved admin match the created admin + self.assertEqual(int(content_json["id"]), student.id) + self.assertEqual(content_json["first_name"], student.first_name) + self.assertEqual(content_json["last_name"], student.last_name) + self.assertEqual(content_json["email"], student.email) From a74c1b3851aa30ee83f971b24a5f860f4fd676b0 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 23:38:48 +0100 Subject: [PATCH 38/86] chore: add teacher tests --- backend/api/tests/test_project.py | 3 +- backend/api/tests/test_student.py | 10 +- backend/api/tests/test_teacher.py | 150 ++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 backend/api/tests/test_teacher.py diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index 16a7d081..18848e0a 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -164,7 +164,8 @@ def test_multiple_project(self): ) 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) # TODO diff --git a/backend/api/tests/test_student.py b/backend/api/tests/test_student.py index 57542435..688ab855 100644 --- a/backend/api/tests/test_student.py +++ b/backend/api/tests/test_student.py @@ -102,7 +102,7 @@ def test_multiple_students(self): # Assert that the parsed JSON is a list with multiple students self.assertEqual(len(content_json), 2) - # Assert the details of the retrieved admins match the created students + # Assert the details of the retrieved students match the created students retrieved_student1, retrieved_student2 = content_json self.assertEqual(int(retrieved_student1["id"]), student1.id) self.assertEqual( @@ -111,12 +111,12 @@ def test_multiple_students(self): retrieved_student1["last_name"], student1.last_name) self.assertEqual(retrieved_student1["email"], student1.email) - self.assertEqual(int(retrieved_student1["id"]), student1.id) + self.assertEqual(int(retrieved_student2["id"]), student2.id) self.assertEqual( - retrieved_student1["first_name"], student1.first_name) + retrieved_student2["first_name"], student2.first_name) self.assertEqual( - retrieved_student1["last_name"], student1.last_name) - self.assertEqual(retrieved_student1["email"], student1.email) + retrieved_student2["last_name"], student2.last_name) + self.assertEqual(retrieved_student2["email"], student2.email) def test_student_detail_view(self): """ diff --git a/backend/api/tests/test_teacher.py b/backend/api/tests/test_teacher.py new file mode 100644 index 00000000..7e2fdd88 --- /dev/null +++ b/backend/api/tests/test_teacher.py @@ -0,0 +1,150 @@ +import json + +from django.test import TestCase +from django.utils import timezone +from django.urls import reverse +from ..models.teacher import Teacher + + +def create_teacher(id, first_name, last_name, email): + # Create an Teacher with the given arguments. + username = f"{first_name}_{last_name}" + return Teacher.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) + + +class TeacherModelTests(TestCase): + def test_no_teacher(self): + """ + able to retrieve no teacher before publishing it. + """ + + response_root = self.client.get(reverse("teacher-list"), follow=True) + # print(response.content) + self.assertEqual(response_root.status_code, 200) + # Assert that the response is JSON + self.assertEqual(response_root.accepted_media_type, "application/json") + # Parse the JSON content from the response + content_json = json.loads(response_root.content.decode("utf-8")) + # Assert that the parsed JSON is an empty list + self.assertEqual(content_json, []) + + 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" + ) + + # Make a GET request to retrieve the teacher + response = self.client.get(reverse("teacher-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one teacher + self.assertEqual(len(content_json), 1) + + # 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["last_name"], teacher.last_name) + self.assertEqual(retrieved_teacher["email"], teacher.email) + + def test_multiple_teachers(self): + """ + Able to retrieve multiple teachers after creating them. + """ + # Create multiple assistant + teacher1 = create_teacher( + 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" + ) + + # Make a GET request to retrieve the teacher + response = self.client.get(reverse("teacher-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple teacher + self.assertEqual(len(content_json), 2) + + # 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["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["email"], teacher2.email) + + def test_teacher_detail_view(self): + """ + Able to retrieve details of a single teacher. + """ + # 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" + ) + + # Make a GET request to retrieve the teacher details + response = self.client.get( + reverse("teacher-detail", args=[str(teacher.id)]), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved teacher match the created teacher + self.assertEqual(int(content_json["id"]), teacher.id) + self.assertEqual(content_json["first_name"], teacher.first_name) + self.assertEqual(content_json["last_name"], teacher.last_name) + self.assertEqual(content_json["email"], teacher.email) From 415e9876834ad77629e99714fc8ef5f65d558a3d Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Fri, 1 Mar 2024 23:42:23 +0100 Subject: [PATCH 39/86] fix: fix linter warnings and mistakes in comments --- backend/api/tests/test_assistant.py | 13 +++++++------ backend/api/tests/test_student.py | 11 ++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index 381e1a6a..e0bc3d9c 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -58,10 +58,10 @@ def test_assistant_exists(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with one admin + # Assert that the parsed JSON is a list with one student self.assertEqual(len(content_json), 1) - # Assert the details of the retrieved admin match the created admin + # Assert the details of the retrieved student match the created student retrieved_assistant = content_json[0] self.assertEqual(int(retrieved_assistant["id"]), assistant.id) self.assertEqual( @@ -99,10 +99,11 @@ def test_multiple_assistant(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with multiple admins + # Assert that the parsed JSON is a list with multiple students self.assertEqual(len(content_json), 2) - # Assert the details of the retrieved admins match the created admins + # Assert the details of the retrieved + # students match the created students retrieved_assistant1, retrieved_assistant2 = content_json self.assertEqual(int(retrieved_assistant1["id"]), assistant1.id) self.assertEqual( @@ -122,7 +123,7 @@ def test_assistant_detail_view(self): """ Able to retrieve details of a single assistant. """ - # Create an admin for testing with the name "Bob Peeters" + # Create an student for testing with the name "Bob Peeters" assistant = create_assistant( id=5, first_name="Bob", @@ -143,7 +144,7 @@ def test_assistant_detail_view(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert the details of the retrieved admin match the created admin + # Assert the details of the retrieved student match the created student self.assertEqual(int(content_json["id"]), assistant.id) self.assertEqual(content_json["first_name"], assistant.first_name) self.assertEqual(content_json["last_name"], assistant.last_name) diff --git a/backend/api/tests/test_student.py b/backend/api/tests/test_student.py index 688ab855..f58f3533 100644 --- a/backend/api/tests/test_student.py +++ b/backend/api/tests/test_student.py @@ -58,10 +58,10 @@ def test_student_exists(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with one admin + # Assert that the parsed JSON is a list with one student self.assertEqual(len(content_json), 1) - # Assert the details of the retrieved admin match the created admin + # 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( @@ -102,7 +102,8 @@ def test_multiple_students(self): # Assert that the parsed JSON is a list with multiple students self.assertEqual(len(content_json), 2) - # Assert the details of the retrieved students match the created students + # Assert the details of the retrieved students + # match the created students retrieved_student1, retrieved_student2 = content_json self.assertEqual(int(retrieved_student1["id"]), student1.id) self.assertEqual( @@ -122,7 +123,7 @@ def test_student_detail_view(self): """ Able to retrieve details of a single student. """ - # Create an admin for testing with the name "Bob Peeters" + # Create an student for testing with the name "Bob Peeters" student = create_student( id=5, first_name="Bob", @@ -143,7 +144,7 @@ def test_student_detail_view(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert the details of the retrieved admin match the created admin + # Assert the details of the retrieved student match the created student self.assertEqual(int(content_json["id"]), student.id) self.assertEqual(content_json["first_name"], student.first_name) self.assertEqual(content_json["last_name"], student.last_name) From 4adaa072817355ff1390315a6c426e72c6830589 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Sat, 2 Mar 2024 10:17:00 +0100 Subject: [PATCH 40/86] 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 0899b23b6ee01bef3a71af4e4a1c166ab9a7eaad Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 11:18:37 +0100 Subject: [PATCH 41/86] chore: add deadline aproaching in and deadline passed functions to project --- backend/api/models/project.py | 12 ++++- backend/api/tests/test_project.py | 83 ++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/backend/api/models/project.py b/backend/api/models/project.py index 29e4264a..f766d82b 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -1,5 +1,6 @@ -from datetime import datetime +from datetime import timedelta, datetime from django.db import models +from django.utils import timezone from api.models.checks import Checks from api.models.course import Course @@ -59,3 +60,12 @@ class Project(models.Model): blank=False, null=False ) + + def deadline_approaching_in(self, days=7): + now = timezone.now() + approaching_date = now + timezone.timedelta(days=days) + return now <= self.deadline <= approaching_date + + def deadline_passed(self): + now = timezone.now() + return now > self.deadline diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index 18848e0a..301d2b7a 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -1,4 +1,5 @@ import json +from datetime import datetime from django.test import TestCase from django.utils import timezone from django.urls import reverse @@ -39,6 +40,76 @@ def create_project( class ProjectModelTests(TestCase): + 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) + checks = create_checks() + past_project = create_project( + name="test", description="descr", visible=True, archived=False, + days=-10, checks=checks, course=course + ) + self.assertIs(past_project.deadline_approaching_in(), False) + + 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) + checks = create_checks() + future_project = create_project( + name="test", description="descr", visible=True, archived=False, + days=6, checks=checks, course=course + ) + self.assertIs(future_project.deadline_approaching_in(days=7), True) + + 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) + checks = create_checks() + future_project = create_project( + name="test", description="descr", visible=True, archived=False, + days=8, checks=checks, course=course + ) + self.assertIs(future_project.deadline_approaching_in(days=7), False) + + 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) + checks = create_checks() + future_project = create_project( + name="test", description="descr", visible=True, archived=False, + days=1, checks=checks, course=course + ) + self.assertIs(future_project.deadline_passed(), False) + + 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) + checks = create_checks() + past_project = create_project( + name="test", description="descr", visible=True, archived=False, + days=-1, checks=checks, course=course + ) + self.assertIs(past_project.deadline_passed(), True) + def test_no_projects(self): """Able to retrieve no projects before creating any.""" response = self.client.get(reverse("group-list"), follow=True) @@ -51,13 +122,13 @@ def test_project_exists(self): """ Able to retrieve a single project after creating it. """ + course = create_course( - id=1, + id=3, name="test course", academic_startyear=2024 - ) + ) checks = create_checks() - project = create_project( name="test project", description="test description", @@ -99,13 +170,11 @@ def test_multiple_project(self): Able to retrieve multiple projects after creating it. """ course = create_course( - id=1, + id=3, name="test course", academic_startyear=2024 - ) - + ) checks = create_checks() - project = create_project( name="test project", description="test description", From 905c8d537e7de72b746b277c7c62708b5857faef Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Sat, 2 Mar 2024 11:22:06 +0100 Subject: [PATCH 42/86] chore: update submission serializer --- backend/api/serializers/submision_serializer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/api/serializers/submision_serializer.py b/backend/api/serializers/submision_serializer.py index 3429022d..95d26dbf 100644 --- a/backend/api/serializers/submision_serializer.py +++ b/backend/api/serializers/submision_serializer.py @@ -5,7 +5,7 @@ class SubmissionFileSerializer(serializers.ModelSerializer): class Meta: model = SubmissionFile - fields = ['id', 'submission', 'file'] + fields = ['file'] class SubmissionSerializer(serializers.ModelSerializer): @@ -16,6 +16,10 @@ class SubmissionSerializer(serializers.ModelSerializer): view_name='group-detail' ) + files = SubmissionFileSerializer(many=True, read_only=True) + class Meta: model = Submission - fields = ['id', 'group', 'submission_number', 'submission_time'] + fields = ['id', 'group', 'submission_number', 'submission_time', + 'files' + ] From 7e208d74028c6d9d0be8913f449dbbeba3651eb2 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Sat, 2 Mar 2024 11:23:34 +0100 Subject: [PATCH 43/86] chore: extend fixtures with submissions + admin --- backend/api/fixtures/admins.yaml | 3 +++ backend/api/fixtures/courses.yaml | 7 +++++++ backend/api/fixtures/groups.yaml | 2 +- backend/api/fixtures/submissions.yaml | 24 ++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 backend/api/fixtures/admins.yaml create mode 100644 backend/api/fixtures/submissions.yaml diff --git a/backend/api/fixtures/admins.yaml b/backend/api/fixtures/admins.yaml new file mode 100644 index 00000000..a0ad3113 --- /dev/null +++ b/backend/api/fixtures/admins.yaml @@ -0,0 +1,3 @@ +- model: api.admin + pk: '2' + fields: {} diff --git a/backend/api/fixtures/courses.yaml b/backend/api/fixtures/courses.yaml index 154f0e0b..4a8677c9 100644 --- a/backend/api/fixtures/courses.yaml +++ b/backend/api/fixtures/courses.yaml @@ -11,4 +11,11 @@ name: Sel2 academic_startyear: 2023 description: Software course + parent_course: 3 +- model: api.course + pk: 3 + fields: + name: Sel1 + academic_startyear: 2022 + description: Software course parent_course: null diff --git a/backend/api/fixtures/groups.yaml b/backend/api/fixtures/groups.yaml index b8adc460..35b2d571 100644 --- a/backend/api/fixtures/groups.yaml +++ b/backend/api/fixtures/groups.yaml @@ -2,7 +2,7 @@ pk: 1 fields: project: 123456 - score: null + score: 7 students: - '1' - '2' diff --git a/backend/api/fixtures/submissions.yaml b/backend/api/fixtures/submissions.yaml new file mode 100644 index 00000000..0b8f876d --- /dev/null +++ b/backend/api/fixtures/submissions.yaml @@ -0,0 +1,24 @@ +- model: api.submission + pk: 1 + fields: + group: 1 + submission_number: 1 + submission_time: '2021-01-01T00:00:00Z' +- model: api.submission + pk: 2 + fields: + group: 1 + submission_number: 2 + submission_time: '2021-01-02T00:00:00Z' + + +- model: api.submissionfile + pk: 1 + fields: + submission: 1 + file: 'submissions/1/1/1.txt' +- model: api.submissionfile + pk: 2 + fields: + submission: 2 + file: 'submissions/1/2/1.txt' From c5552430582f76a25fa4995cb3bed22e579845cf Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 11:34:19 +0100 Subject: [PATCH 44/86] chore: merge basic_app --- backend/api/fixtures/admins.yaml | 3 ++ backend/api/fixtures/courses.yaml | 7 +++++ backend/api/fixtures/groups.yaml | 2 +- backend/api/fixtures/submissions.yaml | 24 ++++++++++++++++ backend/api/serializers/admin_serializer.py | 7 +++++ .../api/serializers/assistant_serializer.py | 6 ++++ backend/api/serializers/student_serializer.py | 6 ++++ .../api/serializers/submision_serializer.py | 8 ++++-- backend/api/serializers/teacher_serializer.py | 6 ++++ backend/authentication/serializers.py | 28 +++++++++++++++---- 10 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 backend/api/fixtures/admins.yaml create mode 100644 backend/api/fixtures/submissions.yaml diff --git a/backend/api/fixtures/admins.yaml b/backend/api/fixtures/admins.yaml new file mode 100644 index 00000000..a0ad3113 --- /dev/null +++ b/backend/api/fixtures/admins.yaml @@ -0,0 +1,3 @@ +- model: api.admin + pk: '2' + fields: {} diff --git a/backend/api/fixtures/courses.yaml b/backend/api/fixtures/courses.yaml index 154f0e0b..4a8677c9 100644 --- a/backend/api/fixtures/courses.yaml +++ b/backend/api/fixtures/courses.yaml @@ -11,4 +11,11 @@ name: Sel2 academic_startyear: 2023 description: Software course + parent_course: 3 +- model: api.course + pk: 3 + fields: + name: Sel1 + academic_startyear: 2022 + description: Software course parent_course: null diff --git a/backend/api/fixtures/groups.yaml b/backend/api/fixtures/groups.yaml index b8adc460..35b2d571 100644 --- a/backend/api/fixtures/groups.yaml +++ b/backend/api/fixtures/groups.yaml @@ -2,7 +2,7 @@ pk: 1 fields: project: 123456 - score: null + score: 7 students: - '1' - '2' diff --git a/backend/api/fixtures/submissions.yaml b/backend/api/fixtures/submissions.yaml new file mode 100644 index 00000000..0b8f876d --- /dev/null +++ b/backend/api/fixtures/submissions.yaml @@ -0,0 +1,24 @@ +- model: api.submission + pk: 1 + fields: + group: 1 + submission_number: 1 + submission_time: '2021-01-01T00:00:00Z' +- model: api.submission + pk: 2 + fields: + group: 1 + submission_number: 2 + submission_time: '2021-01-02T00:00:00Z' + + +- model: api.submissionfile + pk: 1 + fields: + submission: 1 + file: 'submissions/1/1/1.txt' +- model: api.submissionfile + pk: 2 + fields: + submission: 2 + file: 'submissions/1/2/1.txt' diff --git a/backend/api/serializers/admin_serializer.py b/backend/api/serializers/admin_serializer.py index 2603278b..c7749f87 100644 --- a/backend/api/serializers/admin_serializer.py +++ b/backend/api/serializers/admin_serializer.py @@ -3,6 +3,13 @@ class AdminSerializer(serializers.ModelSerializer): + + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + class Meta: model = Admin fields = [ diff --git a/backend/api/serializers/assistant_serializer.py b/backend/api/serializers/assistant_serializer.py index 210f7404..540f9f32 100644 --- a/backend/api/serializers/assistant_serializer.py +++ b/backend/api/serializers/assistant_serializer.py @@ -9,6 +9,12 @@ class AssistantSerializer(serializers.ModelSerializer): read_only=True, ) + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + class Meta: model = Assistant fields = [ diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index 214d637b..3ea93733 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -14,6 +14,12 @@ class StudentSerializer(serializers.ModelSerializer): read_only=True, ) + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + class Meta: model = Student fields = [ diff --git a/backend/api/serializers/submision_serializer.py b/backend/api/serializers/submision_serializer.py index 3429022d..95d26dbf 100644 --- a/backend/api/serializers/submision_serializer.py +++ b/backend/api/serializers/submision_serializer.py @@ -5,7 +5,7 @@ class SubmissionFileSerializer(serializers.ModelSerializer): class Meta: model = SubmissionFile - fields = ['id', 'submission', 'file'] + fields = ['file'] class SubmissionSerializer(serializers.ModelSerializer): @@ -16,6 +16,10 @@ class SubmissionSerializer(serializers.ModelSerializer): view_name='group-detail' ) + files = SubmissionFileSerializer(many=True, read_only=True) + class Meta: model = Submission - fields = ['id', 'group', 'submission_number', 'submission_time'] + 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 c35bea68..8fbb98e9 100644 --- a/backend/api/serializers/teacher_serializer.py +++ b/backend/api/serializers/teacher_serializer.py @@ -9,6 +9,12 @@ class TeacherSerializer(serializers.ModelSerializer): read_only=True, ) + faculties = serializers.HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + class Meta: model = Teacher fields = [ diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 9c7e8173..f33e6c91 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -1,5 +1,8 @@ from django.contrib.auth.models import update_last_login -from rest_framework.serializers import CharField, EmailField, ModelSerializer, ValidationError, Serializer +from rest_framework.serializers import ( + CharField, EmailField, ModelSerializer, ValidationError, + Serializer, HyperlinkedIdentityField, HyperlinkedRelatedField +) from rest_framework_simplejwt.tokens import RefreshToken, AccessToken from rest_framework_simplejwt.settings import api_settings from authentication.signals import user_created, user_login @@ -65,23 +68,36 @@ def validate(self, data): '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() - + email = EmailField() + + faculties = HyperlinkedRelatedField( + many=True, + read_only=True, + view_name='faculty-detail' + ) + + notifications = HyperlinkedIdentityField( + view_name='notification-detail', + read_only=True, + ) + class Meta: model = User fields = [ 'id', 'username', 'email', 'first_name', 'last_name', - 'faculty', - 'last_enrolled', 'last_login', 'create_time' + 'faculties', + 'last_enrolled', 'last_login', 'create_time', + 'notifications' ] def get_or_create(self, validated_data: dict) -> User: """Create or fetch the user based on the validated data.""" - return User.objects.get_or_create(**validated_data) \ No newline at end of file + return User.objects.get_or_create(**validated_data) From e5b1db6da9106d1d1fab9176ae7d55d23a9121c0 Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Sat, 2 Mar 2024 12:03:54 +0100 Subject: [PATCH 45/86] 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 e306c8b6691e7f923d09c51776b0cf71df96cc70 Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Sat, 2 Mar 2024 12:04:14 +0100 Subject: [PATCH 46/86] chore: add checks --- backend/api/fixtures/checks.yaml | 26 +++++++++++++++++++ backend/api/fixtures/projects.yaml | 2 +- backend/api/serializers/checks_serializer.py | 21 ++++++++------- backend/api/serializers/project_serializer.py | 7 +++++ 4 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 backend/api/fixtures/checks.yaml diff --git a/backend/api/fixtures/checks.yaml b/backend/api/fixtures/checks.yaml new file mode 100644 index 00000000..5a9a7795 --- /dev/null +++ b/backend/api/fixtures/checks.yaml @@ -0,0 +1,26 @@ +- model: api.checks + pk: 1 + fields: + dockerfile: 'path/to/Dockerfile' + allowed_file_extensions: + - 1 + - 2 + forbidden_file_extensions: + - 3 + - 4 +- model: api.fileextension + pk: 1 + fields: + extension: 'py' +- model: api.fileextension + pk: 2 + fields: + extension: 'js' +- model: api.fileextension + pk: 3 + fields: + extension: 'html' +- model: api.fileextension + pk: 4 + fields: + extension: 'php' diff --git a/backend/api/fixtures/projects.yaml b/backend/api/fixtures/projects.yaml index 2e42bd30..5d9692ea 100644 --- a/backend/api/fixtures/projects.yaml +++ b/backend/api/fixtures/projects.yaml @@ -7,5 +7,5 @@ archived: false start_date: 2024-02-26 00:00:00+00:00 deadline: 2024-02-27 00:00:00+00:00 - checks: null + checks: 1 course: 2 diff --git a/backend/api/serializers/checks_serializer.py b/backend/api/serializers/checks_serializer.py index 51bbfd84..dc312e4b 100644 --- a/backend/api/serializers/checks_serializer.py +++ b/backend/api/serializers/checks_serializer.py @@ -2,16 +2,19 @@ from ..models.checks import Checks, FileExtension -class ChecksSerializer(serializers.ModelSerializer): +class FileExtensionSerializer(serializers.ModelSerializer): class Meta: - model = Checks - fields = ['id', 'dockerfile'] + model = FileExtension + fields = ['extension'] -class FileExtensionSerializer(serializers.ModelSerializer): +class ChecksSerializer(serializers.ModelSerializer): + + allowed_file_extensions = FileExtensionSerializer(many=True) + + forbidden_file_extensions = FileExtensionSerializer(many=True) + class Meta: - model = FileExtension - fields = [ - 'id', 'extension', - 'allowed_file_extensions', 'forbidden_file_extensions' - ] + model = Checks + fields = ['id', 'dockerfile', 'allowed_file_extensions', + 'forbidden_file_extensions'] diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index be2195c5..a04058f0 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -3,12 +3,19 @@ class ProjectSerializer(serializers.ModelSerializer): + course = serializers.HyperlinkedRelatedField( many=False, read_only=True, view_name='course-detail' ) + checks = serializers.HyperlinkedRelatedField( + many=False, + read_only=True, + view_name='check-detail' + ) + class Meta: model = Project fields = [ From e78d1c0f5b47b87dfa2aaefa1947f8812e0ef72b Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 12:54:16 +0100 Subject: [PATCH 47/86] chore: add togle functions to project --- backend/api/models/project.py | 8 +++++++ backend/api/models/submission.py | 3 ++- backend/api/tests/test_project.py | 36 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/backend/api/models/project.py b/backend/api/models/project.py index f766d82b..4c142be1 100644 --- a/backend/api/models/project.py +++ b/backend/api/models/project.py @@ -69,3 +69,11 @@ def deadline_approaching_in(self, days=7): def deadline_passed(self): now = timezone.now() return now > self.deadline + + def toggle_visible(self): + self.visible = not (self.visible) + self.save() + + def toggle_archived(self): + self.archived = not (self.archived) + self.save() diff --git a/backend/api/models/submission.py b/backend/api/models/submission.py index 54a00133..a94c9360 100644 --- a/backend/api/models/submission.py +++ b/backend/api/models/submission.py @@ -1,6 +1,7 @@ from django.db import models from api.models.group import Group + class Submission(models.Model): """Model for submission of a project by a group of students.""" @@ -49,4 +50,4 @@ class SubmissionFile(models.Model): file = models.FileField( blank=False, null=False - ) \ No newline at end of file + ) diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index 301d2b7a..02a32ea7 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -40,6 +40,42 @@ def create_project( class ProjectModelTests(TestCase): + def test_toggle_visible(self): + """ + toggle the visible state of a project. + """ + 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 + ) + self.assertIs(past_project.visible, True) + past_project.toggle_visible() + self.assertIs(past_project.visible, False) + past_project.toggle_visible() + self.assertIs(past_project.visible, True) + + def test_toggle_archived(self): + """ + toggle the archived state of a project. + """ + 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 + ) + + self.assertIs(past_project.archived, True) + past_project.toggle_archived() + self.assertIs(past_project.archived, False) + past_project.toggle_archived() + self.assertIs(past_project.archived, True) + + def test_deadline_approaching_in_with_past_Project(self): """ deadline_approaching_in() returns False for Projects whose Deadline From 85ddc09d7415343ef157f86c2c05dbbe6f8c8766 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 14:35:54 +0100 Subject: [PATCH 48/86] chore: add submison checks --- backend/api/tests/test_checks.py | 26 ++-- backend/api/tests/test_group.py | 12 +- backend/api/tests/test_project.py | 10 +- backend/api/tests/test_submision.py | 227 ++++++++++++++++++++++++++++ 4 files changed, 253 insertions(+), 22 deletions(-) create mode 100644 backend/api/tests/test_submision.py diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index 2d00e8b6..2d5b8d15 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -76,8 +76,8 @@ def test_fileExtension_exists(self): # Assert the details of the retrieved admin match the created admin retrieved_fileExtension = content_json[0] - self.assertEqual( - int(retrieved_fileExtension["id"]), fileExtension.id) + # self.assertEqual( + # int(retrieved_fileExtension["id"]), fileExtension.id) self.assertEqual( retrieved_fileExtension["extension"], fileExtension.extension) @@ -112,13 +112,13 @@ def test_multiple_fileExtension(self): # Assert the details of the retrieved admins match the created admins retrieved_fileExtension1, retrieved_fileExtension2 = content_json - self.assertEqual( - int(retrieved_fileExtension1["id"]), fileExtension1.id) + # self.assertEqual( + # int(retrieved_fileExtension1["id"]), fileExtension1.id) self.assertEqual( retrieved_fileExtension1["extension"], fileExtension1.extension) - self.assertEqual( - int(retrieved_fileExtension2["id"]), fileExtension2.id) + # self.assertEqual( + # int(retrieved_fileExtension2["id"]), fileExtension2.id) self.assertEqual( retrieved_fileExtension2["extension"], fileExtension2.extension) @@ -150,7 +150,7 @@ def test_fileExtension_detail_view(self): # Assert the details of the retrieved fileExtension # match the created fileExtension - self.assertEqual(int(content_json["id"]), fileExtension.id) + # self.assertEqual(int(content_json["id"]), fileExtension.id) self.assertEqual(content_json["extension"], fileExtension.extension) @@ -205,14 +205,18 @@ def test_checks_exists(self): self.assertEqual(len(retrieved_allowed_file_extensions), 2) self.assertEqual( - retrieved_allowed_file_extensions[0], fileExtension1.extension) + retrieved_allowed_file_extensions[0]["extension"], + fileExtension1.extension) self.assertEqual( - retrieved_allowed_file_extensions[1], fileExtension4.extension) + retrieved_allowed_file_extensions[1]["extension"], + fileExtension4.extension) retrieved_forbidden_file_extensions = retrieved_checks[ "forbidden_file_extensions"] self.assertEqual(len(retrieved_forbidden_file_extensions), 2) self.assertEqual( - retrieved_forbidden_file_extensions[0], fileExtension2.extension) + retrieved_forbidden_file_extensions[0]["extension"], + fileExtension2.extension) self.assertEqual( - retrieved_forbidden_file_extensions[1], fileExtension3.extension) + retrieved_forbidden_file_extensions[1]["extension"], + fileExtension3.extension) diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index 5734f50c..c1601c19 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -45,6 +45,10 @@ def create_student(id, first_name, last_name, email): ) +def create_group(project, score): + return Group.objects.create(project=project, score=score) + + class GroupModelTests(TestCase): def test_no_groups(self): """Able to retrieve no groups before creating any.""" @@ -75,7 +79,7 @@ def test_group_exists(self): email="john.doe@example.com" ) - group = Group.objects.create(project=project, score=80) + group = create_group(project=project, score=10) group.students.add(student) response = self.client.get(reverse("group-list"), follow=True) @@ -124,10 +128,10 @@ def test_multiple_groups(self): email="jane.doe@example.com" ) - group1 = Group.objects.create(project=project1, score=80) + group1 = create_group(project=project1, score=10) group1.students.add(student1) - group2 = Group.objects.create(project=project2, score=90) + group2 = create_group(project=project2, score=10) group2.students.add(student1, student2) response = self.client.get(reverse("group-list"), follow=True) @@ -169,7 +173,7 @@ def test_group_detail_view(self): email="john.doe@example.com") - group = Group.objects.create(project=project, score=80) + group = create_group(project=project, score=10) group.students.add(student) response = self.client.get( diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index 02a32ea7..ec573519 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -1,5 +1,4 @@ import json -from datetime import datetime from django.test import TestCase from django.utils import timezone from django.urls import reverse @@ -75,7 +74,6 @@ def test_toggle_archived(self): past_project.toggle_archived() self.assertIs(past_project.archived, True) - def test_deadline_approaching_in_with_past_Project(self): """ deadline_approaching_in() returns False for Projects whose Deadline @@ -198,7 +196,7 @@ def test_project_exists(self): self.assertEqual(retrieved_project["description"], project.description) self.assertEqual(retrieved_project["visible"], project.visible) self.assertEqual(retrieved_project["archived"], project.archived) - # self.assertEqual(retrieved_project["checks"], expected_checks_url) + self.assertEqual(retrieved_project["checks"], expected_checks_url) self.assertEqual(retrieved_project["course"], expected_course_url) def test_multiple_project(self): @@ -254,8 +252,7 @@ def test_multiple_project(self): self.assertEqual(retrieved_project["description"], project.description) self.assertEqual(retrieved_project["visible"], project.visible) self.assertEqual(retrieved_project["archived"], project.archived) - # TODO - # self.assertEqual(retrieved_project["checks"], expected_checks_url) + self.assertEqual(retrieved_project["checks"], expected_checks_url) self.assertEqual(retrieved_project["course"], expected_course_url) retrieved_project = content_json[1] @@ -273,6 +270,5 @@ def test_multiple_project(self): retrieved_project["description"], project2.description) self.assertEqual(retrieved_project["visible"], project2.visible) self.assertEqual(retrieved_project["archived"], project2.archived) - # TODO - # self.assertEqual(retrieved_project["checks"], expected_checks_url) + self.assertEqual(retrieved_project["checks"], expected_checks_url) self.assertEqual(retrieved_project["course"], expected_course_url) diff --git a/backend/api/tests/test_submision.py b/backend/api/tests/test_submision.py new file mode 100644 index 00000000..63344a06 --- /dev/null +++ b/backend/api/tests/test_submision.py @@ -0,0 +1,227 @@ +import json +from datetime import timedelta +from django.test import TestCase +from django.utils import timezone +from django.urls import reverse +from ..models.submission import Submission, SubmissionFile +from ..models.project import Project +from ..models.group import Group +from ..models.course import Course + + +def create_course(name, academic_startyear, description=None, + parent_course=None): + """ + Create a Course with the given arguments. + """ + return Course.objects.create( + name=name, + academic_startyear=academic_startyear, + description=description, + parent_course=parent_course + ) + + +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 + ) + + +def create_group(project, score): + return Group.objects.create(project=project, score=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() + ) + + +def create_submissionFile(submission, file): + # Create an SubmissionFile with the given arguments. + return SubmissionFile.objects.create( + submission=submission, + file=file + ) + + +class SubmissionModelTests(TestCase): + def test_no_submission(self): + """ + able to retrieve no submission before publishing it. + """ + + response_root = self.client.get( + reverse("submission-list"), follow=True) + # print(response.content) + self.assertEqual(response_root.status_code, 200) + # Assert that the response is JSON + self.assertEqual(response_root.accepted_media_type, "application/json") + # Parse the JSON content from the response + content_json = json.loads(response_root.content.decode("utf-8")) + # Assert that the parsed JSON is an empty list + self.assertEqual(content_json, []) + + def test_submission_exists(self): + """ + Able to retrieve a single submission after creating it. + """ + 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 + ) + + # Make a GET request to retrieve the submission + response = self.client.get(reverse("submission-list"), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one submission + self.assertEqual(len(content_json), 1) + + # Assert the details of the retrieved submission + # match the created submission + retrieved_submission = content_json[0] + expected_group_url = "http://testserver" + reverse( + "group-detail", args=[str(group.id)] + ) + self.assertEqual(int(retrieved_submission["id"]), submission.id) + self.assertEqual( + 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 + ) + 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 + ) + + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with one submission + self.assertEqual(len(content_json), 2) + + # Assert the details of the retrieved submission + # match the created submission + retrieved_submission = content_json[0] + expected_group_url = "http://testserver" + reverse( + "group-detail", args=[str(group.id)] + ) + self.assertEqual(int(retrieved_submission["id"]), submission1.id) + self.assertEqual( + int(retrieved_submission["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)] + ) + self.assertEqual(int(retrieved_submission["id"]), submission2.id) + self.assertEqual( + int(retrieved_submission["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 + ) + 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 + ) + + # Make a GET request to retrieve the submission + response = self.client.get( + reverse("submission-detail", args=[str(submission.id)]), + follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved submission + # match the created submission + retrieved_submission = content_json + expected_group_url = "http://testserver" + reverse( + "group-detail", args=[str(group.id)] + ) + self.assertEqual(int(retrieved_submission["id"]), submission.id) + self.assertEqual( + int(retrieved_submission["submission_number"]), + submission.submission_number + ) + self.assertEqual(retrieved_submission["group"], expected_group_url) \ No newline at end of file From e971b1b3aa7c5b089ca88c004f966f664a00171e Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 14:52:24 +0100 Subject: [PATCH 49/86] fix: fix small comments mistakes --- backend/api/tests/test_admin.py | 1 - backend/api/tests/test_assistant.py | 16 +++++++++------- backend/api/tests/test_checks.py | 26 ++++++++------------------ backend/api/tests/test_group.py | 1 + backend/api/tests/test_project.py | 5 +++++ backend/api/tests/test_student.py | 2 +- backend/api/tests/test_submision.py | 7 ++++--- 7 files changed, 28 insertions(+), 30 deletions(-) diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index eaddd911..5863c9d2 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -29,7 +29,6 @@ def test_no_admins(self): """ response_root = self.client.get(reverse("admin-list"), follow=True) - # print(response.content) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index e0bc3d9c..7e9e3701 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -7,7 +7,7 @@ def create_assistant(id, first_name, last_name, email): - # Create an Assistant with the given arguments. + # Create an assistant with the given arguments. username = f"{first_name}_{last_name}" return Assistant.objects.create( id=id, @@ -58,10 +58,11 @@ def test_assistant_exists(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with one student + # Assert that the parsed JSON is a list with one assistant self.assertEqual(len(content_json), 1) - # Assert the details of the retrieved student match the created student + # Assert the details of the retrieved assistant + # match the created assistant retrieved_assistant = content_json[0] self.assertEqual(int(retrieved_assistant["id"]), assistant.id) self.assertEqual( @@ -99,11 +100,11 @@ def test_multiple_assistant(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with multiple students + # Assert that the parsed JSON is a list with multiple assistant self.assertEqual(len(content_json), 2) # Assert the details of the retrieved - # students match the created students + # assistant match the created assistant retrieved_assistant1, retrieved_assistant2 = content_json self.assertEqual(int(retrieved_assistant1["id"]), assistant1.id) self.assertEqual( @@ -123,7 +124,7 @@ def test_assistant_detail_view(self): """ Able to retrieve details of a single assistant. """ - # Create an student for testing with the name "Bob Peeters" + # Create an assistant for testing with the name "Bob Peeters" assistant = create_assistant( id=5, first_name="Bob", @@ -144,7 +145,8 @@ def test_assistant_detail_view(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert the details of the retrieved student match the created student + # Assert the details of the retrieved assistant + # match the created assistant self.assertEqual(int(content_json["id"]), assistant.id) self.assertEqual(content_json["first_name"], assistant.first_name) self.assertEqual(content_json["last_name"], assistant.last_name) diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index 2d5b8d15..1eb1ce75 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -17,19 +17,15 @@ def create_fileExtension(id, extension): def create_checks(id, allowed_file_extensions, forbidden_file_extensions): - - # Create a Checks with the given arguments. - + """Create a Checks with the given arguments.""" check = Checks.objects.create( id=id, ) for ext in allowed_file_extensions: check.allowed_file_extensions.add(ext) - for ext in forbidden_file_extensions: check.forbidden_file_extensions.add(ext) - return check @@ -38,7 +34,6 @@ def test_no_fileExtension(self): """ able to retrieve no FileExtension before publishing it. """ - response_root = self.client.get( reverse("fileExtension-list"), follow=True) # print(response.content) @@ -71,13 +66,12 @@ def test_fileExtension_exists(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with one admin + # Assert that the parsed JSON is a list with one fileExtension self.assertEqual(len(content_json), 1) - # Assert the details of the retrieved admin match the created admin + # Assert the details of the retrieved fileExtension + # match the created fileExtension retrieved_fileExtension = content_json[0] - # self.assertEqual( - # int(retrieved_fileExtension["id"]), fileExtension.id) self.assertEqual( retrieved_fileExtension["extension"], fileExtension.extension) @@ -107,18 +101,15 @@ def test_multiple_fileExtension(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with multiple admins + # Assert that the parsed JSON is a list with multiple fileExtension self.assertEqual(len(content_json), 2) - # Assert the details of the retrieved admins match the created admins + # Assert the details of the retrieved fileExtension + # match the created fileExtension retrieved_fileExtension1, retrieved_fileExtension2 = content_json - # self.assertEqual( - # int(retrieved_fileExtension1["id"]), fileExtension1.id) self.assertEqual( retrieved_fileExtension1["extension"], fileExtension1.extension) - # self.assertEqual( - # int(retrieved_fileExtension2["id"]), fileExtension2.id) self.assertEqual( retrieved_fileExtension2["extension"], fileExtension2.extension) @@ -126,7 +117,7 @@ def test_fileExtension_detail_view(self): """ Able to retrieve details of a single fileExtension. """ - # Create an fileExtension for testing with the name "Bob Peeters" + # Create an fileExtension for testing. fileExtension = create_fileExtension( id=3, extension="zip" @@ -150,7 +141,6 @@ def test_fileExtension_detail_view(self): # Assert the details of the retrieved fileExtension # match the created fileExtension - # self.assertEqual(int(content_json["id"]), fileExtension.id) self.assertEqual(content_json["extension"], fileExtension.extension) diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index c1601c19..c8767ff2 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -46,6 +46,7 @@ def create_student(id, first_name, last_name, email): def create_group(project, score): + """Create a Group with the given arguments.""" return Group.objects.create(project=project, score=score) diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index ec573519..af7fb11a 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -8,11 +8,15 @@ 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) def create_checks(): + """Create a Checks with the given arguments.""" return Checks.objects.create() @@ -25,6 +29,7 @@ def create_project( checks, course ): + """Create a Project with the given arguments.""" deadline = timezone.now() + timezone.timedelta(days=days) return Project.objects.create( diff --git a/backend/api/tests/test_student.py b/backend/api/tests/test_student.py index f58f3533..01620891 100644 --- a/backend/api/tests/test_student.py +++ b/backend/api/tests/test_student.py @@ -7,7 +7,7 @@ def create_student(id, first_name, last_name, email): - # Create an Assistant with the given arguments. + # Create an student with the given arguments. username = f"{first_name}_{last_name}" return Student.objects.create( id=id, diff --git a/backend/api/tests/test_submision.py b/backend/api/tests/test_submision.py index 63344a06..ea8e630f 100644 --- a/backend/api/tests/test_submision.py +++ b/backend/api/tests/test_submision.py @@ -34,11 +34,12 @@ def create_project(name, description, days, course): def create_group(project, score): + """Create a Group with the given arguments.""" return Group.objects.create(project=project, score=score) def create_submission(group, submission_number): - # Create an Submission with the given arguments. + """Create an Submission with the given arguments.""" return Submission.objects.create( group=group, submission_number=submission_number, @@ -47,7 +48,7 @@ def create_submission(group, submission_number): def create_submissionFile(submission, file): - # Create an SubmissionFile with the given arguments. + """Create an SubmissionFile with the given arguments.""" return SubmissionFile.objects.create( submission=submission, file=file @@ -224,4 +225,4 @@ def test_submission_detail_view(self): int(retrieved_submission["submission_number"]), submission.submission_number ) - self.assertEqual(retrieved_submission["group"], expected_group_url) \ No newline at end of file + self.assertEqual(retrieved_submission["group"], expected_group_url) From 22dcaa28ed5e4b02d17161d61d5542746d071d10 Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 2 Mar 2024 18:08:30 +0100 Subject: [PATCH 50/86] build: create backend testing workflow --- backend/.github/workflows/backend-tests.yaml | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 backend/.github/workflows/backend-tests.yaml diff --git a/backend/.github/workflows/backend-tests.yaml b/backend/.github/workflows/backend-tests.yaml new file mode 100644 index 00000000..66c0d432 --- /dev/null +++ b/backend/.github/workflows/backend-tests.yaml @@ -0,0 +1,23 @@ +name: backend-tests + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + +jobs: + test: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Execute tests + run: python manage.py test \ No newline at end of file From 637c4fbc107af20949d92083ecfac1529ea48e2c Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Sat, 2 Mar 2024 18:24:42 +0100 Subject: [PATCH 51/86] 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 1ba19f0fce8d070616ef09568174adbd40e22b69 Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 2 Mar 2024 18:45:31 +0100 Subject: [PATCH 52/86] build: temporarily add automated-testing branch for automated testing --- backend/.github/workflows/backend-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.github/workflows/backend-tests.yaml b/backend/.github/workflows/backend-tests.yaml index 66c0d432..b057f67a 100644 --- a/backend/.github/workflows/backend-tests.yaml +++ b/backend/.github/workflows/backend-tests.yaml @@ -2,7 +2,7 @@ name: backend-tests on: push: - branches: [main, development] + branches: [main, development, automated-testing] pull_request: branches: [main, development] From 333d3c953591c15b7b280251d9668aeec037f319 Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 2 Mar 2024 18:48:21 +0100 Subject: [PATCH 53/86] fix: move .github folder to root --- {backend/.github => .github}/workflows/backend-tests.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {backend/.github => .github}/workflows/backend-tests.yaml (100%) diff --git a/backend/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml similarity index 100% rename from backend/.github/workflows/backend-tests.yaml rename to .github/workflows/backend-tests.yaml From c27bdba2538f8e8a2e801e7d22d771e9153d079b Mon Sep 17 00:00:00 2001 From: Bram Meir Date: Sat, 2 Mar 2024 18:51:59 +0100 Subject: [PATCH 54/86] 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 e8687f515bb5bfccaefbef95ef725ee9115d6440 Mon Sep 17 00:00:00 2001 From: francis Date: Sat, 2 Mar 2024 18:56:29 +0100 Subject: [PATCH 55/86] fix: file references --- .github/workflows/backend-tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml index b057f67a..29990feb 100644 --- a/.github/workflows/backend-tests.yaml +++ b/.github/workflows/backend-tests.yaml @@ -18,6 +18,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install -r ./backend/requirements.txt - name: Execute tests - run: python manage.py test \ No newline at end of file + run: python ./backend/manage.py test \ No newline at end of file From 055306cde20eb92fa019e501ed4675fac0a5885e Mon Sep 17 00:00:00 2001 From: bsilkyn Date: Sat, 2 Mar 2024 19:23:05 +0100 Subject: [PATCH 56/86] 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 57/86] 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 9d55664d80add7478a8fe16514a2c4f6c0061c9d Mon Sep 17 00:00:00 2001 From: Francis Vauterin <159532420+francisvaut@users.noreply.github.com> Date: Sat, 2 Mar 2024 20:45:34 +0100 Subject: [PATCH 58/86] build: add tests to workflow_dispatch group for testing purposes --- .github/workflows/backend-tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml index 29990feb..0266c44e 100644 --- a/.github/workflows/backend-tests.yaml +++ b/.github/workflows/backend-tests.yaml @@ -5,6 +5,7 @@ on: branches: [main, development, automated-testing] pull_request: branches: [main, development] + workflow_dispatch: jobs: test: @@ -20,4 +21,4 @@ jobs: python -m pip install --upgrade pip pip install -r ./backend/requirements.txt - name: Execute tests - run: python ./backend/manage.py test \ No newline at end of file + run: python ./backend/manage.py test From 35e43472997a1b71ce21d782250ea8f326311c11 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 20:45:45 +0100 Subject: [PATCH 59/86] chore: add test for admins faculty --- backend/api/tests/test_admin.py | 84 +++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index 5863c9d2..6bddb68b 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -5,21 +5,41 @@ from django.urls import reverse from ..models.admin import Admin +from authentication.models import Faculty +def create_faculty(name): + """ + Create a Faculty with the given arguments.""" + return Faculty.objects.create( + name=name + ) -def create_admin(id, first_name, last_name, email): +def create_admin(id, first_name, last_name, email, faculty=None): """ Create a Admin with the given arguments. """ username = f"{first_name}_{last_name}" - return Admin.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), - ) + if faculty is None: + return Admin.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now() + ) + else: + admin = Admin.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + create_time=timezone.now(), + ) + for fac in faculty: + admin.faculties.add(fac) + return admin class AdminModelTests(TestCase): @@ -145,3 +165,49 @@ def test_admin_detail_view(self): self.assertEqual(content_json["first_name"], admin.first_name) self.assertEqual(content_json["last_name"], admin.last_name) self.assertEqual(content_json["email"], admin.email) + + def test_admin_faculty(self): + """ + Able to retrieve faculty details of a single admin. + """ + # Create an admin for testing with the name "Bob Peeters" + faculty = create_faculty(name="testing faculty") + admin = create_admin( + id=5, + first_name="Bob", + last_name="Peeters", + email="bob.peeters@example.com", + faculty=[faculty] + ) + + # Make a GET request to retrieve the admin details + response = self.client.get( + reverse("admin-detail", args=[str(admin.id)]), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved admin match the created admin + self.assertEqual(int(content_json["id"]), admin.id) + self.assertEqual(content_json["first_name"], admin.first_name) + self.assertEqual(content_json["last_name"], admin.last_name) + self.assertEqual(content_json["email"], admin.email) + print(content_json["faculties"]) + + response = self.client.get(content_json["faculties"][0], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + self.assertEqual(content_json["name"], faculty.name) From 434579fc12b7bdc921cde2a3b1f4a3ab73995ea3 Mon Sep 17 00:00:00 2001 From: Francis Vauterin <159532420+francisvaut@users.noreply.github.com> Date: Sat, 2 Mar 2024 20:53:02 +0100 Subject: [PATCH 60/86] chore: remove automated-testing branch from testing workflow --- .github/workflows/backend-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml index 0266c44e..cd8149f7 100644 --- a/.github/workflows/backend-tests.yaml +++ b/.github/workflows/backend-tests.yaml @@ -2,7 +2,7 @@ name: backend-tests on: push: - branches: [main, development, automated-testing] + branches: [main, development] pull_request: branches: [main, development] workflow_dispatch: From 28fa50aea144a01502fcbdd0972b19f7a43dc02a Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 21:07:57 +0100 Subject: [PATCH 61/86] chore: assistant tests endpoints --- backend/api/tests/test_admin.py | 2 + backend/api/tests/test_assistant.py | 181 ++++++++++++++++++++++++++-- 2 files changed, 173 insertions(+), 10 deletions(-) diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index 6bddb68b..963db1cb 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -7,6 +7,7 @@ from ..models.admin import Admin from authentication.models import Faculty + def create_faculty(name): """ Create a Faculty with the given arguments.""" @@ -14,6 +15,7 @@ def create_faculty(name): name=name ) + def create_admin(id, first_name, last_name, email, faculty=None): """ Create a Admin with the given arguments. diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index 7e9e3701..11cc49e2 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -4,21 +4,62 @@ from django.utils import timezone from django.urls import reverse from api.models.assistant import Assistant +from api.models.course import Course +from authentication.models import Faculty -def create_assistant(id, first_name, last_name, email): - # Create an assistant with the given arguments. - username = f"{first_name}_{last_name}" - return Assistant.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), +def create_course(name, academic_startyear, description=None, + parent_course=None): + """ + Create a Course with the given arguments. + """ + return Course.objects.create( + name=name, + academic_startyear=academic_startyear, + description=description, + parent_course=parent_course + ) + + +def create_faculty(name): + """Create a Faculty with the given arguments.""" + return Faculty.objects.create( + name=name ) +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() + ) + + if faculty is not None: + for fac in faculty: + assistant.faculties.add(fac) + + if courses is not None: + for cours in courses: + assistant.courses.add(cours) + + return assistant + + class AssistantModelTests(TestCase): def test_no_assistant(self): """ @@ -151,3 +192,123 @@ def test_assistant_detail_view(self): self.assertEqual(content_json["first_name"], assistant.first_name) self.assertEqual(content_json["last_name"], assistant.last_name) self.assertEqual(content_json["email"], assistant.email) + + def test_assistant_faculty(self): + """ + Able to retrieve faculty details of a single assistant. + """ + # Create an assistant for testing with the name "Bob Peeters" + faculty = create_faculty(name="testing faculty") + assistant = create_assistant( + id=5, + first_name="Bob", + last_name="Peeters", + email="bob.peeters@example.com", + faculty=[faculty] + ) + + # Make a GET request to retrieve the assistant details + response = self.client.get( + reverse("assistant-detail", args=[str(assistant.id)]), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved assistant + # match the created assistant + self.assertEqual(int(content_json["id"]), assistant.id) + self.assertEqual(content_json["first_name"], assistant.first_name) + self.assertEqual(content_json["last_name"], assistant.last_name) + self.assertEqual(content_json["email"], assistant.email) + print(content_json["faculties"]) + + response = self.client.get(content_json["faculties"][0], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + self.assertEqual(content_json["name"], faculty.name) + + def test_assistant_courses(self): + """ + Able to retrieve courses details of a single assistant. + """ + # Create an assistant for testing with the name "Bob Peeters" + course1 = create_course( + name="Introduction to Computer Science", + academic_startyear=2022, + 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." + ) + + assistant = create_assistant( + id=5, + first_name="Bob", + last_name="Peeters", + email="bob.peeters@example.com", + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved assistant + # match the created assistant + self.assertEqual(int(content_json["id"]), assistant.id) + self.assertEqual(content_json["first_name"], assistant.first_name) + self.assertEqual(content_json["last_name"], assistant.last_name) + self.assertEqual(content_json["email"], assistant.email) + print(content_json["faculties"]) + + response = self.client.get(content_json["courses"], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple assistant + self.assertEqual(len(content_json), 2) + + 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(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(content["description"], course2.description) From f461709576655fe4473c4540b0d5a2d6f01b71cf Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 21:19:27 +0100 Subject: [PATCH 62/86] chore: add student endpoint tests --- backend/api/tests/test_assistant.py | 3 - backend/api/tests/test_student.py | 180 ++++++++++++++++++++++++++-- 2 files changed, 169 insertions(+), 14 deletions(-) diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index 11cc49e2..4e8af6a9 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -67,7 +67,6 @@ def test_no_assistant(self): """ response_root = self.client.get(reverse("assistant-list"), follow=True) - # print(response.content) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") @@ -226,7 +225,6 @@ def test_assistant_faculty(self): self.assertEqual(content_json["first_name"], assistant.first_name) self.assertEqual(content_json["last_name"], assistant.last_name) self.assertEqual(content_json["email"], assistant.email) - print(content_json["faculties"]) response = self.client.get(content_json["faculties"][0], follow=True) @@ -283,7 +281,6 @@ def test_assistant_courses(self): self.assertEqual(content_json["first_name"], assistant.first_name) self.assertEqual(content_json["last_name"], assistant.last_name) self.assertEqual(content_json["email"], assistant.email) - print(content_json["faculties"]) response = self.client.get(content_json["courses"], follow=True) diff --git a/backend/api/tests/test_student.py b/backend/api/tests/test_student.py index 01620891..df03625d 100644 --- a/backend/api/tests/test_student.py +++ b/backend/api/tests/test_student.py @@ -4,21 +4,62 @@ from django.utils import timezone from django.urls import reverse from ..models.student import Student +from ..models.course import Course +from authentication.models import Faculty -def create_student(id, first_name, last_name, email): - # Create an student with the given arguments. - username = f"{first_name}_{last_name}" - return Student.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), +def create_course(name, academic_startyear, description=None, + parent_course=None): + """ + Create a Course with the given arguments. + """ + return Course.objects.create( + name=name, + academic_startyear=academic_startyear, + description=description, + parent_course=parent_course + ) + + +def create_faculty(name): + """Create a Faculty with the given arguments.""" + return Faculty.objects.create( + name=name ) +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() + ) + + if faculty is not None: + for fac in faculty: + student.faculties.add(fac) + + if courses is not None: + for cours in courses: + student.courses.add(cours) + + return student + + class StudentModelTests(TestCase): def test_no_student(self): """ @@ -26,7 +67,6 @@ def test_no_student(self): """ response_root = self.client.get(reverse("student-list"), follow=True) - # print(response.content) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") @@ -149,3 +189,121 @@ def test_student_detail_view(self): self.assertEqual(content_json["first_name"], student.first_name) self.assertEqual(content_json["last_name"], student.last_name) self.assertEqual(content_json["email"], student.email) + + def test_student_faculty(self): + """ + Able to retrieve faculty details of a single student. + """ + # Create an student for testing with the name "Bob Peeters" + faculty = create_faculty(name="testing faculty") + student = create_student( + id=5, + first_name="Bob", + last_name="Peeters", + email="bob.peeters@example.com", + faculty=[faculty] + ) + + # Make a GET request to retrieve the student details + response = self.client.get( + reverse("student-detail", args=[str(student.id)]), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved student + # match the created student + self.assertEqual(int(content_json["id"]), student.id) + self.assertEqual(content_json["first_name"], student.first_name) + self.assertEqual(content_json["last_name"], student.last_name) + self.assertEqual(content_json["email"], student.email) + + response = self.client.get(content_json["faculties"][0], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + self.assertEqual(content_json["name"], faculty.name) + + def test_student_courses(self): + """ + Able to retrieve courses details of a single student. + """ + # Create an student for testing with the name "Bob Peeters" + course1 = create_course( + name="Introduction to Computer Science", + academic_startyear=2022, + 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." + ) + + student = create_student( + id=5, + first_name="Bob", + last_name="Peeters", + email="bob.peeters@example.com", + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved student + # match the created student + self.assertEqual(int(content_json["id"]), student.id) + self.assertEqual(content_json["first_name"], student.first_name) + self.assertEqual(content_json["last_name"], student.last_name) + self.assertEqual(content_json["email"], student.email) + + response = self.client.get(content_json["courses"], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple student + self.assertEqual(len(content_json), 2) + + 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(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(content["description"], course2.description) From e948ce1a440baaef74111b0d501aee1f7483c804 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 21:23:04 +0100 Subject: [PATCH 63/86] chore: add teacher endpoint tests --- backend/api/tests/test_teacher.py | 179 ++++++++++++++++++++++++++++-- 1 file changed, 169 insertions(+), 10 deletions(-) diff --git a/backend/api/tests/test_teacher.py b/backend/api/tests/test_teacher.py index 7e2fdd88..5122ab0b 100644 --- a/backend/api/tests/test_teacher.py +++ b/backend/api/tests/test_teacher.py @@ -4,21 +4,62 @@ from django.utils import timezone from django.urls import reverse from ..models.teacher import Teacher +from ..models.course import Course +from authentication.models import Faculty -def create_teacher(id, first_name, last_name, email): - # Create an Teacher with the given arguments. - username = f"{first_name}_{last_name}" - return Teacher.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), +def create_course(name, academic_startyear, description=None, + parent_course=None): + """ + Create a Course with the given arguments. + """ + return Course.objects.create( + name=name, + academic_startyear=academic_startyear, + description=description, + parent_course=parent_course + ) + + +def create_faculty(name): + """Create a Faculty with the given arguments.""" + return Faculty.objects.create( + name=name ) +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() + ) + + if faculty is not None: + for fac in faculty: + teacher.faculties.add(fac) + + if courses is not None: + for cours in courses: + teacher.courses.add(cours) + + return teacher + + class TeacherModelTests(TestCase): def test_no_teacher(self): """ @@ -148,3 +189,121 @@ def test_teacher_detail_view(self): self.assertEqual(content_json["first_name"], teacher.first_name) self.assertEqual(content_json["last_name"], teacher.last_name) self.assertEqual(content_json["email"], teacher.email) + + def test_teacher_faculty(self): + """ + Able to retrieve faculty details of a single teacher. + """ + # Create an teacher for testing with the name "Bob Peeters" + faculty = create_faculty(name="testing faculty") + teacher = create_teacher( + id=5, + first_name="Bob", + last_name="Peeters", + email="bob.peeters@example.com", + faculty=[faculty] + ) + + # Make a GET request to retrieve the teacher details + response = self.client.get( + reverse("teacher-detail", args=[str(teacher.id)]), follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved teacher + # match the created teacher + self.assertEqual(int(content_json["id"]), teacher.id) + self.assertEqual(content_json["first_name"], teacher.first_name) + self.assertEqual(content_json["last_name"], teacher.last_name) + self.assertEqual(content_json["email"], teacher.email) + + response = self.client.get(content_json["faculties"][0], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + self.assertEqual(content_json["name"], faculty.name) + + def test_teacher_courses(self): + """ + Able to retrieve courses details of a single teacher. + """ + # Create an teacher for testing with the name "Bob Peeters" + course1 = create_course( + name="Introduction to Computer Science", + academic_startyear=2022, + 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." + ) + + teacher = create_teacher( + id=5, + first_name="Bob", + last_name="Peeters", + email="bob.peeters@example.com", + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved teacher + # match the created teacher + self.assertEqual(int(content_json["id"]), teacher.id) + self.assertEqual(content_json["first_name"], teacher.first_name) + self.assertEqual(content_json["last_name"], teacher.last_name) + self.assertEqual(content_json["email"], teacher.email) + + response = self.client.get(content_json["courses"], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple teacher + self.assertEqual(len(content_json), 2) + + 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(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(content["description"], course2.description) From 5a6bfec9e80890268e79186e955abd181d311605 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 22:17:25 +0100 Subject: [PATCH 64/86] chore: add course endpoints tests --- backend/api/tests/test_course.py | 353 +++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) diff --git a/backend/api/tests/test_course.py b/backend/api/tests/test_course.py index 21ff40b7..df05eb90 100644 --- a/backend/api/tests/test_course.py +++ b/backend/api/tests/test_course.py @@ -1,8 +1,97 @@ import json from django.test import TestCase +from django.utils import timezone from django.urls import reverse from ..models.course import Course +from ..models.teacher import Teacher +from ..models.assistant import Assistant +from ..models.student import Student +from ..models.project import Project + + +def create_project( + name, + description, + visible, + archived, + days, + course +): + """Create a Project with the given arguments.""" + deadline = timezone.now() + timezone.timedelta(days=days) + + return Project.objects.create( + name=name, + description=description, + visible=visible, + archived=archived, + deadline=deadline, + course=course + ) + + +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() + ) + return student + + +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() + ) + return assistant + + +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() + ) + return teacher def create_course(name, academic_startyear, description=None, @@ -113,3 +202,267 @@ def test_course_detail_view(self): self.assertEqual( content_json["academic_startyear"], course.academic_startyear) self.assertEqual(content_json["description"], course.description) + + def test_course_teachers(self): + """ + Able to retrieve teachers details of a single course. + """ + teacher1 = create_teacher( + id=5, + first_name="Simon", + last_name="Mignolet", + email="simon.mignolet@ugent.be" + ) + + teacher2 = create_teacher( + 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." + ) + course.teachers.add(teacher1) + course.teachers.add(teacher2) + + response = self.client.get( + reverse("course-detail", args=[str(course.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")) + + self.assertEqual(content_json["name"], course.name) + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple teachers + self.assertEqual(len(content_json), 2) + + content = content_json[0] + self.assertEqual(int(content["id"]), teacher1.id) + self.assertEqual(content["first_name"], teacher1.first_name) + self.assertEqual(content["last_name"], teacher1.last_name) + self.assertEqual(content["email"], teacher1.email) + + content = content_json[1] + self.assertEqual(int(content["id"]), teacher2.id) + self.assertEqual(content["first_name"], teacher2.first_name) + self.assertEqual(content["last_name"], teacher2.last_name) + self.assertEqual(content["email"], teacher2.email) + + def test_course_assistant(self): + """ + Able to retrieve assistant details of a single course. + """ + assistant1 = create_assistant( + id=5, + first_name="Simon", + last_name="Mignolet", + email="simon.mignolet@ugent.be" + ) + + assistant2 = create_assistant( + 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." + ) + course.assistants.add(assistant1) + course.assistants.add(assistant2) + + response = self.client.get( + reverse("course-detail", args=[str(course.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")) + + self.assertEqual(content_json["name"], course.name) + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple teachers + self.assertEqual(len(content_json), 2) + + content = content_json[0] + self.assertEqual(int(content["id"]), assistant1.id) + self.assertEqual(content["first_name"], assistant1.first_name) + self.assertEqual(content["last_name"], assistant1.last_name) + self.assertEqual(content["email"], assistant1.email) + + content = content_json[1] + self.assertEqual(int(content["id"]), assistant2.id) + self.assertEqual(content["first_name"], assistant2.first_name) + self.assertEqual(content["last_name"], assistant2.last_name) + self.assertEqual(content["email"], assistant2.email) + + def test_course_student(self): + """ + Able to retrieve student details of a single course. + """ + student1 = create_student( + id=5, + first_name="Simon", + last_name="Mignolet", + email="simon.mignolet@ugent.be" + ) + + student2 = create_student( + 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." + ) + course.students.add(student1) + course.students.add(student2) + + response = self.client.get( + reverse("course-detail", args=[str(course.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")) + + self.assertEqual(content_json["name"], course.name) + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple student + self.assertEqual(len(content_json), 2) + + content = content_json[0] + self.assertEqual(int(content["id"]), student1.id) + self.assertEqual(content["first_name"], student1.first_name) + self.assertEqual(content["last_name"], student1.last_name) + self.assertEqual(content["email"], student1.email) + + content = content_json[1] + self.assertEqual(int(content["id"]), student2.id) + self.assertEqual(content["first_name"], student2.first_name) + self.assertEqual(content["last_name"], student2.last_name) + self.assertEqual(content["email"], student2.email) + + def test_course_project(self): + """ + Able to retrieve project details of a single course. + """ + course = create_course( + name="Chemistry 101", + academic_startyear=2022, + description="An introductory chemistry course." + ) + + project1 = create_project( + name="become champions", + description="win the jpl", + visible=True, + archived=False, + days=50, + course=course + ) + + project2 = create_project( + name="become european champion", + description="win the cfl", + visible=True, + archived=False, + days=50, + course=course + ) + + response = self.client.get( + reverse("course-detail", args=[str(course.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")) + + self.assertEqual(content_json["name"], course.name) + 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) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert that the parsed JSON is a list with multiple projects + self.assertEqual(len(content_json), 2) + + content = content_json[0] + self.assertEqual(int(content["id"]), project1.id) + self.assertEqual(content["name"], project1.name) + self.assertEqual(content["description"], project1.description) + self.assertEqual(content["visible"], project1.visible) + self.assertEqual(content["archived"], project1.archived) + + content = content_json[1] + self.assertEqual(int(content["id"]), project2.id) + self.assertEqual(content["name"], project2.name) + self.assertEqual(content["description"], project2.description) + self.assertEqual(content["visible"], project2.visible) + self.assertEqual(content["archived"], project2.archived) From 84520f9a8fcfea66bf050738418c0ea2ee4c136a Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 22:30:09 +0100 Subject: [PATCH 65/86] chore: add endpoint tests submision --- backend/api/tests/test_submision.py | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/backend/api/tests/test_submision.py b/backend/api/tests/test_submision.py index ea8e630f..07e904c6 100644 --- a/backend/api/tests/test_submision.py +++ b/backend/api/tests/test_submision.py @@ -226,3 +226,63 @@ def test_submission_detail_view(self): 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 + ) + 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 + ) + + # Make a GET request to retrieve the submission + response = self.client.get( + reverse("submission-detail", args=[str(submission.id)]), + follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + # Assert the details of the retrieved submission + # match the created submission + retrieved_submission = content_json + self.assertEqual(int(retrieved_submission["id"]), submission.id) + self.assertEqual( + int(retrieved_submission["submission_number"]), + submission.submission_number + ) + + response = self.client.get(content_json["group"], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + expected_project_url = "http://testserver" + reverse( + "project-detail", args=[str(project.id)]) + + self.assertEqual(int(content_json["id"]), group.id) + self.assertEqual(content_json["project"], expected_project_url) + self.assertEqual(content_json["score"], group.score) From 6d9a54eb0dbb9d333a295d37e69920c0185d59e8 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 22:48:21 +0100 Subject: [PATCH 66/86] chore: add group api endpoint --- backend/api/tests/test_group.py | 115 ++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index c8767ff2..e0320a44 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -190,3 +190,118 @@ def test_group_detail_view(self): self.assertEqual(int(content_json["id"]), group.id) self.assertEqual(content_json["project"], expected_project_url) self.assertEqual(content_json["score"], group.score) + + def test_group_project(self): + """Able to retrieve details of a single group.""" + course = create_course( + name="sel2", + academic_startyear=2023 + ) + + project = create_project( + 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") + + group = create_group(project=project, score=10) + group.students.add(student) + + response = self.client.get( + 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")) + + self.assertEqual(int(content_json["id"]), group.id) + self.assertEqual(content_json["score"], group.score) + + response = self.client.get(content_json["project"], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + expected_course_url = "http://testserver" + reverse( + "course-detail", args=[str(course.id)] + ) + + self.assertEqual(content_json["name"], project.name) + self.assertEqual(content_json["description"], project.description) + self.assertEqual(content_json["visible"], project.visible) + self.assertEqual(content_json["archived"], project.archived) + self.assertEqual(content_json["course"], expected_course_url) + + def test_group_students(self): + """Able to retrieve students details of a group.""" + course = create_course( + name="sel2", + academic_startyear=2023 + ) + + project = create_project( + 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") + + student2 = create_student( + 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) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.accepted_media_type, "application/json") + + content_json = json.loads(response.content.decode("utf-8")) + + self.assertEqual(int(content_json["id"]), group.id) + self.assertEqual(content_json["score"], group.score) + + response = self.client.get(content_json["students"], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + self.assertEqual(len(content_json), 2) + + content = content_json[0] + self.assertEqual(int(content["id"]), student1.id) + self.assertEqual(content["first_name"], student1.first_name) + self.assertEqual(content["last_name"], student1.last_name) + self.assertEqual(content["email"], student1.email) + + content = content_json[1] + self.assertEqual(int(content["id"]), student2.id) + self.assertEqual(content["first_name"], student2.first_name) + self.assertEqual(content["last_name"], student2.last_name) + self.assertEqual(content["email"], student2.email) From c09f5e0e15af40bd203c32652ad23d354574542b Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 23:11:20 +0100 Subject: [PATCH 67/86] chore: add api tests project endpoints --- backend/api/tests/test_project.py | 176 +++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 3 deletions(-) diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index af7fb11a..b4e4058d 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -4,7 +4,7 @@ from django.urls import reverse from ..models.project import Project from ..models.course import Course -from ..models.checks import Checks +from ..models.checks import Checks, FileExtension def create_course(id, name, academic_startyear): @@ -15,9 +15,35 @@ def create_course(id, name, academic_startyear): id=id, name=name, academic_startyear=academic_startyear) -def create_checks(): +def create_fileExtension(id, extension): + """ + Create a FileExtension with the given arguments. + """ + return FileExtension.objects.create( + id=id, + extension=extension + ) + + +def create_checks( + id=None, + allowed_file_extensions=None, + forbidden_file_extensions=None): """Create a Checks with the given arguments.""" - return Checks.objects.create() + if id is None and allowed_file_extensions is None: + # extra if to make line shorter + if forbidden_file_extensions is None: + return Checks.objects.create() + + check = Checks.objects.create( + id=id, + ) + + for ext in allowed_file_extensions: + check.allowed_file_extensions.add(ext) + for ext in forbidden_file_extensions: + check.forbidden_file_extensions.add(ext) + return check def create_project( @@ -277,3 +303,147 @@ def test_multiple_project(self): self.assertEqual(retrieved_project["archived"], project2.archived) self.assertEqual(retrieved_project["checks"], expected_checks_url) self.assertEqual(retrieved_project["course"], expected_course_url) + + 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 + ) + checks = create_checks() + project = create_project( + name="test project", + description="test description", + visible=True, + archived=False, + days=7, + checks=checks, + course=course + ) + + response = self.client.get(reverse("project-list"), 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")) + + self.assertEqual(len(content_json), 1) + + retrieved_project = content_json[0] + + expected_checks_url = "http://testserver" + reverse( + "check-detail", args=[str(checks.id)] + ) + + self.assertEqual(retrieved_project["name"], project.name) + self.assertEqual(retrieved_project["description"], project.description) + self.assertEqual(retrieved_project["visible"], project.visible) + self.assertEqual(retrieved_project["archived"], project.archived) + self.assertEqual(retrieved_project["checks"], expected_checks_url) + + response = self.client.get(retrieved_project["course"], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + 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["description"], course.description) + + 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 + ) + fileExtension1 = create_fileExtension(id=1, extension="jpg") + fileExtension2 = create_fileExtension(id=2, extension="png") + fileExtension3 = create_fileExtension(id=3, extension="tar") + fileExtension4 = create_fileExtension(id=4, extension="wfp") + checks = create_checks( + id=5, + allowed_file_extensions=[fileExtension1, fileExtension4], + forbidden_file_extensions=[fileExtension2, fileExtension3] + ) + project = create_project( + name="test project", + description="test description", + visible=True, + archived=False, + days=7, + checks=checks, + course=course + ) + + response = self.client.get(reverse("project-list"), 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")) + + self.assertEqual(len(content_json), 1) + + retrieved_project = content_json[0] + + expected_course_url = "http://testserver" + reverse( + "course-detail", args=[str(course.id)] + ) + + self.assertEqual(retrieved_project["name"], project.name) + self.assertEqual(retrieved_project["description"], project.description) + self.assertEqual(retrieved_project["visible"], project.visible) + self.assertEqual(retrieved_project["archived"], project.archived) + self.assertEqual(retrieved_project["course"], expected_course_url) + + response = self.client.get(retrieved_project["checks"], follow=True) + + # Check if the response was successful + self.assertEqual(response.status_code, 200) + + # Assert that the response is JSON + self.assertEqual(response.accepted_media_type, "application/json") + + # Parse the JSON content from the response + content_json = json.loads(response.content.decode("utf-8")) + + self.assertEqual(int(content_json["id"]), checks.id) + + # Assert the file extensions of the retrieved + # Checks match the created 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) + self.assertEqual( + retrieved_allowed_file_extensions[1]["extension"], + fileExtension4.extension) + + 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) + self.assertEqual( + retrieved_forbidden_file_extensions[1]["extension"], + fileExtension3.extension) From ebed9ed03903a62e79695d726fe686810e0f3475 Mon Sep 17 00:00:00 2001 From: Tybo Verslype Date: Sat, 2 Mar 2024 23:36:06 +0100 Subject: [PATCH 68/86] fix: removed prints --- backend/api/tests/test_admin.py | 1 - backend/api/tests/test_checks.py | 1 - backend/api/tests/test_submision.py | 1 - backend/api/tests/test_teacher.py | 1 - 4 files changed, 4 deletions(-) diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index 963db1cb..292e1f21 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -200,7 +200,6 @@ def test_admin_faculty(self): self.assertEqual(content_json["first_name"], admin.first_name) self.assertEqual(content_json["last_name"], admin.last_name) self.assertEqual(content_json["email"], admin.email) - print(content_json["faculties"]) response = self.client.get(content_json["faculties"][0], follow=True) diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index 1eb1ce75..e4247956 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -36,7 +36,6 @@ def test_no_fileExtension(self): """ response_root = self.client.get( reverse("fileExtension-list"), follow=True) - # print(response.content) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") diff --git a/backend/api/tests/test_submision.py b/backend/api/tests/test_submision.py index 07e904c6..119415ef 100644 --- a/backend/api/tests/test_submision.py +++ b/backend/api/tests/test_submision.py @@ -63,7 +63,6 @@ def test_no_submission(self): response_root = self.client.get( reverse("submission-list"), follow=True) - # print(response.content) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") diff --git a/backend/api/tests/test_teacher.py b/backend/api/tests/test_teacher.py index 5122ab0b..2e3d8c6e 100644 --- a/backend/api/tests/test_teacher.py +++ b/backend/api/tests/test_teacher.py @@ -67,7 +67,6 @@ def test_no_teacher(self): """ response_root = self.client.get(reverse("teacher-list"), follow=True) - # print(response.content) self.assertEqual(response_root.status_code, 200) # Assert that the response is JSON self.assertEqual(response_root.accepted_media_type, "application/json") From af95e0f562761c1d010f52ea570164759f5bdbea Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 4 Mar 2024 10:12:50 +0100 Subject: [PATCH 69/86] build: add default linting checks --- .github/workflows/backend-tests.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml index cd8149f7..9144dd55 100644 --- a/.github/workflows/backend-tests.yaml +++ b/.github/workflows/backend-tests.yaml @@ -9,7 +9,7 @@ on: jobs: test: - runs-on: self-hosted + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python 3.11 @@ -19,6 +19,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install flake8 pip install -r ./backend/requirements.txt - name: Execute tests run: python ./backend/manage.py test + - name: Execute linting checks + run: flake8 ./backend From 2315ef96519d988624fe9cbc47d14ffb5dfd0628 Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 4 Mar 2024 10:16:46 +0100 Subject: [PATCH 70/86] build: seperate testing and linting workflow --- .github/workflows/backend-linting.yaml | 25 +++++++++++++++++++++++++ .github/workflows/backend-tests.yaml | 2 -- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/backend-linting.yaml diff --git a/.github/workflows/backend-linting.yaml b/.github/workflows/backend-linting.yaml new file mode 100644 index 00000000..cbd8e9f5 --- /dev/null +++ b/.github/workflows/backend-linting.yaml @@ -0,0 +1,25 @@ +name: backend-linting + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + pip install -r ./backend/requirements.txt + - name: Execute linting checks + run: flake8 ./backend diff --git a/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml index 9144dd55..85ba4f2e 100644 --- a/.github/workflows/backend-tests.yaml +++ b/.github/workflows/backend-tests.yaml @@ -23,5 +23,3 @@ jobs: pip install -r ./backend/requirements.txt - name: Execute tests run: python ./backend/manage.py test - - name: Execute linting checks - run: flake8 ./backend From 5656ce396c9fde00d002e63634b3d22e0e707269 Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 4 Mar 2024 10:28:51 +0100 Subject: [PATCH 71/86] build: flake8 config --- .github/workflows/backend-linting.yaml | 2 +- backend/.flake8 | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 backend/.flake8 diff --git a/.github/workflows/backend-linting.yaml b/.github/workflows/backend-linting.yaml index cbd8e9f5..e3a7c8e5 100644 --- a/.github/workflows/backend-linting.yaml +++ b/.github/workflows/backend-linting.yaml @@ -22,4 +22,4 @@ jobs: pip install flake8 pip install -r ./backend/requirements.txt - name: Execute linting checks - run: flake8 ./backend + run: flake8 --config ./backend/.flake8 ./backend diff --git a/backend/.flake8 b/backend/.flake8 new file mode 100644 index 00000000..ee3e436d --- /dev/null +++ b/backend/.flake8 @@ -0,0 +1,15 @@ +[flake8] + +# Ignore unused imports +ignore = F401 + +max-line-length = 119 + +max-complexity = 10 + +exclude = .git, + __pycache__, + .venv, + venv, + migrations + From 8057562d71abb7b6ab43acd6a216ee7b771ddd1a Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 4 Mar 2024 16:33:13 +0100 Subject: [PATCH 72/86] 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() From 6740434204d2c3510ee379e608b4001407d4b56d Mon Sep 17 00:00:00 2001 From: francis Date: Mon, 4 Mar 2024 22:11:39 +0100 Subject: [PATCH 73/86] fix: change github actions server to self-hosted --- .github/workflows/backend-linting.yaml | 2 +- .github/workflows/backend-tests.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend-linting.yaml b/.github/workflows/backend-linting.yaml index e3a7c8e5..d145985d 100644 --- a/.github/workflows/backend-linting.yaml +++ b/.github/workflows/backend-linting.yaml @@ -9,7 +9,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - uses: actions/checkout@v4 - name: Set up Python 3.11 diff --git a/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml index 85ba4f2e..48768a80 100644 --- a/.github/workflows/backend-tests.yaml +++ b/.github/workflows/backend-tests.yaml @@ -9,7 +9,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - uses: actions/checkout@v4 - name: Set up Python 3.11 From 7092a2a367cfddbfaf5d32c867fdc307ca77e20d Mon Sep 17 00:00:00 2001 From: EwoutV Date: Mon, 4 Mar 2024 18:28:52 +0100 Subject: [PATCH 74/86] feat: moved admin class as class field for user --- backend/api/models/admin.py | 10 --- backend/api/serializers/admin_serializer.py | 20 ------ backend/api/urls.py | 6 +- backend/api/views/admin_view.py | 9 ++- .../views/users.py => api/views/user_view.py} | 2 +- backend/authentication/models.py | 6 +- backend/authentication/permissions.py | 6 ++ backend/authentication/services/__init__.py | 0 backend/authentication/services/users.py | 70 ------------------- backend/authentication/urls.py | 24 ++----- backend/authentication/views.py | 38 ++++++++++ backend/authentication/views/__init__.py | 0 backend/authentication/views/auth.py | 35 ---------- backend/ypovoli/settings.py | 5 +- 14 files changed, 67 insertions(+), 164 deletions(-) delete mode 100644 backend/api/models/admin.py delete mode 100644 backend/api/serializers/admin_serializer.py rename backend/{authentication/views/users.py => api/views/user_view.py} (78%) create mode 100644 backend/authentication/permissions.py delete mode 100644 backend/authentication/services/__init__.py create mode 100644 backend/authentication/views.py delete mode 100644 backend/authentication/views/__init__.py delete mode 100644 backend/authentication/views/auth.py diff --git a/backend/api/models/admin.py b/backend/api/models/admin.py deleted file mode 100644 index f577ae9b..00000000 --- a/backend/api/models/admin.py +++ /dev/null @@ -1,10 +0,0 @@ -from authentication.models import User - - -class Admin(User): - """This model represents the admin. - It extends the User model from the authentication app with - admin-specific attributes. - """ - - # At the moment, there are no additional attributes for the Admin model. diff --git a/backend/api/serializers/admin_serializer.py b/backend/api/serializers/admin_serializer.py deleted file mode 100644 index 7b060e69..00000000 --- a/backend/api/serializers/admin_serializer.py +++ /dev/null @@ -1,20 +0,0 @@ -from rest_framework import serializers -from ..models.admin import Admin - - -class AdminSerializer(serializers.ModelSerializer): - faculties = serializers.HyperlinkedRelatedField( - 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", - ] diff --git a/backend/api/urls.py b/backend/api/urls.py index 450301ca..64b83a23 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,4 +1,5 @@ from django.urls import include, path +from api.views import user_view from api.views import teacher_view from api.views import admin_view from api.views import assistant_view @@ -12,6 +13,7 @@ from rest_framework.routers import DefaultRouter router = DefaultRouter() +router.register(r"users", user_view.UserViewSet, basename="user") 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") @@ -21,9 +23,7 @@ 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"fileExtensions", checks_view.FileExtensionViewSet, basename="fileExtension") router.register(r"faculties", faculty_view.facultyViewSet, basename="faculty") urlpatterns = [ diff --git a/backend/api/views/admin_view.py b/backend/api/views/admin_view.py index f44fdcdb..5c4fabe3 100644 --- a/backend/api/views/admin_view.py +++ b/backend/api/views/admin_view.py @@ -1,8 +1,7 @@ from rest_framework import viewsets -from ..models.admin import Admin -from ..serializers.admin_serializer import AdminSerializer - +from authentication.serializers import UserSerializer +from authentication.models import User class AdminViewSet(viewsets.ModelViewSet): - queryset = Admin.objects.all() - serializer_class = AdminSerializer + queryset = User.objects.filter(is_staff=True) + serializer_class = UserSerializer diff --git a/backend/authentication/views/users.py b/backend/api/views/user_view.py similarity index 78% rename from backend/authentication/views/users.py rename to backend/api/views/user_view.py index cea6e4a9..870243da 100644 --- a/backend/authentication/views/users.py +++ b/backend/api/views/user_view.py @@ -4,6 +4,6 @@ from authentication.serializers import UserSerializer -class UsersView(ListModelMixin, RetrieveModelMixin, GenericViewSet): +class UserViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): queryset = User.objects.all() serializer_class = UserSerializer diff --git a/backend/authentication/models.py b/backend/authentication/models.py index a9cce277..7142b631 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -1,8 +1,8 @@ import datetime from django.db import models -from django.db.models import CharField, EmailField, IntegerField, DateTimeField -from django.contrib.auth.models import AbstractBaseUser +from django.db.models import CharField, EmailField, IntegerField, DateTimeField, BooleanField +from django.contrib.auth.models import AbstractBaseUser, AbstractUser class User(AbstractBaseUser): @@ -17,6 +17,8 @@ class User(AbstractBaseUser): username = CharField(max_length=12, unique=True) + is_staff = BooleanField(default=False, null=False) + email = EmailField(null=False, unique=True) first_name = CharField(max_length=50, null=False) diff --git a/backend/authentication/permissions.py b/backend/authentication/permissions.py new file mode 100644 index 00000000..ae7293b5 --- /dev/null +++ b/backend/authentication/permissions.py @@ -0,0 +1,6 @@ +from rest_framework.permissions import BasePermission, DjangoModelPermissions + + +class CASPermission(BasePermission): + def has_permission(self, request, view): + pass \ No newline at end of file diff --git a/backend/authentication/services/__init__.py b/backend/authentication/services/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/authentication/services/users.py b/backend/authentication/services/users.py index dcafcdf7..e69de29b 100644 --- a/backend/authentication/services/users.py +++ b/backend/authentication/services/users.py @@ -1,70 +0,0 @@ -from authentication.models import User - - -def exists(user_id: str) -> bool: - """Check if a user exists""" - return User.objects.filter(id=user_id).exists() - - -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, - faculty: str = None, - last_enrolled: 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, - ) - - -def get_or_create( - user_id: str, - username: str, - email: str, - first_name: str, - last_name: str, - faculty: str = None, - last_enrolled: 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, - ) - - return user diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 4e53f3d0..4365ca54 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -1,26 +1,16 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView, - TokenVerifyView, -) -from authentication.views.auth import WhoAmIView, LoginView, LogoutView, TokenEchoView -from authentication.views.users import UsersView +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView +from authentication.views import CASViewSet router = DefaultRouter() -router.register("users", UsersView, basename="user") +router.register(f"cas", CASViewSet,"cas") urlpatterns = [ - # USER endpoints. - 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("", include(router.urls)), # 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"), + path("token", TokenObtainPairView.as_view(), name="token"), + path("token/refresh", TokenRefreshView.as_view(), name="token-refresh"), + path("token/verify", TokenVerifyView.as_view(), name="token-verify") ] diff --git a/backend/authentication/views.py b/backend/authentication/views.py new file mode 100644 index 00000000..b5993489 --- /dev/null +++ b/backend/authentication/views.py @@ -0,0 +1,38 @@ +from django.shortcuts import redirect +from rest_framework.decorators import action +from rest_framework.viewsets import ViewSet +from rest_framework.request import Request +from rest_framework.response import Response +from authentication.serializers import UserSerializer +from authentication.cas.client import client +from ypovoli import settings + +class CASViewSet(ViewSet): + + @action(detail=False, methods=['get']) + def login(self, _: Request) -> Response: + """Attempt to log in. Redirect to our single CAS endpoint.""" + return redirect(client.get_login_url()) + + @action(detail=False, methods=['get']) + def logout(self, _: Request) -> Response: + """Attempt to log out. Redirect to our single CAS endpoint. + Normally would only allow POST requests to a logout endpoint. + Since the CAS logout location handles the actual logout, we should accept GET requests. + """ + return redirect(client.get_logout_url(service_url=settings.API_ENDPOINT)) + + @action(detail=False, methods=['get'], url_path='whoami') + def who_am_i(self, request: Request) -> Response: + """Get the user account data for the logged-in user. + The logged-in user is determined by the provided access token in the + Authorization HTTP header. + """ + return Response( + UserSerializer(request.user).data + ) + + @action(detail=False, methods=['get']) + def echo(self, request: Request) -> Response: + """Echo the obtained CAS token for development and testing.""" + return Response(request.query_params.get('ticket')) \ No newline at end of file diff --git a/backend/authentication/views/__init__.py b/backend/authentication/views/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/authentication/views/auth.py b/backend/authentication/views/auth.py deleted file mode 100644 index fea0ded0..00000000 --- a/backend/authentication/views/auth.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.shortcuts import redirect -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.request import Request -from rest_framework.permissions import IsAuthenticated -from authentication.serializers import UserSerializer -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) - - -class LogoutView(APIView): - permission_classes = [IsAuthenticated] - - 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")) diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index 6663866c..3582b40b 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -66,6 +66,9 @@ "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework_simplejwt.authentication.JWTAuthentication" ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ] } SIMPLE_JWT = { @@ -84,7 +87,7 @@ # Application endpoints CAS_ENDPOINT = "https://login.ugent.be" -CAS_RESPONSE = "https://localhost:8080/auth/echo" +CAS_RESPONSE = "https://localhost:8080/auth/cas/echo" API_ENDPOINT = "https://localhost:8080" # Database From 5a1fff12ab9625b8f1a2eecc7a758fa41dfbd859 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Mon, 4 Mar 2024 20:52:55 +0100 Subject: [PATCH 75/86] fix: test authentication (wip) --- backend/api/migrations/0002_delete_admin.py | 16 +++++++++++++ backend/api/tests/test_admin.py | 16 +++++++++---- ...03_user_is_staff_alter_user_create_time.py | 23 +++++++++++++++++++ backend/authentication/models.py | 14 +++++++---- backend/authentication/permissions.py | 11 ++++++--- backend/authentication/views.py | 8 +++++-- backend/ypovoli/settings.py | 2 +- 7 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 backend/api/migrations/0002_delete_admin.py create mode 100644 backend/authentication/migrations/0003_user_is_staff_alter_user_create_time.py diff --git a/backend/api/migrations/0002_delete_admin.py b/backend/api/migrations/0002_delete_admin.py new file mode 100644 index 00000000..036781ac --- /dev/null +++ b/backend/api/migrations/0002_delete_admin.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.2 on 2024-03-04 18:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='Admin', + ), + ] diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index d6d44888..ac4b3768 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -4,8 +4,7 @@ from django.utils import timezone from django.urls import reverse -from ..models.admin import Admin -from authentication.models import Faculty +from authentication.models import Faculty, User def create_faculty(name): @@ -20,29 +19,38 @@ def create_admin(id, first_name, last_name, email, faculty=None): """ username = f"{first_name}_{last_name}" if faculty is None: - return Admin.objects.create( + return User.objects.create( id=id, first_name=first_name, last_name=last_name, username=username, email=email, + is_staff=True, create_time=timezone.now(), ) else: - admin = Admin.objects.create( + admin = User.objects.create( id=id, first_name=first_name, last_name=last_name, username=username, email=email, + is_staff=True, create_time=timezone.now(), ) + for fac in faculty: admin.faculties.add(fac) + return admin class AdminModelTests(TestCase): + def setUp(self): + self.client.force_login( + create_admin('admin', 'admin', 'admin', 'admin@admin.com') + ) + def test_no_admins(self): """ able to retrieve no admin before publishing it. diff --git a/backend/authentication/migrations/0003_user_is_staff_alter_user_create_time.py b/backend/authentication/migrations/0003_user_is_staff_alter_user_create_time.py new file mode 100644 index 00000000..6e1a9080 --- /dev/null +++ b/backend/authentication/migrations/0003_user_is_staff_alter_user_create_time.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.2 on 2024-03-04 18:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0002_remove_faculty_user_remove_user_faculty_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_staff', + field=models.BooleanField(default=False), + ), + 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 7142b631..6e487258 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -1,8 +1,8 @@ -import datetime - +from datetime import MINYEAR +from typing import Self, Type from django.db import models -from django.db.models import CharField, EmailField, IntegerField, DateTimeField, BooleanField -from django.contrib.auth.models import AbstractBaseUser, AbstractUser +from django.db.models import CharField, EmailField, IntegerField, DateTimeField, BooleanField, Model +from django.contrib.auth.models import AbstractBaseUser, AbstractUser, PermissionsMixin class User(AbstractBaseUser): @@ -35,6 +35,12 @@ class User(AbstractBaseUser): USERNAME_FIELD = "username" EMAIL_FIELD = "email" + def has_role(self, model: Type[Self]): + """Simple generic implementation of roles. + This function looks if there exists a model (inheriting from User) with the same ID. + """ + model.objects.exists(self.id) + class Faculty(models.Model): """This model represents a faculty.""" diff --git a/backend/authentication/permissions.py b/backend/authentication/permissions.py index ae7293b5..5056df97 100644 --- a/backend/authentication/permissions.py +++ b/backend/authentication/permissions.py @@ -1,6 +1,11 @@ -from rest_framework.permissions import BasePermission, DjangoModelPermissions +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.viewsets import ViewSet class CASPermission(BasePermission): - def has_permission(self, request, view): - pass \ No newline at end of file + def has_permission(self, request: Request, view: ViewSet): + """Check whether a user has permission in the CAS flow context.""" + return request.user.is_authenticated or view.action not in [ + 'logout', 'whoami' + ] \ No newline at end of file diff --git a/backend/authentication/views.py b/backend/authentication/views.py index b5993489..db2e7484 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -3,13 +3,17 @@ from rest_framework.viewsets import ViewSet from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.permissions import AllowAny, IsAuthenticated from authentication.serializers import UserSerializer from authentication.cas.client import client from ypovoli import settings class CASViewSet(ViewSet): + # The IsAuthenticated class is applied by default, + # but it's good to be verbose when it comes to security. + authentication_classes = [IsAuthenticated] - @action(detail=False, methods=['get']) + @action(detail=False, methods=['get'], permission_classes=[AllowAny]) def login(self, _: Request) -> Response: """Attempt to log in. Redirect to our single CAS endpoint.""" return redirect(client.get_login_url()) @@ -32,7 +36,7 @@ def who_am_i(self, request: Request) -> Response: UserSerializer(request.user).data ) - @action(detail=False, methods=['get']) + @action(detail=False, methods=['get'], permission_classes=[AllowAny]) def echo(self, request: Request) -> Response: """Echo the obtained CAS token for development and testing.""" return Response(request.query_params.get('ticket')) \ No newline at end of file diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index 3582b40b..880f2f80 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -67,7 +67,7 @@ "rest_framework_simplejwt.authentication.JWTAuthentication" ], 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', + 'rest_framework.permissions.IsAuthenticated' ] } From 588a450dcaf6d23971faef80b62e10fdbf2e5539 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 15:12:08 +0100 Subject: [PATCH 76/86] chore: fixed test with authorization --- backend/api/tests/test_admin.py | 10 ++++---- backend/api/tests/test_assistant.py | 12 ++++++---- backend/api/tests/test_checks.py | 21 ++++++++++++----- backend/api/tests/test_course.py | 21 ++++++++++------- backend/api/tests/test_group.py | 18 ++++++++++----- backend/api/tests/test_project.py | 16 +++++++++---- backend/api/tests/test_student.py | 17 +++++++++----- .../{test_submision.py => test_submission.py} | 19 ++++++++++----- backend/api/tests/test_teacher.py | 16 ++++++++----- backend/authentication/models.py | 11 +++++++++ .../tests/test_authentication_serializer.py | 1 - .../tests/test_authentication_views.py | 23 +++++++++---------- backend/authentication/views.py | 16 ++++++++----- backend/ypovoli/settings.py | 2 +- 14 files changed, 130 insertions(+), 73 deletions(-) rename backend/api/tests/{test_submision.py => test_submission.py} (95%) diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index ac4b3768..5de600b2 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -1,9 +1,7 @@ import json - -from django.test import TestCase from django.utils import timezone from django.urls import reverse - +from rest_framework.test import APITestCase from authentication.models import Faculty, User @@ -45,10 +43,10 @@ def create_admin(id, first_name, last_name, email, faculty=None): return admin -class AdminModelTests(TestCase): +class AdminModelTests(APITestCase): def setUp(self): - self.client.force_login( - create_admin('admin', 'admin', 'admin', 'admin@admin.com') + self.client.force_authenticate( + User.get_dummy_admin() ) def test_no_admins(self): diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index 15d53aa1..81332915 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -1,11 +1,10 @@ import json - -from django.test import TestCase from django.utils import timezone from django.urls import reverse +from rest_framework.test import APITestCase from api.models.assistant import Assistant from api.models.course import Course -from authentication.models import Faculty +from authentication.models import Faculty, User def create_course(name, academic_startyear, description=None, parent_course=None): @@ -50,7 +49,12 @@ def create_assistant(id, first_name, last_name, email, faculty=None, courses=Non return assistant -class AssistantModelTests(TestCase): +class AssistantModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_no_assistant(self): """ able to retrieve no assistant before publishing it. diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index f7273144..b47fe651 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -1,9 +1,8 @@ import json - -from django.test import TestCase from django.urls import reverse - -from ..models.checks import FileExtension, Checks +from rest_framework.test import APITestCase +from authentication.models import User +from api.models.checks import FileExtension, Checks def create_fileExtension(id, extension): @@ -26,7 +25,12 @@ def create_checks(id, allowed_file_extensions, forbidden_file_extensions): return check -class FileExtensionModelTests(TestCase): +class FileExtensionModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_no_fileExtension(self): """ able to retrieve no FileExtension before publishing it. @@ -126,7 +130,12 @@ def test_fileExtension_detail_view(self): self.assertEqual(content_json["extension"], fileExtension.extension) -class ChecksModelTests(TestCase): +class ChecksModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_no_checks(self): """ Able to retrieve no Checks before publishing it. diff --git a/backend/api/tests/test_course.py b/backend/api/tests/test_course.py index 97de9259..a2c3d165 100644 --- a/backend/api/tests/test_course.py +++ b/backend/api/tests/test_course.py @@ -1,13 +1,13 @@ import json - -from django.test import TestCase from django.utils import timezone from django.urls import reverse -from ..models.course import Course -from ..models.teacher import Teacher -from ..models.assistant import Assistant -from ..models.student import Student -from ..models.project import Project +from rest_framework.test import APITestCase +from authentication.models import User +from api.models.course import Course +from api.models.teacher import Teacher +from api.models.assistant import Assistant +from api.models.student import Student +from api.models.project import Project def create_project(name, description, visible, archived, days, course): @@ -84,7 +84,12 @@ def create_course(name, academic_startyear, description=None, parent_course=None ) -class CourseModelTests(TestCase): +class CourseModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_no_courses(self): """ Able to retrieve no courses before publishing any. diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index dafbc1a8..f10e87d4 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -1,12 +1,13 @@ import json from datetime import timedelta -from django.test import TestCase from django.urls import reverse from django.utils import timezone -from ..models.project import Project -from ..models.student import Student -from ..models.group import Group -from ..models.course import Course +from rest_framework.test import APITestCase +from authentication.models import User +from api.models.project import Project +from api.models.student import Student +from api.models.group import Group +from api.models.course import Course def create_course(name, academic_startyear, description=None, parent_course=None): @@ -46,7 +47,12 @@ def create_group(project, score): return Group.objects.create(project=project, score=score) -class GroupModelTests(TestCase): +class GroupModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_no_groups(self): """Able to retrieve no groups before creating any.""" response = self.client.get(reverse("group-list"), follow=True) diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index 72f3fbef..ae9f2efb 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -1,10 +1,11 @@ import json -from django.test import TestCase from django.utils import timezone from django.urls import reverse -from ..models.project import Project -from ..models.course import Course -from ..models.checks import Checks, FileExtension +from rest_framework.test import APITestCase +from authentication.models import User +from api.models.project import Project +from api.models.course import Course +from api.models.checks import Checks, FileExtension def create_course(id, name, academic_startyear): @@ -58,7 +59,12 @@ def create_project(name, description, visible, archived, days, checks, course): ) -class ProjectModelTests(TestCase): +class ProjectModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_toggle_visible(self): """ toggle the visible state of a project. diff --git a/backend/api/tests/test_student.py b/backend/api/tests/test_student.py index c43a89e7..1fced767 100644 --- a/backend/api/tests/test_student.py +++ b/backend/api/tests/test_student.py @@ -1,11 +1,10 @@ import json - -from django.test import TestCase from django.utils import timezone from django.urls import reverse -from ..models.student import Student -from ..models.course import Course -from authentication.models import Faculty +from rest_framework.test import APITestCase +from api.models.student import Student +from api.models.course import Course +from authentication.models import Faculty, User def create_course(name, academic_startyear, description=None, parent_course=None): @@ -50,7 +49,13 @@ def create_student(id, first_name, last_name, email, faculty=None, courses=None) return student -class StudentModelTests(TestCase): +class StudentModelTests(APITestCase): + + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_no_student(self): """ able to retrieve no student before publishing it. diff --git a/backend/api/tests/test_submision.py b/backend/api/tests/test_submission.py similarity index 95% rename from backend/api/tests/test_submision.py rename to backend/api/tests/test_submission.py index 51b571ea..fa7a4386 100644 --- a/backend/api/tests/test_submision.py +++ b/backend/api/tests/test_submission.py @@ -1,12 +1,13 @@ import json from datetime import timedelta -from django.test import TestCase from django.utils import timezone from django.urls import reverse -from ..models.submission import Submission, SubmissionFile -from ..models.project import Project -from ..models.group import Group -from ..models.course import Course +from rest_framework.test import APITestCase +from authentication.models import User +from api.models.submission import Submission, SubmissionFile +from api.models.project import Project +from api.models.group import Group +from api.models.course import Course def create_course(name, academic_startyear, description=None, parent_course=None): @@ -46,7 +47,13 @@ def create_submissionFile(submission, file): return SubmissionFile.objects.create(submission=submission, file=file) -class SubmissionModelTests(TestCase): +class SubmissionModelTests(APITestCase): + + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_no_submission(self): """ able to retrieve no submission before publishing it. diff --git a/backend/api/tests/test_teacher.py b/backend/api/tests/test_teacher.py index dc07da70..ec58ec95 100644 --- a/backend/api/tests/test_teacher.py +++ b/backend/api/tests/test_teacher.py @@ -1,11 +1,10 @@ import json - -from django.test import TestCase from django.utils import timezone from django.urls import reverse -from ..models.teacher import Teacher -from ..models.course import Course -from authentication.models import Faculty +from rest_framework.test import APITestCase +from api.models.teacher import Teacher +from api.models.course import Course +from authentication.models import Faculty, User def create_course(name, academic_startyear, description=None, parent_course=None): @@ -50,7 +49,12 @@ def create_teacher(id, first_name, last_name, email, faculty=None, courses=None) return teacher -class TeacherModelTests(TestCase): +class TeacherModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_no_teacher(self): """ able to retrieve no teacher before publishing it. diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 6e487258..a2dea074 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -41,6 +41,17 @@ def has_role(self, model: Type[Self]): """ model.objects.exists(self.id) + @staticmethod + def get_dummy_admin(): + return User( + id="admin", + first_name="Nikkus", + last_name="Derdinus", + username="nderdinus", + email="nikkus@ypovoli.be", + is_staff=True + ) + class Faculty(models.Model): """This model represents a faculty.""" diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py index 736ed2f0..b5ef46c4 100644 --- a/backend/authentication/tests/test_authentication_serializer.py +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -18,7 +18,6 @@ FIRST_NAME = "Dummy" LAST_NAME = "McDickwad" - class UserSerializerModelTests(TestCase): def test_invalid_email_makes_user_serializer_invalid(self): """ diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index ce1ad7e5..068e1d93 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -9,16 +9,15 @@ class TestWhomAmIView(APITestCase): def setUp(self): """Create a user and generate a token for that user""" - user_data = { + user_data = User.objects.create(**{ "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 {AccessToken().for_user(self.user)}' def test_who_am_i_view_get_returns_user_if_existing_and_authenticated(self): """ @@ -27,7 +26,7 @@ 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("cas-whoami")) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) self.assertEqual(content["id"], self.user.id) @@ -42,12 +41,12 @@ def test_who_am_i_view_get_does_not_return_viewer_if_deleted_but_authenticated( self.user.delete() self.client.credentials(HTTP_AUTHORIZATION=self.token) - response = self.client.get(reverse("auth.whoami")) + response = self.client.get(reverse("cas-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("cas-whoami")) self.assertEqual(response.status_code, 401) @@ -67,7 +66,7 @@ def test_logout_view_authenticated_logout_url(self): 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")) + response = self.client.post(reverse("cas-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 @@ -76,14 +75,14 @@ def test_logout_view_authenticated_logout_url(self): 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("cas-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("cas-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 @@ -95,6 +94,6 @@ 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}) + response = self.client.get(reverse("cas-echo"), data={"ticket": ticket}) content = response.rendered_content.decode("utf-8").strip('"') self.assertEqual(content, ticket) diff --git a/backend/authentication/views.py b/backend/authentication/views.py index db2e7484..1b66d8d2 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -11,14 +11,14 @@ class CASViewSet(ViewSet): # The IsAuthenticated class is applied by default, # but it's good to be verbose when it comes to security. - authentication_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] - @action(detail=False, methods=['get'], permission_classes=[AllowAny]) + @action(detail=False, methods=['GET'], permission_classes=[AllowAny]) def login(self, _: Request) -> Response: """Attempt to log in. Redirect to our single CAS endpoint.""" return redirect(client.get_login_url()) - @action(detail=False, methods=['get']) + @action(detail=False, methods=['GET']) def logout(self, _: Request) -> Response: """Attempt to log out. Redirect to our single CAS endpoint. Normally would only allow POST requests to a logout endpoint. @@ -26,17 +26,21 @@ def logout(self, _: Request) -> Response: """ return redirect(client.get_logout_url(service_url=settings.API_ENDPOINT)) - @action(detail=False, methods=['get'], url_path='whoami') + @action(detail=False, methods=['GET'], url_path='whoami', url_name='whoami') def who_am_i(self, request: Request) -> Response: """Get the user account data for the logged-in user. The logged-in user is determined by the provided access token in the Authorization HTTP header. """ + user_serializer = UserSerializer(request.user, context={ + 'request': request + }) + return Response( - UserSerializer(request.user).data + user_serializer.data ) - @action(detail=False, methods=['get'], permission_classes=[AllowAny]) + @action(detail=False, methods=['GET'], permission_classes=[AllowAny]) def echo(self, request: Request) -> Response: """Echo the obtained CAS token for development and testing.""" return Response(request.query_params.get('ticket')) \ No newline at end of file diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index 880f2f80..e5f197c1 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -110,7 +110,7 @@ TIME_ZONE = "UTC" USE_I18N = True USE_L10N = False -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ From f1c18b3247247079e1fa4a51d719efae6f9e8735 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 15:26:47 +0100 Subject: [PATCH 77/86] chore: merge --- backend/api/migrations/0001_initial.py | 301 ++++-------------- backend/api/migrations/0002_delete_admin.py | 16 - .../authentication/migrations/0001_initial.py | 61 ++-- ...culty_user_remove_user_faculty_and_more.py | 27 -- .../migrations/0003_alter_user_create_time.py | 17 - ...03_user_is_staff_alter_user_create_time.py | 23 -- backend/authentication/models.py | 2 +- .../tests/test_authentication_views.py | 6 +- 8 files changed, 82 insertions(+), 371 deletions(-) delete mode 100644 backend/api/migrations/0002_delete_admin.py delete mode 100644 backend/authentication/migrations/0002_remove_faculty_user_remove_user_faculty_and_more.py delete mode 100644 backend/authentication/migrations/0003_alter_user_create_time.py delete mode 100644 backend/authentication/migrations/0003_user_is_staff_alter_user_create_time.py diff --git a/backend/api/migrations/0001_initial.py b/backend/api/migrations/0001_initial.py index 6b842090..1254180c 100644 --- a/backend/api/migrations/0001_initial.py +++ b/backend/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.2 on 2024-02-28 21:55 +# Generated by Django 5.0.2 on 2024-03-05 14:25 import datetime import django.db.models.deletion @@ -7,300 +7,115 @@ class Migration(migrations.Migration): + initial = True dependencies = [ - ("authentication", "0001_initial"), + ('authentication', '0001_initial'), ] operations = [ migrations.CreateModel( - 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, - ), - ), - ], - options={ - "abstract": False, - }, - 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/migrations/0002_delete_admin.py b/backend/api/migrations/0002_delete_admin.py deleted file mode 100644 index 036781ac..00000000 --- a/backend/api/migrations/0002_delete_admin.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-04 18:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0001_initial'), - ] - - operations = [ - migrations.DeleteModel( - name='Admin', - ), - ] diff --git a/backend/authentication/migrations/0001_initial.py b/backend/authentication/migrations/0001_initial.py index 8895c9c4..3265f487 100644 --- a/backend/authentication/migrations/0001_initial.py +++ b/backend/authentication/migrations/0001_initial.py @@ -1,59 +1,38 @@ -# Generated by Django 5.0.2 on 2024-02-28 21:55 +# Generated by Django 5.0.2 on 2024-03-05 14:25 -from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): + initial = True - dependencies = [] + dependencies = [ + ] operations = [ migrations.CreateModel( - name="User", + name='Faculty', 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)), + ('name', models.CharField(max_length=50, primary_key=True, serialize=False)), ], - options={ - "abstract": False, - }, ), migrations.CreateModel( - name="Faculty", + name='User', 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 - ), - ), + ('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)), + ('is_staff', models.BooleanField(default=False)), + ('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_add=True)), + ('faculties', models.ManyToManyField(blank=True, related_name='users', to='authentication.faculty')), ], - ), - migrations.AddField( - model_name="user", - name="faculty", - field=models.ManyToManyField( - blank=True, related_name="faculties", to="authentication.faculty" - ), + options={ + 'abstract': False, + }, ), ] 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 deleted file mode 100644 index 6323b90c..00000000 --- a/backend/authentication/migrations/0002_remove_faculty_user_remove_user_faculty_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.0.2 on 2024-02-29 13:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("authentication", "0001_initial"), - ] - - operations = [ - migrations.RemoveField( - model_name="faculty", - name="user", - ), - migrations.RemoveField( - model_name="user", - name="faculty", - ), - migrations.AddField( - 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 deleted file mode 100644 index f65d50cf..00000000 --- a/backend/authentication/migrations/0003_alter_user_create_time.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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/migrations/0003_user_is_staff_alter_user_create_time.py b/backend/authentication/migrations/0003_user_is_staff_alter_user_create_time.py deleted file mode 100644 index 6e1a9080..00000000 --- a/backend/authentication/migrations/0003_user_is_staff_alter_user_create_time.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-04 18:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('authentication', '0002_remove_faculty_user_remove_user_faculty_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='is_staff', - field=models.BooleanField(default=False), - ), - 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 a2dea074..8a8787f4 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -27,7 +27,7 @@ class User(AbstractBaseUser): faculties = models.ManyToManyField("Faculty", related_name="users", blank=True) - last_enrolled = IntegerField(default=datetime.MINYEAR, null=True) + last_enrolled = IntegerField(default=MINYEAR, null=True) create_time = DateTimeField(auto_now_add=True) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 068e1d93..960e689d 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -9,7 +9,7 @@ class TestWhomAmIView(APITestCase): def setUp(self): """Create a user and generate a token for that user""" - user_data = User.objects.create(**{ + self.user = User.objects.create(**{ "id": "1234", "username": "ddickwd", "email": "dummy@dummy.com", @@ -66,7 +66,7 @@ def test_logout_view_authenticated_logout_url(self): 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("cas-logout")) + response = self.client.get(reverse("cas-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 @@ -75,7 +75,7 @@ def test_logout_view_authenticated_logout_url(self): 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("cas-logout")) + response = self.client.get(reverse("cas-logout")) self.assertEqual(response.status_code, 401) From fe100d8d9a29b875ac4b62f513bd35370e201d15 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 15:32:03 +0100 Subject: [PATCH 78/86] chore: fixed linting errors --- backend/api/views/admin_view.py | 1 + backend/authentication/permissions.py | 2 +- backend/authentication/tests/test_authentication_serializer.py | 1 + backend/authentication/urls.py | 2 +- backend/authentication/views.py | 3 ++- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/api/views/admin_view.py b/backend/api/views/admin_view.py index 5c4fabe3..63fdab43 100644 --- a/backend/api/views/admin_view.py +++ b/backend/api/views/admin_view.py @@ -2,6 +2,7 @@ from authentication.serializers import UserSerializer from authentication.models import User + class AdminViewSet(viewsets.ModelViewSet): queryset = User.objects.filter(is_staff=True) serializer_class = UserSerializer diff --git a/backend/authentication/permissions.py b/backend/authentication/permissions.py index 5056df97..d852d767 100644 --- a/backend/authentication/permissions.py +++ b/backend/authentication/permissions.py @@ -8,4 +8,4 @@ def has_permission(self, request: Request, view: ViewSet): """Check whether a user has permission in the CAS flow context.""" return request.user.is_authenticated or view.action not in [ 'logout', 'whoami' - ] \ No newline at end of file + ] diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py index b5ef46c4..736ed2f0 100644 --- a/backend/authentication/tests/test_authentication_serializer.py +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -18,6 +18,7 @@ FIRST_NAME = "Dummy" LAST_NAME = "McDickwad" + class UserSerializerModelTests(TestCase): def test_invalid_email_makes_user_serializer_invalid(self): """ diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 4365ca54..2214cc67 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -4,7 +4,7 @@ from authentication.views import CASViewSet router = DefaultRouter() -router.register(f"cas", CASViewSet,"cas") +router.register("cas", CASViewSet, "cas") urlpatterns = [ # AUTH endpoints. diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 1b66d8d2..c5f85e29 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -8,6 +8,7 @@ from authentication.cas.client import client from ypovoli import settings + class CASViewSet(ViewSet): # The IsAuthenticated class is applied by default, # but it's good to be verbose when it comes to security. @@ -43,4 +44,4 @@ def who_am_i(self, request: Request) -> Response: @action(detail=False, methods=['GET'], permission_classes=[AllowAny]) def echo(self, request: Request) -> Response: """Echo the obtained CAS token for development and testing.""" - return Response(request.query_params.get('ticket')) \ No newline at end of file + return Response(request.query_params.get('ticket')) From b16c1fed345c71e0c1ef216f4e5e9eb86845533c Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 15:36:46 +0100 Subject: [PATCH 79/86] test: linting warning? --- backend/authentication/tests/test_authentication_serializer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py index 736ed2f0..b5ef46c4 100644 --- a/backend/authentication/tests/test_authentication_serializer.py +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -18,7 +18,6 @@ FIRST_NAME = "Dummy" LAST_NAME = "McDickwad" - class UserSerializerModelTests(TestCase): def test_invalid_email_makes_user_serializer_invalid(self): """ From 237d32dc5d9769ba712e3ca04133e80b4fd1a306 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 15:37:08 +0100 Subject: [PATCH 80/86] test: linting warning --- backend/authentication/tests/test_authentication_serializer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/authentication/tests/test_authentication_serializer.py b/backend/authentication/tests/test_authentication_serializer.py index b5ef46c4..736ed2f0 100644 --- a/backend/authentication/tests/test_authentication_serializer.py +++ b/backend/authentication/tests/test_authentication_serializer.py @@ -18,6 +18,7 @@ FIRST_NAME = "Dummy" LAST_NAME = "McDickwad" + class UserSerializerModelTests(TestCase): def test_invalid_email_makes_user_serializer_invalid(self): """ From 73d04b111f648e932ef787ab9bdeb293a950e730 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 17:09:19 +0100 Subject: [PATCH 81/86] feat: session authentication for smooth browser-specific experence --- backend/authentication/permissions.py | 10 ++- backend/authentication/serializers.py | 64 ++++++++++++------- backend/authentication/services/users.py | 0 .../tests/test_authentication_views.py | 8 --- backend/authentication/views.py | 25 ++++++-- backend/ypovoli/settings.py | 5 +- backend/ypovoli/urls.py | 5 +- 7 files changed, 70 insertions(+), 47 deletions(-) delete mode 100644 backend/authentication/services/users.py diff --git a/backend/authentication/permissions.py b/backend/authentication/permissions.py index d852d767..b9ff5906 100644 --- a/backend/authentication/permissions.py +++ b/backend/authentication/permissions.py @@ -1,11 +1,9 @@ from rest_framework.permissions import BasePermission from rest_framework.request import Request from rest_framework.viewsets import ViewSet +from ypovoli import settings -class CASPermission(BasePermission): - def has_permission(self, request: Request, view: ViewSet): - """Check whether a user has permission in the CAS flow context.""" - return request.user.is_authenticated or view.action not in [ - 'logout', 'whoami' - ] +class IsDebug(BasePermission): + def has_permission(self, request: Request, view: ViewSet) -> bool: + return settings.DEBUG diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 1ec9b9a0..1ce1b446 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -1,4 +1,6 @@ +from typing import Tuple from django.contrib.auth.models import update_last_login +from django.contrib.auth import login from rest_framework.serializers import ( CharField, EmailField, @@ -20,20 +22,53 @@ class CASTokenObtainSerializer(Serializer): 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"]) + # Validate the ticket and get CAS attributes. + attributes = self._validate_ticket(data["ticket"]) + + # Fetch a user model from the CAS attributes. + user, created = self._fetch_user_from_cas(attributes) + + # Update the user's last login. + if api_settings.UPDATE_LAST_LOGIN: + update_last_login(self, user) + + # Login and send authentication signals. + if "request" in self.context: + login(self.context["request"], user) + + user_login.send( + sender=self, user=user + ) + + if created: + user_created.send( + sender=self, attributes=attributes, user=user + ) + + # Return access tokens for the now logged-in user. + return { + "access": str( + AccessToken.for_user(user) + ), + "refresh": str( + RefreshToken.for_user(user) + ), + } + + def _validate_ticket(self, ticket: str) -> dict: + """Validate a CAS ticket using the CAS client""" + response = client.perform_service_validate(ticket=ticket) if response.error: raise ValidationError(response.error) - # Validation success: create user if it doesn't exist yet. - attributes = response.data.get("attributes", dict) + return response.data.get("attributes", dict) + def _fetch_user_from_cas(self, attributes: dict) -> Tuple[User, bool]: if attributes.get("lastenrolled"): attributes["lastenrolled"] = int(attributes.get("lastenrolled").split()[0]) @@ -51,22 +86,7 @@ def validate(self, data): if not user.is_valid(): raise ValidationError(user.errors) - 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) - - # Send signal upon creation. - if created: - user_created.send(sender=self, attributes=attributes, user=user) - - return { - "access": str(AccessToken.for_user(user)), - "refresh": str(RefreshToken.for_user(user)), - } + return user.get_or_create(user.validated_data) class UserSerializer(ModelSerializer): @@ -91,6 +111,6 @@ class Meta: model = User fields = "__all__" - def get_or_create(self, validated_data: dict) -> User: + def get_or_create(self, validated_data: dict) -> Tuple[User, bool]: """Create or fetch the user based on the validated data.""" return User.objects.get_or_create(**validated_data) diff --git a/backend/authentication/services/users.py b/backend/authentication/services/users.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 960e689d..4eecc815 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -89,11 +89,3 @@ 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""" - ticket = "This is a ticket." - response = self.client.get(reverse("cas-echo"), data={"ticket": ticket}) - content = response.rendered_content.decode("utf-8").strip('"') - self.assertEqual(content, ticket) diff --git a/backend/authentication/views.py b/backend/authentication/views.py index c5f85e29..cc39aa74 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -1,10 +1,13 @@ from django.shortcuts import redirect +from django.contrib.auth import login, logout from rest_framework.decorators import action from rest_framework.viewsets import ViewSet from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.exceptions import AuthenticationFailed from rest_framework.permissions import AllowAny, IsAuthenticated -from authentication.serializers import UserSerializer +from authentication.permissions import IsDebug +from authentication.serializers import UserSerializer, CASTokenObtainSerializer from authentication.cas.client import client from ypovoli import settings @@ -20,12 +23,16 @@ def login(self, _: Request) -> Response: return redirect(client.get_login_url()) @action(detail=False, methods=['GET']) - def logout(self, _: Request) -> Response: + def logout(self, request: Request) -> Response: """Attempt to log out. Redirect to our single CAS endpoint. Normally would only allow POST requests to a logout endpoint. Since the CAS logout location handles the actual logout, we should accept GET requests. """ - return redirect(client.get_logout_url(service_url=settings.API_ENDPOINT)) + logout(request) + + return redirect( + client.get_logout_url(service_url=settings.API_ENDPOINT) + ) @action(detail=False, methods=['GET'], url_path='whoami', url_name='whoami') def who_am_i(self, request: Request) -> Response: @@ -41,7 +48,15 @@ def who_am_i(self, request: Request) -> Response: user_serializer.data ) - @action(detail=False, methods=['GET'], permission_classes=[AllowAny]) + @action(detail=False, methods=['GET'], permission_classes=[IsDebug]) def echo(self, request: Request) -> Response: """Echo the obtained CAS token for development and testing.""" - return Response(request.query_params.get('ticket')) + token_serializer = CASTokenObtainSerializer(data=request.query_params, context={ + 'request': request + }) + + if token_serializer.is_valid(): + return Response(token_serializer.validated_data) + + raise AuthenticationFailed(token_serializer.errors) + diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index e5f197c1..43fa8513 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -64,7 +64,8 @@ "rest_framework.renderers.JSONRenderer", ], "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework_simplejwt.authentication.JWTAuthentication" + "rest_framework_simplejwt.authentication.JWTAuthentication", + "rest_framework.authentication.SessionAuthentication" ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated' @@ -79,9 +80,7 @@ } AUTH_USER_MODEL = "authentication.User" - ROOT_URLCONF = "ypovoli.urls" - WSGI_APPLICATION = "ypovoli.wsgi.application" # Application endpoints diff --git a/backend/ypovoli/urls.py b/backend/ypovoli/urls.py index 4dc43b32..1a1ca5c0 100644 --- a/backend/ypovoli/urls.py +++ b/backend/ypovoli/urls.py @@ -25,7 +25,7 @@ default_version="v1", ), public=True, - permission_classes=(permissions.AllowAny,), + permission_classes=[permissions.AllowAny,], ) @@ -36,8 +36,7 @@ path("auth/", include("authentication.urls")), path("notifications/", include("notifications.urls")), # Swagger documentation. - path( - "swagger/", + path("swagger/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui", ), From 771cd9c4fca00bbede0cb050569268fbc0b747f6 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 17:16:55 +0100 Subject: [PATCH 82/86] fix: linting issues --- .../authentication/tests/test_authentication_views.py | 1 - backend/authentication/views.py | 3 +-- backend/ypovoli/urls.py | 9 ++------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/backend/authentication/tests/test_authentication_views.py b/backend/authentication/tests/test_authentication_views.py index 4eecc815..8e7a4155 100644 --- a/backend/authentication/tests/test_authentication_views.py +++ b/backend/authentication/tests/test_authentication_views.py @@ -88,4 +88,3 @@ def test_login_view_returns_login_url(self): server_url=settings.CAS_ENDPOINT, service_url=settings.CAS_RESPONSE ) self.assertEqual(response["Location"], url) - diff --git a/backend/authentication/views.py b/backend/authentication/views.py index cc39aa74..f029defd 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -1,5 +1,5 @@ from django.shortcuts import redirect -from django.contrib.auth import login, logout +from django.contrib.auth import logout from rest_framework.decorators import action from rest_framework.viewsets import ViewSet from rest_framework.request import Request @@ -59,4 +59,3 @@ def echo(self, request: Request) -> Response: return Response(token_serializer.validated_data) raise AuthenticationFailed(token_serializer.errors) - diff --git a/backend/ypovoli/urls.py b/backend/ypovoli/urls.py index 1a1ca5c0..25e30a72 100644 --- a/backend/ypovoli/urls.py +++ b/backend/ypovoli/urls.py @@ -36,11 +36,6 @@ 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"), ] From 634bdc7ad5917ca599a4581d196702cfc79e4929 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 17:20:59 +0100 Subject: [PATCH 83/86] fix: admin fixtures and student fields --- backend/api/fixtures/admins.yaml | 3 --- backend/api/serializers/student_serializer.py | 12 +----------- backend/ypovoli/settings.py | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 backend/api/fixtures/admins.yaml diff --git a/backend/api/fixtures/admins.yaml b/backend/api/fixtures/admins.yaml deleted file mode 100644 index a0ad3113..00000000 --- a/backend/api/fixtures/admins.yaml +++ /dev/null @@ -1,3 +0,0 @@ -- model: api.admin - pk: '2' - fields: {} diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index fddbd8d2..9040fe1c 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -19,14 +19,4 @@ class StudentSerializer(serializers.ModelSerializer): class Meta: model = Student - fields = [ - "id", - "first_name", - "last_name", - "email", - "faculties", - "last_enrolled", - "create_time", - "courses", - "groups", - ] + fields = '__all__' diff --git a/backend/ypovoli/settings.py b/backend/ypovoli/settings.py index 43fa8513..32355200 100644 --- a/backend/ypovoli/settings.py +++ b/backend/ypovoli/settings.py @@ -109,7 +109,7 @@ TIME_ZONE = "UTC" USE_I18N = True USE_L10N = False -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ From ec5e2b39502b3bf19fba0ad34285a7e7293f766b Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 17:50:08 +0100 Subject: [PATCH 84/86] chore: faculties as list of names instead of links --- backend/api/serializers/student_serializer.py | 6 +----- backend/authentication/serializers.py | 8 +++++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index 9040fe1c..4430d3f7 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from ..models.student import Student +from api.models.student import Student class StudentSerializer(serializers.ModelSerializer): @@ -13,10 +13,6 @@ class StudentSerializer(serializers.ModelSerializer): read_only=True, ) - faculties = serializers.HyperlinkedRelatedField( - many=True, read_only=True, view_name="faculty-detail" - ) - class Meta: model = Student fields = '__all__' diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 1ce1b446..0eefe7a6 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -13,7 +13,7 @@ from rest_framework_simplejwt.tokens import RefreshToken, AccessToken from rest_framework_simplejwt.settings import api_settings from authentication.signals import user_created, user_login -from authentication.models import User +from authentication.models import User, Faculty from authentication.cas.client import client @@ -114,3 +114,9 @@ class Meta: def get_or_create(self, validated_data: dict) -> Tuple[User, bool]: """Create or fetch the user based on the validated data.""" return User.objects.get_or_create(**validated_data) + + +class FacultySerializer(ModelSerializer): + class Meta: + model = Faculty + fields = "__all__" From a4a5a1e9d258edda0056d0eab357879300d62b61 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 5 Mar 2024 17:57:20 +0100 Subject: [PATCH 85/86] fix: revert student serializer --- backend/api/serializers/student_serializer.py | 4 ++++ backend/authentication/serializers.py | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index 4430d3f7..9cd1f245 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -13,6 +13,10 @@ class StudentSerializer(serializers.ModelSerializer): read_only=True, ) + faculties = serializers.HyperlinkedRelatedField( + many=True, read_only=True, view_name="faculty-detail" + ) + class Meta: model = Student fields = '__all__' diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 0eefe7a6..60771277 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -114,9 +114,3 @@ class Meta: def get_or_create(self, validated_data: dict) -> Tuple[User, bool]: """Create or fetch the user based on the validated data.""" return User.objects.get_or_create(**validated_data) - - -class FacultySerializer(ModelSerializer): - class Meta: - model = Faculty - fields = "__all__" From 47c8675979d2acd8dff27f1a114d2f3625d0b48e Mon Sep 17 00:00:00 2001 From: francis Date: Tue, 5 Mar 2024 18:06:01 +0100 Subject: [PATCH 86/86] fix: execute tests in backend folder --- .github/workflows/backend-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend-tests.yaml b/.github/workflows/backend-tests.yaml index 48768a80..c8ec08da 100644 --- a/.github/workflows/backend-tests.yaml +++ b/.github/workflows/backend-tests.yaml @@ -22,4 +22,4 @@ jobs: pip install flake8 pip install -r ./backend/requirements.txt - name: Execute tests - run: python ./backend/manage.py test + run: cd backend; python manage.py test