added additional pages (department, absence list, student groups)

This commit is contained in:
Faraphel 2023-12-17 00:20:54 +01:00
parent c3c4623931
commit 2f2332766e
13 changed files with 255 additions and 32 deletions

View file

@ -24,38 +24,45 @@ class AdminUser(admin.ModelAdmin):
class AdminDepartment(admin.ModelAdmin): class AdminDepartment(admin.ModelAdmin):
list_display = ("id", "name", "email") list_display = ("id", "name", "email")
search_fields = ("id", "name", "email") search_fields = ("id", "name", "email")
readonly_fields = ("id",)
@admin.register(models.StudentGroup) @admin.register(models.StudentGroup)
class AdminStudentGroup(admin.ModelAdmin): class AdminStudentGroup(admin.ModelAdmin):
list_display = ("id", "name", "owner", "department") list_display = ("id", "name", "owner", "department")
search_fields = ("id", "name", "owner", "department") search_fields = ("id", "name", "owner", "department")
list_filter = ("department",)
readonly_fields = ("id",)
@admin.register(models.TeachingUnit) @admin.register(models.TeachingUnit)
class AdminTeachingUnit(admin.ModelAdmin): class AdminTeachingUnit(admin.ModelAdmin):
list_display = ("id", "name", "email") list_display = ("id", "name", "email")
search_fields = ("id", "name", "email") search_fields = ("id", "name", "email")
readonly_fields = ("id",)
@admin.register(models.StudentCard) @admin.register(models.StudentCard)
class AdminStudentCard(admin.ModelAdmin): class AdminStudentCard(admin.ModelAdmin):
list_display = ("id", "uid", "owner") list_display = ("id", "uid", "department", "owner")
search_fields = ("id", "uid", "owner") search_fields = ("id", "uid", "department", "owner")
readonly_fields = ("uid",) readonly_fields = ("id", "uid",)
list_filter = ("department",)
@admin.register(models.TeachingSession) @admin.register(models.TeachingSession)
class AdminTeachingSession(admin.ModelAdmin): class AdminTeachingSession(admin.ModelAdmin):
list_display = ("id", "start", "end", "duration", "teacher") list_display = ("id", "start", "end", "unit", "duration", "teacher")
search_fields = ("id", "start", "end", "duration", "teacher") search_fields = ("id", "start", "end", "unit", "duration", "teacher")
list_filter = ("start", "duration") readonly_fields = ("id",)
list_filter = ("unit",)
@admin.register(models.Attendance) @admin.register(models.Attendance)
class AdminAttendance(admin.ModelAdmin): class AdminAttendance(admin.ModelAdmin):
list_display = ("id", "date", "student") list_display = ("id", "date", "student")
search_fields = ("id", "date", "student") search_fields = ("id", "date", "student")
readonly_fields = ("id",)
list_filter = ("date",) list_filter = ("date",)
@ -63,6 +70,7 @@ class AdminAttendance(admin.ModelAdmin):
class AdminAbsence(admin.ModelAdmin): class AdminAbsence(admin.ModelAdmin):
list_display = ("id", "message", "student", "start", "end") list_display = ("id", "message", "student", "start", "end")
search_fields = ("id", "message", "student", "start", "end") search_fields = ("id", "message", "student", "start", "end")
readonly_fields = ("id",)
list_filter = ("start", "end") list_filter = ("start", "end")
@ -70,3 +78,4 @@ class AdminAbsence(admin.ModelAdmin):
class AdminAbsenceAttachment(admin.ModelAdmin): class AdminAbsenceAttachment(admin.ModelAdmin):
list_display = ("id", "content", "absence") list_display = ("id", "content", "absence")
search_fields = ("id", "content", "absence") search_fields = ("id", "content", "absence")
readonly_fields = ("id",)

View file

@ -39,17 +39,31 @@ class ModelPermissionHelper:
@classmethod @classmethod
@abstractmethod @abstractmethod
def all_editable_by_user(cls, user: "User") -> QuerySet: def all_editable_by_user(cls, user: "User") -> QuerySet:
"""
Return the list of object that the user can edit
"""
def is_editable_by_user(self, user: "User") -> bool:
""" """
Return True if the user can edit this object Return True if the user can edit this object
""" """
return self in self.all_editable_by_user(user)
@classmethod @classmethod
@abstractmethod @abstractmethod
def all_visible_by_user(cls, user: "User") -> QuerySet: def all_visible_by_user(cls, user: "User") -> QuerySet:
"""
Return the list of object that the user can see
"""
def is_visible_by_user(self, user: "User") -> bool:
""" """
Return True if the user can see this object Return True if the user can see this object
""" """
return self in self.all_visible_by_user(user)
class User(AbstractUser, ModelPermissionHelper): class User(AbstractUser, ModelPermissionHelper):
""" """
@ -522,6 +536,17 @@ class TeachingSession(models.Model, ModelPermissionHelper):
def end(self) -> datetime: def end(self) -> datetime:
return self.start + self.duration return self.start + self.duration
@property
def related_absences(self) -> QuerySet["Absence"]:
"""
Return the sessions that match the user absence
"""
return Absence.objects.filter(
student__in=self.group.students,
start__lte=self.start, end__gte=self.end
).distinct()
# validations # validations
def clean(self): def clean(self):

View file

@ -0,0 +1,42 @@
{% extends "Palto/base.html" %}
{% block body %}
{# table of all the absences #}
<table>
<thead>
<tr>
<th>Identifiant</th>
<th>Département</th>
<th>Étudiant</th>
<th>Période</th>
</tr>
</thead>
<tbody>
{# show the information for every session #}
{% for absence in absences %}
<tr>
<td><a href="{% url "Palto:absence_view" absence.id %}">{{ absence.short_id }}</a></td>
<td><a href="{% url "Palto:department_view" absence.department.id %}">{{ absence.department }}</a></td>
<td><a href="{% url "Palto:profile" absence.student.id %}">{{ absence.student }}</a></td>
<td>{{ absence.start }}<br>{{ absence.end }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{# page navigator #}
{# TODO(Faraphel): page navigator as template ? #}
{# TODO(Faraphel): new absence button #}
<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 %}

View file

@ -9,7 +9,7 @@
</tr> </tr>
<tr> <tr>
<th>Département</th> <th>Département</th>
<td><a href="{# TODO(Faraphel): departement #}">{{ absence.department }}</a></td> <td><a href="{% url "Palto:department_view" absence.department.id %}">{{ absence.department }}</a></td>
</tr> </tr>
<tr> <tr>
<th>Étudiant</th> <th>Étudiant</th>

View file

@ -0,0 +1,53 @@
{% extends "Palto/base.html" %}
{% load dict_tags %}
{% block body %}
{# department's information #}
<table>
<tr>
<th>Identifiant</th>
<td>{{ department.id }}</td>
</tr>
<tr>
<th>Nom</th>
<td>{{ department.name }}</td>
</tr>
<tr>
<th>Mail</th>
<td>{% if department.email != None %}{{ department.email }}{% else %} / {% endif %}</td>
</tr>
</table>
{# department's managers #}
<table>
<thead>
<tr>
<th>Responsables</th>
</tr>
</thead>
<tbody>
{% for manager in department.managers.all %}
<tr>
<td><a href="{% url "Palto:profile" manager.id %}">{{ manager }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{# department's teachers #}
<table>
<thead>
<tr>
<th>Enseignants</th>
</tr>
</thead>
<tbody>
{% for teacher in department.teachers.all %}
<tr>
<td><a href="{% url "Palto:profile" teacher.id %}">{{ teacher }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -1,5 +1,9 @@
{% extends "Palto/base.html" %} {% extends "Palto/base.html" %}
{% block body %} {% block body %}
Hello there. <h1>Palto</h1>
<p>
Palto est un outil de gestion des présences d'élèves dans vos établissements scolaires.
</p>
{% endblock %} {% endblock %}

View file

@ -11,7 +11,7 @@
{% for department, profile_department_data in profile_departments_data.items %} {% for department, profile_department_data in profile_departments_data.items %}
<tr> <tr>
{# department name #} {# department name #}
<th>{{ department.name }}</th> <th><a href="{% url "Palto:department_view" department.id %}">{{ department.name }}</a></th>
{# relation information #} {# relation information #}
<td> <td>
<table> <table>
@ -61,7 +61,7 @@
<td>Groupe Étudiant</td> <td>Groupe Étudiant</td>
<td> <td>
{% for student_group in student_groups %} {% for student_group in student_groups %}
<a href="{# TODO(Faraphel): redirect to group #}">{{ student_group.name }}</a> <a href="{% url "Palto:student_group_view" student_group.id %}">{{ student_group.name }}</a>
{% if not forloop.last %}<br/>{% endif %} {% if not forloop.last %}<br/>{% endif %}
{% endfor %} {% endfor %}
</td> </td>

View file

@ -0,0 +1,39 @@
{% extends "Palto/base.html" %}
{% block body %}
{# group's information #}
<table>
<tr>
<th>Identifiant</th>
<td>{{ group.id }}</td>
</tr>
<tr>
<th>Nom</th>
<td>{{ group.name }}</td>
</tr>
<tr>
<th>Département</th>
<td><a href="{% url "Palto:department_view" group.department.id %}">{{ group.department }}</a></td>
</tr>
<tr>
<th>Propriétaire</th>
<td><a href="{% url "Palto:profile" group.owner.id %}">{{ group.owner }}</a></td>
</tr>
</table>
{# group's students information #}
<table>
<thead>
<tr>
<th>Étudiants</th>
</tr>
</thead>
<tbody>
{% for student in group.students.all %}
<tr>
<td><a href="{% url "Palto:profile" student.id %}">{{ student }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -26,7 +26,7 @@
</tr> </tr>
<tr> <tr>
<th>Groupe</th> <th>Groupe</th>
<td>{{ session.group }}</td> <td><a href="{% url "Palto:student_group_view" session.group.id %}">{{ session.group }}</a></td>
</tr> </tr>
</table> </table>

View file

@ -14,7 +14,7 @@
</tr> </tr>
<tr> <tr>
<th>Département</th> <th>Département</th>
<td href="{# TODO(Faraphel): department url #}">{{ unit.department.name }}</td> <td href="{% url "Palto:department_view" unit.department.id %}">{{ unit.department.name }}</td>
</tr> </tr>
<tr> <tr>
<th>Mail</th> <th>Mail</th>

View file

@ -19,6 +19,12 @@ urlpatterns = [
path("profile/", views.profile_view, name="my_profile"), path("profile/", views.profile_view, name="my_profile"),
path("profile/<uuid:profile_id>/", views.profile_view, name="profile"), path("profile/<uuid:profile_id>/", views.profile_view, name="profile"),
# Student groups
path("student_groups/view/<uuid:group_id>/", views.student_group_view, name="student_group_view"),
# Departments
path("departments/view/<uuid:department_id>/", views.department_view, name="department_view"),
# Units # Units
path("teaching_units/view/<uuid:unit_id>/", views.teaching_unit_view, name="teaching_unit_view"), path("teaching_units/view/<uuid:unit_id>/", views.teaching_unit_view, name="teaching_unit_view"),
@ -27,6 +33,7 @@ urlpatterns = [
path("teaching_sessions/view/<uuid:session_id>/", views.teaching_session_view, name="teaching_session_view"), path("teaching_sessions/view/<uuid:session_id>/", views.teaching_session_view, name="teaching_session_view"),
# Absences # Absences
path("absences/", views.absence_list_view, name="absence_list"),
path("absences/view/<uuid:absence_id>/", views.absence_view, name="absence_view"), path("absences/view/<uuid:absence_id>/", views.absence_view, name="absence_view"),
path("absences/new/", views.new_absence_view, name="absence_new"), path("absences/new/", views.new_absence_view, name="absence_new"),
] ]

View file

@ -1,10 +1,10 @@
from typing import Optional from typing import Optional
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model, Manager from django.db.models import Model, QuerySet
def get_object_or_none(manager: Manager, *args, **kwargs) -> Optional[Model]: def get_object_or_none(manager: QuerySet, *args, **kwargs) -> Optional[Model]:
""" """
Similar to the Manager.get method, but return None instead of raising an error. Similar to the Manager.get method, but return None instead of raising an error.
""" """

View file

@ -8,7 +8,6 @@ from django.contrib.auth import login, authenticate, logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db import IntegrityError
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
@ -72,7 +71,7 @@ def profile_view(request: WSGIRequest, profile_id: uuid.UUID = None):
profile = get_object_or_404(models.User, id=profile_id) profile = get_object_or_404(models.User, id=profile_id)
# check if the user is allowed to see this specific object # check if the user is allowed to see this specific object
if profile not in models.User.all_visible_by_user(request.user): if not profile.is_visible_by_user(request.user):
return HttpResponseForbidden() return HttpResponseForbidden()
# prepare the data and the "complex" query for the template # prepare the data and the "complex" query for the template
@ -124,8 +123,7 @@ def teaching_unit_view(request: WSGIRequest, unit_id: uuid.UUID):
unit = get_object_or_404(models.TeachingUnit, id=unit_id) unit = get_object_or_404(models.TeachingUnit, id=unit_id)
# check if the user is allowed to see this specific object # check if the user is allowed to see this specific object
if unit not in models.TeachingUnit.all_visible_by_user(request.user): if not unit.is_visible_by_user(request.user):
# TODO(Faraphel): syntaxic sugar session.visible_by_user(request.user)
return HttpResponseForbidden() return HttpResponseForbidden()
# render the page # render the page
@ -143,23 +141,14 @@ def teaching_session_view(request: WSGIRequest, session_id: uuid.UUID):
session = get_object_or_404(models.TeachingSession, id=session_id) session = get_object_or_404(models.TeachingSession, id=session_id)
# check if the user is allowed to see this specific object # check if the user is allowed to see this specific object
if session not in models.TeachingSession.all_visible_by_user(request.user): if not session.is_visible_by_user(request.user):
# TODO(Faraphel): syntaxic sugar session.visible_by_user(request.user)
return HttpResponseForbidden() return HttpResponseForbidden()
# prepare the data and the "complex" query for the template # prepare the data and the "complex" query for the template
session_students_data = { session_students_data = {
student: { student: {
"attendance": get_object_or_none( "attendance": get_object_or_none(models.Attendance.objects, session=session, student=student),
models.Attendance.objects, "absence": get_object_or_none(session.related_absences, student=student)
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() for student in session.group.students.all()
@ -181,8 +170,7 @@ def absence_view(request: WSGIRequest, absence_id: uuid.UUID):
absence = get_object_or_404(models.Absence, id=absence_id) absence = get_object_or_404(models.Absence, id=absence_id)
# check if the user is allowed to see this specific object # check if the user is allowed to see this specific object
if absence not in models.Absence.all_visible_by_user(request.user): if not absence.is_visible_by_user(request.user):
# TODO(Faraphel): syntaxic sugar session.visible_by_user(request.user)
return HttpResponseForbidden() return HttpResponseForbidden()
# render the page # render the page
@ -236,3 +224,59 @@ def new_absence_view(request: WSGIRequest):
form_new_absence=form_new_absence, form_new_absence=form_new_absence,
) )
) )
def absence_list_view(request):
# get all the absences that the user can see, sorted by starting date
raw_absences = models.Absence.all_visible_by_user(request.user).order_by("start")
# paginate them to avoid having too many elements at the same time
paginator = Paginator(raw_absences, ELEMENT_PER_PAGE)
# get only the session for the requested page
page = request.GET.get("page", 0)
absences = paginator.get_page(page)
# render the page
return render(
request,
"Palto/absence_list.html",
context=dict(
absences=absences
)
)
@login_required
def department_view(request: WSGIRequest, department_id: uuid.UUID):
department = get_object_or_404(models.Department, id=department_id)
# check if the user is allowed to see this specific object
if not department.is_visible_by_user(request.user):
return HttpResponseForbidden()
# render the page
return render(
request,
"Palto/department_view.html",
context=dict(
department=department,
)
)
@login_required
def student_group_view(request: WSGIRequest, group_id: uuid.UUID):
group = get_object_or_404(models.StudentGroup, id=group_id)
# check if the user is allowed to see this specific object
if not group.is_visible_by_user(request.user):
return HttpResponseForbidden()
# render the page
return render(
request,
"Palto/student_group.html",
context=dict(
group=group,
)
)