Merge pull request 'Corrections de bugs / tickets Assidiutés' (#916) from iziram/ScoDoc:assiduites_fixes into master

Reviewed-on: ScoDoc/ScoDoc#916
This commit is contained in:
Emmanuel Viennet 2024-06-04 23:09:45 +02:00
commit 963c09976b
15 changed files with 712 additions and 245 deletions

View File

@ -62,6 +62,11 @@ class AjoutAssiOrJustForm(FlaskForm):
if field: if field:
field.errors.append(err_msg) field.errors.append(err_msg)
def disable_all(self):
"Disable all fields"
for field in self:
field.render_kw = {"disabled": True}
date_debut = StringField( date_debut = StringField(
"Date de début", "Date de début",
validators=[validators.Length(max=10)], validators=[validators.Length(max=10)],
@ -175,36 +180,3 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
validators=[DataRequired(message="This field is required.")], validators=[DataRequired(message="This field is required.")],
) )
fichiers = MultipleFileField(label="Ajouter des fichiers") fichiers = MultipleFileField(label="Ajouter des fichiers")
class ChoixDateForm(FlaskForm):
"""
Formulaire de choix de date
(utilisé par la page de choix de date
si la date courante n'est pas dans le semestre)
"""
def __init__(self, *args, **kwargs):
"Init form, adding a filed for our error messages"
super().__init__(*args, **kwargs)
self.ok = True
self.error_messages: list[str] = [] # used to report our errors
def set_error(self, err_msg, field=None):
"Set error message both in form and field"
self.ok = False
self.error_messages.append(err_msg)
if field:
field.errors.append(err_msg)
date = StringField(
"Date",
validators=[validators.Length(max=10)],
render_kw={
"class": "datepicker",
"size": 10,
"id": "date",
},
)
submit = SubmitField("Enregistrer")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -0,0 +1,122 @@
""" """
from flask_wtf import FlaskForm
from wtforms import (
StringField,
SelectField,
RadioField,
TextAreaField,
validators,
SubmitField,
)
from app.scodoc.sco_utils import EtatAssiduite
class EditAssiForm(FlaskForm):
"""
Formulaire de modification d'une assiduité
"""
def __init__(self, *args, **kwargs):
"Init form, adding a filed for our error messages"
super().__init__(*args, **kwargs)
self.ok = True
self.error_messages: list[str] = [] # used to report our errors
def set_error(self, err_msg, field=None):
"Set error message both in form and field"
self.ok = False
self.error_messages.append(err_msg)
if field:
field.errors.append(err_msg)
def disable_all(self):
"Disable all fields"
for field in self:
field.render_kw = {"disabled": True}
assi_etat = RadioField(
"État:",
choices=[
(EtatAssiduite.ABSENT.value, EtatAssiduite.ABSENT.version_lisible()),
(EtatAssiduite.RETARD.value, EtatAssiduite.RETARD.version_lisible()),
(EtatAssiduite.PRESENT.value, EtatAssiduite.PRESENT.version_lisible()),
],
default="absent",
validators=[
validators.DataRequired("spécifiez le type d'évènement à signaler"),
],
)
modimpl = SelectField(
"Module",
choices={}, # will be populated dynamically
)
description = TextAreaField(
"Description",
render_kw={
"id": "description",
"cols": 75,
"rows": 4,
"maxlength": 500,
},
)
date_debut = StringField(
"Date de début",
validators=[validators.Length(max=10)],
render_kw={
"class": "datepicker",
"size": 10,
"id": "assi_date_debut",
},
)
heure_debut = StringField(
"Heure début",
default="",
validators=[validators.Length(max=5)],
render_kw={
"class": "timepicker",
"size": 5,
"id": "assi_heure_debut",
},
)
heure_fin = StringField(
"Heure fin",
default="",
validators=[validators.Length(max=5)],
render_kw={
"class": "timepicker",
"size": 5,
"id": "assi_heure_fin",
},
)
date_fin = StringField(
"Date de fin",
validators=[validators.Length(max=10)],
render_kw={
"class": "datepicker",
"size": 10,
"id": "assi_date_fin",
},
)
entry_date = StringField(
"Date de dépôt ou saisie",
validators=[validators.Length(max=10)],
render_kw={
"class": "datepicker",
"size": 10,
"id": "entry_date",
},
)
entry_time = StringField(
"Heure dépôt",
default="",
validators=[validators.Length(max=5)],
render_kw={
"class": "timepicker",
"size": 5,
"id": "assi_heure_fin",
},
)
submit = SubmitField("Enregistrer")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -360,6 +360,16 @@ class Assiduite(ScoDocModel):
return "Module non spécifié" if traduire else None return "Module non spécifié" if traduire else None
def get_moduleimpl_id(self) -> int | str | None:
"""
Retourne le ModuleImpl associé à l'assiduité
"""
if self.moduleimpl_id is not None:
return self.moduleimpl_id
if self.external_data is not None and "module" in self.external_data:
return self.external_data["module"]
return None
def get_saisie(self) -> str: def get_saisie(self) -> str:
""" """
retourne le texte "saisie le <date> par <User>" retourne le texte "saisie le <date> par <User>"
@ -395,6 +405,14 @@ class Assiduite(ScoDocModel):
if force: if force:
raise ScoValueError("Module non renseigné") raise ScoValueError("Module non renseigné")
@classmethod
def get_assiduite(cls, assiduite_id: int) -> "Assiduite":
"""Assiduité ou 404, cherche uniquement dans le département courant"""
query = Assiduite.query.filter_by(id=assiduite_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
return query.first_or_404()
class Justificatif(ScoDocModel): class Justificatif(ScoDocModel):
""" """
@ -685,10 +703,14 @@ def is_period_conflicting(
date_fin: datetime, date_fin: datetime,
collection: Query, collection: Query,
collection_cls: Assiduite | Justificatif, collection_cls: Assiduite | Justificatif,
obj_id: int = -1,
) -> bool: ) -> bool:
""" """
Vérifie si une date n'entre pas en collision Vérifie si une date n'entre pas en collision
avec les justificatifs ou assiduites déjà présentes avec les justificatifs ou assiduites déjà présentes
On peut donner un objet_id pour exclure un objet de la vérification
(utile pour les modifications)
""" """
# On s'assure que les dates soient avec TimeZone # On s'assure que les dates soient avec TimeZone
@ -696,7 +718,9 @@ def is_period_conflicting(
date_fin = localize_datetime(date_fin) date_fin = localize_datetime(date_fin)
count: int = collection.filter( count: int = collection.filter(
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut collection_cls.date_debut < date_fin,
collection_cls.date_fin > date_debut,
collection_cls.id != obj_id,
).count() ).count()
return count > 0 return count > 0

View File

@ -427,7 +427,9 @@ class JourEval(sco_gen_cal.Jour):
) )
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else "" heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
title = f"{e.description or e.moduleimpl.module.titre_str()}" title = f"{e.moduleimpl.module.titre_str()}"
if e.description:
title += f" : {e.description}"
if heure_debut_txt: if heure_debut_txt:
title += f" de {heure_debut_txt} à {heure_fin_txt}" title += f" de {heure_debut_txt} à {heure_fin_txt}"

