Merge pull request #19 from Faraphel/extra-pages

Added styles and improved some configurations
This commit is contained in:
biloute02 2024-01-17 23:31:34 +01:00 committed by GitHub
commit 5fec4cb691
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1075 additions and 240 deletions

7
.gitignore vendored
View file

@ -1,5 +1,12 @@
# IDE
.idea .idea
# Virtual Environment
venv venv
.venv
# Django
media media
static-collected static-collected
db.sqlite3 db.sqlite3
.env

View file

@ -3,6 +3,9 @@ FROM python:3.12
# Set environment variables for Django # Set environment variables for Django
ENV DJANGO_SETTINGS_MODULE=Palto.settings ENV DJANGO_SETTINGS_MODULE=Palto.settings
ENV DJANGO_SECRET_KEY=""
ENV DATABASE_ENGINE="sqlite"
ENV DEBUG=false
# Set the working directory in the container # Set the working directory in the container
WORKDIR /App WORKDIR /App
@ -10,8 +13,12 @@ WORKDIR /App
# Copy the current directory contents into the container # Copy the current directory contents into the container
COPY . /App COPY . /App
# Install any needed packages specified in requirements.txt # Install requirements
RUN pip install -r requirements.txt RUN python -m pip install -r requirements.txt
# Prepare the server
RUN python manage.py collectstatic --no-input
RUN python manage.py migrate
# Expose the port on which your Django application will run # Expose the port on which your Django application will run
EXPOSE 80 EXPOSE 80

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

@ -0,0 +1,167 @@
# Generated by Django 5.0.1 on 2024-01-15 17:37
import Palto.Palto.models
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Absence',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('message', models.TextField()),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
],
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
),
migrations.CreateModel(
name='Department',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64, unique=True)),
('email', models.EmailField(max_length=254)),
],
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
),
migrations.CreateModel(
name='AbsenceAttachment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('content', models.FileField(upload_to='absence/attachment/')),
('absence', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='Palto.absence')),
],
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
),
migrations.AddField(
model_name='absence',
name='department',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='Palto.department'),
),
migrations.CreateModel(
name='StudentGroup',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128)),
('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_groups', to='Palto.department')),
],
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
),
migrations.CreateModel(
name='User',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='TeachingUnit',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64)),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teaching_units', to='Palto.department')),
('student_groups', models.ManyToManyField(blank=True, related_name='studying_units', to='Palto.studentgroup')),
('managers', models.ManyToManyField(blank=True, related_name='managing_units', to=settings.AUTH_USER_MODEL)),
('teachers', models.ManyToManyField(blank=True, related_name='teaching_units', to=settings.AUTH_USER_MODEL)),
],
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
),
migrations.CreateModel(
name='TeachingSession',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('start', models.DateTimeField()),
('duration', models.DurationField()),
('note', models.TextField(blank=True)),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teaching_sessions', to='Palto.studentgroup')),
('unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to='Palto.teachingunit')),
('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teaching_sessions', to=settings.AUTH_USER_MODEL)),
],
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
),
migrations.AddField(
model_name='studentgroup',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owning_groups', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='studentgroup',
name='students',
field=models.ManyToManyField(blank=True, related_name='student_groups', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='StudentCard',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('uid', models.BinaryField(max_length=7)),
('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_cards', to='Palto.department')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_cards', to=settings.AUTH_USER_MODEL)),
],
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
),
migrations.AddField(
model_name='department',
name='managers',
field=models.ManyToManyField(blank=True, related_name='managing_departments', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='department',
name='students',
field=models.ManyToManyField(blank=True, related_name='studying_departments', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='department',
name='teachers',
field=models.ManyToManyField(blank=True, related_name='teaching_departments', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='Attendance',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('date', models.DateTimeField()),
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attendances', to='Palto.teachingsession')),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attended_sessions', to=settings.AUTH_USER_MODEL)),
],
bases=(models.Model, Palto.Palto.models.ModelPermissionHelper),
),
migrations.AddField(
model_name='absence',
name='student',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='absences', to=settings.AUTH_USER_MODEL),
),
]

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.all(),
start__lte=self.start, end__gte=self.end
).distinct()
# validations # validations
def clean(self): def clean(self):

View file

