diff --git a/Palto/Palto/api/__init__.py b/Palto/Palto/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Palto/Palto/api/urls.py b/Palto/Palto/api/urls.py new file mode 100644 index 0000000..5682479 --- /dev/null +++ b/Palto/Palto/api/urls.py @@ -0,0 +1,7 @@ +from django.urls import path, include +import Palto.Palto.api.v1.urls as v1_urls + +urlpatterns = [ + # API + path('v1/', include(v1_urls)), +] diff --git a/Palto/Palto/api/v1/__init__.py b/Palto/Palto/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Palto/Palto/api/v1/serializers.py b/Palto/Palto/api/v1/serializers.py new file mode 100644 index 0000000..85783cc --- /dev/null +++ b/Palto/Palto/api/v1/serializers.py @@ -0,0 +1,63 @@ +from rest_framework import serializers + +from Palto.Palto.models import (User, Department, TeachingUnit, StudentCard, TeachingSession, Attendance, Absence, + AbsenceAttachment, StudentGroup) + + +# TODO(Raphaël): Les champs sont-ils sûr ? (carte uid ?) +# TODO(Raphaël): Connection à l'API avec token ? +# TODO(Raphaël): Voir pour les relations + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'first_name', 'last_name'] + + +class DepartmentSerializer(serializers.ModelSerializer): + class Meta: + model = Department + fields = ['id', 'name', 'mail'] + + +class StudentGroupSerializer(serializers.ModelSerializer): + class Meta: + model = StudentGroup + fields = ['id', 'name'] + + +class TeachingUnitSerializer(serializers.ModelSerializer): + class Meta: + model = TeachingUnit + fields = ['id', 'name', 'department'] + + +class StudentCardSerializer(serializers.ModelSerializer): + class Meta: + model = StudentCard + fields = ['id', 'uid', 'owner'] + + +class TeachingSessionSerializer(serializers.ModelSerializer): + class Meta: + model = TeachingSession + fields = ['id', 'start', 'duration', 'note', 'unit', 'group', 'teacher'] + + +class AttendanceSerializer(serializers.ModelSerializer): + class Meta: + model = Attendance + fields = ['id', 'date', 'student'] + + +class AbsenceSerializer(serializers.ModelSerializer): + class Meta: + model = Absence + fields = ['id', 'message', 'student'] + + +class AbsenceAttachmentSerializer(serializers.ModelSerializer): + class Meta: + model = AbsenceAttachment + fields = ['id', 'content', 'absence'] diff --git a/Palto/Palto/api/v1/urls.py b/Palto/Palto/api/v1/urls.py new file mode 100644 index 0000000..0b397ae --- /dev/null +++ b/Palto/Palto/api/v1/urls.py @@ -0,0 +1,19 @@ +from rest_framework import routers + +from .views import (UserViewSet, AbsenceAttachmentViewSet, AbsenceViewSet, AttendanceViewSet, TeachingSessionViewSet, + StudentCardViewSet, TeachingUnitViewSet, StudentGroupViewSet, DepartmentViewSet) + +router = routers.DefaultRouter() + +router.register(r'users', UserViewSet, basename="User") +router.register(r'departments', DepartmentViewSet, basename="Department") +router.register(r'student_groups', StudentGroupViewSet, basename="StudentGroup") +router.register(r'teaching_units', TeachingUnitViewSet, basename="TeachingUnit") +router.register(r'student_cards', StudentCardViewSet, basename="StudentCard") +router.register(r'teaching_sessions', TeachingSessionViewSet, basename="TeachingSession") +router.register(r'attendances', AttendanceViewSet, basename="Attendance") +router.register(r'absences', AbsenceViewSet, basename="Absence") +router.register(r'absence_attachments', AbsenceAttachmentViewSet, basename="AbsenceAttachment") + + +urlpatterns = router.urls diff --git a/Palto/Palto/api/v1/views.py b/Palto/Palto/api/v1/views.py new file mode 100644 index 0000000..9007ebc --- /dev/null +++ b/Palto/Palto/api/v1/views.py @@ -0,0 +1,81 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from .serializers import (UserSerializer, AbsenceAttachmentSerializer, AbsenceSerializer, AttendanceSerializer, + TeachingSessionSerializer, StudentCardSerializer, StudentGroupSerializer, + DepartmentSerializer, TeachingUnitSerializer) +from ...models import (User, AbsenceAttachment, Absence, Attendance, TeachingSession, StudentCard, TeachingUnit, + StudentGroup, Department) + + +class UserViewSet(viewsets.ModelViewSet): + serializer_class = UserSerializer + + def get_queryset(self): + return User.all_visible_to(self.request.user) + + def get_permissions(self): + return User.permissions_for(self.request.user, self.request.method) + + +class DepartmentViewSet(UserViewSet): + serializer_class = DepartmentSerializer + + def get_queryset(self): + return Department.all_visible_to(self.request.user) + + +class StudentGroupViewSet(UserViewSet): + serializer_class = StudentGroupSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return StudentGroup.all_visible_to(self.request.user) + + +class TeachingUnitViewSet(UserViewSet): + serializer_class = TeachingUnitSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return TeachingUnit.all_visible_to(self.request.user) + + +class StudentCardViewSet(UserViewSet): + serializer_class = StudentCardSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return StudentCard.all_visible_to(self.request.user) + + +class TeachingSessionViewSet(UserViewSet): + serializer_class = TeachingSessionSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return TeachingSession.all_visible_to(self.request.user) + + +class AttendanceViewSet(UserViewSet): + serializer_class = AttendanceSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return Attendance.all_visible_to(self.request.user) + + +class AbsenceViewSet(UserViewSet): + serializer_class = AbsenceSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return Absence.all_visible_to(self.request.user) + + +class AbsenceAttachmentViewSet(UserViewSet): + serializer_class = AbsenceAttachmentSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return AbsenceAttachment.all_visible_to(self.request.user) diff --git a/Palto/Palto/models.py b/Palto/Palto/models.py index 841a76f..be7787f 100644 --- a/Palto/Palto/models.py +++ b/Palto/Palto/models.py @@ -1,13 +1,33 @@ import uuid +from abc import abstractmethod, ABC 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 rest_framework import permissions # Create your models here. -class User(AbstractUser): +class ModelApiMixin(ABC): + @classmethod + @abstractmethod + def all_visible_to(cls, user: "User") -> QuerySet: + """ + Return all the objects visible to a user. + """ + + @classmethod + @abstractmethod + def permissions_for(cls, user: "User", method: str) -> permissions.BasePermission: + """ + Return the permissions for a user and the method used to access the object. + """ + + +class User(AbstractUser, ModelApiMixin): """ A user. @@ -19,8 +39,45 @@ class User(AbstractUser): def __repr__(self): return f"<{self.__class__.__name__} id={str(self.id)[:8]} username={self.username!r}>" + @staticmethod + def multiple_related_departments(users: Iterable["User"]) -> QuerySet["Department"]: + """ + Return all the related departments from multiple users. + """ -class Department(models.Model): + return Department.objects.filter( + Q(managers__in=users) | + Q(teachers__in=users) | + Q(students__in=users) + ).distinct() + + @property + def related_departments(self) -> QuerySet["Department"]: + """ + The list of departments related with the user. + """ + + return self.multiple_related_departments([self]) + + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + if user.is_superuser: + queryset = User.objects.all() + else: + queryset = Department.multiple_related_users(user.related_departments) + + return queryset.order_by("pk") + + @classmethod + def permissions_for(cls, user: "User", method: str) -> permissions.BasePermission: + # TODO: ??? + if method in permissions.SAFE_METHODS: + return permissions.AllowAny() + + return permissions.IsAdminUser() + + +class Department(models.Model, ModelApiMixin): """ A scholar department. @@ -42,8 +99,32 @@ class Department(models.Model): def __str__(self): return self.name + @staticmethod + def multiple_related_users(departments: Iterable["Department"]) -> QuerySet["User"]: + """ + Return all the related users from multiple departments. + """ -class StudentGroup(models.Model): + return User.objects.filter( + Q(managing_departments__in=departments) | + Q(teaching_departments__in=departments) | + Q(studying_departments__in=departments) + ).distinct() + + @property + def related_users(self) -> QuerySet["User"]: + """ + The list of users related with the department. + """ + + return self.multiple_related_users([self]) + + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + return cls.objects.all().order_by("pk") + + +class StudentGroup(models.Model, ModelApiMixin): """ A student group. @@ -56,6 +137,7 @@ class StudentGroup(models.Model): id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36) name: str = models.CharField(max_length=128) + department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="student_groups") students = models.ManyToManyField(to=get_user_model(), blank=True, related_name="student_groups") def __repr__(self): @@ -64,8 +146,24 @@ class StudentGroup(models.Model): def __str__(self): return self.name + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + if user.is_superuser: + queryset = cls.objects.all() -class TeachingUnit(models.Model): + else: + queryset = cls.objects.filter( + # get all the groups where the user is + Q(students=user) | + # get all the groups where the department is managed by the user + Q(department=user.managing_departments) + # TODO: prof ? rôle créateur du groupe ? + ).distinct() + + return queryset.order_by("pk") + + +class TeachingUnit(models.Model, ModelApiMixin): """ A teaching unit. @@ -90,8 +188,21 @@ class TeachingUnit(models.Model): def __str__(self): return self.name + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + if user.is_superuser: + queryset = cls.objects.all() -class StudentCard(models.Model): + else: + queryset = cls.objects.filter( + # get all the units with a common department with the user + Q(department__in=user.related_departments) + ) + + return queryset.order_by("pk") + + +class StudentCard(models.Model, ModelApiMixin): """ A student card. @@ -106,8 +217,23 @@ class StudentCard(models.Model): def __repr__(self): return f"<{self.__class__.__name__} id={str(self.id)[:8]} owner={self.owner.username!r}>" + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + if user.is_superuser: + queryset = cls.objects.all() -class TeachingSession(models.Model): + else: + queryset = cls.objects.filter( + # get all the cards that are owned by the user + Q(owner=user) | + # get all the cards where the owner is studying in a department where the user is a manager + Q(owner__studying_departments__managers=user) + ).distinct() + + return queryset.order_by("pk") + + +class TeachingSession(models.Model, ModelApiMixin): """ A session of a teaching unit. @@ -136,8 +262,27 @@ class TeachingSession(models.Model): def end(self) -> datetime: return self.start + self.duration + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + if user.is_superuser: + queryset = cls.objects.all() -class Attendance(models.Model): + else: + queryset = cls.objects.filter( + # get all the sessions where the user is a teacher + Q(teacher=user) | + # get all the sessions where the user is in the group + Q(group__students=user) | + # get all the sessions where the user is managing the unit + Q(unit__managers=user) | + # get all the sessions where the user is managing the department + Q(unit__department__managers=user) + ).distinct() + + return queryset.order_by("pk") + + +class Attendance(models.Model, ModelApiMixin): """ A student attendance to a session. @@ -167,8 +312,27 @@ class Attendance(models.Model): f">" ) + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + if user.is_superuser: + queryset = cls.objects.all() -class Absence(models.Model): + else: + queryset = cls.objects.filter( + # get all the session where the user was the teacher + Q(session__teacher=user) | + # get all the session where the user was the student + Q(student=user) | + # get all the sessions where the user is managing the unit + Q(session__unit__managers=user) | + # get all the sessions where the user is managing the department + Q(session__unit__department__managers=user) + ).distinct() + + return queryset.order_by("pk") + + +class Absence(models.Model, ModelApiMixin): """ A student justified absence to a session. @@ -193,8 +357,27 @@ class Absence(models.Model): def __str__(self): return f"[{str(self.id)[:8]}] {self.student}" + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + if user.is_superuser: + queryset = cls.objects.all() -class AbsenceAttachment(models.Model): + else: + queryset = cls.objects.filter( + # get all the absence where the user was the teacher + Q(session__teacher=user) | + # get all the absence where the user was the student + Q(student=user) | + # get all the absences where the user is managing the unit + Q(session__unit__managers=user) | + # get all the absences where the user is managing the department + Q(session__unit__department__managers=user) + ).distinct() + + return queryset.order_by("pk") + + +class AbsenceAttachment(models.Model, ModelApiMixin): """ An attachment to a student justified absence. @@ -208,3 +391,22 @@ class AbsenceAttachment(models.Model): def __repr__(self): return f"<{self.__class__.__name__} id={str(self.id)[:8]} content={self.content!r}>" + + @classmethod + def all_visible_to(cls, user: "User") -> QuerySet["User"]: + if user.is_superuser: + queryset = cls.objects.all() + + else: + queryset = cls.objects.filter( + # get all the absence attachments where the user was the teacher + Q(absence__session__teacher=user) | + # get all the absence attachments where the user was the student + Q(absence__student=user) | + # get all the absence attachments where the user is managing the unit + Q(absence__session__unit__managers=user) | + # get all the absence attachments where the user is managing the department + Q(absence__session__unit__department__managers=user) + ).distinct() + + return queryset.order_by("pk") diff --git a/Palto/settings.py b/Palto/settings.py index 7d3d7cb..cf4ad64 100644 --- a/Palto/settings.py +++ b/Palto/settings.py @@ -151,7 +151,21 @@ REST_FRAMEWORK = { # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' - ] + ], + + # Default rate limiting + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle', + ], + 'DEFAULT_THROTTLE_RATES': { + 'anon': '2/min', + 'user': '15/min' + }, + + # Allow up to 30 elements per page + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 30 } # User model diff --git a/Palto/urls.py b/Palto/urls.py index 17dadbc..62ebde1 100644 --- a/Palto/urls.py +++ b/Palto/urls.py @@ -14,10 +14,12 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +import debug_toolbar from django.contrib import admin -from django.urls import path, include, re_path +from django.urls import path, re_path, include from django.views.static import serve +from Palto.Palto.api import urls as api_urls from Palto import settings @@ -26,11 +28,11 @@ urlpatterns = [ # ... # API - path('api/', include('rest_framework.urls')), # API REST + path('api/', include(api_urls)), # Api REST # Debug path('admin/', admin.site.urls), # Admin page - path("__debug__/", include("debug_toolbar.urls")), # Debug toolbar + path("__debug__/", include(debug_toolbar.urls)), # Debug toolbar ] diff --git a/requirements.txt b/requirements.txt index b1ba11c..86d73a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ django djangorestframework -django-filter django-debug-toolbar django-extensions Werkzeug