commit
07812d21fe
31 changed files with 754 additions and 112 deletions
8
.idea/.gitignore
vendored
8
.idea/.gitignore
vendored
|
@ -1,8 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
|
@ -1,29 +0,0 @@
|
||||||
<?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" />
|
|
||||||
</content>
|
|
||||||
<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,6 +0,0 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Black">
|
|
||||||
<option name="sdkName" value="Python 3.11" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (Palto-Server)" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/Palto-Server.iml" filepath="$PROJECT_DIR$/.idea/Palto-Server.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -9,7 +9,7 @@ from django.contrib import admin
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
# TODO: plus de list_filter sur "department" ?
|
# TODO(Faraphel): plus de list_filter sur "department" ?
|
||||||
|
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
@ -34,8 +34,8 @@ class AdminStudentGroup(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(models.TeachingUnit)
|
@admin.register(models.TeachingUnit)
|
||||||
class AdminTeachingUnit(admin.ModelAdmin):
|
class AdminTeachingUnit(admin.ModelAdmin):
|
||||||
list_display = ("id", "name")
|
list_display = ("id", "name", "email")
|
||||||
search_fields = ("id", "name")
|
search_fields = ("id", "name", "email")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.StudentCard)
|
@admin.register(models.StudentCard)
|
||||||
|
@ -61,9 +61,9 @@ class AdminAttendance(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(models.Absence)
|
@admin.register(models.Absence)
|
||||||
class AdminAbsence(admin.ModelAdmin):
|
class AdminAbsence(admin.ModelAdmin):
|
||||||
list_display = ("id", "message", "student", "department", "start", "end")
|
list_display = ("id", "message", "student", "start", "end")
|
||||||
search_fields = ("id", "message", "student", "department", "start", "end")
|
search_fields = ("id", "message", "student", "start", "end")
|
||||||
list_filter = ("department", "start", "end")
|
list_filter = ("start", "end")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.AbsenceAttachment)
|
@admin.register(models.AbsenceAttachment)
|
||||||
|
|
|
@ -9,6 +9,8 @@ from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
|
||||||
|
|
||||||
import Palto.Palto.api.v1.urls as v1_urls
|
import Palto.Palto.api.v1.urls as v1_urls
|
||||||
|
|
||||||
|
app_name = "PaltoAPI"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Authentification (JWT)
|
# Authentification (JWT)
|
||||||
path('auth/jwt/token/', TokenObtainPairView.as_view(), name='token'),
|
path('auth/jwt/token/', TokenObtainPairView.as_view(), name='token'),
|
||||||
|
|
|
@ -12,7 +12,7 @@ from rest_framework.exceptions import PermissionDenied
|
||||||
from Palto.Palto import models
|
from Palto.Palto import models
|
||||||
|
|
||||||
|
|
||||||
# TODO: voir les relations inversées ?
|
# TODO(Faraphel): voir les relations inversées ?
|
||||||
|
|
||||||
|
|
||||||
class ModelSerializerContrains(serializers.ModelSerializer):
|
class ModelSerializerContrains(serializers.ModelSerializer):
|
||||||
|
|
|
@ -9,6 +9,8 @@ from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
app_name = "PaltoAPIv1"
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
|
|
||||||
router.register(r'users', views.UserViewSet, basename="User")
|
router.register(r'users', views.UserViewSet, basename="User")
|
||||||
|
@ -21,5 +23,4 @@ router.register(r'attendances', views.AttendanceViewSet, basename="Attendance")
|
||||||
router.register(r'absences', views.AbsenceViewSet, basename="Absence")
|
router.register(r'absences', views.AbsenceViewSet, basename="Absence")
|
||||||
router.register(r'absence_attachments', views.AbsenceAttachmentViewSet, basename="AbsenceAttachment")
|
router.register(r'absence_attachments', views.AbsenceAttachmentViewSet, basename="AbsenceAttachment")
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
|
|
@ -90,10 +90,8 @@ class FakeTeachingUnitFactory(factory.django.DjangoModelFactory):
|
||||||
model = models.TeachingUnit
|
model = models.TeachingUnit
|
||||||
|
|
||||||
name: str = factory.Faker("administrative_unit")
|
name: str = factory.Faker("administrative_unit")
|
||||||
|
email: str = factory.Faker("company_email")
|
||||||
department: models.Department = factory.SubFactory(
|
department: models.Department = factory.SubFactory(FakeDepartmentFactory)
|
||||||
FakeDepartmentFactory
|
|
||||||
)
|
|
||||||
|
|
||||||
@factory.post_generation
|
@factory.post_generation
|
||||||
def managers(self, create, extracted, **kwargs):
|
def managers(self, create, extracted, **kwargs):
|
||||||
|
|
45
Palto/Palto/forms.py
Normal file
45
Palto/Palto/forms.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from Palto.Palto import models
|
||||||
|
|
||||||
|
|
||||||
|
# Common
|
||||||
|
class MultipleFileInput(forms.ClearableFileInput):
|
||||||
|
allow_multiple_selected = True
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleFileField(forms.FileField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault("widget", MultipleFileInput())
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self, data, initial=None):
|
||||||
|
file_cleaner = super().clean
|
||||||
|
if isinstance(data, (list, tuple)):
|
||||||
|
return [file_cleaner(d, initial) for d in data]
|
||||||
|
else:
|
||||||
|
return file_cleaner(data, initial)
|
||||||
|
|
||||||
|
|
||||||
|
# Users
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(forms.Form):
|
||||||
|
username = forms.CharField()
|
||||||
|
password = forms.CharField(widget=forms.PasswordInput)
|
||||||
|
|
||||||
|
|
||||||
|
# Objects
|
||||||
|
|
||||||
|
|
||||||
|
class NewAbsenceForm(forms.Form):
|
||||||
|
department = forms.ModelChoiceField(queryset=None)
|
||||||
|
start = forms.DateTimeField(widget=forms.TextInput(attrs=dict(type='datetime-local')))
|
||||||
|
end = forms.DateTimeField(widget=forms.TextInput(attrs=dict(type='datetime-local')))
|
||||||
|
message = forms.CharField(widget=forms.Textarea)
|
||||||
|
attachments = MultipleFileField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, student: models.User, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields["department"].queryset = student.studying_departments.all()
|
|
@ -15,7 +15,7 @@ from django.db import models
|
||||||
from django.db.models import QuerySet, Q, F
|
from django.db.models import QuerySet, Q, F
|
||||||
|
|
||||||
|
|
||||||
# TODO(Raphaël): split permissions from models for readability
|
# TODO(Faraphel): split permissions from models for readability
|
||||||
|
|
||||||
|
|
||||||
class ModelPermissionHelper:
|
class ModelPermissionHelper:
|
||||||
|
@ -61,7 +61,14 @@ class User(AbstractUser, ModelPermissionHelper):
|
||||||
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)
|
||||||
|
|
||||||
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={self.short_id} username={self.username!r}>"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.first_name} {self.last_name.upper()}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def multiple_related_departments(users: Iterable["User"]) -> QuerySet["Department"]:
|
def multiple_related_departments(users: Iterable["User"]) -> QuerySet["Department"]:
|
||||||
|
@ -138,11 +145,15 @@ class Department(models.Model, ModelPermissionHelper):
|
||||||
students = models.ManyToManyField(to=User, blank=True, related_name="studying_departments")
|
students = models.ManyToManyField(to=User, blank=True, related_name="studying_departments")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} name={self.name!r}>"
|
return f"<{self.__class__.__name__} id={self.short_id} name={self.name!r}>"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def multiple_related_users(departments: Iterable["Department"]) -> QuerySet["User"]:
|
def multiple_related_users(departments: Iterable["Department"]) -> QuerySet["User"]:
|
||||||
"""
|
"""
|
||||||
|
@ -209,22 +220,26 @@ class StudentGroup(models.Model, ModelPermissionHelper):
|
||||||
students = models.ManyToManyField(to=User, blank=True, related_name="student_groups")
|
students = models.ManyToManyField(to=User, blank=True, related_name="student_groups")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} name={self.name!r}>"
|
return f"<{self.__class__.__name__} id={self.short_id} name={self.name!r}>"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
# validators
|
# validators
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# owner check
|
# owner check
|
||||||
if self.department not in self.owner.teaching_departments:
|
if self.department not in self.owner.teaching_departments.all():
|
||||||
raise ValidationError("The owner is not related to the department.")
|
raise ValidationError("The owner is not related to the department.")
|
||||||
|
|
||||||
# students check
|
# students check
|
||||||
if not all(self.department in student.studying_departments for student in self.students.all()):
|
if not all(self.department in student.studying_departments.all() for student in self.students.all()):
|
||||||
raise ValidationError("A student is not related to the department.")
|
raise ValidationError("A student is not related to the department.")
|
||||||
|
|
||||||
# permissions
|
# permissions
|
||||||
|
@ -251,9 +266,9 @@ class StudentGroup(models.Model, ModelPermissionHelper):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# the user can only interact with a related departments
|
# the user can only interact with a related departments
|
||||||
"department": lambda data: user.managing_departments | user.teaching_departments,
|
"department": lambda data: (user.managing_departments | user.teaching_departments).all(),
|
||||||
# the owner must be a teacher or a manager of this department
|
# the owner must be a teacher or a manager of this department
|
||||||
"owner": lambda data: data["department"].managers | data["department"].teachers,
|
"owner": lambda data: (data["department"].managers | data["department"].teachers).all(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -303,6 +318,7 @@ class TeachingUnit(models.Model, ModelPermissionHelper):
|
||||||
|
|
||||||
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=64)
|
name: str = models.CharField(max_length=64)
|
||||||
|
email: str = models.EmailField(null=True, blank=True)
|
||||||
|
|
||||||
department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="teaching_units")
|
department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="teaching_units")
|
||||||
|
|
||||||
|
@ -311,26 +327,30 @@ class TeachingUnit(models.Model, ModelPermissionHelper):
|
||||||
student_groups = models.ManyToManyField(to=StudentGroup, blank=True, related_name="studying_units")
|
student_groups = models.ManyToManyField(to=StudentGroup, blank=True, related_name="studying_units")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} name={self.name!r}>"
|
return f"<{self.__class__.__name__} id={self.short_id} name={self.name!r}>"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
# validations
|
# validations
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# managers check
|
# managers check
|
||||||
if not all(self.department in manager.managing_departments for manager in self.managers.all()):
|
if not all(self.department in manager.managing_departments.all() for manager in self.managers.all()):
|
||||||
raise ValidationError("A manager is not related to the department.")
|
raise ValidationError("A manager is not related to the department.")
|
||||||
|
|
||||||
# teachers check
|
# teachers check
|
||||||
if not all(self.department in teacher.teaching_departments for teacher in self.teachers.all()):
|
if not all(self.department in teacher.teaching_departments.all() for teacher in self.teachers.all()):
|
||||||
raise ValidationError("A teacher is not related to the department.")
|
raise ValidationError("A teacher is not related to the department.")
|
||||||
|
|
||||||
# student groups check
|
# student groups check
|
||||||
if not all(self.department in student_group.department for student_group in self.student_groups.all()):
|
if not all(self.department in student_group.department.all() for student_group in self.student_groups.all()):
|
||||||
raise ValidationError("A student group is not related to the department.")
|
raise ValidationError("A student group is not related to the department.")
|
||||||
|
|
||||||
# permissions
|
# permissions
|
||||||
|
@ -353,7 +373,7 @@ class TeachingUnit(models.Model, ModelPermissionHelper):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# a user can only interact with a related departments
|
# a user can only interact with a related departments
|
||||||
"department": lambda data: user.managing_departments | user.teaching_departments
|
"department": lambda data: (user.managing_departments | user.teaching_departments).all()
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -403,7 +423,11 @@ class StudentCard(models.Model, ModelPermissionHelper):
|
||||||
owner: User = models.ForeignKey(to=User, 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):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} owner={self.owner.username!r}>"
|
return f"<{self.__class__.__name__} id={self.short_id} owner={self.owner.username!r}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
# validations
|
# validations
|
||||||
|
|
||||||
|
@ -411,7 +435,7 @@ class StudentCard(models.Model, ModelPermissionHelper):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# owner check
|
# owner check
|
||||||
if self.department not in self.owner.studying_departments:
|
if self.department not in self.owner.studying_departments.all():
|
||||||
raise ValidationError("The student is not related to the department.")
|
raise ValidationError("The student is not related to the department.")
|
||||||
|
|
||||||
# permissions
|
# permissions
|
||||||
|
@ -433,7 +457,7 @@ class StudentCard(models.Model, ModelPermissionHelper):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# a user can only interact with a related departments
|
# a user can only interact with a related departments
|
||||||
"department": lambda field, data: field in user.managing_departments,
|
"department": lambda field, data: field in user.managing_departments.all(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -485,11 +509,15 @@ class TeachingSession(models.Model, ModelPermissionHelper):
|
||||||
teacher = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="teaching_sessions")
|
teacher = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="teaching_sessions")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} id={str(self.id)[:8]} unit={self.unit.name!r} start={self.start}>"
|
return f"<{self.__class__.__name__} id={self.short_id} unit={self.unit.name!r} start={self.start}>"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.unit.name} ({self.start})"
|
return f"{self.unit.name} ({self.start})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def end(self) -> datetime:
|
def end(self) -> datetime:
|
||||||
return self.start + self.duration
|
return self.start + self.duration
|
||||||
|
@ -499,12 +527,12 @@ class TeachingSession(models.Model, ModelPermissionHelper):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# group check
|
# department check
|
||||||
if self.unit.department not in self.group.department:
|
if self.unit.department != self.group.department:
|
||||||
raise ValidationError("The group is not related to the unit department.")
|
raise ValidationError("The group is not related to the unit department.")
|
||||||
|
|
||||||
# teacher check
|
# teacher check
|
||||||
if self.unit not in self.teacher.teaching_units:
|
if self.unit not in self.teacher.teaching_units.all():
|
||||||
raise ValidationError("The teacher is not related to the unit.")
|
raise ValidationError("The teacher is not related to the unit.")
|
||||||
|
|
||||||
# permissions
|
# permissions
|
||||||
|
@ -542,7 +570,7 @@ class TeachingSession(models.Model, ModelPermissionHelper):
|
||||||
user.teaching_units |
|
user.teaching_units |
|
||||||
# all the units of the department the user is managing
|
# all the units of the department the user is managing
|
||||||
TeachingUnit.objects.filter(pk__in=user.managing_departments.values("teaching_units"))
|
TeachingUnit.objects.filter(pk__in=user.managing_departments.values("teaching_units"))
|
||||||
)
|
).all()
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -606,19 +634,23 @@ class Attendance(models.Model, ModelPermissionHelper):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
f"<{self.__class__.__name__} "
|
f"<{self.__class__.__name__} "
|
||||||
f"id={str(self.id)[:8]} "
|
f"id={self.short_id} "
|
||||||
f"student={self.student.username} "
|
f"student={self.student.username} "
|
||||||
f"session={str(self.session.id)[:8]}"
|
f"session={self.session.short_id}"
|
||||||
f">"
|
f">"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
# validations
|
# validations
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# student check
|
# student check
|
||||||
if self.student not in self.session.group.students:
|
if self.student not in self.session.group.students.all():
|
||||||
raise ValidationError("The student is not related to the student group.")
|
raise ValidationError("The student is not related to the student group.")
|
||||||
|
|
||||||
# permissions
|
# permissions
|
||||||
|
@ -659,7 +691,7 @@ class Attendance(models.Model, ModelPermissionHelper):
|
||||||
pk__in=user.managing_departments.values("teaching_units")
|
pk__in=user.managing_departments.values("teaching_units")
|
||||||
).values("sessions")
|
).values("sessions")
|
||||||
)
|
)
|
||||||
)
|
).all()
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -718,7 +750,7 @@ class Absence(models.Model, ModelPermissionHelper):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
f"<{self.__class__.__name__} "
|
f"<{self.__class__.__name__} "
|
||||||
f"id={str(self.id)[:8]} "
|
f"id={self.short_id} "
|
||||||
f"department={self.department} "
|
f"department={self.department} "
|
||||||
f"student={self.student.username} "
|
f"student={self.student.username} "
|
||||||
f"start={self.start} "
|
f"start={self.start} "
|
||||||
|
@ -727,7 +759,11 @@ class Absence(models.Model, ModelPermissionHelper):
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"[{str(self.id)[:8]}] {self.student}"
|
return f"[{self.short_id}] {self.student}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
# validations
|
# validations
|
||||||
|
|
||||||
|
@ -735,7 +771,7 @@ class Absence(models.Model, ModelPermissionHelper):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# student check
|
# student check
|
||||||
if self.department not in self.student.studying_departments:
|
if self.department not in self.student.studying_departments.all():
|
||||||
raise ValidationError("The student is not related to the department.")
|
raise ValidationError("The student is not related to the department.")
|
||||||
|
|
||||||
# properties
|
# properties
|
||||||
|
@ -772,7 +808,7 @@ class Absence(models.Model, ModelPermissionHelper):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# all the departments the user is studying in
|
# all the departments the user is studying in
|
||||||
"department": lambda data: user.studying_departments,
|
"department": lambda data: user.studying_departments.all(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -828,7 +864,11 @@ class AbsenceAttachment(models.Model, ModelPermissionHelper):
|
||||||
absence = models.ForeignKey(to=Absence, on_delete=models.CASCADE, related_name="attachments")
|
absence = models.ForeignKey(to=Absence, on_delete=models.CASCADE, related_name="attachments")
|
||||||
|
|
||||||
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={self.short_id} content={self.content!r}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_id(self) -> str:
|
||||||
|
return str(self.id)[:8]
|
||||||
|
|
||||||
# permissions
|
# permissions
|
||||||
|
|
||||||
|
@ -850,7 +890,7 @@ class AbsenceAttachment(models.Model, ModelPermissionHelper):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# all the departments the user is studying in
|
# all the departments the user is studying in
|
||||||
"absence": lambda data: user.absences,
|
"absence": lambda data: user.absences.all(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
1
Palto/Palto/static/Palto/favicon.svg
Normal file
1
Palto/Palto/static/Palto/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.3 KiB |
11
Palto/Palto/templates/Palto/absence_new.html
Normal file
11
Palto/Palto/templates/Palto/absence_new.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "Palto/base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table>
|
||||||
|
{{ form_new_absence.as_table }}
|
||||||
|
</table>
|
||||||
|
<input type="submit" value="Créer">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
35
Palto/Palto/templates/Palto/absence_view.html
Normal file
35
Palto/Palto/templates/Palto/absence_view.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends "Palto/base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{# absence's information #}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Identifiant</th>
|
||||||
|
<td>{{ absence.id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Département</th>
|
||||||
|
<td><a href="{# TODO(Faraphel): departement #}">{{ absence.department }}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Étudiant</th>
|
||||||
|
<td><a href="{% url "Palto:profile" absence.student.id %}">{{ absence.student }}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Période</th>
|
||||||
|
<td>{{ absence.start }}<br>{{ absence.end }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{# absence's message #}
|
||||||
|
<p>
|
||||||
|
{{ absence.message }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{# absence's attachments #}
|
||||||
|
<div>
|
||||||
|
{% for attachment in absence.attachments.all %}
|
||||||
|
<a href="{{ attachment.content.url }}" target="_blank">{{ attachment.content.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
20
Palto/Palto/templates/Palto/base.html
Normal file
20
Palto/Palto/templates/Palto/base.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="{% static 'Palto/favicon.svg' %}"/>
|
||||||
|
|
||||||
|
<title>{% block title %}Palto{% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{# navigation #}
|
||||||
|
{% include "Palto/navigation.html" %}
|
||||||
|
|
||||||
|
{# body #}
|
||||||
|
{% block body %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
5
Palto/Palto/templates/Palto/homepage.html
Normal file
5
Palto/Palto/templates/Palto/homepage.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "Palto/base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
Hello there.
|
||||||
|
{% endblock %}
|
11
Palto/Palto/templates/Palto/login.html
Normal file
11
Palto/Palto/templates/Palto/login.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "Palto/base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table>
|
||||||
|
{{ form_login.as_table }}
|
||||||
|
</table>
|
||||||
|
<input type="submit" value="Se connecter">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
11
Palto/Palto/templates/Palto/navigation.html
Normal file
11
Palto/Palto/templates/Palto/navigation.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<img src="{% static "Palto/favicon.svg" %}" alt="Palto's icon" width="5%">
|
||||||
|
<a>Palto</a>
|
||||||
|
|
||||||
|
<a href="{% url "Palto:homepage" %}">Home</a>
|
||||||
|
<a href="{% url "Palto:my_profile" %}">Profile</a>
|
||||||
|
<a href="{% url "Palto:login" %}">Login</a>
|
||||||
|
<a href="{% url "Palto:logout" %}">Logout</a>
|
||||||
|
</nav>
|
76
Palto/Palto/templates/Palto/profile.html
Normal file
76
Palto/Palto/templates/Palto/profile.html
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{% extends "Palto/base.html" %}
|
||||||
|
{% load dict_tags %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{{ profile.username }}
|
||||||
|
{{ profile.email }}
|
||||||
|
{% if profile.is_superuser %}Administrator{% endif %}
|
||||||
|
|
||||||
|
{# user related departments table #}
|
||||||
|
<table>
|
||||||
|
{% for department, profile_department_data in profile_departments_data.items %}
|
||||||
|
<tr>
|
||||||
|
{# department name #}
|
||||||
|
<th>{{ department.name }}</th>
|
||||||
|
{# relation information #}
|
||||||
|
<td>
|
||||||
|
<table>
|
||||||
|
{# user managing the department #}
|
||||||
|
{% if profile_department_data|dict_get:"is_manager" %}
|
||||||
|
<tr>
|
||||||
|
<td>Responsable de Département</td>
|
||||||
|
<td>/</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{# user managing units #}
|
||||||
|
{% with managing_units=profile_department_data|dict_get:"managing_units" %}
|
||||||
|
{% if managing_units|length > 0 %}
|
||||||
|
<tr>
|
||||||
|
<td>Responsable d'UE</td>
|
||||||
|
<td>
|
||||||
|
{% for managing_unit in managing_units %}
|
||||||
|
<a href="{% url "Palto:teaching_unit_view" managing_unit.id %}">
|
||||||
|
{{ managing_unit.name }}
|
||||||
|
</a>
|
||||||
|
{% if not forloop.last %}<br/>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{# user teaching units #}
|
||||||
|
{% with teaching_units=profile_department_data|dict_get:"teaching_units" %}
|
||||||
|
{% if teaching_units|length > 0 %}
|
||||||
|
<tr>
|
||||||
|
<td>Enseignant</td>
|
||||||
|
<td>
|
||||||
|
{% for teaching_unit in teaching_units %}
|
||||||
|
<a href="{% url "Palto:teaching_unit_view" teaching_unit.id %}">
|
||||||
|
{{ teaching_unit.name }}
|
||||||
|
</a>
|
||||||
|
{% if not forloop.last %}<br/>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{# user studying groups #}
|
||||||
|
{% with student_groups=profile_department_data|dict_get:"student_groups" %}
|
||||||
|
{% if student_groups|length > 0 %}
|
||||||
|
<tr>
|
||||||
|
<td>Groupe Étudiant</td>
|
||||||
|
<td>
|
||||||
|
{% for student_group in student_groups %}
|
||||||
|
<a href="{# TODO(Faraphel): redirect to group #}">{{ student_group.name }}</a>
|
||||||
|
{% if not forloop.last %}<br/>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
41
Palto/Palto/templates/Palto/teaching_session_list.html
Normal file
41
Palto/Palto/templates/Palto/teaching_session_list.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends "Palto/base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{# table of all the sessions #}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Identifiant</th>
|
||||||
|
<th>UE</th>
|
||||||
|
<th>Horaire</th>
|
||||||
|
<th>Enseignant</th>
|
||||||
|
<th>Effectif</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{# show the information for every session #}
|
||||||
|
{% for session in sessions %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url "Palto:teaching_session_view" session.id %}">{{ session.short_id }}</a></td>
|
||||||
|
<td><a href="{% url "Palto:teaching_unit_view" session.unit.id %}">{{ session.unit.name }}</a></td>
|
||||||
|
<td>{{ session.start }}<br>{{ session.end }}</td>
|
||||||
|
<td><a href="{% url "Palto:profile" session.teacher.id %}">{{ session.teacher }}</a></td>
|
||||||
|
<td>{{ session.attendances.all|length }} / {{ session.group.students.all|length }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{# page navigator #}
|
||||||
|
<div>
|
||||||
|
{% if sessions.has_previous %}
|
||||||
|
<a href="?page={{ sessions.previous_page_number }}">Previous</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a>{{ sessions.number }}</a>
|
||||||
|
|
||||||
|
{% if sessions.has_next %}
|
||||||
|
<a href="?page={{ sessions.next_page_number }}">Next</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
66
Palto/Palto/templates/Palto/teaching_session_view.html
Normal file
66
Palto/Palto/templates/Palto/teaching_session_view.html
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
{% extends "Palto/base.html" %}
|
||||||
|
{% load dict_tags %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{# session's information #}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Identifiant</th>
|
||||||
|
<td>{{ session.id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Début</th>
|
||||||
|
<td>{{ session.start }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Durée</th>
|
||||||
|
<td>{{ session.duration }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Unité d'Enseignement</th>
|
||||||
|
<td><a href="{% url "Palto:teaching_unit_view" session.unit.id %}">{{ session.unit }}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Enseignant</th>
|
||||||
|
<td><a href="{% url "Palto:profile" session.teacher.id %}">{{ session.teacher }}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Groupe</th>
|
||||||
|
<td>{{ session.group }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{# session's students information #}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Elève</th>
|
||||||
|
<th>Présence</th>
|
||||||
|
<th>Absence</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for student, session_student_data in session_students_data.items %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url "Palto:profile" student.id %}">{{ student }}</a></td>
|
||||||
|
<td>
|
||||||
|
{% with attendance=session_student_data|dict_get:"attendance" %}
|
||||||
|
{% if attendance != None %}
|
||||||
|
{{ attendance.date }}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% with absence=session_student_data|dict_get:"absence" %}
|
||||||
|
{% if absence != None %}
|
||||||
|
<a href="{% url "Palto:absence_view" absence.id %}">Détails</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{# TODO(Raphaël): export boutton #}
|
||||||
|
{% endblock %}
|
60
Palto/Palto/templates/Palto/teaching_unit_view.html
Normal file
60
Palto/Palto/templates/Palto/teaching_unit_view.html
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{% extends "Palto/base.html" %}
|
||||||
|
{% load dict_tags %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{# unit's information #}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Identifiant</th>
|
||||||
|
<td>{{ unit.id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Nom</th>
|
||||||
|
<td>{{ unit.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Département</th>
|
||||||
|
<td href="{# TODO(Faraphel): department url #}">{{ unit.department.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Mail</th>
|
||||||
|
<td>{% if unit.email != None %}{{ unit.email }}{% else %} / {% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Sessions</th>
|
||||||
|
<td>{{ unit.sessions.all|length }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{# unit's managers #}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Responsables</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for manager in unit.managers.all %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url "Palto:profile" manager.id %}">{{ manager }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{# unit's teachers #}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Enseignants</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for teacher in unit.teachers.all %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url "Palto:profile" teacher.id %}">{{ teacher }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
0
Palto/Palto/templatetags/__init__.py
Normal file
0
Palto/Palto/templatetags/__init__.py
Normal file
6
Palto/Palto/templatetags/dict_tags.py
Normal file
6
Palto/Palto/templatetags/dict_tags.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.template.defaulttags import register
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def dict_get(d: dict, key: str):
|
||||||
|
return d.get(key)
|
32
Palto/Palto/urls.py
Normal file
32
Palto/Palto/urls.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
"""
|
||||||
|
Urls for the Palto project's API.
|
||||||
|
|
||||||
|
This file list all the urls for the Palto API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
from Palto.Palto import views
|
||||||
|
|
||||||
|
app_name = "Palto"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Base
|
||||||
|
path("", views.homepage_view, name="homepage"),
|
||||||
|
|
||||||
|
# User
|
||||||
|
path("login/", views.login_view, name="login"),
|
||||||
|
path("logout/", views.logout_view, name="logout"),
|
||||||
|
path("profile/", views.profile_view, name="my_profile"),
|
||||||
|
path("profile/<uuid:profile_id>/", views.profile_view, name="profile"),
|
||||||
|
|
||||||
|
# Units
|
||||||
|
path("teaching_units/view/<uuid:unit_id>/", views.teaching_unit_view, name="teaching_unit_view"),
|
||||||
|
|
||||||
|
# Sessions
|
||||||
|
path("teaching_sessions/", views.teaching_session_list_view, name="teaching_session_list"),
|
||||||
|
path("teaching_sessions/view/<uuid:session_id>/", views.teaching_session_view, name="teaching_session_view"),
|
||||||
|
|
||||||
|
# Absences
|
||||||
|
path("absences/view/<uuid:absence_id>/", views.absence_view, name="absence_view"),
|
||||||
|
path("absences/new/", views.new_absence_view, name="absence_new"),
|
||||||
|
]
|
15
Palto/Palto/utils.py
Normal file
15
Palto/Palto/utils.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db.models import Model, Manager
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_or_none(manager: Manager, *args, **kwargs) -> Optional[Model]:
|
||||||
|
"""
|
||||||
|
Similar to the Manager.get method, but return None instead of raising an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return manager.get(*args, **kwargs)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return None
|
|
@ -3,7 +3,236 @@ 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.
|
A view is what control the content of a page, prepare the correct data, react to a form, render the correct template.
|
||||||
"""
|
"""
|
||||||
|
import uuid
|
||||||
|
from django.contrib.auth import login, authenticate, logout
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.http import HttpResponseForbidden
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
|
||||||
|
from Palto.Palto import models, forms
|
||||||
|
from Palto.Palto.utils import get_object_or_none
|
||||||
|
|
||||||
|
ELEMENT_PER_PAGE: int = 30
|
||||||
|
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
def homepage_view(request: WSGIRequest):
|
||||||
|
return render(request, "Palto/homepage.html")
|
||||||
|
|
||||||
|
|
||||||
|
def login_view(request: WSGIRequest):
|
||||||
|
# create a login form
|
||||||
|
form_login = forms.LoginForm(request.POST)
|
||||||
|
|
||||||
|
if form_login.is_valid():
|
||||||
|
# try to authenticate this user with the credentials
|
||||||
|
user = authenticate(
|
||||||
|
username=form_login.cleaned_data["username"],
|
||||||
|
password=form_login.cleaned_data["password"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
# if the user was authenticated, log the user in.
|
||||||
|
login(request, user)
|
||||||
|
# redirect him to the main page
|
||||||
|
return redirect("Palto:homepage")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# otherwise the credentials were invalid.
|
||||||
|
form_login.add_error(field=None, error="Invalid credentials.")
|
||||||
|
|
||||||
|
# return the page
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"Palto/login.html",
|
||||||
|
context=dict(
|
||||||
|
form_login=form_login
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def logout_view(request: WSGIRequest):
|
||||||
|
# disconnect the user from the website
|
||||||
|
logout(request)
|
||||||
|
# redirect him to the main page
|
||||||
|
return redirect("Palto:homepage")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile_view(request: WSGIRequest, profile_id: uuid.UUID = None):
|
||||||
|
if profile_id is None:
|
||||||
|
# if the profile id is not given, redirect to the page of the current user.
|
||||||
|
return redirect("Palto:profile", request.user.id)
|
||||||
|
|
||||||
|
# get the corresponding user from its id.
|
||||||
|
profile = get_object_or_404(models.User, id=profile_id)
|
||||||
|
|
||||||
|
# check if the user is allowed to see this specific object
|
||||||
|
if profile not in models.User.all_visible_by_user(request.user):
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
# prepare the data and the "complex" query for the template
|
||||||
|
profile_departments_data = {
|
||||||
|
department: {
|
||||||
|
"is_manager": profile in department.managers.all(),
|
||||||
|
"managing_units": models.TeachingUnit.objects.filter(department=department, managers=profile).all(),
|
||||||
|
"teaching_units": models.TeachingUnit.objects.filter(department=department, teachers=profile).all(),
|
||||||
|
"student_groups": models.StudentGroup.objects.filter(department=department, students=profile).all(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for department in profile.related_departments
|
||||||
|
}
|
||||||
|
|
||||||
|
# render the page
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"Palto/profile.html",
|
||||||
|
context=dict(
|
||||||
|
profile=profile,
|
||||||
|
profile_departments_data=profile_departments_data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def teaching_session_list_view(request: WSGIRequest):
|
||||||
|
# get all the sessions that the user can see, sorted by starting date
|
||||||
|
raw_sessions = models.TeachingSession.all_visible_by_user(request.user).order_by("start")
|
||||||
|
# paginate them to avoid having too many elements at the same time
|
||||||
|
paginator = Paginator(raw_sessions, ELEMENT_PER_PAGE)
|
||||||
|
|
||||||
|
# get only the session for the requested page
|
||||||
|
page = request.GET.get("page", 0)
|
||||||
|
sessions = paginator.get_page(page)
|
||||||
|
|
||||||
|
# render the page
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"Palto/teaching_session_list.html",
|
||||||
|
context=dict(
|
||||||
|
sessions=sessions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def teaching_unit_view(request: WSGIRequest, unit_id: uuid.UUID):
|
||||||
|
unit = get_object_or_404(models.TeachingUnit, id=unit_id)
|
||||||
|
|
||||||
|
# check if the user is allowed to see this specific object
|
||||||
|
if unit not in models.TeachingUnit.all_visible_by_user(request.user):
|
||||||
|
# TODO(Faraphel): syntaxic sugar session.visible_by_user(request.user)
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
# render the page
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"Palto/teaching_unit_view.html",
|
||||||
|
context=dict(
|
||||||
|
unit=unit,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def teaching_session_view(request: WSGIRequest, session_id: uuid.UUID):
|
||||||
|
session = get_object_or_404(models.TeachingSession, id=session_id)
|
||||||
|
|
||||||
|
# check if the user is allowed to see this specific object
|
||||||
|
if session not in models.TeachingSession.all_visible_by_user(request.user):
|
||||||
|
# TODO(Faraphel): syntaxic sugar session.visible_by_user(request.user)
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
# prepare the data and the "complex" query for the template
|
||||||
|
session_students_data = {
|
||||||
|
student: {
|
||||||
|
"attendance": get_object_or_none(
|
||||||
|
models.Attendance.objects,
|
||||||
|
session=session,
|
||||||
|
student=student
|
||||||
|
),
|
||||||
|
"absence": get_object_or_none(
|
||||||
|
models.Absence.objects,
|
||||||
|
student=student,
|
||||||
|
start__lte=session.start, end__gte=session.end
|
||||||
|
), # TODO(Faraphel): property ?
|
||||||
|
}
|
||||||
|
|
||||||
|
for student in session.group.students.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
# render the page
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"Palto/teaching_session_view.html",
|
||||||
|
context=dict(
|
||||||
|
session=session,
|
||||||
|
session_students_data=session_students_data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def absence_view(request: WSGIRequest, absence_id: uuid.UUID):
|
||||||
|
absence = get_object_or_404(models.Absence, id=absence_id)
|
||||||
|
|
||||||
|
# check if the user is allowed to see this specific object
|
||||||
|
if absence not in models.Absence.all_visible_by_user(request.user):
|
||||||
|
# TODO(Faraphel): syntaxic sugar session.visible_by_user(request.user)
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
# render the page
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"Palto/absence_view.html",
|
||||||
|
context=dict(
|
||||||
|
absence=absence,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def new_absence_view(request: WSGIRequest):
|
||||||
|
# check if the user can create an absence
|
||||||
|
if not models.Absence.can_user_create(request.user):
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
# create a form for the new absence
|
||||||
|
form_new_absence = forms.NewAbsenceForm(request.user, request.POST, request.FILES)
|
||||||
|
|
||||||
|
if form_new_absence.is_valid():
|
||||||
|
print(form_new_absence.files, form_new_absence.cleaned_data)
|
||||||
|
|
||||||
|
absence, is_created = models.Absence.objects.get_or_create(
|
||||||
|
student=request.user,
|
||||||
|
start=form_new_absence.cleaned_data["start"],
|
||||||
|
end=form_new_absence.cleaned_data["end"],
|
||||||
|
department=form_new_absence.cleaned_data["department"],
|
||||||
|
message=form_new_absence.cleaned_data["message"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_created:
|
||||||
|
# if the absence already existed, show an error
|
||||||
|
form_new_absence.add_error(None, "This absence already exists.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# add the attachments files to the absence
|
||||||
|
for file in form_new_absence.cleaned_data["attachments"]:
|
||||||
|
absence.attachments.create(
|
||||||
|
content=file
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect("Palto:homepage") # TODO(Faraphel): redirect to absence list
|
||||||
|
|
||||||
|
# render the page
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"Palto/absence_new.html",
|
||||||
|
context=dict(
|
||||||
|
form_new_absence=form_new_absence,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -191,7 +191,7 @@ AUTH_USER_MODEL = "Palto.User"
|
||||||
|
|
||||||
|
|
||||||
# CORS settings
|
# CORS settings
|
||||||
# TODO(Raphaël): Only in debug !
|
# TODO(Faraphel): Only in debug !
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,16 +19,17 @@ from django.contrib import admin
|
||||||
from django.urls import path, re_path, include
|
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
|
||||||
|
from Palto.Palto import urls as palto_views_urls
|
||||||
|
from Palto.Palto.api import urls as palto_api_urls
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Application
|
# Application
|
||||||
# ...
|
path('', include(palto_views_urls)),
|
||||||
|
|
||||||
# API
|
# API
|
||||||
path('api/', include(api_urls)), # Api REST
|
path('api/', include(palto_api_urls)), # Api REST
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
path('admin/', admin.site.urls), # Admin page
|
path('admin/', admin.site.urls), # Admin page
|
||||||
|
|
Loading…
Reference in a new issue