detailled profile, session and session list pages

This commit is contained in:
Faraphel 2023-12-13 23:12:51 +01:00
parent 82591b27a2
commit 784d4d4001
8 changed files with 252 additions and 54 deletions

View file

@ -61,11 +61,15 @@ class User(AbstractUser, ModelPermissionHelper):
id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36)
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
def multiple_related_departments(users: Iterable["User"]) -> QuerySet["Department"]:
"""
@ -141,11 +145,15 @@ class Department(models.Model, ModelPermissionHelper):
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}>"
return f"<{self.__class__.__name__} id={self.short_id} name={self.name!r}>"
def __str__(self):
return self.name
@property
def short_id(self) -> str:
return str(self.id)[:8]
@staticmethod
def multiple_related_users(departments: Iterable["Department"]) -> QuerySet["User"]:
"""
@ -212,22 +220,26 @@ class StudentGroup(models.Model, ModelPermissionHelper):
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}>"
return f"<{self.__class__.__name__} id={self.short_id} name={self.name!r}>"
def __str__(self):
return self.name
@property
def short_id(self) -> str:
return str(self.id)[:8]
# validators
def clean(self):
super().clean()
# 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.")
# 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.")
# permissions
@ -254,9 +266,9 @@ class StudentGroup(models.Model, ModelPermissionHelper):
return {
# 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
"owner": lambda data: data["department"].managers | data["department"].teachers,
"owner": lambda data: (data["department"].managers | data["department"].teachers).all(),
}
@classmethod
@ -314,26 +326,30 @@ class TeachingUnit(models.Model, ModelPermissionHelper):
student_groups = models.ManyToManyField(to=StudentGroup, blank=True, related_name="studying_units")
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):
return self.name
@property
def short_id(self) -> str:
return str(self.id)[:8]
# validations
def clean(self):
super().clean()
# 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.")
# 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.")
# 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.")
# permissions
@ -356,7 +372,7 @@ class TeachingUnit(models.Model, ModelPermissionHelper):
return {
# 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
@ -406,7 +422,11 @@ class StudentCard(models.Model, ModelPermissionHelper):
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}>"
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
@ -414,7 +434,7 @@ class StudentCard(models.Model, ModelPermissionHelper):
super().clean()
# 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.")
# permissions
@ -436,7 +456,7 @@ class StudentCard(models.Model, ModelPermissionHelper):
return {
# 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
@ -488,11 +508,15 @@ class TeachingSession(models.Model, ModelPermissionHelper):
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}>"
return f"<{self.__class__.__name__} id={self.short_id} unit={self.unit.name!r} start={self.start}>"
def __str__(self):
return f"{self.unit.name} ({self.start})"
@property
def short_id(self) -> str:
return str(self.id)[:8]
@property
def end(self) -> datetime:
return self.start + self.duration
@ -502,12 +526,12 @@ class TeachingSession(models.Model, ModelPermissionHelper):
def clean(self):
super().clean()
# group check
if self.unit.department not in self.group.department:
# department check
if self.unit.department != 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:
if self.unit not in self.teacher.teaching_units.all():
raise ValidationError("The teacher is not related to the unit.")
# permissions
@ -545,7 +569,7 @@ class TeachingSession(models.Model, ModelPermissionHelper):
user.teaching_units |
# all the units of the department the user is managing
TeachingUnit.objects.filter(pk__in=user.managing_departments.values("teaching_units"))
)
).all()
}
@classmethod
@ -609,19 +633,23 @@ class Attendance(models.Model, ModelPermissionHelper):
def __repr__(self):
return (
f"<{self.__class__.__name__} "
f"id={str(self.id)[:8]} "
f"id={self.short_id} "
f"student={self.student.username} "
f"session={str(self.session.id)[:8]}"
f"session={self.session.short_id}"
f">"
)
@property
def short_id(self) -> str:
return str(self.id)[:8]
# validations
def clean(self):
super().clean()
# 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.")
# permissions
@ -662,7 +690,7 @@ class Attendance(models.Model, ModelPermissionHelper):
pk__in=user.managing_departments.values("teaching_units")
).values("sessions")
)
)
).all()
}
@classmethod
@ -721,7 +749,7 @@ class Absence(models.Model, ModelPermissionHelper):
def __repr__(self):
return (
f"<{self.__class__.__name__} "
f"id={str(self.id)[:8]} "
f"id={self.short_id} "
f"department={self.department} "
f"student={self.student.username} "
f"start={self.start} "
@ -730,7 +758,11 @@ class Absence(models.Model, ModelPermissionHelper):
)
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
@ -738,7 +770,7 @@ class Absence(models.Model, ModelPermissionHelper):
super().clean()
# 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.")
# properties
@ -775,7 +807,7 @@ class Absence(models.Model, ModelPermissionHelper):
return {
# all the departments the user is studying in
"department": lambda data: user.studying_departments,
"department": lambda data: user.studying_departments.all(),
}
@classmethod
@ -831,7 +863,11 @@ class AbsenceAttachment(models.Model, ModelPermissionHelper):
absence = models.ForeignKey(to=Absence, on_delete=models.CASCADE, related_name="attachments")
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
@ -853,7 +889,7 @@ class AbsenceAttachment(models.Model, ModelPermissionHelper):
return {
# all the departments the user is studying in
"absence": lambda data: user.absences,
"absence": lambda data: user.absences.all(),
}
@classmethod

View file

@ -1,9 +1,74 @@
{% extends "Palto/base.html" %}
{% load dict_tags %}
{% block body %}
<div>
{{ 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="{# TODO(Faraphel): redirect to unit #}">{{ 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="{# TODO(Faraphel): redirect to unit #}">{{ 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>
</div>
{% endblock %}

View file

@ -1,27 +1,66 @@
{% extends "Palto/base.html" %}
{% load dict_tags %}
{% block body %}
{# table of all the sessions #}
{# 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>{{ session.unit }}</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>Identifiant</th>
<th>Début</th>
<th>Durée</th>
<th>Unité d'Enseignement</th>
<th>Enseignant</th>
<th>Groupe</th>
<th>Elève</th>
<th>Présence</th>
<th>Absence</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ session.id }}</td>
<td>{{ session.start }}</td>
<td>{{ session.duration }}</td>
<td>{{ session.unit }}</td>
<td>{{ session.teacher }}</td>
<td>{{ session.group }}</td>
</tr>
{% 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:"attendance" %}
{% if absence != None %}
<a href="{# TODO(Raphaël): when absence page added #}">...</a>
{% endif %}
{% endwith %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# TODO(Raphaël): export boutton #}
{% endblock %}

View file

@ -6,16 +6,21 @@
<thead>
<tr>
<th>Identifiant</th>
<th>Début</th>
<th>Durée</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" session.id %}">{{ session.id }}</a></td>
<td>{{ session.start }}</td>
<td>{{ session.duration }}</td>
<td><a href="{% url "Palto:teaching_session" session.id %}">{{ session.short_id }}</a></td>
<td><a href="{# TODO(Faraphel): add when teaching units #}">{{ 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>

View file

View file

@ -0,0 +1,6 @@
from django.template.defaulttags import register
@register.filter
def dict_get(d: dict, key: str):
return d.get(key)

15
Palto/Palto/utils.py Normal file
View 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

View file

@ -13,7 +13,7 @@ from django.shortcuts import render, get_object_or_404, redirect
from Palto.Palto import models
from Palto.Palto.forms import LoginForm
from Palto.Palto.utils import get_object_or_none
ELEMENT_PER_PAGE: int = 30
@ -71,12 +71,25 @@ def profile_view(request: WSGIRequest, profile_id: uuid.UUID = None):
# get the corresponding user from its id.
profile = get_object_or_404(models.User, id=profile_id)
# 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=profile,
profile_departments_data=profile_departments_data,
)
)
@ -110,11 +123,30 @@ def teaching_session_view(request: WSGIRequest, session_id: uuid.UUID):
# TODO: 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__gte=session.start, end__lte=session.end
),
}
for student in session.group.students.all()
}
# render the page
return render(
request,
"Palto/teaching_session.html",
context=dict(
session=session
session=session,
session_students_data=session_students_data,
)
)
)