From a731a03133c170e0b9d64898b81116eea215f4fd Mon Sep 17 00:00:00 2001 From: Faraphel Date: Sun, 3 Dec 2023 16:57:34 +0100 Subject: [PATCH] implemented editable and visible filters by user for permissions --- .github/workflows/django-test.yaml | 30 ++ Palto/Palto/admin.py | 8 +- Palto/Palto/api/v1/permissions.py | 283 ++++++++++--------- Palto/Palto/api/v1/tests.py | 169 +++++++----- Palto/Palto/api/v1/views.py | 52 ++-- Palto/Palto/factories.py | 4 +- Palto/Palto/models.py | 425 ++++++++++++++++++++++++++++- Palto/Palto/tests.py | 57 +++- 8 files changed, 780 insertions(+), 248 deletions(-) create mode 100644 .github/workflows/django-test.yaml diff --git a/.github/workflows/django-test.yaml b/.github/workflows/django-test.yaml new file mode 100644 index 0000000..d5007b2 --- /dev/null +++ b/.github/workflows/django-test.yaml @@ -0,0 +1,30 @@ +name: Django CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.12] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + run: | + python manage.py test diff --git a/Palto/Palto/admin.py b/Palto/Palto/admin.py index 64cd5fc..388aed0 100644 --- a/Palto/Palto/admin.py +++ b/Palto/Palto/admin.py @@ -9,6 +9,9 @@ from django.contrib import admin from . import models +# TODO: plus de list_filter sur "department" ? + + # Register your models here. @admin.register(models.User) class AdminUser(admin.ModelAdmin): @@ -58,8 +61,9 @@ class AdminAttendance(admin.ModelAdmin): @admin.register(models.Absence) class AdminAbsence(admin.ModelAdmin): - list_display = ("id", "message", "student") - search_fields = ("id", "message", "student") + list_display = ("id", "message", "student", "department", "start", "end") + search_fields = ("id", "message", "student", "department", "start", "end") + list_filter = ("department", "start", "end") @admin.register(models.AbsenceAttachment) diff --git a/Palto/Palto/api/v1/permissions.py b/Palto/Palto/api/v1/permissions.py index 85ad32d..46b5f91 100644 --- a/Palto/Palto/api/v1/permissions.py +++ b/Palto/Palto/api/v1/permissions.py @@ -10,201 +10,200 @@ from Palto.Palto import models class UserPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.User) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: + # TODO: has_permission check for authentication + + def has_permission(self, request, view) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, allow everybody return True - if request.method in permissions.SAFE_METHODS: - # if the user is in one of the same department as the requesting user, allow read - if obj in models.Department.multiple_related_users(request.user.related_departments): - return True + if models.User.can_user_create(request.user): + # for writing, only allowed users + return True return False + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.User.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.User.all_editable_by_user(request.user) + class DepartmentPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.Department) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: - return True - - # if the group department is managed by the user, allow all - if obj in request.user.managing_departments: - return True - + def has_permission(self, request, view) -> bool: if request.method in permissions.SAFE_METHODS: - # allow read to everybody + # for reading, allow everybody + return True + + if models.Department.can_user_create(request.user): + # for writing, only allowed users return True return False + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.Department.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.Department.all_editable_by_user(request.user) + class StudentGroupPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.StudentGroup) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: - return True - - # if the group department is managed by the user, allow all - if obj.department in request.user.managing_departments: - return True - - # if the user is the owner of the group, allow all - if obj.owner is request.user: - return True - + def has_permission(self, request, view) -> bool: if request.method in permissions.SAFE_METHODS: - # if the student is in the group, allow read - if obj in request.user.student_groups: - return True + # for reading, allow everybody + return True - # if the user is a teacher from the same department, allow read - if obj.department in request.user.teaching_departments: - return True + if models.StudentGroup.can_user_create(request.user): + # for writing, only allowed users + return True return False + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.StudentGroup.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.StudentGroup.all_editable_by_user(request.user) + class TeachingUnitPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.TeachingUnit) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: - return True - - # if the teaching unit department is managed by the user, allow all - if obj.department in request.user.managing_departments: - return True - - # if the teaching unit is managed by the user, allow all - if obj in request.user.managing_units: - return True - + def has_permission(self, request, view) -> bool: if request.method in permissions.SAFE_METHODS: - # if the user is related to the department, allow read - if obj.department in request.user.related_departments: - return True + # for reading, allow everybody + return True + + if models.TeachingUnit.can_user_create(request.user): + # for writing, only allowed users + return True return False + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.TeachingUnit.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.TeachingUnit.all_editable_by_user(request.user) + class StudentCardPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.StudentCard) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: - return True - - # if the card department is managed by the user, allow all - if obj.department in request.user.managing_departments: - return True - + def has_permission(self, request, view) -> bool: if request.method in permissions.SAFE_METHODS: - # if the owner of the card is the user, allow read - if obj.owner is request.user: - return True + # for reading, allow everybody + return True + + if models.StudentCard.can_user_create(request.user): + # for writing, only allowed users + return True return False + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.StudentCard.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.StudentCard.all_editable_by_user(request.user) + class TeachingSessionPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.TeachingSession) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: - return True - - # if the teacher is the user, allow all - if obj.teacher is request.user: - return True - - # if the unit of the session is managed by the user, allow all - if obj.unit in request.user.managing_units: - return True - - # if the department of the session is managed by the user, allow all - if obj.unit.department in request.user.managing_departments: - return True - + def has_permission(self, request, view) -> bool: if request.method in permissions.SAFE_METHODS: - # if the user was one of the student, allow read - if request.user in obj.group.students: - return True + # for reading, allow everybody + return True + + if models.TeachingSession.can_user_create(request.user): + # for writing, only allowed users + return True return False + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.TeachingSession.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.TeachingSession.all_editable_by_user(request.user) + class AttendancePermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.Attendance) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: - return True - - # if the teacher is the user, allow all - if obj.session.teacher is request.user: - return True - - # if the unit of the session is managed by the user, allow all - if obj.session.unit in request.user.managing_units: - return True - - # if the department of the session is managed by the user, allow all - if obj.session.unit.department in request.user.managing_departments: - return True - + def has_permission(self, request, view) -> bool: if request.method in permissions.SAFE_METHODS: - # if the user was the student, allow read - if obj.student is request.user: - return True + # for reading, allow everybody + return True + + if models.Attendance.can_user_create(request.user): + # for writing, only allowed users + return True return False + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.Attendance.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.Attendance.all_editable_by_user(request.user) + class AbsencePermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.Absence) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: - return True - - # if the department of the session is managed by the user, allow all - if obj.session.unit.department in request.user.managing_departments: - return True - - # if the user was the student, allow all - if obj.student is request.user: - return True - + def has_permission(self, request, view) -> bool: if request.method in permissions.SAFE_METHODS: - # if the unit of the session is managed by the user, allow read - if obj.session.unit in request.user.managing_units: - return True + # for reading, allow everybody + return True - # if the teacher is the user, allow read - if obj.session.teacher is request.user: - return True + if models.Absence.can_user_create(request.user): + # for writing, only allowed users + return True return False + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.Absence.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.Absence.all_editable_by_user(request.user) + class AbsenceAttachmentPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj: models.AbsenceAttachment) -> bool: - # if the requesting user is admin, allow all - if request.user.is_superuser: - return True - - # if the department of the session is managed by the user, allow all - if obj.absence.session.unit.department in request.user.managing_departments: - return True - - # if the user was the student, allow all - if obj.absence.student is request.user: - return True - + def has_permission(self, request, view) -> bool: if request.method in permissions.SAFE_METHODS: - # if the unit of the session is managed by the user, allow read - if obj.absence.session.unit in request.user.managing_units: - return True + # for reading, allow everybody + return True - # if the teacher is the user, allow read - if obj.absence.session.teacher is request.user: - return True + if models.AbsenceAttachment.can_user_create(request.user): + # for writing, only allowed users + return True return False + + def has_object_permission(self, request, view, obj: models.User) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, only allow if the user can see the object + return obj in models.AbsenceAttachment.all_visible_by_user(request.user) + + else: + # for writing, only allow if the user can edit the object + return obj in models.AbsenceAttachment.all_editable_by_user(request.user) \ No newline at end of file diff --git a/Palto/Palto/api/v1/tests.py b/Palto/Palto/api/v1/tests.py index fb6950c..00c2921 100644 --- a/Palto/Palto/api/v1/tests.py +++ b/Palto/Palto/api/v1/tests.py @@ -4,96 +4,121 @@ Tests for the Palto project's API v1. Everything to test the API v1 is described here. """ -from django import test +from rest_framework import status +from rest_framework import test -from Palto.Palto import models, factories +from Palto.Palto import factories +from Palto.Palto.api.v1 import serializers -class UserTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of users - """ - - user = factories.FakeUserFactory() +class TokenJwtTestCase(test.APITestCase): + """ + Test the JWT token creation + """ -class DepartmentTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of departments - """ +class UserApiTestCase(test.APITestCase): + # fake user data for creations test + USER_CREATION_DATA: dict = { + "username": "billybob", + "first_name": "Billy", + "last_name": "Bob", + "email": "billy.bob@billybob.fr" + } - department = factories.FakeDepartmentFactory() + def setUp(self): + self.user_admin = factories.FakeUserFactory(is_superuser=True) + self.user_other = factories.FakeUserFactory() + + def test_permission_admin(self): + """ Test the API permission for an administrator """ + + # TODO: use reverse to get the url ? + self.client.force_login(self.user_admin) + + # check for a get request + response = self.client.get("/api/v1/users/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # check for a post request + response = self.client.post("/api/v1/users/", data=self.USER_CREATION_DATA) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_permission_anonymous(self): + """ Test the API permission for an anonymous user """ + + # TODO: use reverse to get the url ? + self.client.logout() + + # check for a get request + response = self.client.get("/api/v1/users/") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # check for a post request + response = self.client.post("/api/v1/users/", data=self.USER_CREATION_DATA) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_permission_unrelated(self): + """ Test the API permission for an unrelated user """ + + # TODO: use reverse to get the url ? + self.client.force_login(self.user_other) + + # check for a get request and that he can't see anybody + response = self.client.get("/api/v1/users/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["count"], 0) + + # check for a post request + response = self.client.post("/api/v1/users/", data=self.USER_CREATION_DATA) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_permission_related(self): + """ Test the API permission for a related user """ + + # TODO: use reverse to get the url ? + student1, student2 = factories.FakeUserFactory(), factories.FakeUserFactory() + department = factories.FakeDepartmentFactory(students=(student1, student2)) + + self.client.force_login(student1) + + # check for a get request and that he can see the other student + response = self.client.get("/api/v1/users/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(serializers.UserSerializer(student2).data, response.json()["results"]) + + # check for a post request + response = self.client.post("/api/v1/users/", data=self.USER_CREATION_DATA) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) -class StudentGroupTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of student groups - """ - - student_group = factories.FakeStudentGroupFactory() +class DepartmentApiTestCase(test.APITestCase): + pass -class TeachingUnitTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of teaching units - """ - - teaching_unit = factories.FakeTeachingUnitFactory() +class StudentGroupApiTestCase(test.APITestCase): + pass -class StudentCardTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of student cards - """ - - student_card = factories.FakeStudentCardFactory() +class TeachingUnitApiTestCase(test.APITestCase): + pass -class TeachingSessionTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of teaching sessions - """ - - teaching_session = factories.FakeTeachingSessionFactory() +class StudentCardApiTestCase(test.APITestCase): + pass -class AttendanceTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of attendances - """ - - attendance = factories.FakeAttendanceFactory() +class TeachingSessionApiTestCase(test.APITestCase): + pass -class AbsenceTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of absences - """ - - absence = factories.FakeAbsenceFactory() +class AttendanceApiTestCase(test.APITestCase): + pass -class AbsenceAttachmentTestCase(test.TestCase): - @staticmethod - def test_creation(): - """ - Test the creation of absence attachments - """ +class AbsenceApiTestCase(test.APITestCase): + pass - absence_attachment = factories.FakeAbsenceAttachmentFactory() + +class AbsenceAttachmentApiTestCase(test.APITestCase): + pass diff --git a/Palto/Palto/api/v1/views.py b/Palto/Palto/api/v1/views.py index 1efdcc0..486e9cf 100644 --- a/Palto/Palto/api/v1/views.py +++ b/Palto/Palto/api/v1/views.py @@ -14,53 +14,71 @@ from ... import models class UserViewSet(viewsets.ModelViewSet): serializer_class = serializers.UserSerializer - queryset = models.User.objects.all() permission_classes = [IsAuthenticated, permissions.UserPermission] + def get_queryset(self): + return models.User.all_visible_by_user(self.request.user) -class DepartmentViewSet(UserViewSet): + +class DepartmentViewSet(viewsets.ModelViewSet): serializer_class = serializers.DepartmentSerializer - queryset = models.Department.objects.all() permission_classes = [permissions.DepartmentPermission] + def get_queryset(self): + return models.Department.all_visible_by_user(self.request.user) -class StudentGroupViewSet(UserViewSet): + +class StudentGroupViewSet(viewsets.ModelViewSet): serializer_class = serializers.StudentGroupSerializer - queryset = models.StudentGroup.objects.all() permission_classes = [IsAuthenticated, permissions.StudentGroupPermission] + def get_queryset(self): + return models.StudentGroup.all_visible_by_user(self.request.user) -class TeachingUnitViewSet(UserViewSet): + +class TeachingUnitViewSet(viewsets.ModelViewSet): serializer_class = serializers.TeachingUnitSerializer - queryset = models.TeachingUnit.objects.all() permission_classes = [IsAuthenticated, permissions.TeachingUnitPermission] + def get_queryset(self): + return models.TeachingUnit.all_visible_by_user(self.request.user) -class StudentCardViewSet(UserViewSet): + +class StudentCardViewSet(viewsets.ModelViewSet): serializer_class = serializers.StudentCardSerializer - queryset = models.StudentCard.objects.all() permission_classes = [IsAuthenticated, permissions.StudentCardPermission] + def get_queryset(self): + return models.StudentCard.all_visible_by_user(self.request.user) -class TeachingSessionViewSet(UserViewSet): + +class TeachingSessionViewSet(viewsets.ModelViewSet): serializer_class = serializers.TeachingSessionSerializer - queryset = models.TeachingSession.objects.all() permission_classes = [IsAuthenticated, permissions.TeachingSessionPermission] + def get_queryset(self): + return models.TeachingSession.all_visible_by_user(self.request.user) -class AttendanceViewSet(UserViewSet): + +class AttendanceViewSet(viewsets.ModelViewSet): serializer_class = serializers.AttendanceSerializer - queryset = models.Attendance.objects.all() permission_classes = [IsAuthenticated, permissions.AttendancePermission] + def get_queryset(self): + return models.Attendance.all_visible_by_user(self.request.user) -class AbsenceViewSet(UserViewSet): + +class AbsenceViewSet(viewsets.ModelViewSet): serializer_class = serializers.AbsenceSerializer - queryset = models.Absence.objects.all() permission_classes = [IsAuthenticated, permissions.AbsencePermission] + def get_queryset(self): + return models.Absence.all_visible_by_user(self.request.user) -class AbsenceAttachmentViewSet(UserViewSet): + +class AbsenceAttachmentViewSet(viewsets.ModelViewSet): serializer_class = serializers.AbsenceAttachmentSerializer - queryset = models.AbsenceAttachment.objects.all() permission_classes = [IsAuthenticated, permissions.AbsenceAttachmentPermission] + + def get_queryset(self): + return models.AbsenceAttachment.all_visible_by_user(self.request.user) diff --git a/Palto/Palto/factories.py b/Palto/Palto/factories.py index c2ce10a..54def7a 100644 --- a/Palto/Palto/factories.py +++ b/Palto/Palto/factories.py @@ -196,6 +196,6 @@ class FakeAbsenceAttachmentFactory(factory.django.DjangoModelFactory): class Meta: model = models.AbsenceAttachment - content = factory.django.FileField() + content: str = factory.django.FileField() - absence = factory.SubFactory(FakeAbsenceFactory) + absence: models.Absence = factory.SubFactory(FakeAbsenceFactory) diff --git a/Palto/Palto/models.py b/Palto/Palto/models.py index 0a126af..eadb3b9 100644 --- a/Palto/Palto/models.py +++ b/Palto/Palto/models.py @@ -5,16 +5,41 @@ Models are the class that represent and abstract the database. """ import uuid +from abc import abstractmethod from datetime import datetime, timedelta from typing import Iterable from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser from django.db import models -from django.db.models import QuerySet, Q +from django.db.models import QuerySet, Q, F -class User(AbstractUser): +class ModelPermissionHelper: + + @classmethod + @abstractmethod + def can_user_create(cls, user: "User") -> bool: + """ + Return True if the user can create a new instance of this object + """ + + @classmethod + @abstractmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + """ + Return True if the user can edit this object + """ + + @classmethod + @abstractmethod + def all_visible_by_user(cls, user: "User") -> QuerySet: + """ + Return True if the user can see this object + """ + + +class User(AbstractUser, ModelPermissionHelper): """ A user. @@ -46,8 +71,38 @@ class User(AbstractUser): return self.multiple_related_departments([self]) + # permissions -class Department(models.Model): + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + # TODO: propriétaire d'établissement + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = QuerySet() + # TODO: propriétaire d'établissement + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + # if the user is in one of the same department as the requesting user + queryset = Department.multiple_related_users(user.related_departments) + + return queryset.order_by("pk") + + +class Department(models.Model, ModelPermissionHelper): """ A scholar department. @@ -89,8 +144,33 @@ class Department(models.Model): return self.multiple_related_users([self]) + # permissions -class StudentGroup(models.Model): + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = QuerySet() + # TODO: propriétaire d'établissement ? + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + # everybody can see all the departments + queryset = cls.objects.all() + + return queryset.order_by("pk") + + +class StudentGroup(models.Model, ModelPermissionHelper): """ A student group. @@ -113,8 +193,51 @@ class StudentGroup(models.Model): def __str__(self): return self.name + # permissions -class TeachingUnit(models.Model): + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + # TODO: department managers can create group + # TODO: can teacher create group ? + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the owner of the group, allow write + Q(owner=user) | + # if the user is a department manager, allow write + Q(department__managers=user) + ).distinct() + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the owner of the group, allow read + Q(owner=user) | + # if the user is one of the student, allow read + Q(students=user) | + # if the user is a department manager, allow read + Q(department__managers=user) | + # if the user is one of the teachers, allow read + Q(department__teachers=user) + ).distinct() + + return queryset.order_by("pk") + + +class TeachingUnit(models.Model, ModelPermissionHelper): """ A teaching unit. @@ -139,8 +262,48 @@ class TeachingUnit(models.Model): def __str__(self): return self.name + # permissions -class StudentCard(models.Model): + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + # TODO: allow department manager + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is a manager of the department, allow write + Q(department__managers=user) | + # if the user is the manager of the unit, allow write + Q(managers=user) + ).distinct() + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is a manager of the department, allow read + Q(department__managers=user) | + # if the user is the manager of the unit, allow read + Q(managers=user) | + # if the department is related to the user, allow read + Q(department=user.related_departments) + ).distinct() + + return queryset.order_by("pk") + + +class StudentCard(models.Model, ModelPermissionHelper): """ A student card. @@ -156,8 +319,44 @@ class StudentCard(models.Model): def __repr__(self): return f"<{self.__class__.__name__} id={str(self.id)[:8]} owner={self.owner.username!r}>" + # permissions -class TeachingSession(models.Model): + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + # TODO: Allow new student cards by department managers ? + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is a manager of the department + Q(department__managers=user) + ).distinct() + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the owner + Q(owner=user) | + # if the user is a manager of the department + Q(department__managers=user) + ).distinct() + + return queryset.order_by("pk") + + +class TeachingSession(models.Model, ModelPermissionHelper): """ A session of a teaching unit. @@ -186,8 +385,52 @@ class TeachingSession(models.Model): def end(self) -> datetime: return self.start + self.duration + # permissions -class Attendance(models.Model): + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + # TODO: Allow new teaching session by managers or teachers + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the teacher, allow write + Q(teacher=user) | + # if the user is managing the unit, allow write + Q(unit__managers=user) | + # if the user is managing the department, allow write + Q(unit__department__managers=user) + ).distinct() + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the teacher, allow read + Q(teacher=user) | + # if the user is managing the unit, allow read + Q(unit__managers=user) | + # if the user is managing the department, allow read + Q(unit__department__managers=user) | + # if the user is part of the group, allow read + Q(group__students=user) + ).distinct() + + return queryset.order_by("pk") + + +class Attendance(models.Model, ModelPermissionHelper): """ A student attendance to a session. @@ -217,8 +460,53 @@ class Attendance(models.Model): f">" ) + # permissions -class Absence(models.Model): + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + # TODO: Allow new attendance by managers or teachers + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user was the teacher, allow write + Q(session__teacher=user) | + # if the user is manager of the unit, allow write + Q(session__unit__managers=user) | + # if the user is manager of the department, allow write + Q(session__unit__department__managers=user) + ).distinct() + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user was the teacher, allow read + Q(session__teacher=user) | + # if the user is manager of the unit, allow read + Q(session__unit__managers=user) | + # if the user is manager of the department, allow read + Q(session__unit__department__managers=user) | + + # if the user is the student, allow read + Q(student=user) + ).distinct() + + return queryset.order_by("pk") + + +class Absence(models.Model, ModelPermissionHelper): """ A student justified absence to a session. @@ -228,22 +516,88 @@ class Absence(models.Model): id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) message: str = models.TextField() + department: Department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="absences") student: User = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="absented_sessions") - sessions: TeachingSession = models.ManyToManyField(to=TeachingSession, blank=True, related_name="absences") + start: datetime = models.DateTimeField() + end: datetime = models.DateTimeField() def __repr__(self): return ( f"<{self.__class__.__name__} " f"id={str(self.id)[:8]} " - f"student={self.student.username}" + f"department={self.department} " + f"student={self.student.username} " + f"start={self.start} " + f"end={self.end}" f">" ) def __str__(self): return f"[{str(self.id)[:8]}] {self.student}" + # properties -class AbsenceAttachment(models.Model): + def related_sessions(self) -> QuerySet[TeachingSession]: + """ + Return the sessions that match the user absence + """ + + return TeachingSession.objects.filter( + # every session where the student participate + Q(group__students=self.student) & + # every session that start between the start and the end of our absence + Q(start__range=(self.start, self.end)) + ).distinct() + + # permissions + + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + # TODO: Allow new absence by students + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the student, allow write + Q(student=user) + ).distinct() + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the student, allow read + Q(student=user) | + # if the user is related with the session, allow read + ( + # if the sessions start between the start and the end of the absence + Q(department__teaching_units__sessions__start__range=(F("start"), F("end"))) & + ( + # the user is a manager of the department + Q(department__managers=user) | + # the user is a manager of the unit + Q(department__teaching_units__teachers=user) | + # the user is the teacher of the session + Q(department__teaching_units__sessions__teacher=user) + ) + ) + ).distinct() + + return queryset.order_by("pk") + + +class AbsenceAttachment(models.Model, ModelPermissionHelper): """ An attachment to a student justified absence. @@ -257,3 +611,50 @@ class AbsenceAttachment(models.Model): def __repr__(self): return f"<{self.__class__.__name__} id={str(self.id)[:8]} content={self.content!r}>" + + # permissions + + @classmethod + def can_user_create(cls, user: "User") -> bool: + # if the requesting user is admin + return user.is_superuser + # TODO: Allow new absence attachment by students + + @classmethod + def all_editable_by_user(cls, user: "User") -> QuerySet: + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the student, allow write + Q(absence__student=user) + ).distinct() + + return queryset.order_by("pk") + + @classmethod + def all_visible_by_user(cls, user: "User"): + if user.is_superuser: + # if the requesting user is admin + queryset = cls.objects.all() + else: + queryset = cls.objects.filter( + # if the user is the student, allow read + Q(absence__student=user) | + # if the user is related with the session, allow read + ( + # if the sessions start between the start and the end of the absence + Q(absence__department__teaching_units__sessions__start__range=(F("start"), F("end"))) & + ( + # the user is a manager of the department + Q(absence__department__managers=user) | + # the user is a manager of the unit + Q(absence__department__teaching_units__teachers=user) | + # the user is the teacher of the session + Q(absence__department__teaching_units__sessions__teacher=user) + ) + ) + ).distinct() + + return queryset.order_by("pk") diff --git a/Palto/Palto/tests.py b/Palto/Palto/tests.py index 69f7fd6..ed27c6f 100644 --- a/Palto/Palto/tests.py +++ b/Palto/Palto/tests.py @@ -4,6 +4,61 @@ Tests for the Palto project. Tests allow to easily check after modifying the logic behind a feature that everything still work as intended. """ -from django.test import TestCase +from django import test + +from Palto.Palto import factories + # Create your tests here. +class UserTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeUserFactory() + + +class DepartmentTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeDepartmentFactory() + + +class StudentGroupTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeStudentGroupFactory() + + +class TeachingUnitTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeTeachingUnitFactory() + + +class StudentCardTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeStudentCardFactory() + + +class TeachingSessionTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeTeachingSessionFactory() + + +class AttendanceTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeAttendanceFactory() + + +class AbsenceTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeAbsenceFactory() + + +class AbsenceAttachmentTestCase(test.TestCase): + @staticmethod + def test_creation(): + factories.FakeAbsenceAttachmentFactory()