View File

@ -978,7 +978,7 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
if not authuser.has_permission(Permission.AbsChange): if not authuser.has_permission(Permission.AbsChange):
return "" return ""
return f""" return f"""
<button onclick="window.location='{ <a class="stdlink" href="{
url_for( url_for(
"assiduites.signal_assiduites_group", "assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
@ -987,7 +987,7 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
formsemestre_id=groups_infos.formsemestre_id, formsemestre_id=groups_infos.formsemestre_id,
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
) )
}';">Saisie du jour ({datetime.date.today().strftime('%d/%m/%Y')})</button> }">Saisie du jour ({datetime.date.today().strftime('%d/%m/%Y')})</a>
""" """
@ -1000,14 +1000,14 @@ def form_choix_saisie_semaine(groups_infos):
moduleimpl_id = query_args.get("moduleimpl_id", [None])[0] moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
semaine = datetime.datetime.now().strftime("%G-W%V") semaine = datetime.datetime.now().strftime("%G-W%V")
return f""" return f"""
<button onclick="window.location='{url_for( <a class="stdlink" href="{url_for(
"assiduites.signal_assiduites_hebdo", "assiduites.signal_assiduites_hebdo",
group_ids=",".join(map(str,groups_infos.group_ids)), group_ids=",".join(map(str,groups_infos.group_ids)),
week=semaine, week=semaine,
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id, formsemestre_id=groups_infos.formsemestre_id,
moduleimpl_id=moduleimpl_id moduleimpl_id=moduleimpl_id
)}';">Saisie à la semaine</button> )}">Saisie à la semaine (semaine {''.join(semaine[-2:])})</a>
""" """

View File