@ -0,0 +1,47 @@
/* font */
* {
font-family: "Century Gothic", sans-serif;
}
/* color-scheme */
@media (prefers-color-scheme: light) {
:root {
--primary: #00345F;
--secondary: #EF800A;
--foreground: #1B1B1B;
--background: #FFFFFF;
}
}
@media (prefers-color-scheme: dark) {
:root {
--primary: #00549F;
--secondary: #EF800A;
--foreground: #FFFFFF;
--background: #1B1B1B;
}
}
/* links */
*:link, *:visited {
color: var(--primary);
text-decoration: none;
}
*:link:hover, *:visited:hover {
color: var(--secondary);
}
/* table */
table, tr, th, td {
border-color: var(--foreground) !important;
}
/* tweaks */
body {
margin: 0;
color: var(--foreground);
background-color: var(--background);
}

View file

@ -0,0 +1,9 @@
.title {
font-size: 500%;
text-align: center;
}
.text {
width: 60%;
margin: auto;
}

View file

@ -0,0 +1,11 @@
#login-form {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#login-form input {
box-sizing: border-box;
width: 100%;
}

View file

@ -0,0 +1,28 @@
nav {
display: flex;
align-items: center;
margin: 4px 16px;
}
#navigation-header {
display: flex;
margin-right: auto;
align-items: center;
gap: 10px;
}
#navigation-app-name {
font-size: 150%;
}
#navigation-title {
font-size: 300%;
}
#navigation-buttons {
display: flex;
margin-left: auto;
align-items: center;
gap: 10px;
}

View file

@ -0,0 +1,32 @@
#user-informations {
text-align: center;
margin: 5% auto;
}
#user-name {
font-size: 200%;
font-weight: bold;
text-decoration: underline;
}
#user-relations {
border-collapse: collapse;
margin: auto;
text-align: center;
}
#user-relations > tbody > tr > td, #user-relations > tbody > tr > th {
border: 1px solid black;
padding: 4px;
}
.user-relation {
border-style: hidden;
border-collapse: collapse;
width: 100%;
}
.user-relation > tbody > tr > td, .user-relation > tbody > tr > th {
border: 1px solid black;
padding: 4px;
}

View file

@ -0,0 +1,34 @@
#table-informations {
border-collapse: collapse;
border: 2px solid black;
margin: 5% auto;
}
#table-informations th {
text-align: right;
border: 1px solid black;
padding: 4px;
}
#table-informations td {
text-align: left;
border: 1px solid black;
padding: 4px;
}
#table-relations {
display: flex;
width: 60%;
margin: auto;
}
.table-relation {
border-collapse: collapse;
border: 2px solid black;
margin: 0 auto auto auto;
}
.table-relation th, .table-relation td {
border: 2px solid black;
padding: 4px;
}

Binary file not shown.

View file

