Assiduites : pagination + tri + options tableaux

This commit is contained in:
Iziram 2024-01-11 17:24:01 +01:00
parent 76bedfb303
commit 023e3a4c04
7 changed files with 380 additions and 63 deletions

View File

@ -618,6 +618,7 @@ def compute_assiduites_justified(
Returns: Returns:
list[int]: la liste des assiduités qui ont été justifiées. list[int]: la liste des assiduités qui ont été justifiées.
""" """
# TODO à optimiser (car très long avec 40000 assiduités)
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant # Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
if justificatifs is None: if justificatifs is None:
justificatifs: list[Justificatif] = Justificatif.query.filter_by( justificatifs: list[Justificatif] = Justificatif.query.filter_by(

View File

@ -688,6 +688,7 @@ def invalidate_assiduites_count(etudid: int, sem: dict):
sco_cache.AbsSemEtudCache.delete(key) sco_cache.AbsSemEtudCache.delete(key)
# Non utilisé
def invalidate_assiduites_count_sem(sem: dict): def invalidate_assiduites_count_sem(sem: dict):
"""Invalidate (clear) cached abs counts for all the students of this semestre""" """Invalidate (clear) cached abs counts for all the students of this semestre"""
inscriptions = ( inscriptions = (
@ -756,3 +757,5 @@ def simple_invalidate_cache(obj: dict, etudid: str | int = None):
etudid = etudid if etudid is not None else obj["etudid"] etudid = etudid if etudid is not None else obj["etudid"]
invalidate_assiduites_etud_date(etudid, date_debut) invalidate_assiduites_etud_date(etudid, date_debut)
invalidate_assiduites_etud_date(etudid, date_fin) invalidate_assiduites_etud_date(etudid, date_fin)
sco_cache.RequeteTableauAssiduiteCache.delete_with(f"tableau-etud-{etudid}")

View File

@ -396,3 +396,56 @@ class ValidationsSemestreCache(ScoDocCache):
prefix = "VSC" prefix = "VSC"
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point) timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)
class SimpleIndexCache(ScoDocCache):
prefix = "INDEX"
class RequeteTableauAssiduiteCache(ScoDocCache):
"""
clé : "<titre_tableau>:<type_obj>:<show_pres>:<show_retard>>:<order_col>:<order>"
Valeur = liste de dicts
"""
prefix = "TABASSI"
timeout = 60 * 60 # Une heure
@classmethod
def set(cls, oid: str, value: object):
"""Ajoute une entrée au cache. Ajoute la clé dans la liste des clés du cache"""
keys_index = cls.get_index()
# On met à jour l'index
if oid not in keys_index:
keys_index.append(oid)
SimpleIndexCache.set(cls.prefix + "_index", keys_index)
# On cache la valeur
return super().set(oid, value)
@classmethod
def get_index(cls) -> list:
"""récupère la liste des clés des entrées du cache"""
# on définie un index des clés pour faciliter l'invalidation
keys_index: list = SimpleIndexCache.get(cls.prefix + "_index")
if keys_index is None:
keys_index = []
return keys_index
@classmethod
def delete_with(cls, start: str):
"""Invalide toutes les entrées de cache commençant par <start>"""
keys_index: list[str] = cls.get_index()
key: str
filtered_keys_index: list = [key for key in keys_index if key.startswith(start)]
for key in filtered_keys_index:
cls.delete(key)
SimpleIndexCache.set(
cls.prefix + "_index",
[k for k in keys_index if k not in filtered_keys_index],
)

View File

@ -1,3 +1,51 @@
function loadAssi(count, deb) {
let c = 0;
let a = new Date(deb);
a.setHours(0, 0, 0, 0);
const etat = ["present", "absent", "retard"];
const etudid = 17888;
const path = getUrl() + `/api/assiduite/${etudid}/create`;
const assiduites = [];
while (c < count) {
if (a.getDay() > 0 && a.getDay() < 6) {
c++;
const date = a.toISOString().split("T")[0];
const assis = [
{
date_debut: date + "T08:00",
date_fin: date + "T10:00",
etat: etat[Math.floor(Math.random() * 3)],
},
{
date_debut: date + "T10:15",
date_fin: date + "T12:15",
etat: etat[Math.floor(Math.random() * 3)],
},
{
date_debut: date + "T13:15",
date_fin: date + "T15:15",
etat: etat[Math.floor(Math.random() * 3)],
},
{
date_debut: date + "T15:30",
date_fin: date + "T17:00",
etat: etat[Math.floor(Math.random() * 3)],
},
];
assiduites.push(...assis);
}
a = new Date(a.valueOf() + 24 * 3600 * 1000);
}
async_post(
path,
assiduites,
() => {},
() => {}
);
}
// <=== CONSTANTS and GLOBALS ===> // <=== CONSTANTS and GLOBALS ===>
let url; let url;

View File

@ -1,14 +1,33 @@
from datetime import datetime from datetime import datetime
from flask import url_for from flask import url_for
from flask_sqlalchemy.query import Pagination, Query from flask_sqlalchemy.query import Query
from sqlalchemy import desc, literal, union from sqlalchemy import desc, literal, union, asc
from app import db, g from app import db, g
from app.auth.models import User from app.auth.models import User
from app.models import Assiduite, Identite, Justificatif from app.models import Assiduite, Identite, Justificatif
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
from app.tables import table_builder as tb from app.tables import table_builder as tb
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
class Pagination:
def __init__(self, collection: list, page: int = 1, per_page: int = -1):
self.total_pages = 1
if per_page != -1:
q, r = len(collection) // per_page, len(collection) % per_page
self.total_pages = q if r == 0 else q + 1
current_page: int = min(self.total_pages, page)
self.collection = (
collection
if per_page == -1
else collection[per_page * (current_page - 1) : per_page * (current_page)]
)
def items(self) -> list:
return self.collection
class ListeAssiJusti(tb.Table): class ListeAssiJusti(tb.Table):
@ -18,13 +37,15 @@ class ListeAssiJusti(tb.Table):
""" """
NB_PAR_PAGE: int = 25 NB_PAR_PAGE: int = 25
MAX_PAR_PAGE: int = 200 MAX_PAR_PAGE: int = 1000
def __init__( def __init__(
self, self,
table_data: "AssiJustifData", table_data: "AssiJustifData",
filtre: "AssiFiltre" = None, filtre: "AssiFiltre" = None,
options: "AssiDisplayOptions" = None, options: "AssiDisplayOptions" = None,
no_pagination: bool = False,
titre: str = "",
**kwargs, **kwargs,
) -> None: ) -> None:
""" """
@ -41,11 +62,16 @@ class ListeAssiJusti(tb.Table):
# Gestion des options, par défaut un objet Options vide # Gestion des options, par défaut un objet Options vide
self.options = options if options is not None else AssiDisplayOptions() self.options = options if options is not None else AssiDisplayOptions()
self.no_pagination: bool = no_pagination
self.total_page: int = None self.total_page: int = None
# les lignes du tableau # les lignes du tableau
self.rows: list["RowAssiJusti"] = [] self.rows: list["RowAssiJusti"] = []
# Titre du tableau, utilisé pour le cache
self.titre = titre
# Instanciation de la classe parent # Instanciation de la classe parent
super().__init__( super().__init__(
row_class=RowAssiJusti, row_class=RowAssiJusti,
@ -65,6 +91,22 @@ class ListeAssiJusti(tb.Table):
# Récupération du filtrage des objets -> 0 : tout, 1 : Assi, 2: Justi # Récupération du filtrage des objets -> 0 : tout, 1 : Assi, 2: Justi
type_obj = self.filtre.type_obj() type_obj = self.filtre.type_obj()
cle_cache: str = ":".join(
map(
str,
[
self.titre,
type_obj,
self.options.show_pres,
self.options.show_reta,
self.options.order[0],
self.options.order[1],
],
)
)
r = RequeteTableauAssiduiteCache().get(cle_cache)
if r is None:
if type_obj in [0, 1]: if type_obj in [0, 1]:
assiduites_query_etudiants = self.table_data.assiduites_query assiduites_query_etudiants = self.table_data.assiduites_query
@ -89,35 +131,46 @@ class ListeAssiJusti(tb.Table):
query_justificatif=justificatifs_query_etudiants, query_justificatif=justificatifs_query_etudiants,
) )
# Tri de la query si option
if self.options.order is not None:
order_sort: str = asc if self.options.order[1] else desc
order_col: str = self.options.order[0]
query_finale: Query = query_finale.order_by(order_sort(order_col))
r = query_finale.all()
RequeteTableauAssiduiteCache.set(cle_cache, r)
# Paginer la requête pour ne pas envoyer trop d'informations au client # Paginer la requête pour ne pas envoyer trop d'informations au client
pagination: Pagination = self.paginer(query_finale) pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
self.total_pages: int = pagination.pages self.total_pages = pagination.total_pages
# Générer les lignes de la page # Générer les lignes de la page
for ligne in pagination.items: for ligne in pagination.items():
row: RowAssiJusti = self.row_class(self, ligne._asdict()) row: RowAssiJusti = self.row_class(self, ligne._asdict())
row.ajouter_colonnes() row.ajouter_colonnes()
self.add_row(row) self.add_row(row)
def paginer(self, query: Query) -> Pagination: def paginer(self, collection: list, no_pagination: bool = False) -> Pagination:
""" """
Applique la pagination à une requête SQLAlchemy en fonction des paramètres de la classe. Applique une pagination à une collection en fonction des paramètres de la classe.
Cette méthode prend une requête SQLAlchemy et applique la pagination en utilisant les Cette méthode prend une collection et applique la pagination en utilisant les
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`. attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
Args: Args:
query (Query): La requête SQLAlchemy à paginer. Il s'agit d'une requête qui a déjà collection (list): La collection à paginer. Il s'agit par exemple d'une requête qui a déjà
été construite et qui est prête à être exécutée. été construite et qui est prête à être exécutée.
Returns: Returns:
Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée. Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée.
Note: Note:
Cette méthode ne modifie pas la requête originale; elle renvoie plutôt un nouvel Cette méthode ne modifie pas la collection originelle; elle renvoie plutôt un nouvel
objet qui contient les résultats paginés. objet qui contient les résultats paginés.
""" """
return query.paginate( return Pagination(
page=self.options.page, per_page=self.options.nb_ligne_page, error_out=False collection,
self.options.page,
-1 if no_pagination else self.options.nb_ligne_page,
) )
def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None): def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None):
@ -210,7 +263,7 @@ class ListeAssiJusti(tb.Table):
# Combiner les requêtes avec une union # Combiner les requêtes avec une union
query_combinee = union(*queries).alias("combinee") query_combinee = union(*queries).alias("combinee")
query_combinee = db.session.query(query_combinee).order_by(desc("date_debut")) query_combinee = db.session.query(query_combinee)
return query_combinee return query_combinee
@ -241,30 +294,46 @@ class RowAssiJusti(tb.Row):
# Type d'objet # Type d'objet
self._type() self._type()
# Date de début
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
# En excel, on export les "vraes dates". # En excel, on export les "vraes dates".
# En HTML, on écrit en français (on laisse les dates pour le tri) # En HTML, on écrit en français (on laisse les dates pour le tri)
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
date_affichees: list[str] = [
self.ligne["date_debut"].strftime("%d/%m/%y de %H:%M"), # date début
self.ligne["date_fin"].strftime("%d/%m/%y de %H:%M"), # date fin
]
if multi_days:
date_affichees[0] = self.ligne["date_debut"].strftime("%d/%m/%y")
date_affichees[1] = self.ligne["date_fin"].strftime("%d/%m/%y")
self.add_cell( self.add_cell(
"date_debut", "date_debut",
"Date de début", "Date de début",
self.ligne["date_debut"].strftime("%d/%m/%y") date_affichees[0],
if multi_days
else self.ligne["date_debut"].strftime("%d/%m/%y de %H:%M"),
data={"order": self.ligne["date_debut"]}, data={"order": self.ligne["date_debut"]},
raw_content=self.ligne["date_debut"], raw_content=self.ligne["date_debut"],
column_classes={"date", "date-debut"}, column_classes={
"date",
"date-debut",
"external-sort",
"external-type:date_debut",
},
) )
# Date de fin # Date de fin
self.add_cell( self.add_cell(
"date_fin", "date_fin",
"Date de fin", "Date de fin",
self.ligne["date_fin"].strftime("%d/%m/%y") date_affichees[1],
if multi_days
else self.ligne["date_fin"].strftime("à %H:%M"),
raw_content=self.ligne["date_fin"], # Pour excel raw_content=self.ligne["date_fin"], # Pour excel
data={"order": self.ligne["date_fin"]}, data={"order": self.ligne["date_fin"]},
column_classes={"date", "date-fin"}, column_classes={
"date",
"date-fin",
"external-sort",
"external-type:date_fin",
},
) )
# Ajout des colonnes optionnelles # Ajout des colonnes optionnelles
@ -283,7 +352,11 @@ class RowAssiJusti(tb.Row):
data={"order": self.ligne["entry_date"] or ""}, data={"order": self.ligne["entry_date"] or ""},
raw_content=self.ligne["entry_date"], raw_content=self.ligne["entry_date"],
classes=["small-font"], classes=["small-font"],
column_classes={"entry_date"}, column_classes={
"entry_date",
"external-sort",
"external-type:entry_date",
},
) )
def _type(self) -> None: def _type(self) -> None:
@ -541,6 +614,7 @@ class AssiDisplayOptions:
show_etu: str | bool = True, show_etu: str | bool = True,
show_actions: str | bool = True, show_actions: str | bool = True,
show_module: str | bool = False, show_module: str | bool = False,
order: tuple[str, str | bool] = None,
): ):
self.page: int = page self.page: int = page
self.nb_ligne_page: int = nb_ligne_page self.nb_ligne_page: int = nb_ligne_page
@ -554,6 +628,10 @@ class AssiDisplayOptions:
self.show_actions = to_bool(show_actions) self.show_actions = to_bool(show_actions)
self.show_module = to_bool(show_module) self.show_module = to_bool(show_module)
self.order = (
("date_debut", False) if order is None else (order[0], to_bool(order[1]))
)
def remplacer(self, **kwargs): def remplacer(self, **kwargs):
"Positionne options booléennes selon arguments" "Positionne options booléennes selon arguments"
for k, v in kwargs.items(): for k, v in kwargs.items():
@ -565,6 +643,12 @@ class AssiDisplayOptions:
self.nb_ligne_page = min( self.nb_ligne_page = min(
self.nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE self.nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE
) )
elif k == "order":
setattr(
self,
k,
("date_debut", False) if v is None else (v[0], to_bool(v[1])),
)
class AssiJustifData: class AssiJustifData:

View File

@ -1,6 +1,6 @@
<div> <div>
<div class="sco_box_title">{{ titre }}</div> <div class="sco_box_title">{{ titre }}</div>
<div id="options-tableau"> <div class="options-tableau">
{% if afficher_options != false %} {% if afficher_options != false %}
<input type="checkbox" id="show_pres" name="show_pres" <input type="checkbox" id="show_pres" name="show_pres"
onclick="updateTableau()" {{'checked' if options.show_pres else ''}}> onclick="updateTableau()" {{'checked' if options.show_pres else ''}}>
@ -17,33 +17,82 @@
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a> <a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
<br> <br>
{% endif %} {% endif %}
<label for="nb_ligne_page">Nombre de lignes par page : </label> <label for="nb_ligne_page">Nombre de lignes par page :</label>
<input type="number" name="nb_ligne_page" id="nb_ligne_page" <select name="nb_ligne_page" id="nb_ligne_page" onchange="updateTableau()">
size="4" step="25" min="10" value="{{options.nb_ligne_page}}" {% for i in [25,50,100,1000] %}
onchange="updateTableau()" {% if i == options.nb_ligne_page %}
> <option selected value="{{i}}">{{i}}</option>
{% else %}
<label for="n_page">Page n°</label> <option value="{{i}}">{{i}}</option>
<select name="n_page" id="n_page"> {% endif %}
{% for n in range(1,total_pages+1) %}
<option value="{{n}}" {{'selected' if n == options.page else ''}}>{{n}}</option>
{% endfor %} {% endfor %}
</select> </select>
<br> <br>
</div> </div>
<div class="div-tableau">
{{table.html() | safe}}
<div class="options-tableau">
<!--Pagination basée sur : https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005 -->
<!-- Mettre les flèches -->
{% if total_pages > 1 %}
<ul class="pagination">
<li class="">
<a onclick="navigateToPage({{options.page - 1}})">&lt;</a>
</li>
<!-- Toujours afficher la première page -->
<li class="{% if options.page == 1 %}active{% endif %}">
<a onclick="navigateToPage({{1}})">1</a>
</li>
<!-- Afficher les ellipses si la page courante est supérieure à 2 -->
{% if options.page > 2 %}
<li class="disabled"><span>...</span></li>
{% endif %}
<!-- Afficher la page précédente, la page courante, et la page suivante -->
{% for i in range(options.page - 1, options.page + 2) %}
{% if i > 1 and i < total_pages %}
<li class="{% if options.page == i %}active{% endif %}">
<a onclick="navigateToPage({{i}})">{{ i }}</a>
</li>
{% endif %}
{% endfor %}
<!-- Afficher les ellipses si la page courante est inférieure à l'avant-dernière page -->
{% if options.page < total_pages - 1 %}
<li class="disabled"><span>...</span></li>
{% endif %}
<!-- Toujours afficher la dernière page -->
<li class="{% if options.page == total_pages %}active{% endif %}">
<a onclick="navigateToPage({{total_pages}})">{{ total_pages }}</a>
</li>
<li class="">
<a onclick="navigateToPage({{options.page + 1}})">&gt;</a>
</li>
</ul>
{% else %}
<!-- Afficher un seul bouton si il n'y a qu'une seule page -->
<ul class="pagination">
<li class="active"><a onclick="navigateToPage({{1}})">1</a></li>
</ul>
{% endif %}
</div>
</div> </div>
{{table.html() | safe}} </div>
<script> <script>
function updateTableau() { function updateTableau() {
const url = new URL(location.href); const url = new URL(location.href);
const form = document.getElementById("options-tableau"); const formValues = document.querySelectorAll(".options-tableau *[name]");
const formValues = form.querySelectorAll("*[name]");
formValues.forEach((el) => { formValues.forEach((el) => {
if (el.type == "checkbox") { if (el.type == "checkbox") {
url.searchParams.set(el.name, el.checked) url.searchParams.set(el.name, el.checked)
@ -58,10 +107,56 @@
} }
} }
const total_pages = {{total_pages}};
function navigateToPage(pageNumber){
if(pageNumber > total_pages || pageNumber < 1) return;
const url = new URL(location.href);
url.searchParams.set("n_page", pageNumber)
if (!url.href.endsWith("#options-tableau")) {
location.href = url.href + "#options-tableau";
} else {
location.href = url.href;
}
}
window.addEventListener('load', ()=>{
const table_columns = [...document.querySelectorAll('.external-sort')];
table_columns.forEach((e)=>e.addEventListener('click', ()=>{
// récupération de l'ordre "ascending" / "descending"
let order = e.ariaSort;
// récupération de la colonne à ordonner
// il faut avoir une classe `external-type:<NOM COL>`
let order_col = e.className.split(" ").find((e)=>e.indexOf("external-type:") != -1);
//Création de la nouvelle url avec le tri
const url = new URL(location.href);
url.searchParams.set("order", order);
url.searchParams.set("order_col", order_col.split(":")[1]);
location.href = url.href
}));
});
</script> </script>
<style> <style>
.small-font { .small-font {
font-size: 9pt; font-size: 9pt;
} }
.div-tableau{
display: flex;
flex-direction: column;
align-items: center;
max-width: fit-content;
}
.pagination li{
cursor: pointer;
}
</style> </style>

View File

@ -324,6 +324,7 @@ def ajout_assiduite_etud() -> str | Response:
afficher_etu=False, afficher_etu=False,
filtre=liste_assi.AssiFiltre(type_obj=1), filtre=liste_assi.AssiFiltre(type_obj=1),
options=liste_assi.AssiDisplayOptions(show_module=True), options=liste_assi.AssiDisplayOptions(show_module=True),
cache_key=f"tableau-etud-{etud.id}",
) )
if not is_html: if not is_html:
return tableau return tableau
@ -528,6 +529,7 @@ def liste_assiduites_etud():
afficher_etu=False, afficher_etu=False,
filtre=liste_assi.AssiFiltre(type_obj=0), filtre=liste_assi.AssiFiltre(type_obj=0),
options=liste_assi.AssiDisplayOptions(show_module=True), options=liste_assi.AssiDisplayOptions(show_module=True),
cache_key=f"tableau-etud-{etudid}",
) )
if not tableau[0]: if not tableau[0]:
return tableau[1] return tableau[1]
@ -697,6 +699,7 @@ def ajout_justificatif_etud():
options=liste_assi.AssiDisplayOptions(show_module=False, show_desc=True), options=liste_assi.AssiDisplayOptions(show_module=False, show_desc=True),
afficher_options=False, afficher_options=False,
titre="Justificatifs enregistrés pour cet étudiant", titre="Justificatifs enregistrés pour cet étudiant",
cache_key=f"tableau-etud-{etud.id}",
) )
if not is_html: if not is_html:
return tableau return tableau
@ -1442,6 +1445,7 @@ def _prepare_tableau(
options: liste_assi.AssiDisplayOptions = None, options: liste_assi.AssiDisplayOptions = None,
afficher_options: bool = True, afficher_options: bool = True,
titre="Évènements enregistrés pour cet étudiant", titre="Évènements enregistrés pour cet étudiant",
cache_key: str = "",
) -> tuple[bool, Response | str]: ) -> tuple[bool, Response | str]:
""" """
Prépare un tableau d'assiduités / justificatifs Prépare un tableau d'assiduités / justificatifs
@ -1478,6 +1482,13 @@ def _prepare_tableau(
fmt = request.args.get("fmt", "html") fmt = request.args.get("fmt", "html")
# Ordre
ordre: tuple[str, str | bool] = None
ordre_col: str = request.args.get("order_col", None)
ordre_tri: str = request.args.get("order", None)
if ordre_col is not None and ordre_tri is not None:
ordre = (ordre_col, ordre_tri == "ascending")
if options is None: if options is None:
options: liste_assi.AssiDisplayOptions = liste_assi.AssiDisplayOptions() options: liste_assi.AssiDisplayOptions = liste_assi.AssiDisplayOptions()
@ -1488,14 +1499,21 @@ def _prepare_tableau(
show_reta=show_reta, show_reta=show_reta,
show_desc=show_desc, show_desc=show_desc,
show_etu=afficher_etu, show_etu=afficher_etu,
order=ordre,
) )
import time
a = time.time()
table: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti( table: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti(
table_data=data, table_data=data,
options=options, options=options,
filtre=filtre, filtre=filtre,
no_pagination=fmt.startswith("xls"),
titre=cache_key,
) )
b = time.time()
print(f"génération du tableau : {b-a:.6f}s")
if fmt.startswith("xls"): if fmt.startswith("xls"):
return False, scu.send_file( return False, scu.send_file(
table.excel(), table.excel(),
@ -1541,6 +1559,21 @@ def tableau_assiduite_actions():
flash(f"{objet_name} supprimé") flash(f"{objet_name} supprimé")
return redirect(request.referrer) return redirect(request.referrer)
# Justification d'une assiduité depuis le tableau
if action == "justifier" and obj_type == "assiduite":
# Création du justificatif correspondant
justificatif_correspondant: Justificatif = Justificatif.create_justificatif(
etudiant=objet.etudiant,
date_debut=objet.date_debut,
date_fin=objet.date_fin,
etat=scu.EtatJustificatif.VALIDE,
user_id=current_user.id,
)
compute_assiduites_justified(objet.etudiant.id, [justificatif_correspondant])
flash(f"{objet_name} justifiée")
return redirect(request.referrer)
# Justification d'une assiduité depuis le tableau # Justification d'une assiduité depuis le tableau
if action == "justifier" and obj_type == "assiduite": if action == "justifier" and obj_type == "assiduite":