Assiduite: relecture de code, cosmétique, formattage

This commit is contained in:
Emmanuel Viennet 2023-12-01 13:46:33 +01:00
parent a5e5ad6248
commit 419f1223dd
7 changed files with 457 additions and 456 deletions

View File

@ -896,14 +896,21 @@ def is_valid_filename(filename):
BOOL_STR = { BOOL_STR = {
0: False,
1: True,
"": False, "": False,
"0": False, "0": False,
"1": True, "1": True,
"f": False, "f": False,
"false": False, "false": False,
"o": True,
"on": True,
"n": False, "n": False,
"t": True, "t": True,
"true": True, "true": True,
True: True,
"v": True,
"vrai": True,
"y": True, "y": True,
} }

View File

@ -1,10 +1,10 @@
:root { :root {
--color-present: #6bdb83; --color-present: #6bdb83;
--color-absent: #e62a11; --color-absent: #e62a11;
--color-absent-clair: #F25D4A; --color-absent-clair: #f25d4a;
--color-retard: #f0c865; --color-retard: #f0c865;
--color-justi: #7059FF; --color-justi: #7059ff;
--color-justi-clair: #6885E3; --color-justi-clair: #6885e3;
--color-justi-invalide: #a84476; --color-justi-invalide: #a84476;
--color-nonwork: #badfff; --color-nonwork: #badfff;
@ -18,16 +18,27 @@
--color-def: #d61616; --color-def: #d61616;
--color-conflit: #ff00009c; --color-conflit: #ff00009c;
--color-bg-def: #c8c8c8; --color-bg-def: #c8c8c8;
--color-primary: #7059FF; --color-primary: #7059ff;
--color-secondary: #6f9fff; --color-secondary: #6f9fff;
--color-defaut: #FFF; --color-defaut: #fff;
--color-defaut-dark: #444; --color-defaut-dark: #444;
--color-default-text: #1F1F1F; --color-default-text: #1f1f1f;
--motif-justi: repeating-linear-gradient(
--motif-justi: repeating-linear-gradient(135deg, transparent, transparent 4px, var(--color-justi) 4px, var(--color-justi) 8px); 135deg,
--motif-justi-invalide: repeating-linear-gradient(-135deg, transparent, transparent 4px, var(--color-justi-invalide) 4px, var(--color-justi-invalide) 8px); transparent,
transparent 4px,
var(--color-justi) 4px,
var(--color-justi) 8px
);
--motif-justi-invalide: repeating-linear-gradient(
-135deg,
transparent,
transparent 4px,
var(--color-justi-invalide) 4px,
var(--color-justi-invalide) 8px
);
} }
* { * {
@ -107,7 +118,6 @@
border: none; border: none;
top: -180%; top: -180%;
cursor: grab; cursor: grab;
} }
#l_handle { #l_handle {
@ -121,14 +131,12 @@
.ui-slider-range.ui-widget-header.ui-corner-all { .ui-slider-range.ui-widget-header.ui-corner-all {
background-color: var(--color-warning); background-color: var(--color-warning);
background-image: none; background-image: none;
opacity: 0.50; opacity: 0.5;
visibility: visible; visibility: visible;
} }
/* === Gestion des etuds row === */ /* === Gestion des etuds row === */
.etud_row { .etud_row {
display: grid; display: grid;
grid-template-columns: 2% 20% 55% auto; grid-template-columns: 2% 20% 55% auto;
@ -149,7 +157,6 @@
align-items: center; align-items: center;
height: 50px; height: 50px;
} }
.etud_row.def, .etud_row.def,
@ -220,8 +227,6 @@
position: relative; position: relative;
} }
.etud_row .assiduites_bar .filler { .etud_row .assiduites_bar .filler {
height: 5px; height: 5px;
width: 90%; width: 90%;
@ -277,7 +282,6 @@
background-image: var(--motif-justi-invalide); background-image: var(--motif-justi-invalide);
} }
/* --- Boutons assiduités --- */ /* --- Boutons assiduités --- */
.etud_row .btns_field { .etud_row .btns_field {
grid-column: 4; grid-column: 4;
@ -299,7 +303,6 @@
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
} }
.rbtn::before { .rbtn::before {
@ -313,11 +316,9 @@
border: 1px solid var(--color-defaut-dark); border: 1px solid var(--color-defaut-dark);
} }
.rbtn.present::before { .rbtn.present::before {
background-image: url(../icons/present.svg); background-image: url(../icons/present.svg);
background-color: var(--color-present); background-color: var(--color-present);
} }
.rbtn.absent::before { .rbtn.absent::before {
@ -328,7 +329,6 @@
.rbtn.aucun::before { .rbtn.aucun::before {
background-image: url(../icons/aucun.svg); background-image: url(../icons/aucun.svg);
background-color: var(--color-defaut-dark); background-color: var(--color-defaut-dark);
} }
.rbtn.retard::before { .rbtn.retard::before {
@ -367,10 +367,8 @@
height: 320px; height: 320px;
position: relative; position: relative;
border-radius: 10px; border-radius: 10px;
} }
.close { .close {
color: #111; color: #111;
position: absolute; position: absolute;
@ -411,7 +409,6 @@
padding: 4px; padding: 4px;
} }
.assiduite-info { .assiduite-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -438,7 +435,6 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.action-buttons { .action-buttons {
position: absolute; position: absolute;
text-align: center; text-align: center;
@ -450,7 +446,6 @@
bottom: 5%; bottom: 5%;
} }
/* Ajout de la classe CSS pour la bordure en pointillés */ /* Ajout de la classe CSS pour la bordure en pointillés */
.assiduite.selected { .assiduite.selected {
border: 2px dashed black; border: 2px dashed black;
@ -462,11 +457,16 @@
z-index: 5; z-index: 5;
border: 2px solid #000; border: 2px solid #000;
background-color: rgba(36, 36, 36, 0.25); background-color: rgba(36, 36, 36, 0.25);
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, rgba(81, 81, 81, 0.61) 5px, rgba(81, 81, 81, 0.61) 10px); background-image: repeating-linear-gradient(
135deg,
transparent,
transparent 5px,
rgba(81, 81, 81, 0.61) 5px,
rgba(81, 81, 81, 0.61) 10px
);
border-radius: 5px; border-radius: 5px;
} }
/*<== Info sur l'assiduité sélectionnée ==>*/ /*<== Info sur l'assiduité sélectionnée ==>*/
.modal-assiduite-content { .modal-assiduite-content {
background-color: #fefefe; background-color: #fefefe;
@ -479,7 +479,6 @@
display: none; display: none;
} }
.modal-assiduite-content.show { .modal-assiduite-content.show {
display: block; display: block;
} }
@ -491,7 +490,6 @@
align-items: flex-start; align-items: flex-start;
} }
/*<=== Mass Action ==>*/ /*<=== Mass Action ==>*/
.mass-selection { .mass-selection {
@ -561,9 +559,6 @@
margin: 0; margin: 0;
} }
#page-assiduite-content { #page-assiduite-content {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -604,14 +599,14 @@
align-items: center; align-items: center;
} }
[name='destroyFile'] { [name="destroyFile"] {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
background-image: url(../icons/trash.svg); background-image: url(../icons/trash.svg);
} }
[name='destroyFile']:checked { [name="destroyFile"]:checked {
background-image: url(../icons/remove_circle.svg); background-image: url(../icons/remove_circle.svg);
} }
@ -644,3 +639,13 @@
display: inline-block; display: inline-block;
border: solid 1px #333; border: solid 1px #333;
} }
.assi-liste {
border: 1px solid gray;
border-radius: 12px;
padding: 12px;
}
#options-tableau label {
font-weight: normal;
margin-right: 12px;
}

View File

@ -1,13 +1,14 @@
from app.tables import table_builder as tb
from app.models import Identite, Assiduite, Justificatif
from app.auth.models import User
from datetime import datetime from datetime import datetime
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif
from flask_sqlalchemy.query import Query, Pagination
from sqlalchemy import union, literal, select, desc
from app import db, g
from flask import url_for from flask import url_for
from app import log from flask_sqlalchemy.query import Pagination, Query
from sqlalchemy import desc, literal, union
from app import db, g
from app.auth.models import User
from app.models import Assiduite, Identite, Justificatif
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
from app.tables import table_builder as tb
class ListeAssiJusti(tb.Table): class ListeAssiJusti(tb.Table):
@ -21,9 +22,9 @@ class ListeAssiJusti(tb.Table):
def __init__( def __init__(
self, self,
table_data: "Data", table_data: "AssiJustifData",
filtre: "Filtre" = None, filtre: "AssiFiltre" = None,
options: "Options" = None, options: "AssiDisplayOptions" = None,
**kwargs, **kwargs,
) -> None: ) -> None:
""" """
@ -33,12 +34,12 @@ class ListeAssiJusti(tb.Table):
filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None. filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None.
page (int, optional): numéro de page de la pagination. Defaults to 1. page (int, optional): numéro de page de la pagination. Defaults to 1.
""" """
self.table_data: "Data" = table_data self.table_data: "AssiJustifData" = table_data
# Gestion du filtre, par défaut un filtre vide # Gestion du filtre, par défaut un filtre vide
self.filtre = filtre if filtre is not None else Filtre() self.filtre = filtre if filtre is not None else AssiFiltre()
# 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 Options() self.options = options if options is not None else AssiDisplayOptions()
self.total_page: int = None self.total_page: int = None
@ -383,7 +384,7 @@ class RowAssiJusti(tb.Row):
self.add_cell("actions", "Actions", "&ensp;".join(html), no_excel=True) self.add_cell("actions", "Actions", "&ensp;".join(html), no_excel=True)
class Filtre: class AssiFiltre:
""" """
Classe représentant le filtrage qui sera appliqué aux objets Classe représentant le filtrage qui sera appliqué aux objets
du Tableau `ListeAssiJusti` du Tableau `ListeAssiJusti`
@ -475,8 +476,8 @@ class Filtre:
return self.filtres.get("type_obj", 0) return self.filtres.get("type_obj", 0)
class Options: class AssiDisplayOptions:
VRAI = ["on", "true", "t", "v", "vrai", True, 1] "Options pour affichage tableau"
def __init__( def __init__(
self, self,
@ -494,17 +495,18 @@ class Options:
if self.nb_ligne_page is not None: if self.nb_ligne_page is not None:
self.nb_ligne_page = min(nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE) self.nb_ligne_page = min(nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE)
self.show_pres: bool = show_pres in Options.VRAI self.show_pres = to_bool(show_pres)
self.show_reta: bool = show_reta in Options.VRAI self.show_reta = to_bool(show_reta)
self.show_desc: bool = show_desc in Options.VRAI self.show_desc = to_bool(show_desc)
self.show_etu: bool = show_etu in Options.VRAI self.show_etu = to_bool(show_etu)
self.show_actions: bool = show_actions in Options.VRAI self.show_actions = to_bool(show_actions)
self.show_module: bool = show_module in Options.VRAI self.show_module = to_bool(show_module)
def remplacer(self, **kwargs): def remplacer(self, **kwargs):
"Positionnne options booléennes selon arguments"
for k, v in kwargs.items(): for k, v in kwargs.items():
if k.startswith("show_"): if k.startswith("show_"):
setattr(self, k, v in Options.VRAI) setattr(self, k, to_bool(v))
elif k in ["page", "nb_ligne_page"]: elif k in ["page", "nb_ligne_page"]:
setattr(self, k, int(v)) setattr(self, k, int(v))
if k == "nb_ligne_page": if k == "nb_ligne_page":
@ -513,7 +515,9 @@ class Options:
) )
class Data: class AssiJustifData:
"Les assiduités et justificatifs"
def __init__( def __init__(
self, assiduites_query: Query = None, justificatifs_query: Query = None self, assiduites_query: Query = None, justificatifs_query: Query = None
): ):
@ -521,8 +525,8 @@ class Data:
self.justificatifs_query: Query = justificatifs_query self.justificatifs_query: Query = justificatifs_query
@staticmethod @staticmethod
def from_etudiants(*etudiants: Identite) -> "Data": def from_etudiants(*etudiants: Identite) -> "AssiJustifData":
data = Data() data = AssiJustifData()
data.assiduites_query = Assiduite.query.filter( data.assiduites_query = Assiduite.query.filter(
Assiduite.etudid.in_([e.etudid for e in etudiants]) Assiduite.etudid.in_([e.etudid for e in etudiants])
) )

View File

@ -63,7 +63,7 @@
</fieldset> </fieldset>
</section> </section>
<section class="liste"> <section class="assi-liste">
{{tableau | safe }} {{tableau | safe }}
</section> </section>

View File

@ -55,7 +55,7 @@
</fieldset> </fieldset>
</section> </section>
<section class="liste"> <section class="assi-liste">
{{tableau | safe }} {{tableau | safe }}
</section> </section>

View File

@ -1,50 +1,41 @@
<hr>
<div> <div>
<h3>Options</h3> <div class="sco_box_title">Évènements enregistrés pour cet étudiant</div>
<div id="options-tableau"> <div id="options-tableau">
{% if afficher_options != false %} {% if afficher_options != false %}
<input type="checkbox" id="show_pres" name="show_pres"
onclick="updateTableau()" {{'checked' if options.show_pres else ''}}>
<label for="show_pres">afficher les présences</label> <label for="show_pres">afficher les présences</label>
{% if options.show_pres %}
<input type="checkbox" id="show_pres" name="show_pres" checked>
{% else %}
<input type="checkbox" id="show_pres" name="show_pres">
{% endif %}
<input type="checkbox" id="show_reta" name="show_reta"
onclick="updateTableau()" {{'checked' if options.show_reta else ''}}>
<label for="show_reta">afficher les retards</label> <label for="show_reta">afficher les retards</label>
{% if options.show_reta %}
<input type="checkbox" id="show_reta" name="show_reta" checked> <input type="checkbox" id="show_desc" name="show_desc"
{% else %} onclick="updateTableau()" {{'checked' if options.show_desc else ''}}>
<input type="checkbox" id="show_reta" name="show_reta">
{% endif %}
<label for="with_desc">afficher les descriptions</label> <label for="with_desc">afficher les descriptions</label>
{% if options.show_desc %}
<input type="checkbox" id="show_desc" name="show_desc" checked> <a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
{% else %}
<input type="checkbox" id="show_desc" name="show_desc">
{% endif %}
<br> <br>
{% endif %} {% endif %}
<label for="nb_ligne_page">Nombre de ligne 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" value="{{options.nb_ligne_page}}"> <input type="number" name="nb_ligne_page" id="nb_ligne_page"
size="4" step="25" min="10" value="{{options.nb_ligne_page}}"
onchange="updateTableau()"
>
<label for="n_page">Page n°</label> <label for="n_page">Page n°</label>
<select name="n_page" id="n_page"> <select name="n_page" id="n_page">
{% for n in range(1,total_pages+1) %} {% for n in range(1,total_pages+1) %}
{% if n == options.page %} <option value="{{n}}" {{'selected' if n == options.page else ''}}>{{n}}</option>
<option value="{{n}}" selected>{{n}}</option>
{% else %}
<option value="{{n}}">{{n}}</option>
{% endif %}
{% endfor %} {% endfor %}
</select> </select>
<br> <br>
<button onclick="updateTableau()">valider</button>
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
</div> </div>
</div> </div>
{{tableau | safe}} {{table.html() | safe}}
<script> <script>
@ -60,9 +51,12 @@
url.searchParams.set(el.name, el.value) url.searchParams.set(el.name, el.value)
} }
}) })
if (!url.href.endsWith("#options-tableau")) {
location.href = url.href + "#options-tableau";
} else {
location.href = url.href; location.href = url.href;
} }
}
</script> </script>

View File

@ -28,7 +28,7 @@ import datetime
import re import re
from flask import g, request, render_template, flash from flask import g, request, render_template, flash
from flask import abort, url_for, redirect from flask import abort, url_for, redirect, Response
from flask_login import current_user from flask_login import current_user
from app import db from app import db
@ -255,19 +255,17 @@ def signal_assiduites_etud():
Args: Args:
etudid (int): l'identifiant de l'étudiant etudid (int): l'identifiant de l'étudiant
date_deb, date_fin: heures début et fin (ISO sans timezone)
moduleimpl_id
evaluation_id
saisie_eval : si présent, mode "évaluation"
Returns: Returns:
str: l'html généré str: l'html généré
""" """
# Récupération de l'étudiant concerné
etudid = request.args.get("etudid", -1) etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid) etud = Identite.get_etud(etudid)
if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département")
# gestion évaluations (Appel à la page depuis les évaluations)
# Gestion évaluations (appel à la page depuis les évaluations)
saisie_eval: bool = request.args.get("saisie_eval") is not None saisie_eval: bool = request.args.get("saisie_eval") is not None
date_deb: str = request.args.get("date_deb") date_deb: str = request.args.get("date_deb")
date_fin: str = request.args.get("date_fin") date_fin: str = request.args.get("date_fin")
@ -298,17 +296,17 @@ def signal_assiduites_etud():
], ],
) )
tableau = _preparer_tableau( is_html, tableau = _prepare_tableau(
liste_assi.Data.from_etudiants( liste_assi.AssiJustifData.from_etudiants(
etud, etud,
), ),
filename=f"assiduite-{etudid}", filename=f"assiduite-{etudid}",
afficher_etu=False, afficher_etu=False,
filtre=liste_assi.Filtre(type_obj=1), filtre=liste_assi.AssiFiltre(type_obj=1),
options=liste_assi.Options(show_module=True), options=liste_assi.AssiDisplayOptions(show_module=True),
) )
if not tableau[0]: if not is_html:
return tableau[1] return tableau
# Génération de la page # Génération de la page
return HTMLBuilder( return HTMLBuilder(
header, header,
@ -328,7 +326,7 @@ def signal_assiduites_etud():
etud=etud, etud=etud,
redirect_url=redirect_url, redirect_url=redirect_url,
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
tableau=tableau[1], tableau=tableau,
), ),
# render_template( # render_template(
# "assiduites/pages/signal_assiduites_etud.j2", # "assiduites/pages/signal_assiduites_etud.j2",
@ -390,14 +388,14 @@ def liste_assiduites_etud():
"css/assiduites.css", "css/assiduites.css",
], ],
) )
tableau = _preparer_tableau( tableau = _prepare_tableau(
liste_assi.Data.from_etudiants( liste_assi.AssiJustifData.from_etudiants(
etud, etud,
), ),
filename=f"assiduites-justificatifs-{etudid}", filename=f"assiduites-justificatifs-{etudid}",
afficher_etu=False, afficher_etu=False,
filtre=liste_assi.Filtre(type_obj=0), filtre=liste_assi.AssiFiltre(type_obj=0),
options=liste_assi.Options(show_module=True), options=liste_assi.AssiDisplayOptions(show_module=True),
) )
if not tableau[0]: if not tableau[0]:
return tableau[1] return tableau[1]
@ -505,14 +503,14 @@ def ajout_justificatif_etud():
], ],
) )
tableau = _preparer_tableau( tableau = _prepare_tableau(
liste_assi.Data.from_etudiants( liste_assi.AssiJustifData.from_etudiants(
etud, etud,
), ),
filename=f"justificatifs-{etudid}", filename=f"justificatifs-{etudid}",
afficher_etu=False, afficher_etu=False,
filtre=liste_assi.Filtre(type_obj=2), filtre=liste_assi.AssiFiltre(type_obj=2),
options=liste_assi.Options(show_module=False, show_desc=True), options=liste_assi.AssiDisplayOptions(show_module=False, show_desc=True),
afficher_options=False, afficher_options=False,
) )
if not tableau[0]: if not tableau[0]:
@ -1062,30 +1060,25 @@ def visu_assi_group():
) )
def _preparer_tableau( def _prepare_tableau(
data: liste_assi.Data, data: liste_assi.AssiJustifData,
filename: str = "tableau-assiduites", filename: str = "tableau-assiduites",
afficher_etu: bool = True, afficher_etu: bool = True,
filtre: liste_assi.Filtre = None, filtre: liste_assi.AssiFiltre = None,
options: liste_assi.Options = None, options: liste_assi.AssiDisplayOptions = None,
afficher_options: bool = True, afficher_options: bool = True,
) -> tuple[bool, "Response"]: ) -> tuple[bool, Response | str]:
""" """
_preparer_tableau prépare un tableau d'assiduités / justificatifs Prépare un tableau d'assiduités / justificatifs
Cette fontion récupère dans la requête les arguments : Cette fonction récupère dans la requête les arguments :
valeurs possibles des booléens vrais ["on", "true", "t", "v", "vrai", True, 1]
toute autre valeur est considérée comme fausse.
show_pres : bool -> Affiche les présences, par défaut False show_pres : bool -> Affiche les présences, par défaut False
show_reta : bool -> Affiche les retard, par défaut False show_reta : bool -> Affiche les retard, par défaut False
show_desc : bool -> Affiche les descriptions, par défaut False show_desc : bool -> Affiche les descriptions, par défaut False
Returns: Returns:
tuple[bool | "Reponse" ]: tuple[bool | Reponse|str ]:
- bool : Vrai si la réponse est du Text/HTML - bool : Vrai si la réponse est du Text/HTML
- Reponse : du Text/HTML ou Une Reponse (téléchargement fichier) - Reponse : du Text/HTML ou Une Reponse (téléchargement fichier)
""" """
@ -1111,7 +1104,7 @@ def _preparer_tableau(
fmt = request.args.get("fmt", "html") fmt = request.args.get("fmt", "html")
if options is None: if options is None:
options: liste_assi.Options = liste_assi.Options() options: liste_assi.AssiDisplayOptions = liste_assi.AssiDisplayOptions()
options.remplacer( options.remplacer(
page=page_number, page=page_number,
@ -1138,7 +1131,7 @@ def _preparer_tableau(
return True, render_template( return True, render_template(
"assiduites/widgets/tableau.j2", "assiduites/widgets/tableau.j2",
tableau=table.html(), table=table,
total_pages=table.total_pages, total_pages=table.total_pages,
options=options, options=options,
afficher_options=afficher_options, afficher_options=afficher_options,
@ -1509,11 +1502,7 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
Alors l'absence sera sur la période de l'évaluation Alors l'absence sera sur la période de l'évaluation
Sinon L'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant Sinon L'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant
""" """
etud = Identite.get_etud(etudid)
# Récupération de l'étudiant concerné
etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département")
# Récupération de l'évaluation concernée # Récupération de l'évaluation concernée
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
@ -1549,7 +1538,9 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
# En cas d'erreur # En cas d'erreur
msg: str = see.args[0] msg: str = see.args[0]
if "Duplication" in msg: if "Duplication" in msg:
msg = "Une autre assiduité concerne déjà cette période. En cliquant sur continuer vous serez redirigé vers la page de saisie des assiduités de l'étudiant." msg = """Une autre saisie concerne déjà cette période.
En cliquant sur continuer vous serez redirigé vers la page de
saisie des assiduités de l'étudiant."""
dest: str = url_for( dest: str = url_for(
"assiduites.signal_assiduites_etud", "assiduites.signal_assiduites_etud",
etudid=etudid, etudid=etudid,