commit
70c9977e2b
19 changed files with 1605 additions and 48 deletions
37
.github/workflows/django-test.yaml
vendored
Normal file
37
.github/workflows/django-test.yaml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
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: Check Database Migrations
|
||||
run: |
|
||||
python manage.py makemigrations
|
||||
env:
|
||||
DJANGO_SECRET: ${{ secrets.DJANGO_SECRET }}
|
||||
- name: Run Tests
|
||||
run: |
|
||||
python manage.py test
|
||||
env:
|
||||
DJANGO_SECRET: ${{ secrets.DJANGO_SECRET }}
|
|
@ -1,5 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="django" name="Django">
|
||||
<configuration>
|
||||
<option name="rootFolder" value="$MODULE_DIR$" />
|
||||
<option name="settingsModule" value="Palto/settings.py" />
|
||||
<option name="manageScript" value="manage.py" />
|
||||
<option name="environment" value="<map/>" />
|
||||
<option name="doNotUseTestRunner" value="false" />
|
||||
<option name="trackFilePattern" value="" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
|
@ -7,4 +19,11 @@
|
|||
<orderEntry type="jdk" jdkName="Python 3.11 (Palto-Server)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||
</component>
|
||||
</module>
|
|
@ -1,63 +1,72 @@
|
|||
"""
|
||||
Admin for the Palto project.
|
||||
|
||||
The admin is the admin page configuration, describing which model should be visible and which field in the admin page.
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (Department, StudentGroup, TeachingUnit, StudentCard, TeachingSession, Attendance, Absence,
|
||||
AbsenceAttachment, User)
|
||||
from . import models
|
||||
|
||||
|
||||
# TODO: plus de list_filter sur "department" ?
|
||||
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(User)
|
||||
@admin.register(models.User)
|
||||
class AdminUser(admin.ModelAdmin):
|
||||
list_display = ("id", "username", "email", "first_name", "last_name", "is_staff")
|
||||
search_fields = ("id", "username", "email", "first_name", "last_name", "is_staff")
|
||||
list_filter = ("is_staff",)
|
||||
|
||||
|
||||
@admin.register(Department)
|
||||
@admin.register(models.Department)
|
||||
class AdminDepartment(admin.ModelAdmin):
|
||||
list_display = ("id", "name")
|
||||
search_fields = ("id", "name")
|
||||
list_display = ("id", "name", "email")
|
||||
search_fields = ("id", "name", "email")
|
||||
|
||||
|
||||
@admin.register(StudentGroup)
|
||||
@admin.register(models.StudentGroup)
|
||||
class AdminStudentGroup(admin.ModelAdmin):
|
||||
list_display = ("id", "name")
|
||||
search_fields = ("id", "name")
|
||||
list_display = ("id", "name", "owner", "department")
|
||||
search_fields = ("id", "name", "owner", "department")
|
||||
|
||||
|
||||
@admin.register(TeachingUnit)
|
||||
@admin.register(models.TeachingUnit)
|
||||
class AdminTeachingUnit(admin.ModelAdmin):
|
||||
list_display = ("id", "name")
|
||||
search_fields = ("id", "name")
|
||||
|
||||
|
||||
@admin.register(StudentCard)
|
||||
@admin.register(models.StudentCard)
|
||||
class AdminStudentCard(admin.ModelAdmin):
|
||||
list_display = ("id", "uid", "owner")
|
||||
search_fields = ("id", "uid", "owner")
|
||||
readonly_fields = ("uid",)
|
||||
|
||||
|
||||
@admin.register(TeachingSession)
|
||||
@admin.register(models.TeachingSession)
|
||||
class AdminTeachingSession(admin.ModelAdmin):
|
||||
list_display = ("id", "start", "end", "duration", "teacher")
|
||||
search_fields = ("id", "start", "end", "duration", "teacher")
|
||||
list_filter = ("start", "duration")
|
||||
|
||||
|
||||
@admin.register(Attendance)
|
||||
@admin.register(models.Attendance)
|
||||
class AdminAttendance(admin.ModelAdmin):
|
||||
list_display = ("id", "date", "student")
|
||||
search_fields = ("id", "date", "student")
|
||||
list_filter = ("date",)
|
||||
|
||||
|
||||
@admin.register(Absence)
|
||||
@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(AbsenceAttachment)
|
||||
@admin.register(models.AbsenceAttachment)
|
||||
class AdminAbsenceAttachment(admin.ModelAdmin):
|
||||
list_display = ("id", "content", "absence")
|
||||
search_fields = ("id", "content", "absence")
|
||||
|
|
0
Palto/Palto/api/__init__.py
Normal file
0
Palto/Palto/api/__init__.py
Normal file
20
Palto/Palto/api/urls.py
Normal file
20
Palto/Palto/api/urls.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
"""
|
||||
Urls for the Palto project's API.
|
||||
|
||||
This file list all the urls for the Palto API.
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView, TokenVerifyView
|
||||
|
||||
import Palto.Palto.api.v1.urls as v1_urls
|
||||
|
||||
urlpatterns = [
|
||||
# Authentification (JWT)
|
||||
path('auth/jwt/token/', TokenObtainPairView.as_view(), name='token'),
|
||||
path('auth/jwt/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('auth/jwt/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
|
||||
|
||||
# API
|
||||
path('v1/', include(v1_urls)),
|
||||
]
|
0
Palto/Palto/api/v1/__init__.py
Normal file
0
Palto/Palto/api/v1/__init__.py
Normal file
50
Palto/Palto/api/v1/permissions.py
Normal file
50
Palto/Palto/api/v1/permissions.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
"""
|
||||
Permissions for the Palto project's API v1.
|
||||
|
||||
A permission describe which user is allowed to see and modify which objet with the API
|
||||
"""
|
||||
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
from Palto.Palto import models
|
||||
|
||||
|
||||
def permission_from_helper_class(model: Type[models.ModelPermissionHelper]) -> Type[permissions.BasePermission]:
|
||||
"""
|
||||
Create a permission class from a model if it implements ModelPermissionHelper.
|
||||
This make creating permission easier to understand and less redundant.
|
||||
"""
|
||||
|
||||
class Permission(permissions.BasePermission):
|
||||
def has_permission(self, request, view) -> bool:
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
# for reading, allow everybody
|
||||
return True
|
||||
|
||||
if model.can_user_create(request.user):
|
||||
# for writing, only allowed users
|
||||
return True
|
||||
|
||||
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 model.all_visible_by_user(request.user)
|
||||
|
||||
else:
|
||||
# for writing, only allow if the user can edit the object
|
||||
return obj in model.all_editable_by_user(request.user)
|
||||
|
||||
return Permission
|
||||
|
||||
|
||||
UserPermission = permission_from_helper_class(models.User)
|
||||
DepartmentPermission = permission_from_helper_class(models.Department)
|
||||
StudentGroupPermission = permission_from_helper_class(models.StudentGroup)
|
||||
TeachingUnitPermission = permission_from_helper_class(models.TeachingUnit)
|
||||
StudentCardPermission = permission_from_helper_class(models.StudentCard)
|
||||
TeachingSessionPermission = permission_from_helper_class(models.TeachingSession)
|
||||
AttendancePermission = permission_from_helper_class(models.Attendance)
|
||||
AbsencePermission = permission_from_helper_class(models.Absence)
|
||||
AbsenceAttachmentPermission = permission_from_helper_class(models.AbsenceAttachment)
|
114
Palto/Palto/api/v1/serializers.py
Normal file
114
Palto/Palto/api/v1/serializers.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
"""
|
||||
Serializers for the Palto project's API v1.
|
||||
|
||||
A serializers tell the API how should a model should be serialized to be used by an external user.
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from django.forms import model_to_dict
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from Palto.Palto import models
|
||||
|
||||
|
||||
# TODO: voir les relations inversées ?
|
||||
|
||||
|
||||
class ModelSerializerContrains(serializers.ModelSerializer):
|
||||
"""
|
||||
Similar to the base ModelSerializer, but automatically check for contrains for the user
|
||||
when trying to create a new instance or modifying a field.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model: Type[models.ModelPermissionHelper]
|
||||
|
||||
def create(self, validated_data):
|
||||
# get the fields that this user can modify
|
||||
field_contraints = self.Meta.model.user_fields_contraints(self.context["request"].user)
|
||||
|
||||
# for every constraint
|
||||
for field, constraints in field_contraints.items():
|
||||
# check if the value is in the constraints.
|
||||
value = validated_data.get(field)
|
||||
if value in constraints(validated_data):
|
||||
raise PermissionDenied(f"You are not allowed to use this value for the field {field}.")
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# get the fields that this user can modify
|
||||
field_constraints = self.Meta.model.user_fields_contraints(self.context["request"].user)
|
||||
|
||||
# for every constraint
|
||||
for field, constraints in field_constraints.items():
|
||||
# check if the value of the request is in the constraints.
|
||||
value = validated_data.get(field)
|
||||
if value in constraints(validated_data):
|
||||
raise PermissionDenied(f"You are not allowed to use this value for the field {field}.")
|
||||
|
||||
# check if the value of the already existing instance is in the constraints.
|
||||
value = getattr(instance, field, None)
|
||||
if value in constraints(model_to_dict(instance)):
|
||||
raise PermissionDenied(f"You are not allowed to use this value for the field {field}.")
|
||||
|
||||
# check that the user is managing the department
|
||||
if instance.department not in self.context["request"].user.managing_departments:
|
||||
raise PermissionDenied("You don't manage this department.")
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ['id', 'username', 'first_name', 'last_name', 'email']
|
||||
|
||||
|
||||
class DepartmentSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.Department
|
||||
fields = ['id', 'name', 'email', 'managers', 'teachers', 'students']
|
||||
|
||||
|
||||
class StudentGroupSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.StudentGroup
|
||||
fields = ['id', 'name', 'owner', 'department', 'students']
|
||||
|
||||
|
||||
class TeachingUnitSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.TeachingUnit
|
||||
fields = ['id', 'name', 'department', 'managers', 'teachers', 'student_groups']
|
||||
|
||||
|
||||
class StudentCardSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.StudentCard
|
||||
fields = ['id', 'uid', 'department', 'owner']
|
||||
|
||||
|
||||
class TeachingSessionSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.TeachingSession
|
||||
fields = ['id', 'start', 'duration', 'note', 'unit', 'group', 'teacher']
|
||||
|
||||
|
||||
class AttendanceSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.Attendance
|
||||
fields = ['id', 'date', 'student', 'session']
|
||||
|
||||
|
||||
class AbsenceSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.Absence
|
||||
fields = ['id', 'message', 'student', 'session']
|
||||
|
||||
|
||||
class AbsenceAttachmentSerializer(ModelSerializerContrains):
|
||||
class Meta:
|
||||
model = models.AbsenceAttachment
|
||||
fields = ['id', 'content', 'absence']
|
202
Palto/Palto/api/v1/tests.py
Normal file
202
Palto/Palto/api/v1/tests.py
Normal file
|
@ -0,0 +1,202 @@
|
|||
"""
|
||||
Tests for the Palto project's API v1.
|
||||
|
||||
Everything to test the API v1 is described here.
|
||||
"""
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework import test
|
||||
|
||||
from Palto.Palto import factories, models
|
||||
from Palto.Palto.api.v1 import serializers
|
||||
|
||||
|
||||
class TokenJwtTestCase(test.APITestCase):
|
||||
"""
|
||||
Test the JWT token creation
|
||||
"""
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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)
|
||||
self.assertEqual(response.json()["count"], models.User.objects.count())
|
||||
|
||||
# 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 DepartmentApiTestCase(test.APITestCase):
|
||||
# fake department creation test
|
||||
DEPARTMENT_CREATION_DATA: dict = {
|
||||
"name": "UFR des Sciences",
|
||||
"email": "ufr.sciences@university.fr",
|
||||
}
|
||||
|
||||
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/departments/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.json()["count"], models.Department.objects.count())
|
||||
|
||||
# check for a post request
|
||||
response = self.client.post("/api/v1/departments/", data=self.DEPARTMENT_CREATION_DATA)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class StudentGroupApiTestCase(test.APITestCase):
|
||||
def setUp(self):
|
||||
self.user_admin = factories.FakeUserFactory(is_superuser=True)
|
||||
self.user_other = factories.FakeUserFactory()
|
||||
|
||||
# fake group creation data
|
||||
self.test_manager_related = factories.FakeUserFactory()
|
||||
self.test_manager_other = factories.FakeUserFactory()
|
||||
|
||||
self.test_teacher_owner = factories.FakeUserFactory()
|
||||
self.test_teacher_other = factories.FakeUserFactory()
|
||||
|
||||
self.test_students_group = [factories.FakeUserFactory() for _ in range(10)]
|
||||
self.test_students_other = [factories.FakeUserFactory() for _ in range(10)]
|
||||
|
||||
self.test_department = factories.FakeDepartmentFactory(
|
||||
managers=[self.test_manager_related],
|
||||
teachers=[self.test_teacher_owner, self.test_teacher_other],
|
||||
students=[*self.test_students_group, *self.test_students_other],
|
||||
)
|
||||
|
||||
self.student_group_creation_data: dict = {
|
||||
"name": "Groupe 1",
|
||||
"owner": self.test_teacher_owner.pk,
|
||||
"department": self.test_department.pk,
|
||||
"students": map(lambda obj: obj.pk, self.test_students_group)
|
||||
}
|
||||
|
||||
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/student_groups/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.json()["count"], models.StudentGroup.objects.count())
|
||||
|
||||
# check for a post request
|
||||
response = self.client.post("/api/v1/student_groups/", data=self.student_group_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/student_groups/")
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# check for a post request
|
||||
response = self.client.post("/api/v1/student_groups/", data=self.student_group_creation_data)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class TeachingUnitApiTestCase(test.APITestCase):
|
||||
pass
|
||||
|
||||
|
||||
class StudentCardApiTestCase(test.APITestCase):
|
||||
pass
|
||||
|
||||
|
||||
class TeachingSessionApiTestCase(test.APITestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AttendanceApiTestCase(test.APITestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AbsenceApiTestCase(test.APITestCase):
|
||||
pass
|
||||
|
||||
|
||||
class AbsenceAttachmentApiTestCase(test.APITestCase):
|
||||
pass
|
25
Palto/Palto/api/v1/urls.py
Normal file
25
Palto/Palto/api/v1/urls.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
Urls for the Palto project's API v1.
|
||||
|
||||
All the urls for every model of the API are described here.
|
||||
"""
|
||||
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
router.register(r'users', views.UserViewSet, basename="User")
|
||||
router.register(r'departments', views.DepartmentViewSet, basename="Department")
|
||||
router.register(r'student_groups', views.StudentGroupViewSet, basename="StudentGroup")
|
||||
router.register(r'teaching_units', views.TeachingUnitViewSet, basename="TeachingUnit")
|
||||
router.register(r'student_cards', views.StudentCardViewSet, basename="StudentCard")
|
||||
router.register(r'teaching_sessions', views.TeachingSessionViewSet, basename="TeachingSession")
|
||||
router.register(r'attendances', views.AttendanceViewSet, basename="Attendance")
|
||||
router.register(r'absences', views.AbsenceViewSet, basename="Absence")
|
||||
router.register(r'absence_attachments', views.AbsenceAttachmentViewSet, basename="AbsenceAttachment")
|
||||
|
||||
|
||||
urlpatterns = router.urls
|
86
Palto/Palto/api/v1/views.py
Normal file
86
Palto/Palto/api/v1/views.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
"""
|
||||
Views for the Palto project's API v1.
|
||||
|
||||
An API view describe which models should display which files to user with which permissions.
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated, BasePermission
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from . import permissions
|
||||
from . import serializers
|
||||
from ... import models
|
||||
|
||||
|
||||
def view_from_helper_class(
|
||||
model_class: Type[models.ModelPermissionHelper],
|
||||
serializer_class: Type[BaseSerializer],
|
||||
permission_classes: list[Type[BasePermission]],
|
||||
) -> Type[viewsets.ModelViewSet]:
|
||||
"""
|
||||
Create a view class from a model if it implements ModelPermissionHelper.
|
||||
This make creating view easier to understand and less redundant.
|
||||
"""
|
||||
|
||||
class ViewSet(viewsets.ModelViewSet):
|
||||
nonlocal serializer_class, permission_classes, model_class
|
||||
|
||||
def get_serializer_class(self):
|
||||
return serializer_class
|
||||
|
||||
def get_permissions(self):
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def get_queryset(self):
|
||||
return model_class.all_visible_by_user(self.request.user)
|
||||
|
||||
return ViewSet
|
||||
|
||||
|
||||
UserViewSet = view_from_helper_class(
|
||||
model_class=models.User,
|
||||
serializer_class=serializers.UserSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.UserPermission]
|
||||
)
|
||||
DepartmentViewSet = view_from_helper_class(
|
||||
model_class=models.Department,
|
||||
serializer_class=serializers.DepartmentSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.DepartmentPermission]
|
||||
)
|
||||
StudentGroupViewSet = view_from_helper_class(
|
||||
model_class=models.StudentGroup,
|
||||
serializer_class=serializers.StudentGroupSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.StudentGroupPermission]
|
||||
)
|
||||
TeachingUnitViewSet = view_from_helper_class(
|
||||
model_class=models.TeachingUnit,
|
||||
serializer_class=serializers.TeachingUnitSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.TeachingUnitPermission]
|
||||
)
|
||||
StudentCardViewSet = view_from_helper_class(
|
||||
model_class=models.StudentCard,
|
||||
serializer_class=serializers.StudentCardSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.StudentCardPermission]
|
||||
)
|
||||
TeachingSessionViewSet = view_from_helper_class(
|
||||
model_class=models.TeachingSession,
|
||||
serializer_class=serializers.TeachingSessionSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.TeachingSessionPermission]
|
||||
)
|
||||
AttendanceViewSet = view_from_helper_class(
|
||||
model_class=models.Attendance,
|
||||
serializer_class=serializers.AttendanceSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.AttendancePermission]
|
||||
)
|
||||
AbsenceViewSet = view_from_helper_class(
|
||||
model_class=models.Absence,
|
||||
serializer_class=serializers.AbsenceSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.AbsencePermission]
|
||||
)
|
||||
AbsenceAttachmentViewSet = view_from_helper_class(
|
||||
model_class=models.AbsenceAttachment,
|
||||
serializer_class=serializers.AbsenceAttachmentSerializer,
|
||||
permission_classes=[IsAuthenticated, permissions.AbsenceAttachmentPermission]
|
||||
)
|
|
@ -1,3 +1,9 @@
|
|||
"""
|
||||
Apps for the Palto project.
|
||||
|
||||
The app is the configuration for this part of the project.
|
||||
"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
|
187
Palto/Palto/factories.py
Normal file
187
Palto/Palto/factories.py
Normal file
|
@ -0,0 +1,187 @@
|
|||
"""
|
||||
Factories for the Palto project.
|
||||
|
||||
Factories are class that allow for the automatic creation of instances of our models, primarily for testing purpose.
|
||||
"""
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import factory
|
||||
import faker
|
||||
from django.utils import timezone
|
||||
|
||||
from Palto.Palto import models
|
||||
|
||||
|
||||
fake = faker.Faker()
|
||||
|
||||
|
||||
class FakeUserFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.User
|
||||
|
||||
username: str = factory.Sequence(lambda obj: f"{fake.user_name()}{random.randint(1000, 9999)}")
|
||||
first_name: str = factory.Faker("first_name")
|
||||
last_name: str = factory.Faker("last_name")
|
||||
email: str = factory.Faker("email")
|
||||
|
||||
|
||||
class FakeDepartmentFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.Department
|
||||
|
||||
name: str = factory.Faker("company")
|
||||
email: str = factory.Faker("company_email")
|
||||
|
||||
@factory.post_generation
|
||||
def managers(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted is not None:
|
||||
self.managers.add(*extracted)
|
||||
else:
|
||||
self.managers.add(*[FakeUserFactory() for _ in range(random.randint(1, 3))])
|
||||
|
||||
@factory.post_generation
|
||||
def teachers(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted is not None:
|
||||
self.teachers.add(*extracted)
|
||||
else:
|
||||
self.teachers.add(*[FakeUserFactory() for _ in range(random.randint(2, 10))])
|
||||
|
||||
@factory.post_generation
|
||||
def students(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted is not None:
|
||||
self.students.add(*extracted)
|
||||
else:
|
||||
self.students.add(*[FakeUserFactory() for _ in range(random.randint(50, 150))])
|
||||
|
||||
|
||||
class FakeStudentGroupFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.StudentGroup
|
||||
|
||||
name: str = factory.Faker("administrative_unit")
|
||||
|
||||
owner: models.User = factory.SubFactory(FakeUserFactory)
|
||||
department: models.Department = factory.SubFactory(FakeDepartmentFactory)
|
||||
|
||||
@factory.post_generation
|
||||
def students(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted is not None:
|
||||
self.students.add(*extracted)
|
||||
else:
|
||||
# create a group of between 5 and 50 students from this department
|
||||
self.students.add(*self.department.students.order_by('?')[:random.randint(5, 50)])
|
||||
|
||||
|
||||
class FakeTeachingUnitFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.TeachingUnit
|
||||
|
||||
name: str = factory.Faker("administrative_unit")
|
||||
|
||||
department: models.Department = factory.SubFactory(
|
||||
FakeDepartmentFactory
|
||||
)
|
||||
|
||||
@factory.post_generation
|
||||
def managers(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted is not None:
|
||||
self.managers.add(*extracted)
|
||||
else:
|
||||
# create a group of between 1 and 2 managers from the teacher's department
|
||||
self.managers.add(*self.department.teachers.order_by('?')[:random.randint(1, 2)])
|
||||
|
||||
@factory.post_generation
|
||||
def teachers(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted is not None:
|
||||
self.teachers.add(*extracted)
|
||||
else:
|
||||
# create a group of between 2 and 10 teachers from the teacher's department
|
||||
self.teachers.add(*self.department.teachers.order_by('?')[:random.randint(2, 10)])
|
||||
|
||||
@factory.post_generation
|
||||
def student_groups(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
if extracted is not None:
|
||||
self.student_groups.add(*extracted)
|
||||
else:
|
||||
# create a group of between 1 and 2 student groups from the department
|
||||
self.student_groups.add(*[
|
||||
FakeStudentGroupFactory.create(department=self.department)
|
||||
for _ in range(random.randint(1, 2))
|
||||
])
|
||||
|
||||
|
||||
class FakeStudentCardFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.StudentCard
|
||||
|
||||
uid: bytes = factory.Faker("binary", length=7)
|
||||
|
||||
department: models.Department = factory.SubFactory(FakeDepartmentFactory)
|
||||
owner: models.User = factory.SubFactory(FakeUserFactory)
|
||||
|
||||
|
||||
class FakeTeachingSessionFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.TeachingSession
|
||||
|
||||
start: timedelta = factory.Faker("date_time", tzinfo=timezone.get_current_timezone())
|
||||
duration: timedelta = factory.Faker("time_delta")
|
||||
note: str = factory.Faker("paragraph")
|
||||
|
||||
unit: models.TeachingUnit = factory.SubFactory(FakeTeachingUnitFactory)
|
||||
|
||||
group: models.StudentGroup = factory.SubFactory(FakeStudentGroupFactory)
|
||||
teacher: models.User = factory.SubFactory(FakeUserFactory)
|
||||
|
||||
|
||||
class FakeAttendanceFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.Attendance
|
||||
|
||||
date: datetime = factory.Faker("date_time", tzinfo=timezone.get_current_timezone())
|
||||
|
||||
student: models.User = factory.SubFactory(FakeUserFactory)
|
||||
session: models.TeachingSession = factory.SubFactory(FakeTeachingSessionFactory)
|
||||
|
||||
|
||||
class FakeAbsenceFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.Absence
|
||||
|
||||
message: str = factory.Faker("paragraph")
|
||||
|
||||
department: models.Department = factory.SubFactory(FakeDepartmentFactory)
|
||||
student: models.User = factory.SubFactory(FakeUserFactory)
|
||||
start: datetime = factory.Faker("date_time", tzinfo=timezone.get_current_timezone())
|
||||
end: datetime = factory.LazyAttribute(lambda obj: obj.start + timedelta(days=random.randint(1, 8)))
|
||||
|
||||
|
||||
class FakeAbsenceAttachmentFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = models.AbsenceAttachment
|
||||
|
||||
content: str = factory.django.FileField()
|
||||
|
||||
absence: models.Absence = factory.SubFactory(FakeAbsenceFactory)
|
|
@ -1,13 +1,57 @@
|
|||
"""
|
||||
Models for the Palto project.
|
||||
|
||||
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, Callable, Any
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet, Q, F
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class User(AbstractUser):
|
||||
# TODO(Raphaël): split permissions from models for readability
|
||||
|
||||
|
||||
class ModelPermissionHelper:
|
||||
@classmethod
|
||||
def can_user_create(cls, user: "User") -> bool:
|
||||
"""
|
||||
Return True if the user can create a new instance of this object
|
||||
"""
|
||||
|
||||
return user.is_superuser
|
||||
|
||||
@classmethod
|
||||
def user_fields_contraints(cls, user: "User") -> dict[str, Callable[[dict], QuerySet]]:
|
||||
"""
|
||||
Return a dictionary of field associated to a function giving all the allowed values for this field.
|
||||
For example, this can be used to check that a user manage a department before allowing modification.
|
||||
"""
|
||||
|
||||
return {}
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -19,8 +63,65 @@ 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])
|
||||
|
||||
# permissions
|
||||
|
||||
@classmethod
|
||||
def can_user_create(cls, user: "User") -> bool:
|
||||
# if the requesting user is admin
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# if the user is managing a department, allow him to create user
|
||||
if user.managing_departments.count() > 0:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def all_editable_by_user(cls, user: "User") -> QuerySet:
|
||||
queryset = QuerySet()
|
||||
|
||||
if user.is_superuser:
|
||||
# if the requesting user is admin
|
||||
queryset = cls.objects.all()
|
||||
else:
|
||||
# all the users related to a department the user is managing
|
||||
if user.managing_departments.count() > 0:
|
||||
queryset = cls.objects.all()
|
||||
|
||||
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.
|
||||
|
||||
|
@ -29,12 +130,12 @@ class Department(models.Model):
|
|||
"""
|
||||
|
||||
id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36)
|
||||
name: str = models.CharField(max_length=64)
|
||||
mail: str = models.EmailField()
|
||||
name: str = models.CharField(max_length=64, unique=True)
|
||||
email: str = models.EmailField()
|
||||
|
||||
managers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="managing_departments")
|
||||
teachers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="teaching_departments")
|
||||
students = models.ManyToManyField(to=get_user_model(), blank=True, related_name="studying_departments")
|
||||
managers = models.ManyToManyField(to=User, blank=True, related_name="managing_departments")
|
||||
teachers = models.ManyToManyField(to=User, blank=True, related_name="teaching_departments")
|
||||
students = models.ManyToManyField(to=User, blank=True, related_name="studying_departments")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} name={self.name!r}>"
|
||||
|
@ -42,8 +143,55 @@ 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])
|
||||
|
||||
# permissions
|
||||
|
||||
@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 = cls.objects.filter(
|
||||
# if the user is the manager of the department
|
||||
managers=user,
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
|
@ -56,7 +204,9 @@ 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)
|
||||
|
||||
students = models.ManyToManyField(to=get_user_model(), blank=True, related_name="student_groups")
|
||||
department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="student_groups")
|
||||
owner = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="owning_groups")
|
||||
students = models.ManyToManyField(to=User, blank=True, related_name="student_groups")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} name={self.name!r}>"
|
||||
|
@ -64,8 +214,84 @@ class StudentGroup(models.Model):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# validators
|
||||
|
||||
class TeachingUnit(models.Model):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# owner check
|
||||
if self.department not in self.owner.teaching_departments:
|
||||
raise ValidationError("The owner is not related to the department.")
|
||||
|
||||
# students check
|
||||
if not all(self.department in student.studying_departments for student in self.students.all()):
|
||||
raise ValidationError("A student is not related to the department.")
|
||||
|
||||
# permissions
|
||||
|
||||
@classmethod
|
||||
def can_user_create(cls, user: "User") -> bool:
|
||||
# if the requesting user is admin
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# if the user is managing a department
|
||||
if user.managing_departments.count() > 0:
|
||||
return True
|
||||
|
||||
# if the user is teaching a department
|
||||
if user.teaching_departments.count() > 0:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def user_fields_contraints(cls, user: "User") -> dict[str, Callable[[dict], QuerySet]]:
|
||||
# if the user is admin, no contrains
|
||||
if user.is_superuser:
|
||||
return {}
|
||||
|
||||
return {
|
||||
# the user can only interact with a related departments
|
||||
"department": lambda data: user.managing_departments | user.teaching_departments,
|
||||
# the owner must be a teacher or a manager of this department
|
||||
"owner": lambda data: data["department"].managers | data["department"].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 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.
|
||||
|
||||
|
@ -80,8 +306,8 @@ class TeachingUnit(models.Model):
|
|||
|
||||
department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="teaching_units")
|
||||
|
||||
managers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="managing_units")
|
||||
teachers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="teaching_units")
|
||||
managers = models.ManyToManyField(to=User, blank=True, related_name="managing_units")
|
||||
teachers = models.ManyToManyField(to=User, blank=True, related_name="teaching_units")
|
||||
student_groups = models.ManyToManyField(to=StudentGroup, blank=True, related_name="studying_units")
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -90,8 +316,80 @@ class TeachingUnit(models.Model):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# validations
|
||||
|
||||
class StudentCard(models.Model):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# managers check
|
||||
if not all(self.department in manager.managing_departments for manager in self.managers.all()):
|
||||
raise ValidationError("A manager is not related to the department.")
|
||||
|
||||
# teachers check
|
||||
if not all(self.department in teacher.teaching_departments for teacher in self.teachers.all()):
|
||||
raise ValidationError("A teacher is not related to the department.")
|
||||
|
||||
# student groups check
|
||||
if not all(self.department in student_group.department for student_group in self.student_groups.all()):
|
||||
raise ValidationError("A student group is not related to the department.")
|
||||
|
||||
# permissions
|
||||
|
||||
@classmethod
|
||||
def can_user_create(cls, user: "User") -> bool:
|
||||
# if the requesting user is admin
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# if the user is managing a department
|
||||
if user.managing_departments.count() > 0:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def user_fields_contraints(cls, user: "User") -> dict[str, Callable[[dict], QuerySet]]:
|
||||
# if the user is admin, no contrains
|
||||
if user.is_superuser:
|
||||
return {}
|
||||
|
||||
return {
|
||||
# a user can only interact with a related departments
|
||||
"department": lambda data: user.managing_departments | user.teaching_departments
|
||||
}
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -101,13 +399,73 @@ class StudentCard(models.Model):
|
|||
id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36)
|
||||
uid: bytes = models.BinaryField(max_length=7)
|
||||
|
||||
owner: User = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="student_cards")
|
||||
department: Department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="student_cards")
|
||||
owner: User = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="student_cards")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} owner={self.owner.username!r}>"
|
||||
|
||||
# validations
|
||||
|
||||
class TeachingSession(models.Model):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# owner check
|
||||
if self.department not in self.owner.studying_departments:
|
||||
raise ValidationError("The student is not related to the department.")
|
||||
|
||||
# permissions
|
||||
|
||||
@classmethod
|
||||
def can_user_create(cls, user: "User") -> bool:
|
||||
# if the requesting user is admin
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
if user.managing_departments.count() > 0:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def user_fields_contraints(cls, user: "User") -> dict[str, Callable[[Any, dict], bool]]:
|
||||
# if the user is admin, no contrains
|
||||
if user.is_superuser:
|
||||
return {}
|
||||
|
||||
return {
|
||||
# a user can only interact with a related departments
|
||||
"department": lambda field, data: field in user.managing_departments,
|
||||
}
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -124,7 +482,7 @@ class TeachingSession(models.Model):
|
|||
unit = models.ForeignKey(to=TeachingUnit, on_delete=models.CASCADE, related_name="sessions")
|
||||
|
||||
group = models.ForeignKey(to=StudentGroup, on_delete=models.CASCADE, related_name="teaching_sessions")
|
||||
teacher = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="teaching_sessions")
|
||||
teacher = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="teaching_sessions")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} unit={self.unit.name!r} start={self.start}>"
|
||||
|
@ -136,8 +494,95 @@ class TeachingSession(models.Model):
|
|||
def end(self) -> datetime:
|
||||
return self.start + self.duration
|
||||
|
||||
# validations
|
||||
|
||||
class Attendance(models.Model):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# group check
|
||||
if self.unit.department not in self.group.department:
|
||||
raise ValidationError("The group is not related to the unit department.")
|
||||
|
||||
# teacher check
|
||||
if self.unit not in self.teacher.teaching_units:
|
||||
raise ValidationError("The teacher is not related to the unit.")
|
||||
|
||||
# permissions
|
||||
|
||||
@classmethod
|
||||
def can_user_create(cls, user: "User") -> bool:
|
||||
# if the requesting user is admin
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# if the user is managing a department
|
||||
if user.managing_departments.count() > 0:
|
||||
return True
|
||||
|
||||
# if the user is managing a unit
|
||||
if user.managing_units.count() > 0:
|
||||
return True
|
||||
|
||||
# if the user is teaching a unit
|
||||
if user.teaching_units.count() > 0:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def user_fields_contraints(cls, user: "User") -> dict[str, Callable[[Any, dict], bool]]:
|
||||
# if the user is admin, no contrains
|
||||
if user.is_superuser:
|
||||
return {}
|
||||
|
||||
return {
|
||||
# the managers can only interact with their units
|
||||
"unit": lambda data: (
|
||||
# all the units the user is managing
|
||||
user.managing_units |
|
||||
# all the units the user is teaching
|
||||
user.teaching_units |
|
||||
# all the units of the department the user is managing
|
||||
TeachingUnit.objects.filter(pk__in=user.managing_departments.values("teaching_units"))
|
||||
)
|
||||
}
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -148,7 +593,7 @@ class Attendance(models.Model):
|
|||
date: datetime = models.DateTimeField()
|
||||
|
||||
student: User = models.ForeignKey(
|
||||
to=get_user_model(),
|
||||
to=User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="attended_sessions"
|
||||
)
|
||||
|
@ -167,8 +612,95 @@ class Attendance(models.Model):
|
|||
f">"
|
||||
)
|
||||
|
||||
# validations
|
||||
|
||||
class Absence(models.Model):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# student check
|
||||
if self.student not in self.session.group.students:
|
||||
raise ValidationError("The student is not related to the student group.")
|
||||
|
||||
# permissions
|
||||
|
||||
@classmethod
|
||||
def can_user_create(cls, user: "User") -> bool:
|
||||
# if the requesting user is admin
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# if the user is managing a department
|
||||
if user.managing_departments.count() > 0:
|
||||
return True
|
||||
|
||||
# if the user is managing a unit
|
||||
if user.managing_units.count() > 0:
|
||||
return True
|
||||
|
||||
# if the user is teaching a unit
|
||||
if user.teaching_units.count() > 0:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def user_fields_contraints(cls, user: "User") -> dict[str, Callable[[Any, dict], bool]]:
|
||||
# if the user is admin, no contrains
|
||||
if user.is_superuser:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"session": lambda data: (
|
||||
# the sessions that the user has taught
|
||||
user.teaching_sessions |
|
||||
# a session of a unit the user is managing
|
||||
TeachingSession.objects.filter(pk__in=user.managing_units.values("sessions")) |
|
||||
# all the sessions in a department the user is managing
|
||||
TeachingSession.objects.filter(
|
||||
pk__in=TeachingUnit.objects.filter(
|
||||
pk__in=user.managing_departments.values("teaching_units")
|
||||
).values("sessions")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -178,23 +710,112 @@ class Absence(models.Model):
|
|||
id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36)
|
||||
message: str = models.TextField()
|
||||
|
||||
student: User = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="absented_sessions")
|
||||
session: TeachingSession = models.ManyToManyField(to=TeachingSession, blank=True, related_name="absences")
|
||||
department: Department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="absences")
|
||||
student: User = models.ForeignKey(to=User, on_delete=models.CASCADE, 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"department={self.department} "
|
||||
f"student={self.student.username} "
|
||||
f"session={str(self.session.id)[:8]}"
|
||||
f"start={self.start} "
|
||||
f"end={self.end}"
|
||||
f">"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{str(self.id)[:8]}] {self.student}"
|
||||
|
||||
# validations
|
||||
|
||||
class AbsenceAttachment(models.Model):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# student check
|
||||
if self.department not in self.student.studying_departments:
|
||||
raise ValidationError("The student is not related to the department.")
|
||||
|
||||
# properties
|
||||
|
||||
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
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# if the user is a student
|
||||
if user.studying_departments.count() > 0:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def user_fields_contraints(cls, user: "User") -> dict[str, Callable[[dict], QuerySet]]:
|
||||
# if the user is admin, no contrains
|
||||
if user.is_superuser:
|
||||
return {}
|
||||
|
||||
return {
|
||||
# all the departments the user is studying in
|
||||
"department": lambda data: user.studying_departments,
|
||||
}
|
||||
|
||||
@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.
|
||||
|
||||
|
@ -208,3 +829,65 @@ 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
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# if the user is a student
|
||||
if user.objects.count():
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def user_fields_contraints(cls, user: "User") -> dict[str, Callable[[dict], QuerySet]]:
|
||||
# if the user is admin, no contrains
|
||||
if user.is_superuser:
|
||||
return {}
|
||||
|
||||
return {
|
||||
# all the departments the user is studying in
|
||||
"absence": lambda data: user.absences,
|
||||
}
|
||||
|
||||
@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")
|
||||
|
|
|
@ -1,3 +1,64 @@
|
|||
from django.test import TestCase
|
||||
"""
|
||||
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 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()
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
"""
|
||||
Views for the Palto project.
|
||||
|
||||
A view is what control the content of a page, prepare the correct data, react to a form, render the correct template.
|
||||
"""
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
|
@ -10,6 +10,7 @@ For the full list of settings and their values, see
|
|||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
|
@ -50,12 +51,15 @@ INSTALLED_APPS = [
|
|||
'rest_framework',
|
||||
"debug_toolbar",
|
||||
'django_extensions',
|
||||
'rest_framework_simplejwt',
|
||||
'corsheaders',
|
||||
|
||||
"Palto.Palto",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
|
@ -147,12 +151,51 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||
# Rest API configuration
|
||||
# https://www.django-rest-framework.org/
|
||||
REST_FRAMEWORK = {
|
||||
# Default way to authenticate to the REST api.
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
],
|
||||
|
||||
# Use Django's standard `django.contrib.auth` permissions,
|
||||
# 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
|
||||
}
|
||||
|
||||
# JWT Authentification
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||
}
|
||||
|
||||
|
||||
# User model
|
||||
AUTH_USER_MODEL = "Palto.User"
|
||||
|
||||
|
||||
# CORS settings
|
||||
# TODO(Raphaël): Only in debug !
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
|
||||
# Login
|
||||
LOGIN_URL = "/admin/login/"
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
LOGOUT_REDIRECT_URL = "/"
|
||||
|
|
|
@ -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'))
|
||||
"""
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
# Django libraries
|
||||
django
|
||||
djangorestframework
|
||||
django-filter
|
||||
django-debug-toolbar
|
||||
django-extensions
|
||||
djangorestframework-simplejwt[crypto]
|
||||
django-cors-headers
|
||||
Werkzeug
|
||||
|
||||
# Tests libraries
|
||||
faker
|
||||
factory_boy
|
||||
|
||||
# Other librairies
|
||||
markdown
|
||||
|
|
Loading…
Reference in a new issue