@ -118,9 +118,8 @@
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
border-width: 6px; border: 10px solid transparent;
border-style: solid; width: 100%;
border-color: transparent transparent #f9f9f9 transparent;
} }
.assiduite-bubble::after { .assiduite-bubble::after {

View File

@ -53,7 +53,6 @@ async function async_get(path, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec * @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/ */
async function async_post(path, data, success, errors) { async function async_post(path, data, success, errors) {
// console.log("async_post " + path);
let response; let response;
try { try {
response = await fetch(path, { response = await fetch(path, {
@ -655,9 +654,46 @@ function mettreToutLeMonde(etat, el = null) {
if (!confirm("Effacer tout les évènements correspondant à cette plage ?")) { if (!confirm("Effacer tout les évènements correspondant à cette plage ?")) {
return; // annulation return; // annulation
} }
const assiduites_id = lignesEtuds // On récupère les lignes avec une seule assiduité
let assiduites_id = lignesEtuds
.filter((e) => e.getAttribute("type") == "edition") .filter((e) => e.getAttribute("type") == "edition")
.map((e) => Number(e.getAttribute("assiduite_id"))); .map((e) => Number(e.getAttribute("assiduite_id")));
// On récupère les assiduités conflictuelles mais qui sont comprisent
// Dans la plage de suppression
const unDeleted = {};
lignesEtuds
.filter((e) => e.getAttribute("type") == "conflit")
.forEach((e) => {
const etud = etuds.get(Number(e.getAttribute("etudid")));
// On récupère les assiduités couvertent par la plage de suppression
etud.assiduites.forEach((a) => {
const date_debut = new Date(a.date_debut);
const date_fin = new Date(a.date_fin);
// On prend en compte uniquement les assiduités conflictuelles
// (qui intersectent la plage de suppression)
if (
Date.intersect(
{ deb: deb, fin: fin },
{ deb: date_debut, fin: date_fin }
)
) {
// Si l'assiduité est couverte par la plage de suppression
// On l'ajoute à la liste des assiduités à supprimer.
if (
date_debut.isBetween(deb, fin, "[]") &&
date_fin.isBetween(deb, fin, "[]")
) {
assiduites_id.push(a.assiduite_id);
}
// Sinon on ajoute l'étudiant à la liste des étudiants non gérés
else {
unDeleted[a.etudid] = true;
}
}
});
});
afficheLoader(); afficheLoader();
async_post( async_post(
@ -672,6 +708,28 @@ function mettreToutLeMonde(etat, el = null) {
console.error(data.errors); console.error(data.errors);
} }
envoiToastTous("remove", assiduites_id.length); envoiToastTous("remove", assiduites_id.length);
if (Object.keys(unDeleted).length == 0) return;
let unDeletedEtuds = `
<ul>
${Object.keys(unDeleted)
.map((etudid) => {
const etud = etuds.get(Number(etudid));
return `<li>${etud.civilite}. ${etud.nom.toUpperCase()} ${
etud.prenom
}</li>`;
})
.join("")}
</ul>
`;
let html = `
<p>Les assiduités des étudiants suivants n'ont pas été supprimées car elles ne sont pas incluses dans la plage de suppression :</p>
${unDeletedEtuds}
`;
const div = document.createElement("div");
div.innerHTML = html;
openAlertModal("Assiduité non supprimée", div);
}, },
(error) => { (error) => {
console.error("Erreur lors de la suppression de l'assiduité", error); console.error("Erreur lors de la suppression de l'assiduité", error);
@ -681,14 +739,10 @@ function mettreToutLeMonde(etat, el = null) {
return; return;
} }
// Création / édition des assiduités // Création
const assiduitesACreer = lignesEtuds const assiduitesACreer = lignesEtuds
.filter((e) => e.getAttribute("type") == "creation") .filter((e) => e.getAttribute("type") == "creation")
.map((e) => Number(e.getAttribute("etudid"))); .map((e) => Number(e.getAttribute("etudid")));
const assiduitesAEditer = lignesEtuds
.filter((e) => e.getAttribute("type") == "edition")
.map((e) => Number(e.getAttribute("assiduite_id")));
// création // création
const promiseCreate = async_post( const promiseCreate = async_post(
@ -705,29 +759,15 @@ function mettreToutLeMonde(etat, el = null) {
console.error("Erreur lors de la création de l'assiduité", error); console.error("Erreur lors de la création de l'assiduité", error);
} }
); );
const promiseEdit = async_post(
`../../api/assiduites/edit`,
assiduitesAEditer.map((assiduite_id) => {
return { ...assiduiteObjet, assiduite_id };
}),
async (data) => {
if (data.errors.length > 0) {
console.error(data.errors);
}
},
(error) => {
console.error("Erreur lors de l'édition de l'assiduité", error);
}
);
// Affiche un loader // Affiche un loader
afficheLoader(); afficheLoader();
Promise.all([promiseCreate, promiseEdit]).then(async () => { Promise.all([promiseCreate]).then(async () => {
retirerLoader(); retirerLoader();
await recupAssiduites(etuds, $("#date").datepicker("getDate")); await recupAssiduites(etuds, $("#date").datepicker("getDate"));
creerTousLesEtudiants(etuds); creerTousLesEtudiants(etuds);
envoiToastTous(etat, assiduitesACreer.length + assiduitesAEditer.length); envoiToastTous(etat, assiduitesACreer.length);
}); });
} }
@ -888,21 +928,12 @@ function setupAssiduiteBubble(el, assiduite) {
const infos = document.createElement("a"); const infos = document.createElement("a");
infos.className = ""; infos.className = "";
infos.textContent = ``; infos.textContent = ``;
infos.title = "Cliquez pour plus d'informations"; infos.title = "Détails / Modifier";
infos.target = "_blank"; infos.target = "_blank";
infos.href = `tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assiduite.assiduite_id}`; infos.href = `edit_assiduite_etud/${assiduite.assiduite_id}`;
// Ajout d'un lien pour modifier l'assiduité
const modifs = document.createElement("a");
modifs.className = "";
modifs.textContent = `📝`;
modifs.title = "Cliquez pour modifier l'assiduité";
modifs.target = "_blank";
modifs.href = `tableau_assiduite_actions?type=assiduite&action=modifier&obj_id=${assiduite.assiduite_id}`;
const actionsDiv = document.createElement("div"); const actionsDiv = document.createElement("div");
actionsDiv.className = "assiduite-actions"; actionsDiv.className = "assiduite-actions";
actionsDiv.appendChild(modifs);
actionsDiv.appendChild(infos); actionsDiv.appendChild(infos);
bubble.appendChild(actionsDiv); bubble.appendChild(actionsDiv);

View File

@ -600,33 +600,22 @@ class RowAssiJusti(tb.Row):
url: str url: str
html: list[str] = [] html: list[str] = []
# Détails
url = url_for(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="details",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
html.append(f'<a title="Détails" href="{url}"></a>')
# Modifier
if self.ligne["type"] == "justificatif": if self.ligne["type"] == "justificatif":
# Détails/Modifier assiduité
url = url_for( url = url_for(
"assiduites.edit_justificatif_etud", "assiduites.edit_justificatif_etud",
justif_id=self.ligne["obj_id"], justif_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
back_url=request.url,
) )
html.append(f'<a title="Détails/Modifier" href="{url}"></a>')
else: else:
# Détails/Modifier assiduité
url = url_for( url = url_for(
"assiduites.tableau_assiduite_actions", "assiduites.edit_assiduite_etud",
type=self.ligne["type"], assiduite_id=self.ligne["obj_id"],
action="modifier",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
) )
html.append(f'<a title="Modifier" href="{url}">📝</a>') html.append(f'<a title="Détails/Modifier" href="{url}"></a>')
# Supprimer # Supprimer
url = url_for( url = url_for(

View File

@ -8,6 +8,7 @@
import datetime import datetime
from flask import g, url_for from flask import g, url_for
from flask_login import current_user
from app import log from app import log
from app.models import FormSemestre, Identite, Justificatif from app.models import FormSemestre, Identite, Justificatif
from app.tables import table_builder as tb from app.tables import table_builder as tb
@ -15,6 +16,7 @@ from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
import app.scodoc.sco_assiduites as scass import app.scodoc.sco_assiduites as scass
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
class TableAssi(tb.Table): class TableAssi(tb.Table):
@ -171,6 +173,20 @@ class RowAssi(tb.Row):
"justificatifs", "Justificatifs", fmt_num(compte_justificatifs.count()) "justificatifs", "Justificatifs", fmt_num(compte_justificatifs.count())
) )
if current_user.has_permission(Permission.AbsChange):
ajout_url: str = url_for(
"assiduites.ajout_assiduite_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
formsemestre_id=self.table.formsemestre.id,
)
self.add_cell(
"lien_ajout",
"",
f"<a href='{ajout_url}' class='stdlink'>signaler assiduité</a>",
no_excel=True,
)
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]: def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
""" """
Renvoie le comptage (dans la métrique du département) des différents états Renvoie le comptage (dans la métrique du département) des différents états

View File

@ -44,11 +44,33 @@ div.submit > input {
</style> </style>
<div class="tab-content"> <div class="tab-content">
<h2>{{title|safe}}</h2> <h2>{{title|safe}}</h2>
{% if readonly %}
<h3 class="rouge">Vous n'avez pas la permission de modifier ce justificatif</h3>
{% endif %}
{% if justif %} {% if justif %}
<div class="informations">
<div class="info-saisie"> <div class="info-saisie">
Saisie par {{justif.user.get_prenomnom() if justif.user else "inconnu"}} <span>Saisie par {{justif.saisie_par}} le {{justif.entry_date}}</span>
le {{justif.entry_date.strftime(scu.DATEATIME_FMT) if justif.entry_date else "?"}} </div>
<div class="info-row">
<span class="info-label">Assiduités concernées: </span>
{% if justif.justification.assiduites %}
<ul>
{% for assi in justif.justification.assiduites %}
<li><a href="{{url_for('assiduites.edit_assiduite_etud',
assiduite_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)
}}" target="_blank">{{assi.etat}} du {{assi.date_debut}} au
{{assi.date_fin}}</a>
</li>
{% endfor %}
</ul>
{% else %}
<span class="text">Aucune</span>
{% endif %}
</div>
</div> </div>
{% endif %} {% endif %}
@ -110,7 +132,9 @@ div.submit > input {
{% for filename in filenames %} {% for filename in filenames %}
<li><span data-justif_id="{{justif.id}}" class="suppr_fichier_just" <li><span data-justif_id="{{justif.id}}" class="suppr_fichier_just"
>{{scu.icontag("delete_img", alt="supprimer", title="Supprimer")|safe}}</span> >{{scu.icontag("delete_img", alt="supprimer", title="Supprimer")|safe}}</span>
{{filename}}</li> <a href="{{url_for('apiweb.justif_export',justif_id=justif.justif_id,
filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
@ -126,11 +150,28 @@ div.submit > input {
<span class="help" style="margin-left: 12px;">laisser vide pour date courante</span> <span class="help" style="margin-left: 12px;">laisser vide pour date courante</span>
{{ render_field_errors(form, 'entry_date') }} {{ render_field_errors(form, 'entry_date') }}
{% if readonly == False %}
{# Submit #} {# Submit #}
<div class="submit"> <div class="submit">
{{ form.submit }} {{ form.cancel }} {{ form.submit }} {{ form.cancel }}
</div> </div>
<div class="info-row">
<a
style="color:red;"
href="{{url_for(
'assiduites.tableau_assiduite_actions',
type='justificatif',
action='supprimer',
obj_id=justif.justif_id,
scodoc_dept=g.scodoc_dept,
)}}"
>Supprimer le justificatif</a>
</div>
{% endif %}
</fieldset> </fieldset>
</form> </form>
</section> </section>

View File

@ -242,7 +242,7 @@ Calendrier de l'assiduité
document.querySelectorAll('[assi_id]').forEach((el, i) => { document.querySelectorAll('[assi_id]').forEach((el, i) => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
const assi_id = el.getAttribute('assi_id'); const assi_id = el.getAttribute('assi_id');
window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`); window.open(`${SCO_URL}Assiduites/edit_assiduite_etud/${assi_id}`);
}) })
}); });
</script> </script>

View File

@ -0,0 +1,180 @@
{# Ajout d'une "assiduité" sur un étudiant #}
{% extends "sco_page.j2" %}
{% import 'wtf.j2' as wtf %}
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
<style>
.info-row {
margin-top: 12px;
}
.info-label {
font-weight: bold;
}
#assi_etat{
list-style: none;
}
.info-etat {
font-size: 110%;
font-weight: bold;
background-color: rgb(253, 234, 210);
border: 1px solid grey;
border-radius: 4px;
padding: 4px;
}
.info-saisie {
margin-top: 12px;
margin-bottom: 12px;
font-style: italic;
}
</style>
{% endblock %}
{% block app_content %}
<div class="tab-content">
<h2>Détails Assiduité concernant {{etud.html_link_fiche()|safe}}</h2>
<div id="informations">
<div class="info-saisie">
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div>
<div class="info-row">
<span class="info-label">Période :</span> du <b>{{objet.date_debut}}</b> au <b>{{objet.date_fin}}</b>
</div>
<div class="info-row">
<span class="info-label">Module :</span> {{objet.module}}
</div>
<div class="info-row">
<span class="info-label">État de l'assiduité :</span><span class="info-etat">{{objet.etat}}</span>
</div>
<div class="info-row">
<span class="info-label">Description:</span>
{% if objet.description != "" and objet.description is not None %}
<span class="text">{{objet.description}}</span>
{% else %}
<span class="text fontred">Pas de description</span>
{% endif %}
</span>
</div>
{# Affichage des justificatifs si assiduité justifiée #}
{% if objet.etat != "Présence" %}
<div class="info-row">
<span class="info-label">Justifiée: </span>
{% if objet.justification.est_just %}
<span class="text">Oui</span>
{% else %}
<span class="text fontred">Non</span>
{% if not objet.justification.justificatifs %}
<a
href="{{url_for(
'assiduites.tableau_assiduite_actions',
type='assiduite',
action='justifier',
obj_id=objet.assiduite_id,
scodoc_dept=g.scodoc_dept,
)}}"
>Justifier l'assiduité</a>
{% endif %}
{% endif %}
</div>
<div class="info-row">
{% if not objet.justification.justificatifs %}
<span class="text info-label">Pas de justificatif associé</span>
{% else %}
<span class="text info-label">Justificatifs associés:</span>
<ul>
{% for justi in objet.justification.justificatifs %}
<li>
<a href="{{url_for('assiduites.edit_justificatif_etud',
justif_id=justi.justif_id,scodoc_dept=g.scodoc_dept)}}"
target="_blank" rel="noopener noreferrer" style="{{'color:red;' if justi.etat != 'Valide'}}">Justificatif {{justi.etat}} du {{justi.date_debut}} au
{{justi.date_fin}}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
</div>
{% if readonly != True %}
<h2 style="margin-top: 24px;">Modification de l'assiduité</h2>
{% for err_msg in form.error_messages %}
<div class="wtf-error-messages">
{{ err_msg }}
</div>
{% endfor %}
<form id="edit-assiduite-form" method="post">
{{ form.hidden_tag() }}
{# Type d'évènement #}
<div class="radio-assi_etat">
{{ form.assi_etat.label }}
{{ form.assi_etat() }}
</div>
<div class="dates-heures">
{{ form.date_debut.label }}&nbsp;: {{ form.date_debut }}
à {{ form.heure_debut }}
{{ render_field_errors(form, 'date_debut') }}
{{ render_field_errors(form, 'heure_debut') }}
<br>
{{ form.date_fin.label }}&nbsp;: {{ form.date_fin }}
à {{ form.heure_fin }}
{{ render_field_errors(form, 'date_fin') }}
{{ render_field_errors(form, 'heure_fin') }}
<br>
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }} à {{ form.entry_time }}
</div>
<br>
{# Menu module #}
<div class="select-module">
{{ form.modimpl.label }}&nbsp;:
{{ form.modimpl }}
{{ render_field_errors(form, 'modimpl') }}
</div>
{# Description #}
<div>
<div>{{ form.description.label }}</div>
{{ form.description() }}
{{ render_field_errors(form, 'description') }}
</div>
{# Submit #}
<div class="submit info-row">
{{ form.submit }} {{ form.cancel }}
</div>
</form>
<div class="info-row">
<a
style="color:red;"
href="{{url_for(
'assiduites.tableau_assiduite_actions',
type='assiduite',
action='supprimer',
obj_id=objet.assiduite_id,
scodoc_dept=g.scodoc_dept,
)}}"
>Supprimer l'assiduité</a>
</div>
{% else %}
<h3 class="rouge">Vous n'avez pas la permission de modifier cette assiduité</h3>
{% endif %}
</div>
{% endblock app_content %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% include "sco_timepicker.j2" %}
{% endblock scripts %}

View File

@ -571,7 +571,6 @@
} }
let toCreate = []; // [{etudid:<int>}] let toCreate = []; // [{etudid:<int>}]
let toEdit = [];// [{etudid:<int>, assiduite_id:<int>}]
tds.forEach((td) => { tds.forEach((td) => {
// on ne touche pas aux conflits // on ne touche pas aux conflits
@ -585,8 +584,6 @@
const assiduite_id = td.getAttribute("assiduite_id"); const assiduite_id = td.getAttribute("assiduite_id");
if (assiduite_id == "") { if (assiduite_id == "") {
toCreate.push({ etudid: etudid }); toCreate.push({ etudid: etudid });
} else {
toEdit.push({ etudid: etudid, assiduite_id: Number(assiduite_id) });
} }
}) })
@ -598,19 +595,9 @@
} }
}); });
// Modification
toEdit = toEdit.map((el) => {
return {
...assi,
etudid: el.etudid,
assiduite_id: el.assiduite_id,
}
});
// Appel API // Appel API
let counts = { let counts = {
create: toCreate.length, create: toCreate.length,
edit: toEdit.length
} }
const promiseCreate = async_post( const promiseCreate = async_post(
`../../api/assiduites/create`, `../../api/assiduites/create`,
@ -633,35 +620,13 @@
console.error("Erreur lors de la création de l'assiduité", error); console.error("Erreur lors de la création de l'assiduité", error);
} }
); );
const promiseEdit = async_post(
`../../api/assiduites/edit`,
toEdit,
async (data) => {
if (data.errors.length > 0) {
console.error(data.errors);
data.errors.forEach((err) => {
let obj = toEdit[err.indice];
let etu = etuds.find((el) => el.id == obj.etudid);
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
const toast = generateToast(text, "var(--color-error)");
pushToast(toast);
});
}
counts.edit = data.success.length;
},
(error) => {
console.error("Erreur lors de l'édition de l'assiduité", error);
}
);
// Affiche un loader // Affiche un loader
afficheLoader(); afficheLoader();
Promise.all([promiseCreate, promiseEdit]).then(async () => { Promise.all([promiseCreate]).then(async () => {
retirerLoader(); retirerLoader();
await recupAssiduitesHebdo(updateTable); await recupAssiduitesHebdo(updateTable);
envoiToastTous("present", counts.create + counts.edit); envoiToastTous("present", counts.create);
}); });
} }

View File

@ -59,7 +59,7 @@ function getWidth(start, end) {
const duration = (endTime - startTime) / 1000 / 60; const duration = (endTime - startTime) / 1000 / 60;
const percent = (duration / (t_end * 60 - t_start * 60)) * 100; const percent = Math.min((duration / (t_end * 60 - t_start * 60)) * 100, 100);
return percent + "%"; return percent + "%";
} }

View File

@ -36,6 +36,7 @@ from flask_login import current_user
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from markupsafe import Markup from markupsafe import Markup
from werkzeug.exceptions import HTTPException
from app import db, log from app import db, log
from app.comp import res_sem from app.comp import res_sem
@ -48,8 +49,8 @@ from app.forms.assiduite.ajout_assiduite_etud import (
AjoutAssiOrJustForm, AjoutAssiOrJustForm,
AjoutAssiduiteEtudForm, AjoutAssiduiteEtudForm,
AjoutJustificatifEtudForm, AjoutJustificatifEtudForm,
ChoixDateForm,
) )
from app.forms.assiduite.edit_assiduite_etud import EditAssiForm
from app.models import ( from app.models import (
Assiduite, Assiduite,
Departement, Departement,
@ -65,7 +66,7 @@ from app.models import (
from app.scodoc.codes_cursus import UE_STANDARD from app.scodoc.codes_cursus import UE_STANDARD
from app.auth.models import User from app.auth.models import User
from app.models.assiduites import get_assiduites_justif from app.models.assiduites import get_assiduites_justif, is_period_conflicting
from app.tables.list_etuds import RowEtud, TableEtud from app.tables.list_etuds import RowEtud, TableEtud
import app.tables.liste_assiduites as liste_assi import app.tables.liste_assiduites as liste_assi
@ -225,6 +226,18 @@ def ajout_assiduite_etud() -> str | Response:
etudid: int = request.args.get("etudid", -1) etudid: int = request.args.get("etudid", -1)
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)
formsemestre_id = request.args.get("formsemestre_id", None)
# Gestion du semestre
formsemestre: FormSemestre | None = None
sems_etud: list[FormSemestre] = etud.get_formsemestres()
if formsemestre_id:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
formsemestre = formsemestre if formsemestre in sems_etud else None
else:
formsemestre = [sem for sem in sems_etud if sem.est_courant()]
formsemestre = formsemestre[0] if formsemestre else None
# Gestion évaluations (appel à la page depuis les évaluations) # Gestion évaluations (appel à la page depuis les évaluations)
evaluation_id: int | None = request.args.get("evaluation_id") evaluation_id: int | None = request.args.get("evaluation_id")
saisie_eval = evaluation_id is not None saisie_eval = evaluation_id is not None
@ -246,26 +259,22 @@ def ajout_assiduite_etud() -> str | Response:
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire()) modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
choices: OrderedDict = OrderedDict() choices: OrderedDict = OrderedDict()
choices[""] = [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")] choices[""] = [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
for formsemestre_id in modimpls_by_formsemestre:
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
# indique le nom du semestre dans le menu (optgroup) # indique le nom du semestre dans le menu (optgroup)
group_name: str = formsemestre.titre_annee() group_name: str = formsemestre.titre_annee()
choices[group_name] = [ choices[group_name] = [
(m.id, f"{m.module.code} {m.module.abbrev or m.module.titre or ''}") (m.id, f"{m.module.code} {m.module.abbrev or m.module.titre or ''}")
for m in modimpls_by_formsemestre[formsemestre_id] for m in modimpls_by_formsemestre[formsemestre.id]
if m.module.ue.type == UE_STANDARD if m.module.ue.type == UE_STANDARD
] ]
if formsemestre.est_courant():
choices.move_to_end(group_name, last=False)
choices.move_to_end("", last=False) choices.move_to_end("", last=False)
form.modimpl.choices = choices form.modimpl.choices = choices
force_options: dict = None force_options: dict = None
if form.validate_on_submit(): if form.validate_on_submit():
if form.cancel.data: # cancel button if form.cancel.data: # cancel button
return redirect(redirect_url) return redirect(redirect_url)
ok = _record_assiduite_etud(etud, form) ok = _record_assiduite_etud(etud, form, formsemestre=formsemestre)
if ok: if ok:
flash("enregistré") flash("enregistré")
return redirect(redirect_url) return redirect(redirect_url)
@ -293,7 +302,7 @@ def ajout_assiduite_etud() -> str | Response:
form=form, form=form,
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
redirect_url=redirect_url, redirect_url=redirect_url,
sco=ScoData(etud), sco=ScoData(etud, formsemestre=formsemestre),
tableau=tableau, tableau=tableau,
scu=scu, scu=scu,
) )
@ -301,7 +310,9 @@ def ajout_assiduite_etud() -> str | Response:
def _get_dates_from_assi_form( def _get_dates_from_assi_form(
form: AjoutAssiOrJustForm, form: AjoutAssiOrJustForm,
etud: Identite,
from_justif: bool = False, from_justif: bool = False,
formsemestre: FormSemestre | None = None,
) -> tuple[ ) -> tuple[
bool, datetime.datetime | None, datetime.datetime | None, datetime.datetime | None bool, datetime.datetime | None, datetime.datetime | None, datetime.datetime | None
]: ]:
@ -393,6 +404,41 @@ def _get_dates_from_assi_form(
dt_debut_tz_server = dt_debut_tz_server.replace(hour=0, minute=0) dt_debut_tz_server = dt_debut_tz_server.replace(hour=0, minute=0)
dt_fin_tz_server = dt_fin_tz_server.replace(hour=23, minute=59) dt_fin_tz_server = dt_fin_tz_server.replace(hour=23, minute=59)
# Vérification dates contenu dans un semestre de l'étudiant
dates_semestres: list[tuple[datetime.date, datetime.date]] = (
[(sem.date_debut, sem.date_fin) for sem in etud.get_formsemestres()]
if formsemestre is None
else [(formsemestre.date_debut, formsemestre.date_fin)]
)
# Vérification date début
if not any(
[
dt_debut_tz_server.date() >= deb and dt_debut_tz_server.date() <= fin
for deb, fin in dates_semestres
]
):
form.set_error(
"La date de début n'appartient à aucun semestre de l'étudiant"
if formsemestre is None
else "La date de début n'appartient pas au semestre",
form.date_debut,
)
# Vérification date fin
if form.date_fin.data and not any(
[
dt_fin_tz_server.date() >= deb and dt_fin_tz_server.date() <= fin
for deb, fin in dates_semestres
]
):
form.set_error(
"La date de fin n'appartient à aucun semestre de l'étudiant"
if not formsemestre
else "La date de fin n'appartient pas au semestre",
form.date_fin,
)
dt_entry_date_tz_server = ( dt_entry_date_tz_server = (
scu.TIME_ZONE.localize(dt_entry_date) if dt_entry_date else None scu.TIME_ZONE.localize(dt_entry_date) if dt_entry_date else None
) )
@ -402,6 +448,7 @@ def _get_dates_from_assi_form(
def _record_assiduite_etud( def _record_assiduite_etud(
etud: Identite, etud: Identite,
form: AjoutAssiduiteEtudForm, form: AjoutAssiduiteEtudForm,
formsemestre: FormSemestre | None = None,
) -> bool: ) -> bool:
"""Enregistre les données du formulaire de saisie assiduité. """Enregistre les données du formulaire de saisie assiduité.
Returns ok if successfully recorded, else put error info in the form. Returns ok if successfully recorded, else put error info in the form.
@ -415,7 +462,7 @@ def _record_assiduite_etud(
dt_debut_tz_server, dt_debut_tz_server,
dt_fin_tz_server, dt_fin_tz_server,
dt_entry_date_tz_server, dt_entry_date_tz_server,
) = _get_dates_from_assi_form(form) ) = _get_dates_from_assi_form(form, etud, formsemestre=formsemestre)
# Le module (avec "autre") # Le module (avec "autre")
mod_data = form.modimpl.data mod_data = form.modimpl.data
if mod_data: if mod_data:
@ -492,10 +539,8 @@ def _record_assiduite_etud(
assi: Assiduite = conflits.first() assi: Assiduite = conflits.first()
lien: str = url_for( lien: str = url_for(
"assiduites.tableau_assiduite_actions", "assiduites.edit_assiduite_etud",
type="assiduite", assiuite_id=assi.assiduite_id,
action="details",
obj_id=assi.assiduite_id,
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
) )
@ -566,7 +611,7 @@ def bilan_etud():
@bp.route("/edit_justificatif_etud/<int:justif_id>", methods=["GET", "POST"]) @bp.route("/edit_justificatif_etud/<int:justif_id>", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.AbsChange) @permission_required(Permission.ScoView)
def edit_justificatif_etud(justif_id: int): def edit_justificatif_etud(justif_id: int):
""" """
Edition d'un justificatif. Edition d'un justificatif.
@ -578,8 +623,19 @@ def edit_justificatif_etud(justif_id: int):
Returns: Returns:
str: l'html généré str: l'html généré
""" """
justif = Justificatif.get_justificatif(justif_id) try:
justif = Justificatif.get_justificatif(justif_id)
except HTTPException:
flash("Justificatif invalide")
return redirect(url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept))
readonly = not current_user.has_permission(Permission.AbsChange)
form = AjoutJustificatifEtudForm(obj=justif) form = AjoutJustificatifEtudForm(obj=justif)
if readonly:
form.disable_all()
# Set the default value for the etat field # Set the default value for the etat field
if request.method == "GET": if request.method == "GET":
form.date_debut.data = justif.date_debut.strftime(scu.DATE_FMT) form.date_debut.data = justif.date_debut.strftime(scu.DATE_FMT)
@ -606,7 +662,9 @@ def edit_justificatif_etud(justif_id: int):
) )
if form.validate_on_submit(): if form.validate_on_submit():
if form.cancel.data: # cancel button if form.cancel.data or not current_user.has_permission(
Permission.AbsChange
): # cancel button
return redirect(redirect_url) return redirect(redirect_url)
if _record_justificatif_etud(justif.etudiant, form, justif): if _record_justificatif_etud(justif.etudiant, form, justif):
return redirect(redirect_url) return redirect(redirect_url)
@ -621,12 +679,13 @@ def edit_justificatif_etud(justif_id: int):
etud=justif.etudiant, etud=justif.etudiant,
filenames=filenames, filenames=filenames,
form=form, form=form,
justif=justif, justif=_preparer_objet("justificatif", justif),
nb_files=nb_files, nb_files=nb_files,
title=f"Modification justificatif absence de {justif.etudiant.html_link_fiche()}", title=f"Modification justificatif absence de {justif.etudiant.html_link_fiche()}",
redirect_url=redirect_url, redirect_url=redirect_url,
sco=ScoData(justif.etudiant), sco=ScoData(justif.etudiant),
scu=scu, scu=scu,
readonly=not current_user.has_permission(Permission.AbsChange),
) )
@ -682,32 +741,6 @@ def ajout_justificatif_etud():
) )
def _verif_date_form_justif(
form: AjoutJustificatifEtudForm, deb: datetime.datetime, fin: datetime.datetime
) -> tuple[datetime.datetime, datetime.datetime]:
"""Gère les cas suivants :
- si on indique seulement une date de debut : journée 0h-23h59
- si on indique date de debut et heures : journée +heure deb/fin
(déjà géré par _get_dates_from_assi_form)
- Si on indique une date de début et de fin sans heures : Journées 0h-23h59
- Si on indique une date de début et de fin avec heures : On fait un objet avec
datedeb/heuredeb + datefin/heurefin (déjà géré par _get_dates_from_assi_form)
"""
cas: list[bool] = [
# cas 1
not form.date_fin.data and not form.heure_debut.data,
# cas 3
form.date_fin.data != "" and not form.heure_debut.data,
]
if any(cas):
deb = deb.replace(hour=0, minute=0)
fin = fin.replace(hour=23, minute=59)
return deb, fin
def _record_justificatif_etud( def _record_justificatif_etud(
etud: Identite, form: AjoutJustificatifEtudForm, justif: Justificatif | None = None etud: Identite, form: AjoutJustificatifEtudForm, justif: Justificatif | None = None
) -> bool: ) -> bool:
@ -724,7 +757,7 @@ def _record_justificatif_etud(
dt_debut_tz_server, dt_debut_tz_server,
dt_fin_tz_server, dt_fin_tz_server,
dt_entry_date_tz_server, dt_entry_date_tz_server,
) = _get_dates_from_assi_form(form, from_justif=True) ) = _get_dates_from_assi_form(form, etud, from_justif=True)
if not ok: if not ok:
log("_record_justificatif_etud: dates invalides") log("_record_justificatif_etud: dates invalides")
form.set_error("Erreur: dates invalides") form.set_error("Erreur: dates invalides")
@ -1626,20 +1659,18 @@ def _preparer_objet(
# Gestion justification # Gestion justification
if not objet.est_just: objet_prepare["justification"] = {
objet_prepare["justification"] = {"est_just": False} "est_just": objet.est_just,
else: "justificatifs": [],
objet_prepare["justification"] = {"est_just": True, "justificatifs": []} }
if not sans_gros_objet: if not sans_gros_objet:
justificatifs: list[int] = get_assiduites_justif( justificatifs: list[int] = get_assiduites_justif(objet.assiduite_id, False)
objet.assiduite_id, False for justi_id in justificatifs:
justi: Justificatif = Justificatif.query.get(justi_id)
objet_prepare["justification"]["justificatifs"].append(
_preparer_objet("justificatif", justi, sans_gros_objet=True)
) )
for justi_id in justificatifs:
justi: Justificatif = Justificatif.query.get(justi_id)
objet_prepare["justification"]["justificatifs"].append(
_preparer_objet("justificatif", justi, sans_gros_objet=True)
)
else: # objet == "justificatif" else: # objet == "justificatif"
justif: Justificatif = objet justif: Justificatif = objet
@ -1652,9 +1683,8 @@ def _preparer_objet(
objet_prepare["justification"] = {"assiduites": [], "fichiers": {}} objet_prepare["justification"] = {"assiduites": [], "fichiers": {}}
if not sans_gros_objet: if not sans_gros_objet:
assiduites: list[int] = scass.justifies(justif) assiduites: list[Assiduite] = justif.get_assiduites()
for assi_id in assiduites: for assi in assiduites:
assi: Assiduite = Assiduite.query.get(assi_id)
objet_prepare["justification"]["assiduites"].append( objet_prepare["justification"]["assiduites"].append(
_preparer_objet("assiduite", assi, sans_gros_objet=True) _preparer_objet("assiduite", assi, sans_gros_objet=True)
) )
@ -2106,6 +2136,121 @@ def signal_assiduites_hebdo():
) )
@bp.route("edit_assiduite_etud/<int:assiduite_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
def edit_assiduite_etud(assiduite_id: int):
"""
Page affichant les détails d'une assiduité
Si le current_user alors la page propose un formulaire de modification
"""
try:
assi: Assiduite = Assiduite.get_assiduite(assiduite_id=assiduite_id)
except HTTPException:
flash("Assiduité invalide")
return redirect(url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept))
etud: Identite = assi.etudiant
formsemestre: FormSemestre = assi.get_formsemestre()
readonly: bool = not current_user.has_permission(Permission.AbsChange)
form: EditAssiForm = EditAssiForm(request.form)
if readonly:
form.disable_all()
# peuplement moduleimpl_select
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
choices: OrderedDict = OrderedDict()
choices[""] = [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
# indique le nom du semestre dans le menu (optgroup)
group_name: str = formsemestre.titre_annee()
choices[group_name] = [
(m.id, f"{m.module.code} {m.module.abbrev or m.module.titre or ''}")
for m in modimpls_by_formsemestre[formsemestre.id]
if m.module.ue.type == UE_STANDARD
]
choices.move_to_end("", last=False)
form.modimpl.choices = choices
# Vérification formulaire
if form.validate_on_submit():
if form.cancel.data: # cancel button
return redirect(request.referrer)
# vérification des valeurs
# Gestion de l'état
etat = form.assi_etat.data
try:
etat = int(etat)
etat = scu.EtatAssiduite.inverse().get(etat, None)
except ValueError:
etat = None
if etat is None:
form.error_messages.append("État invalide")
form.ok = False
description = form.description.data or ""
description = description.strip()
moduleimpl_id = form.modimpl.data if form.modimpl.data is not None else -1
# Vérifications des dates / horaires
ok, dt_deb, dt_fin, dt_entry = _get_dates_from_assi_form(
form, etud, from_justif=True, formsemestre=formsemestre
)
if ok:
if is_period_conflicting(
dt_deb, dt_fin, etud.assiduites, Assiduite, assi.id
):
form.set_error("La période est en conflit avec une autre assiduité")
form.ok = False
if form.ok:
assi.etat = etat
assi.description = description
if moduleimpl_id != -1:
assi.set_moduleimpl(moduleimpl_id)
assi.date_debut = dt_deb
assi.date_fin = dt_fin
assi.entry_date = dt_entry
db.session.add(assi)
db.session.commit()
scass.simple_invalidate_cache(assi.to_dict(format_api=True), assi.etudid)
flash("enregistré")
return redirect(request.referrer)
# Remplissage du formulaire
form.assi_etat.data = str(assi.etat)
form.description.data = assi.description
moduleimpl_id: int | str | None = assi.get_moduleimpl_id() or ""
form.modimpl.data = str(moduleimpl_id)
form.date_debut.data = assi.date_debut.strftime(scu.DATE_FMT)
form.heure_debut.data = assi.date_debut.strftime(scu.TIME_FMT)
form.date_fin.data = assi.date_fin.strftime(scu.DATE_FMT)
form.heure_fin.data = assi.date_fin.strftime(scu.TIME_FMT)
form.entry_date.data = assi.entry_date.strftime(scu.DATE_FMT)
form.entry_time.data = assi.entry_date.strftime(scu.TIME_FMT)
return render_template(
"assiduites/pages/edit_assiduite_etud.j2",
etud=etud,
sco=ScoData(etud, formsemestre=formsemestre),
form=form,
readonly=True,
objet=_preparer_objet("assiduite", assi),
title=f"Assiduité {etud.nom_short}",
)
def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
"""Génère la liste des assiduités d'un étudiant pour le bulletin mail""" """Génère la liste des assiduités d'un étudiant pour le bulletin mail"""
@ -2463,12 +2608,12 @@ class JourAssi(sco_gen_cal.Jour):
(version journee divisée en demi-journees) (version journee divisée en demi-journees)
""" """
heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00")) heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
plage: tuple[datetime.datetime, datetime.datetime] = ()
if matin: if matin:
heure_matin = scass.str_to_time( heure_matin = scass.str_to_time(
ScoDocSiteConfig.get("assi_morning_time", "08:00") ScoDocSiteConfig.get("assi_morning_time", "08:00")
) )
matin = ( plage = (
# date debut # date debut
scu.localize_datetime( scu.localize_datetime(
datetime.datetime.combine(self.date, heure_matin) datetime.datetime.combine(self.date, heure_matin)
@ -2476,71 +2621,52 @@ class JourAssi(sco_gen_cal.Jour):
# date fin # date fin
scu.localize_datetime(datetime.datetime.combine(self.date, heure_midi)), scu.localize_datetime(datetime.datetime.combine(self.date, heure_midi)),
) )
assiduites_matin = [ else:
assi heure_soir = scass.str_to_time(
for assi in self.assiduites ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
if scu.is_period_overlapping(
(assi.date_debut, assi.date_fin), matin, bornes=False
)
]
justificatifs_matin = [
justi
for justi in self.justificatifs
if scu.is_period_overlapping(
(justi.date_debut, justi.date_fin), matin, bornes=False
)
]
etat = self._get_color_assiduites_cascade(
self._get_etats_from_assiduites(assiduites_matin),
show_pres=self.parent.show_pres,
show_reta=self.parent.show_reta,
) )
est_just = self._get_color_justificatifs_cascade( # séparation en demi journées
self._get_etats_from_justificatifs(justificatifs_matin), plage = (
# date debut
scu.localize_datetime(datetime.datetime.combine(self.date, heure_midi)),
# date fin
scu.localize_datetime(datetime.datetime.combine(self.date, heure_soir)),
) )
return f"color {etat} {est_just}" assiduites = [
heure_soir = scass.str_to_time(
ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
)
# séparation en demi journées
aprem = (
# date debut
scu.localize_datetime(datetime.datetime.combine(self.date, heure_midi)),
# date fin
scu.localize_datetime(datetime.datetime.combine(self.date, heure_soir)),
)
assiduites_aprem = [
assi assi
for assi in self.assiduites for assi in self.assiduites
if scu.is_period_overlapping( if scu.is_period_overlapping(
(assi.date_debut, assi.date_fin), aprem, bornes=False (assi.date_debut, assi.date_fin), plage, bornes=False
) )
] ]
justificatifs_aprem = [ justificatifs = [
justi justi
for justi in self.justificatifs for justi in self.justificatifs
if scu.is_period_overlapping( if scu.is_period_overlapping(
(justi.date_debut, justi.date_fin), aprem, bornes=False (justi.date_debut, justi.date_fin), plage, bornes=False
) )
] ]
etat = self._get_color_assiduites_cascade( etat = self._get_color_assiduites_cascade(
self._get_etats_from_assiduites(assiduites_aprem), self._get_etats_from_assiduites(assiduites),
show_pres=self.parent.show_pres, show_pres=self.parent.show_pres,
show_reta=self.parent.show_reta, show_reta=self.parent.show_reta,
) )
est_just = self._get_color_justificatifs_cascade( est_just = self._get_color_justificatifs_cascade(
self._get_etats_from_justificatifs(justificatifs_aprem), self._get_etats_from_justificatifs(justificatifs),
) )
if est_just == "est_just" and any(
not assi.est_just
for assi in assiduites
if assi.etat != scu.EtatAssiduite.PRESENT
):
est_just = ""
return f"color {etat} {est_just}" return f"color {etat} {est_just}"
def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]: def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]: