base for API system (routes + filters)

This commit is contained in:
Faraphel 2023-11-29 22:59:13 +01:00
parent 23c79e2284
commit 6dcc3a448e
10 changed files with 401 additions and 14 deletions

View file

7
Palto/Palto/api/urls.py Normal file
View file

@ -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)),
]

View file

View file

@ -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']

View file

@ -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

View file

@ -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)

View file

@ -1,13 +1,33 @@
import uuid import uuid
from abc import abstractmethod, ABC
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Iterable
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.db.models import QuerySet, Q
from rest_framework import permissions
# Create your models here. # 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. A user.
@ -19,8 +39,45 @@ class User(AbstractUser):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} id={str(self.id)[:8]} username={self.username!r}>" 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. A scholar department.
@ -42,8 +99,32 @@ class Department(models.Model):
def __str__(self): def __str__(self):
return self.name 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. 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) id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36)
name: str = models.CharField(max_length=128) 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") students = models.ManyToManyField(to=get_user_model(), blank=True, related_name="student_groups")
def __repr__(self): def __repr__(self):
@ -64,8 +146,24 @@ class StudentGroup(models.Model):
def __str__(self): def __str__(self):
return self.name 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. A teaching unit.
@ -90,8 +188,21 @@ class TeachingUnit(models.Model):
def __str__(self): def __str__(self):
return self.name 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. A student card.
@ -106,8 +217,23 @@ class StudentCard(models.Model):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} id={str(self.id)[:8]} owner={self.owner.username!r}>" 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. A session of a teaching unit.
@ -136,8 +262,27 @@ class TeachingSession(models.Model):
def end(self) -> datetime: def end(self) -> datetime:
return self.start + self.duration 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. A student attendance to a session.
@ -167,8 +312,27 @@ class Attendance(models.Model):
f">" 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. A student justified absence to a session.
@ -193,8 +357,27 @@ class Absence(models.Model):
def __str__(self): def __str__(self):
return f"[{str(self.id)[:8]}] {self.student}" 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. An attachment to a student justified absence.
@ -208,3 +391,22 @@ class AbsenceAttachment(models.Model):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} id={str(self.id)[:8]} content={self.content!r}>" 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")

View file

@ -151,7 +151,21 @@ REST_FRAMEWORK = {
# or allow read-only access for unauthenticated users. # or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' '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 # User model

View file

@ -14,10 +14,12 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
import debug_toolbar
from django.contrib import admin 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 django.views.static import serve
from Palto.Palto.api import urls as api_urls
from Palto import settings from Palto import settings
@ -26,11 +28,11 @@ urlpatterns = [
# ... # ...
# API # API
path('api/', include('rest_framework.urls')), # API REST path('api/', include(api_urls)), # Api REST
# Debug # Debug
path('admin/', admin.site.urls), # Admin page path('admin/', admin.site.urls), # Admin page
path("__debug__/", include("debug_toolbar.urls")), # Debug toolbar path("__debug__/", include(debug_toolbar.urls)), # Debug toolbar
] ]

View file

@ -1,6 +1,5 @@
django django
djangorestframework djangorestframework
django-filter
django-debug-toolbar django-debug-toolbar
django-extensions django-extensions
Werkzeug Werkzeug