From 7092a2a367cfddbfaf5d32c867fdc307ca77e20d Mon Sep 17 00:00:00 2001 From: EwoutV Date: Mon, 4 Mar 2024 18:28:52 +0100 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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'))