@ -0,0 +1,52 @@
{% extends "Palto/base/base-features.html" %}
{% block page-title %}
Absences
{% endblock %}
{% block navigation-title %}
Absences
{% endblock %}
{% block body %}
{{ block.super }}
{# 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

@ -1,6 +1,16 @@
{% extends "Palto/base.html" %} {% extends "Palto/base/base-features.html" %}
{% block page-title %}
Nouvelle Absence
{% endblock %}
{% block navigation-title %}
Nouvelle Absence
{% endblock %}
{% block body %} {% block body %}
{{ block.super }}
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<table> <table>

View file

@ -1,6 +1,16 @@
{% extends "Palto/base.html" %} {% extends "Palto/base/base-features.html" %}
{% block page-title %}
Absence
{% endblock %}
{% block navigation-title %}
Absence
{% endblock %}
{% block body %} {% block body %}
{{ block.super }}
{# absence's information #} {# absence's information #}
<table> <table>
<tr> <tr>
@ -9,7 +19,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

@ -1,20 +0,0 @@
{% 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,31 @@
{% extends "Palto/base/base.html" %}
{% load static %}
{% block style %}
{{ block.super }}
<link rel="stylesheet" href="{% static "Palto/css/navigation.css" %}" />
{% endblock %}
{% block body %}
{{ block.super }}
<nav>
<div id="navigation-header">
<img id="navigation-icon" src="{% static "Palto/favicon.svg" %}" alt="Palto's icon" />
<a id="navigation-app-name">Palto</a>
</div>
<div id="navigation-title">
{% block navigation-title %}
{% endblock %}
</div>
<div id="navigation-buttons">
<a href="{% url "Palto:homepage" %}">Accueil</a>
{% if request.user.is_authenticated %}
<a href="{% url "Palto:my_profile" %}">Profil</a>
<a href="{% url "Palto:logout" %}">Déconnexion</a>
{% else %}
<a href="{% url "Palto:login" %}">Connexion</a>
{% endif %}
</div>
</nav>
{% endblock %}

View file

@ -0,0 +1,30 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
{# base meta #}
<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 page-title %}Palto{% endblock %}</title>
{# base style #}
{% block style %}
<style>
{# font #}
@font-face {
font-family: "Century Gothic";
src: url("{% static 'Palto/font/CenturyGothic.ttf' %}");
}
</style>
<link rel="stylesheet" href="{% static "Palto/css/base.css" %}" />
{% endblock %}
</head>
<body>
{# body #}
{% block body %}
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,78 @@
{% extends "Palto/base/base-features.html" %}
{% load dict_tags %}
{% load static %}
{% block page-title %}
Départment {{ department.name }}
{% endblock %}
{% block navigation-title %}
Départment {{ department.name }}
{% endblock %}
{% block style %}
{{ block.super }}
<link rel="stylesheet" href="{% static "Palto/css/table_view.css" %}" />
{% endblock %}
{% block body %}
{{ block.super }}
{# department's information #}
<table id="table-informations">
<tr>
<th>Identifiant</th>
<td>{{ department.id }}</td>
</tr>
<tr>
<th>Nom</th>
<td>{{ department.name }}</td>
</tr>
<tr>
<th>Mail</th>
<td><a href="mailto:{{ department.email }}">{{ department.email }}</a></td>
</tr>
<tr>
<th>Enseignants</th>
<td>{{ department.teachers.count }}</td>
</tr>
<tr>
<th>Étudiants</th>
<td>{{ department.students.count }}</td>
</tr>
</table>
<div id="table-relations">
{# department's managers #}
<table class="table-relation">
<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 class="table-relation">
<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>
</div>
{% endblock %}

View file

@ -1,5 +1,25 @@
{% extends "Palto/base.html" %} {% extends "Palto/base/base-features.html" %}
{% load static %}
{% block page-title %}
Accueil
{% endblock %}
{% block navigation-title %}
Accueil
{% endblock %}
{% block style %}
{{ block.super }}
<link rel="stylesheet" href="{% static "Palto/css/homepage.css" %}" />
{% endblock %}
{% block body %} {% block body %}
Hello there. {{ block.super }}
<h1 class="title">Palto</h1>
<p class="text">
Palto est un outil de gestion des présences d'élèves dans vos établissements scolaires.
</p>
{% endblock %} {% endblock %}

View file

@ -1,10 +1,26 @@
{% extends "Palto/base.html" %} {% extends "Palto/base/base-features.html" %}
{% load static %}
{% block page-title %}
Connexion
{% endblock %}
{% block navigation-title %}
Connexion
{% endblock %}
{% block style %}
{{ block.super }}
<link rel="stylesheet" href="{% static "Palto/css/login.css" %}" />
{% endblock %}
{% block body %} {% block body %}
<form method="POST"> {{ block.super }}
<form id="login-form" method="POST">
{% csrf_token %} {% csrf_token %}
<table> <table>
{{ form_login.as_table }} {{ form_login.as_p }}
</table> </table>
<input type="submit" value="Se connecter"> <input type="submit" value="Se connecter">
</form> </form>

View file

@ -1,11 +0,0 @@
{% 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

@ -1,20 +1,44 @@
{% extends "Palto/base.html" %} {% extends "Palto/base/base-features.html" %}
{% load static %}
{% load dict_tags %} {% load dict_tags %}
{% block page-title %}
Profil
{% endblock %}
{% block navigation-title %}
Profil de {{ profile.first_name|title }} {{ profile.last_name|upper }}
{% endblock %}
{% block style %}
{{ block.super }}
<link rel="stylesheet" href="{% static "Palto/css/profile.css" %}" />
{% endblock %}
{% block body %} {% block body %}
{{ profile.username }} {{ block.super }}
{{ profile.email }}
{% if profile.is_superuser %}Administrator{% endif %} {# user informations #}
<table id="user-informations">
<tbody>
<tr><td id="user-name">{{ profile.first_name|title }} {{ profile.last_name|upper }}</td></tr>
<tr><td id="user-username">{{ profile.username }}</td></tr>
<tr><td id="user-mail"><a href="mailto:{{ profile.email }}">{{ profile.email }}</a></td></tr>
<tr><td id="user-role">{% if profile.is_superuser %}Administrator{% endif %}</td></tr>
</tbody>
</table>
{# user related departments table #} {# user related departments table #}
<table> <table id="user-relations">
<tbody>
{% 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 class="user-relation">
<tbody>
{# user managing the department #} {# user managing the department #}
{% if profile_department_data|dict_get:"is_manager" %} {% if profile_department_data|dict_get:"is_manager" %}
<tr> <tr>
@ -61,16 +85,18 @@
<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>
</tr> </tr>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</tbody>
</table> </table>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table> </table>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "Palto/base/base-features.html" %}
{% load static %}
{% block page-title %}
Groupe Étudiant {{ group.name }}
{% endblock %}
{% block navigation-title %}
Groupe Étudiant {{ group.name }}
{% endblock %}
{% block style %}
{{ block.super }}
<link rel="stylesheet" href="{% static "Palto/css/table_view.css" %}" />
{% endblock %}
{% block body %}
{{ block.super }}
{# group's information #}
<table id="table-informations">
<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>
<div id="table-relations">
{# group's students information #}
<table class="table-relation">
<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>
</div>
{% endblock %}

View file

@ -1,6 +1,15 @@
{% extends "Palto/base.html" %} {% extends "Palto/base/base-features.html" %}
{% block page-title %}
Sessions
{% endblock %}
{% block navigation-title %}
Sessions
{% endblock %}
{% block body %} {% block body %}
{{ block.super }}
{# table of all the sessions #} {# table of all the sessions #}
<table> <table>
<thead> <thead>

View file

@ -1,9 +1,25 @@
{% extends "Palto/base.html" %} {% extends "Palto/base/base-features.html" %}
{% load dict_tags %} {% load dict_tags %}
{% load static %}
{% block page-title %}
Session de {{ session.unit.name }}
{% endblock %}
{% block navigation-title %}
Session de {{ session.unit.name }}
{% endblock %}
{% block style %}
{{ block.super }}
<link rel="stylesheet" href="{% static "Palto/css/table_view.css" %}" />
{% endblock %}
{% block body %} {% block body %}
{{ block.super }}
{# session's information #} {# session's information #}
<table> <table id="table-informations">
<tr> <tr>
<th>Identifiant</th> <th>Identifiant</th>
<td>{{ session.id }}</td> <td>{{ session.id }}</td>
@ -26,12 +42,13 @@
</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>
<div id="table-relations">
{# session's students information #} {# session's students information #}
<table> <table class="table-relation">
<thead> <thead>
<tr> <tr>
<th>Elève</th> <th>Elève</th>
@ -61,6 +78,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{# TODO(Raphaël): export boutton #} {# TODO(Raphaël): export boutton #}
{% endblock %} {% endblock %}

View file

@ -1,9 +1,25 @@
{% extends "Palto/base.html" %} {% extends "Palto/base/base-features.html" %}
{% load dict_tags %} {% load dict_tags %}
{% load static %}
{% block page-title %}
Unité d'Enseignement de {{ unit.name }}
{% endblock %}
{% block navigation-title %}
Unité d'Enseignement de {{ unit.name }}
{% endblock %}
{% block style %}
{{ block.super }}
<link rel="stylesheet" href="{% static "Palto/css/table_view.css" %}" />
{% endblock %}
{% block body %} {% block body %}
{{ block.super }}
{# unit's information #} {# unit's information #}
<table> <table id="table-informations">
<tr> <tr>
<th>Identifiant</th> <th>Identifiant</th>
<td>{{ unit.id }}</td> <td>{{ unit.id }}</td>
@ -14,7 +30,7 @@
</tr> </tr>
<tr> <tr>
<th>Département</th> <th>Département</th>
<td href="{# TODO(Faraphel): department url #}">{{ unit.department.name }}</td> <td><a href="{% url "Palto:department_view" unit.department.id %}">{{ unit.department.name }}</a></td>
</tr> </tr>
<tr> <tr>
<th>Mail</th> <th>Mail</th>
@ -26,8 +42,9 @@
</tr> </tr>
</table> </table>
<div id="table-relations">
{# unit's managers #} {# unit's managers #}
<table> <table class="table-relation">
<thead> <thead>
<tr> <tr>
<th>Responsables</th> <th>Responsables</th>
@ -43,7 +60,7 @@
</table> </table>
{# unit's teachers #} {# unit's teachers #}
<table> <table class="table-relation">
<thead> <thead>
<tr> <tr>
<th>Enseignants</th> <th>Enseignants</th>
@ -57,4 +74,5 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{% endblock %} {% endblock %}

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
@ -25,6 +24,8 @@ def homepage_view(request: WSGIRequest):
def login_view(request: WSGIRequest): def login_view(request: WSGIRequest):
# create a login form # create a login form
if request.POST:
form_login = forms.LoginForm(request.POST) form_login = forms.LoginForm(request.POST)
if form_login.is_valid(): if form_login.is_valid():
@ -43,6 +44,8 @@ def login_view(request: WSGIRequest):
else: else:
# otherwise the credentials were invalid. # otherwise the credentials were invalid.
form_login.add_error(field=None, error="Invalid credentials.") form_login.add_error(field=None, error="Invalid credentials.")
else:
form_login = forms.LoginForm()
# return the page # return the page
return render( return render(
@ -72,7 +75,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 +127,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 +145,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 +174,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
@ -226,7 +218,7 @@ def new_absence_view(request: WSGIRequest):
content=file content=file
) )
return redirect("Palto:homepage") # TODO(Faraphel): redirect to absence list return redirect("Palto:absence_list")
# render the page # render the page
return render( return render(
@ -236,3 +228,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,
)
)

View file

@ -7,10 +7,11 @@ For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
""" """
import os import dotenv
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Palto.settings') from utils import env
dotenv.load_dotenv(env.create_dotenv())
application = get_asgi_application() application = get_asgi_application()

View file

@ -10,9 +10,12 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/ https://docs.djangoproject.com/en/4.2/ref/settings/
""" """
import os import os
import warnings
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from django.core.management.utils import get_random_secret_key
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -21,22 +24,16 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ["DJANGO_SECRET"] SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
if not SECRET_KEY:
SECRET_KEY = get_random_secret_key()
warnings.warn('The Django secret key should be defined in the "DJANGO_SECRET_KEY" variable environment.')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = os.getenv("DEBUG", "false").lower() in ["1", "true"]
ALLOWED_HOSTS = [ ALLOWED_HOSTS = os.getenv("", "localhost 0.0.0.0").split(" ")
"127.0.0.1", INTERNAL_IPS = ["localhost"]
"localhost",
"0.0.0.0",
]
INTERNAL_IPS = [
"127.0.0.1",
"localhost",
"0.0.0.0",
]
# Application definition # Application definition
@ -93,13 +90,31 @@ WSGI_APPLICATION = 'Palto.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = { _DATABASES = {
'default': { "sqlite": {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3', 'NAME': BASE_DIR / os.getenv("DATABASE_SQLITE_FILENAME", "db.sqlite3"),
} },
"mysql": {
'ENGINE': 'django.db.backends.mysql',
"NAME": os.getenv("MYSQL_POSTGRES_NAME"),
"USER": os.getenv("MYSQL_POSTGRES_USER"),
"PASSWORD": os.getenv("MYSQL_POSTGRES_PASSWORD"),
"HOST": os.getenv("MYSQL_POSTGRES_HOST", "localhost"),
"PORT": os.getenv("MYSQL_POSTGRES_PORT", "5432"),
},
"postgres": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DATABASE_POSTGRES_NAME"),
"USER": os.getenv("DATABASE_POSTGRES_USER"),
"PASSWORD": os.getenv("DATABASE_POSTGRES_PASSWORD"),
"HOST": os.getenv("DATABASE_POSTGRES_HOST", "localhost"),
"PORT": os.getenv("DATABASE_POSTGRES_PORT", "5432"),
},
} }
DATABASES = {"default": _DATABASES[os.getenv("DATABASE_ENGINE", "sqlite")]}
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

View file

@ -7,10 +7,8 @@ For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
""" """
import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Palto.settings')
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -1 +1,45 @@
# M1-Projet-Serveur # Palto-Server
(This is a project realised for our University, it will not be maintained after and it should not be used outside of testing)
Palto is a project to check students attendances at their school classes.
It allows teachers to create sessions containing students that should be present.
They can then scan their student card with the NFC technology and they will be automatically marked as present to
this session.
# Installation
## Classic
1. Install `python >= 3.11`
2. Create a virtual environment with `python -m venv ./.venv/`. The next steps will be inside it.
3. Install the dependencies with `python -m pip install -r ./requirements.txt`.
4. Modify the `Palto/settings.py` file to setup your database and other settings.
5. Make the migrations with `python ./manage.py makemigrations`.
6. Apply the migrations to the database with `python ./manage.py migrate`.
7. Run the program by with `python ./manage.py runserver`.
## Docker
1. Start a terminal in the directory of the project.
2. Run `docker build`.
3. Change the environment variables to match your configuration.
# Advanced Settings
## Debug Mode
By default, the server is launch in production mode.
This disables the automatic static files serving since they are considered as being already served by nginx or apache.
You can start with the environment variable `DEBUG=true` to start it in development mode.
## Secret Key
You should set a django secret key manually in the `DJANGO_SECRET_KEY` environment variable. You can get one by
opening a python interpreter with django and calling the function `django.core.management.utils.get_random_secret_key()`.
## Database
The database used by default is `sqlite`. This is not recommended to keep it since it won't be saved by docker after
a restart if no volume are set, and it is considered a slow database engine. Using a `postgres` database is recommended.
You can find more details about the database in the configuration `settings.py`.

View file

@ -6,7 +6,9 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Palto.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Palto.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc: