Merge pull request #18 from Faraphel/base-pages

Pages de Base
This commit is contained in:
biloute02 2023-12-17 14:35:51 +01:00 committed by GitHub
commit 07812d21fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 754 additions and 112 deletions

8
.idea/.gitignore vendored
View file

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

View file

@ -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="&lt;map/&gt;" />
<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>

View file

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View file

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

View file

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

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View 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 %}

View 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 %}

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

View file

@ -0,0 +1,5 @@
{% extends "Palto/base.html" %}
{% block body %}
Hello there.
{% endblock %}

View 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 %}

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

View 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 %}

View 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 %}

View 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 %}

View 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 %}

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)

32
Palto/Palto/urls.py Normal file
View 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
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

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

View file

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

View file

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