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
# Virtual Environment
venv
.venv
# Django
media
static-collected
db.sqlite3
.env

View file

@ -3,6 +3,9 @@ FROM python:3.12
# Set environment variables for Django
ENV DJANGO_SETTINGS_MODULE=Palto.settings
ENV DJANGO_SECRET_KEY=""
ENV DATABASE_ENGINE="sqlite"
ENV DEBUG=false
# Set the working directory in the container
WORKDIR /App
@ -10,8 +13,12 @@ WORKDIR /App
# Copy the current directory contents into the container
COPY . /App
# Install any needed packages specified in requirements.txt
RUN pip install -r requirements.txt
# Install requirements
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 80

View file

@ -24,38 +24,45 @@ class AdminUser(admin.ModelAdmin):
class AdminDepartment(admin.ModelAdmin):
list_display = ("id", "name", "email")
search_fields = ("id", "name", "email")
readonly_fields = ("id",)
@admin.register(models.StudentGroup)
class AdminStudentGroup(admin.ModelAdmin):
list_display = ("id", "name", "owner", "department")
search_fields = ("id", "name", "owner", "department")
list_filter = ("department",)
readonly_fields = ("id",)
@admin.register(models.TeachingUnit)
class AdminTeachingUnit(admin.ModelAdmin):
list_display = ("id", "name", "email")
search_fields = ("id", "name", "email")
readonly_fields = ("id",)
@admin.register(models.StudentCard)
class AdminStudentCard(admin.ModelAdmin):
list_display = ("id", "uid", "owner")
search_fields = ("id", "uid", "owner")
readonly_fields = ("uid",)
list_display = ("id", "uid", "department", "owner")
search_fields = ("id", "uid", "department", "owner")
readonly_fields = ("id", "uid",)
list_filter = ("department",)
@admin.register(models.TeachingSession)
class AdminTeachingSession(admin.ModelAdmin):
list_display = ("id", "start", "end", "duration", "teacher")
search_fields = ("id", "start", "end", "duration", "teacher")
list_filter = ("start", "duration")
list_display = ("id", "start", "end", "unit", "duration", "teacher")
search_fields = ("id", "start", "end", "unit", "duration", "teacher")
readonly_fields = ("id",)
list_filter = ("unit",)
@admin.register(models.Attendance)
class AdminAttendance(admin.ModelAdmin):
list_display = ("id", "date", "student")
search_fields = ("id", "date", "student")
readonly_fields = ("id",)
list_filter = ("date",)
@ -63,6 +70,7 @@ class AdminAttendance(admin.ModelAdmin):
class AdminAbsence(admin.ModelAdmin):
list_display = ("id", "message", "student", "start", "end")
search_fields = ("id", "message", "student", "start", "end")
readonly_fields = ("id",)
list_filter = ("start", "end")
@ -70,3 +78,4 @@ class AdminAbsence(admin.ModelAdmin):
class AdminAbsenceAttachment(admin.ModelAdmin):
list_display = ("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
@abstractmethod
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 self in self.all_editable_by_user(user)
@classmethod
@abstractmethod
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 self in self.all_visible_by_user(user)
class User(AbstractUser, ModelPermissionHelper):
"""
@ -522,6 +536,17 @@ class TeachingSession(models.Model, ModelPermissionHelper):
def end(self) -> datetime:
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
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.super }}
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<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.super }}
{# absence's information #}
<table>
<tr>
@ -9,7 +19,7 @@
</tr>
<tr>
<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>
<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 %}
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 %}

View file

@ -1,11 +1,27 @@
{% 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 %}
<form method="POST">
{{ block.super }}
<form id="login-form" method="POST">
{% csrf_token %}
<table>
{{ form_login.as_table }}
{{ form_login.as_p }}
</table>
<input type="submit" value="Se connecter">
</form>
{% endblock %}
{% endblock %}

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,76 +1,102 @@
{% extends "Palto/base.html" %}
{% extends "Palto/base/base-features.html" %}
{% load static %}
{% 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 %}
{{ profile.username }}
{{ profile.email }}
{% if profile.is_superuser %}Administrator{% endif %}
{{ block.super }}
{# 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 #}
<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 %}
<table id="user-relations">
<tbody>
{% for department, profile_department_data in profile_departments_data.items %}
<tr>
{# department name #}
<th><a href="{% url "Palto:department_view" department.id %}">{{ department.name }}</a></th>
{# relation information #}
<td>
<table class="user-relation">
<tbody>
{# user managing the department #}
{% if profile_department_data|dict_get:"is_manager" %}
<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>
<td>Responsable de Département</td>
<td>/</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 %}
{% 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="{% url "Palto:student_group_view" student_group.id %}">{{ student_group.name }}</a>
{% if not forloop.last %}<br/>{% endif %}
{% endfor %}
</td>
</tr>
{% endif %}
{% endwith %}
</tbody>
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% 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.super }}
{# table of all the sessions #}
<table>
<thead>

View file

@ -1,9 +1,25 @@
{% extends "Palto/base.html" %}
{% extends "Palto/base/base-features.html" %}
{% 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.super }}
{# session's information #}
<table>
<table id="table-informations">
<tr>
<th>Identifiant</th>
<td>{{ session.id }}</td>
@ -26,41 +42,43 @@
</tr>
<tr>
<th>Groupe</th>
<td>{{ session.group }}</td>
<td><a href="{% url "Palto:student_group_view" session.group.id %}">{{ session.group }}</a></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 %}
<div id="table-relations">
{# session's students information #}
<table class="table-relation">
<thead>
<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>
<th>Elève</th>
<th>Présence</th>
<th>Absence</th>
</tr>
{% endfor %}
</tbody>
</table>
</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>
</div>
{# TODO(Raphaël): export boutton #}
{% endblock %}

View file

@ -1,9 +1,25 @@
{% extends "Palto/base.html" %}
{% extends "Palto/base/base-features.html" %}
{% 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.super }}
{# unit's information #}
<table>
<table id="table-informations">
<tr>
<th>Identifiant</th>
<td>{{ unit.id }}</td>
@ -14,7 +30,7 @@
</tr>
<tr>
<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>
<th>Mail</th>
@ -26,35 +42,37 @@
</tr>
</table>
{# unit's managers #}
<table>
<thead>
<tr>
<th>Responsables</th>
</tr>
</thead>
<tbody>
{% for manager in unit.managers.all %}
<div id="table-relations">
{# unit's managers #}
<table class="table-relation">
<thead>
<tr>
<td><a href="{% url "Palto:profile" manager.id %}">{{ manager }}</a></td>
<th>Responsables</th>
</tr>
{% endfor %}
</tbody>
</table>
{# unit's teachers #}
<table>
<thead>
<tr>
<th>Enseignants</th>
</tr>
</thead>
<tbody>
{% for teacher in unit.teachers.all %}
</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 class="table-relation">
<thead>
<tr>
<td><a href="{% url "Palto:profile" teacher.id %}">{{ teacher }}</a></td>
<th>Enseignants</th>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for teacher in unit.teachers.all %}
<tr>
<td><a href="{% url "Palto:profile" teacher.id %}">{{ teacher }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View file

@ -19,6 +19,12 @@ urlpatterns = [
path("profile/", views.profile_view, name="my_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
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"),
# Absences
path("absences/", views.absence_list_view, name="absence_list"),
path("absences/view/<uuid:absence_id>/", views.absence_view, name="absence_view"),
path("absences/new/", views.new_absence_view, name="absence_new"),
]

View file

@ -1,10 +1,10 @@
from typing import Optional
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.
"""

View file

@ -8,7 +8,6 @@ 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
@ -25,24 +24,28 @@ def homepage_view(request: WSGIRequest):
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 request.POST:
form_login = forms.LoginForm(request.POST)
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")
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"]
)
else:
# otherwise the credentials were invalid.
form_login.add_error(field=None, error="Invalid credentials.")
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.")
else:
form_login = forms.LoginForm()
# return the page
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)
# 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()
# 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)
# 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)
if not unit.is_visible_by_user(request.user):
return HttpResponseForbidden()
# 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)
# 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)
if not session.is_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 ?
"attendance": get_object_or_none(models.Attendance.objects, session=session, student=student),
"absence": get_object_or_none(session.related_absences, student=student)
}
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)
# 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)
if not absence.is_visible_by_user(request.user):
return HttpResponseForbidden()
# render the page
@ -226,7 +218,7 @@ def new_absence_view(request: WSGIRequest):
content=file
)
return redirect("Palto:homepage") # TODO(Faraphel): redirect to absence list
return redirect("Palto:absence_list")
# render the page
return render(
@ -236,3 +228,59 @@ def new_absence_view(request: WSGIRequest):
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/
"""
import os
import dotenv
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()

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/
"""
import os
import warnings
from datetime import timedelta
from pathlib import Path
from django.core.management.utils import get_random_secret_key
# Build paths inside the project like this: BASE_DIR / 'subdir'.
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/
# 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!
DEBUG = True
DEBUG = os.getenv("DEBUG", "false").lower() in ["1", "true"]
ALLOWED_HOSTS = [
"127.0.0.1",
"localhost",
"0.0.0.0",
]
INTERNAL_IPS = [
"127.0.0.1",
"localhost",
"0.0.0.0",
]
ALLOWED_HOSTS = os.getenv("", "localhost 0.0.0.0").split(" ")
INTERNAL_IPS = ["localhost"]
# Application definition
@ -93,13 +90,31 @@ WSGI_APPLICATION = 'Palto.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
_DATABASES = {
"sqlite": {
'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
# 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/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Palto.settings')
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():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Palto.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Palto.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc: