forked from ScoDoc/ScoDoc
Merge pull request 'Importation d'assiduités depuis un fichier Excel' (#955) from iziram/ScoDoc:master into master
Reviewed-on: ScoDoc/ScoDoc#955
This commit is contained in:
commit
427672b396
@ -910,6 +910,31 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||||||
}">Ajouter une partition</a></h4>"""
|
}">Ajouter une partition</a></h4>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Formulaire importation Assiduité excel (si autorisé)
|
||||||
|
if current_user.has_permission(Permission.AbsChange):
|
||||||
|
H.append(
|
||||||
|
f"""<p>
|
||||||
|
<a class="stdlink" href="{url_for('assiduites.feuille_abs_formsemestre',
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id)}">
|
||||||
|
Importation de l'assiduité depuis un fichier excel</a>
|
||||||
|
</p>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Lien Traitement Justificatifs:
|
||||||
|
|
||||||
|
if current_user.has_permission(
|
||||||
|
Permission.AbsJustifView
|
||||||
|
) and current_user.has_permission(Permission.JustifValidate):
|
||||||
|
H.append(
|
||||||
|
f"""<p>
|
||||||
|
<a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id)}">
|
||||||
|
Traitement des justificatifs d'absence</a>
|
||||||
|
</p>"""
|
||||||
|
)
|
||||||
|
|
||||||
H.append("</div>")
|
H.append("</div>")
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
@ -1134,20 +1159,6 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
|||||||
"</div>",
|
"</div>",
|
||||||
]
|
]
|
||||||
|
|
||||||
# --- Lien Traitement Justificatifs:
|
|
||||||
|
|
||||||
if current_user.has_permission(
|
|
||||||
Permission.AbsJustifView
|
|
||||||
) and current_user.has_permission(Permission.JustifValidate):
|
|
||||||
H.append(
|
|
||||||
f"""<p>
|
|
||||||
<a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id)}">
|
|
||||||
Traitement des justificatifs d'absence</a>
|
|
||||||
</p>"""
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Lien mail enseignants:
|
# --- Lien mail enseignants:
|
||||||
adrlist = list(mails_enseignants - {None, ""})
|
adrlist = list(mails_enseignants - {None, ""})
|
||||||
if adrlist:
|
if adrlist:
|
||||||
|
114
app/templates/assiduites/pages/feuille_abs_formsemestre.j2
Normal file
114
app/templates/assiduites/pages/feuille_abs_formsemestre.j2
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#excel-content {
|
||||||
|
margin: 4px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
#excel-errors {
|
||||||
|
background-color: rgb(241, 209, 209);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stdlink {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star::after {
|
||||||
|
content: "*";
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.opt::after {
|
||||||
|
content: "*";
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% endblock styles %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
|
||||||
|
<h1>Importation de l'assiduité depuis un fichier excel</h1>
|
||||||
|
<h2 style="color: crimson;">{{titre_form}}</h2>
|
||||||
|
<div class="scobox">
|
||||||
|
<div id="excel-content">
|
||||||
|
<p class="hint">Avertissement : le fichier doit respecter le format suivant</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
colonne A : Identifiant de l'étudiant
|
||||||
|
</li>
|
||||||
|
<li class="star">colonne B : Date de début</li>
|
||||||
|
<li class="star">colonne C : Date de fin</li>
|
||||||
|
<li class="opt">colonne D : État (ABS,RET,PRE, ABS si vide)</li>
|
||||||
|
<li class="opt">colonne E : code du module</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="hint"><span class="opt"></span> : Colonne optionnelle, les cases peuvent être vides</p>
|
||||||
|
<p class="hint"><span class="star"></span> : Formats autorisés :
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre>aaaa-mm-jjThh:mm:ss</pre>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<pre>jj/mm/aaaa hh:mm:ss</pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="hint">La première ligne du fichier <strong>ne doit pas</strong> être une ligne d'entête</p>
|
||||||
|
|
||||||
|
|
||||||
|
<form action="" method="post" enctype="multipart/form-data">
|
||||||
|
<label for="type_identifiant">Type d'identifiant d'étudiant (etudid, ine, nip)</label>
|
||||||
|
<select name="type_identifiant" id="type_identifiant">
|
||||||
|
<option value="etudid">ETUDID</option>
|
||||||
|
<option value="ine">INE</option>
|
||||||
|
<option value="nip">NIP</option>
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<label for="file">
|
||||||
|
Sélectionnez un fichier:
|
||||||
|
|
||||||
|
</label>
|
||||||
|
<input id="file" type="file" name="file"
|
||||||
|
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" />
|
||||||
|
<br>
|
||||||
|
<button type="submit">Importer</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if erreurs %}
|
||||||
|
<div class="scobox erreurs" id="excel-errors">
|
||||||
|
<div>
|
||||||
|
<p class="hint">Les erreurs suivantes ont été trouvées dans le fichier excel :</p>
|
||||||
|
<ul>
|
||||||
|
{% for erreur in erreurs %}
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
{{erreur[0]}}
|
||||||
|
→
|
||||||
|
Ligne : {{erreur[1][0]}}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock app_content %}
|
@ -113,6 +113,26 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#excel-content {
|
||||||
|
margin: 4px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
#excel-errors {
|
||||||
|
background-color: rgb(241, 209, 209);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stdlink{
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@ -657,6 +677,8 @@
|
|||||||
matin.textContent = `${temps.matin.debut} à ${temps.matin.fin}`;
|
matin.textContent = `${temps.matin.debut} à ${temps.matin.fin}`;
|
||||||
apresmidi.textContent = `${temps.apresmidi.debut} à ${temps.apresmidi.fin}`;
|
apresmidi.textContent = `${temps.apresmidi.debut} à ${temps.apresmidi.fin}`;
|
||||||
|
|
||||||
|
document.getElementById("excel-heures").value = Object.values(temps).map((el)=>Object.values(el).join(",")).join(",");
|
||||||
|
|
||||||
recupAssiduitesHebdo(updateTable);
|
recupAssiduitesHebdo(updateTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,6 +707,14 @@
|
|||||||
|
|
||||||
updateTemps(temps);
|
updateTemps(temps);
|
||||||
|
|
||||||
|
const select = document.getElementById("moduleimpl_select");
|
||||||
|
const excelModule = document.getElementById("excel-module");
|
||||||
|
select.addEventListener("change", (ev)=>{
|
||||||
|
excelModule.value = ev.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
excelModule.value = select.value;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -782,9 +812,59 @@ document.addEventListener("DOMContentLoaded", ()=>{
|
|||||||
{{moduleimpl_select | safe}}
|
{{moduleimpl_select | safe}}
|
||||||
</label>
|
</label>
|
||||||
<button onclick="changeWeek(false)">Semaine suivante</button>
|
<button onclick="changeWeek(false)">Semaine suivante</button>
|
||||||
<span><a href="{{url_choix_semaine}}" class="stdlink">autre semaine<a></span>
|
<span><a href="{{url_choix_semaine}}" class="stdlink">autre semaine</a></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if readonly %}
|
||||||
|
{% else %}
|
||||||
|
<div class="scobox">
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<span>Importation à partir d'un fichier excel</span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div id="excel-content">
|
||||||
|
<a href="feuille_abs_hebdo?{{query_string|safe}}" class="stdlink">Télécharger le modèle</a>
|
||||||
|
<p class="hint">Avertissement : l'assiduité sera enregistrée en prenant en compte le module sélectionné plus haut et les heures sélectionnées plus bas.</p>
|
||||||
|
<form action="feuille_abs_hebdo?{{query_string|safe}}" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="heures" value="" id="excel-heures">
|
||||||
|
<input type="hidden" name="moduleimpl_id" value="" id="excel-module">
|
||||||
|
<label for="file">
|
||||||
|
Sélectionnez un fichier:
|
||||||
|
<input id="file" type="file" name="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Importer</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if erreurs %}
|
||||||
|
<div class="scobox erreurs" id="excel-errors">
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>Erreurs</summary>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="hint">Les erreurs suivantes ont été trouvées dans le fichier excel :</p>
|
||||||
|
<ul>
|
||||||
|
{% for erreur in erreurs %}
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
{{erreur[0]}}
|
||||||
|
→
|
||||||
|
Colonne : {{erreur[1][1]}} - Ligne : {{erreur[1][0]}}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h3 id="tableau-dates">
|
<h3 id="tableau-dates">
|
||||||
Le matin <a href="#" id="text-matin" title="Cliquer pour modifier les horaires">9h à 12h</a> et l'après-midi de <a href="#" id="text-apresmidi" title="Cliquer pour modifier les horaires">13h à 17h</a>
|
Le matin <a href="#" id="text-matin" title="Cliquer pour modifier les horaires">9h à 12h</a> et l'après-midi de <a href="#" id="text-apresmidi" title="Cliquer pour modifier les horaires">13h à 17h</a>
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -31,7 +31,7 @@ import re
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from flask import g, request, render_template, flash
|
from flask import g, request, render_template, flash
|
||||||
from flask import abort, url_for, redirect, Response
|
from flask import abort, url_for, redirect, Response, session
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ from markupsafe import Markup
|
|||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from app import db, log
|
from app import db, log
|
||||||
|
from app.api import tools
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.decorators import (
|
from app.decorators import (
|
||||||
@ -59,6 +60,7 @@ from app.models import (
|
|||||||
GroupDescr,
|
GroupDescr,
|
||||||
Identite,
|
Identite,
|
||||||
Justificatif,
|
Justificatif,
|
||||||
|
Module,
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
ScoDocSiteConfig,
|
ScoDocSiteConfig,
|
||||||
Scolog,
|
Scolog,
|
||||||
@ -78,8 +80,9 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_moduleimpl
|
from app.scodoc import sco_moduleimpl
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_groups_view
|
from app.scodoc import sco_groups_view, sco_groups
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
|
from app.scodoc import sco_excel
|
||||||
from app.scodoc import sco_find_etud
|
from app.scodoc import sco_find_etud
|
||||||
from app.scodoc import sco_assiduites as scass
|
from app.scodoc import sco_assiduites as scass
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
@ -2006,6 +2009,8 @@ def signal_assiduites_hebdo():
|
|||||||
moduleimpl_id=moduleimpl_id,
|
moduleimpl_id=moduleimpl_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
erreurs: list = session.pop("feuille_abs_hebdo-erreurs", [])
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"assiduites/pages/signal_assiduites_hebdo.j2",
|
"assiduites/pages/signal_assiduites_hebdo.j2",
|
||||||
title="Assiduité: saisie hebdomadaire",
|
title="Assiduité: saisie hebdomadaire",
|
||||||
@ -2022,6 +2027,8 @@ def signal_assiduites_hebdo():
|
|||||||
dept_id=g.scodoc_dept_id,
|
dept_id=g.scodoc_dept_id,
|
||||||
),
|
),
|
||||||
url_choix_semaine=url_choix_semaine,
|
url_choix_semaine=url_choix_semaine,
|
||||||
|
query_string=request.query_string.decode(encoding="utf-8"),
|
||||||
|
erreurs=erreurs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -2218,9 +2225,592 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("feuille_abs_hebdo", methods=["GET", "POST"])
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.AbsChange)
|
||||||
|
def feuille_abs_hebdo():
|
||||||
|
"""
|
||||||
|
GET : Renvoie un tableau excel pour permettre la saisie des absences
|
||||||
|
POST: Enregistre les absences saisies et renvoie sur la page de saisie hebdomadaire
|
||||||
|
Affiche un choix de semaine si "week" n'est pas renseigné
|
||||||
|
|
||||||
|
Si POST:
|
||||||
|
renvoie sur la page saisie_assiduites_hebdo
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Récupération des groupes
|
||||||
|
group_ids: str = request.args.get("group_ids", "")
|
||||||
|
if group_ids == "":
|
||||||
|
raise ScoValueError("Paramètre 'group_ids' manquant", dest_url=request.referrer)
|
||||||
|
|
||||||
|
# Vérification du semestre
|
||||||
|
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||||
|
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
|
||||||
|
# Vériication de la semaine
|
||||||
|
week: str = request.args.get("week", datetime.datetime.now().strftime("%G-W%V"))
|
||||||
|
|
||||||
|
regex_iso8601 = r"^\d{4}-W\d{2}$"
|
||||||
|
if week and not re.match(regex_iso8601, week):
|
||||||
|
raise ScoValueError("Semaine invalide", dest_url=request.referrer)
|
||||||
|
|
||||||
|
fs_deb_iso8601 = formsemestre.date_debut.strftime("%Y-W%W")
|
||||||
|
fs_fin_iso8601 = formsemestre.date_fin.strftime("%Y-W%W")
|
||||||
|
|
||||||
|
# Utilisation de la propriété de la norme iso 8601
|
||||||
|
# les chaines sont triables par ordre alphanumérique croissant
|
||||||
|
# et produiront le même ordre que les dates par ordre chronologique croissant
|
||||||
|
if (not week) or week < fs_deb_iso8601 or week > fs_fin_iso8601:
|
||||||
|
if week:
|
||||||
|
flash(
|
||||||
|
"""La semaine n'est pas dans le semestre,
|
||||||
|
choisissez la semaine sur laquelle saisir l'assiduité"""
|
||||||
|
)
|
||||||
|
return sco_gen_cal.calendrier_choix_date(
|
||||||
|
date_debut=formsemestre.date_debut,
|
||||||
|
date_fin=formsemestre.date_fin,
|
||||||
|
url=url_for(
|
||||||
|
"assiduites.feuilles_abs_hebdo",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
group_ids=group_ids,
|
||||||
|
week="placeholder",
|
||||||
|
),
|
||||||
|
mode="semaine",
|
||||||
|
titre="Choix de la semaine",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Vérification des groupes
|
||||||
|
group_ids = group_ids.split(",") if group_ids != "" else []
|
||||||
|
|
||||||
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||||
|
group_ids, formsemestre_id=formsemestre.id, select_all_when_unspecified=True
|
||||||
|
)
|
||||||
|
if not groups_infos.members:
|
||||||
|
return (
|
||||||
|
html_sco_header.sco_header(
|
||||||
|
page_title="Assiduité: feuille saisie hebdomadaire"
|
||||||
|
)
|
||||||
|
+ "<h3>Aucun étudiant ! </h3>"
|
||||||
|
+ html_sco_header.sco_footer()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Gestion des jours
|
||||||
|
jours: dict[str, list[str]] = {
|
||||||
|
"lun": [
|
||||||
|
"Lundi",
|
||||||
|
datetime.datetime.strptime(week + "-1", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||||
|
],
|
||||||
|
"mar": [
|
||||||
|
"Mardi",
|
||||||
|
datetime.datetime.strptime(week + "-2", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||||
|
],
|
||||||
|
"mer": [
|
||||||
|
"Mercredi",
|
||||||
|
datetime.datetime.strptime(week + "-3", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||||
|
],
|
||||||
|
"jeu": [
|
||||||
|
"Jeudi",
|
||||||
|
datetime.datetime.strptime(week + "-4", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||||
|
],
|
||||||
|
"ven": [
|
||||||
|
"Vendredi",
|
||||||
|
datetime.datetime.strptime(week + "-5", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||||
|
],
|
||||||
|
"sam": [
|
||||||
|
"Samedi",
|
||||||
|
datetime.datetime.strptime(week + "-6", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||||
|
],
|
||||||
|
"dim": [
|
||||||
|
"Dimanche",
|
||||||
|
datetime.datetime.strptime(week + "-7", "%G-W%V-%u").strftime("%d/%m/%Y"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
non_travail = sco_preferences.get_preference("non_travail")
|
||||||
|
non_travail = non_travail.replace(" ", "").split(",")
|
||||||
|
|
||||||
|
hebdo_jours: list[tuple[bool, str]] = []
|
||||||
|
for key, val in jours.items():
|
||||||
|
hebdo_jours.append((key in non_travail, val))
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
url_saisie: str = url_for(
|
||||||
|
"assiduites.signal_assiduites_hebdo",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
**request.args,
|
||||||
|
)
|
||||||
|
# Vérification du fichier
|
||||||
|
file = request.files.get("file", None)
|
||||||
|
if file is None or file.filename == "":
|
||||||
|
flash("Erreur : Pas de fichier")
|
||||||
|
return redirect(url_saisie)
|
||||||
|
# Vérification des heures
|
||||||
|
heures: list[str] = request.form.get("heures", "").split(",")
|
||||||
|
if len(heures) != 4:
|
||||||
|
flash("Erreur : Les heures sont incorrectes")
|
||||||
|
return redirect(url_saisie)
|
||||||
|
# Récupération du moduleimpl
|
||||||
|
moduleimpl_id = request.form.get("moduleimpl_id")
|
||||||
|
|
||||||
|
# Enregistrement des assiduites
|
||||||
|
erreurs = _import_feuille_abs_hebdo(
|
||||||
|
file,
|
||||||
|
heures=["08:15", "13:00", "13:00", "18:15"],
|
||||||
|
moduleimpl_id=moduleimpl_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if erreurs:
|
||||||
|
session["feuille_abs_hebdo-erreurs"] = erreurs
|
||||||
|
|
||||||
|
return redirect(url_saisie)
|
||||||
|
|
||||||
|
filename = f"feuille_signal_abs_{week}"
|
||||||
|
xls = _excel_feuille_abs(
|
||||||
|
formsemestre=formsemestre, groups_infos=groups_infos, hebdo_jours=hebdo_jours
|
||||||
|
)
|
||||||
|
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("feuille_abs_formsemestre", methods=["GET", "POST"])
|
||||||
|
@scodoc
|
||||||
|
def feuille_abs_formsemestre():
|
||||||
|
"""
|
||||||
|
Permet l'importation d'une liste d'assiduités depuis un fichier excel
|
||||||
|
GET:
|
||||||
|
Affiche un formulaire pour l'import et les erreurs lors de l'import
|
||||||
|
POST:
|
||||||
|
Nécessite un fichier excel contenant une liste d'assiduités (file)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Récupération du formsemestre
|
||||||
|
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||||
|
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
erreurs: list = []
|
||||||
|
if request.method == "POST":
|
||||||
|
# Récupération et vérification du fichier
|
||||||
|
file = request.files.get("file")
|
||||||
|
if file is None or file.filename == "":
|
||||||
|
raise ScoValueError("Pas de fichier", dest_url=request.referrer)
|
||||||
|
|
||||||
|
# Récupération du type d'identifiant
|
||||||
|
type_identifiant: str = request.form.get("type_identifiant", "etudid")
|
||||||
|
|
||||||
|
# Importation du fichier
|
||||||
|
erreurs = _import_excel_assiduites_list(
|
||||||
|
file, formsemestre=formsemestre, type_etud_identifiant=type_identifiant
|
||||||
|
)
|
||||||
|
|
||||||
|
if erreurs:
|
||||||
|
flash("Erreurs lors de l'importation, voir bas de page", "error")
|
||||||
|
else:
|
||||||
|
flash("Importation réussie")
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"assiduites/pages/feuille_abs_formsemestre.j2",
|
||||||
|
erreurs=erreurs,
|
||||||
|
titre_form=formsemestre.titre_annee(),
|
||||||
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --- Fonctions internes ---
|
# --- Fonctions internes ---
|
||||||
|
|
||||||
|
|
||||||
|
def _import_feuille_abs_hebdo(
|
||||||
|
file, heures: list[str], moduleimpl_id: int = None
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
Importe un fichier excel au format de la feuille d'absence hebdomadaire
|
||||||
|
(voir _excel_feuille_abs)
|
||||||
|
|
||||||
|
Génère les assiduités correspondantes et retourne une liste d'erreurs
|
||||||
|
|
||||||
|
Les erreurs sont sous la forme :
|
||||||
|
(message, [num_ligne, ...contenu_ligne])
|
||||||
|
|
||||||
|
Attention : num_ligne correspond au numéro de la ligne dans le fichier excel
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: list = sco_excel.excel_file_to_list(file)
|
||||||
|
erreurs: list = []
|
||||||
|
|
||||||
|
# Récupération des jours (entête du tableau)
|
||||||
|
jours: list[str] = [
|
||||||
|
str_jour.split(" ")[1] for str_jour in data[1][4][4:] if str_jour
|
||||||
|
]
|
||||||
|
|
||||||
|
lettres: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
# On récupère uniquement les lignes d'étudiants (on ignore les headers)
|
||||||
|
data: list = data[1][6:]
|
||||||
|
|
||||||
|
# Chaque ligne commence par [!etudid][nom][prenom][groupe]
|
||||||
|
for num, ligne in enumerate(data):
|
||||||
|
etudid = ligne[0].replace("!", "") # on enlève le point d'exclamation
|
||||||
|
try:
|
||||||
|
etud = Identite.get_etud(etudid)
|
||||||
|
except HTTPException as exc:
|
||||||
|
erreurs.append((exc.description, [num + 6, "A"] + ligne))
|
||||||
|
continue
|
||||||
|
|
||||||
|
for i, etat in enumerate(ligne[4:]):
|
||||||
|
try:
|
||||||
|
etat: str = etat.strip().upper()
|
||||||
|
if etat:
|
||||||
|
# Vérification de l'état
|
||||||
|
if etat not in ["ABS", "RET", "PRE"]:
|
||||||
|
raise ScoValueError(f"État invalide => {etat}")
|
||||||
|
|
||||||
|
etat: scu.EtatAssiduite = {
|
||||||
|
"ABS": scu.EtatAssiduite.ABSENT,
|
||||||
|
"RET": scu.EtatAssiduite.RETARD,
|
||||||
|
"PRE": scu.EtatAssiduite.PRESENT,
|
||||||
|
}.get(etat, scu.EtatAssiduite.ABSENT)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Génération des dates de début et de fin de l'assiduité
|
||||||
|
heure_debut: str = heures[0] if i % 2 == 0 else heures[2]
|
||||||
|
heure_fin: str = heures[1] if i % 2 == 0 else heures[3]
|
||||||
|
|
||||||
|
try:
|
||||||
|
date_debut: datetime.datetime = datetime.datetime.strptime(
|
||||||
|
jours[i // 2] + " " + heure_debut, "%d/%m/%Y %H:%M"
|
||||||
|
)
|
||||||
|
date_fin: datetime.datetime = datetime.datetime.strptime(
|
||||||
|
jours[i // 2] + " " + heure_fin, "%d/%m/%Y %H:%M"
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ScoValueError("Dates Invalides") from exc
|
||||||
|
|
||||||
|
# On met les dates à la timezone du serveur
|
||||||
|
date_debut = scu.TIME_ZONE.localize(date_debut)
|
||||||
|
date_fin = scu.TIME_ZONE.localize(date_fin)
|
||||||
|
|
||||||
|
# Création de l'assiduité
|
||||||
|
assiduite: Assiduite = Assiduite.create_assiduite(
|
||||||
|
etud=etud,
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if moduleimpl_id:
|
||||||
|
assiduite.set_moduleimpl(moduleimpl_id)
|
||||||
|
|
||||||
|
db.session.add(assiduite)
|
||||||
|
scass.simple_invalidate_cache(assiduite.to_dict())
|
||||||
|
except HTTPException as exc:
|
||||||
|
erreurs.append((exc.description, [num + 6, lettres[i + 4]] + ligne))
|
||||||
|
except ScoValueError as exc:
|
||||||
|
erreurs.append((exc.args[0], [num + 6, lettres[i + 4]] + ligne))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# On commit les changements
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return erreurs
|
||||||
|
|
||||||
|
|
||||||
|
def _import_excel_assiduites_list(
|
||||||
|
file, formsemestre: FormSemestre, type_etud_identifiant: str = "etudid"
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
Importe un fichier excel contenant une liste d'assiduités
|
||||||
|
sous le format :
|
||||||
|
|
||||||
|
| etudid/nip/ine | date_debut | date_fin | etat [optionnel -> "ABS"] | Module [optionnel -> None] |
|
||||||
|
|
||||||
|
Génère les assiduités correspondantes et retourne une liste d'erreurs
|
||||||
|
|
||||||
|
Les erreurs sont sous la forme :
|
||||||
|
(message, [num_ligne, ...contenu_ligne])
|
||||||
|
|
||||||
|
Attention : num_ligne correspond au numéro de la ligne dans le fichier excel
|
||||||
|
"""
|
||||||
|
|
||||||
|
# On récupère les données du fichier
|
||||||
|
data: list = sco_excel.excel_file_to_list(file)
|
||||||
|
|
||||||
|
# On récupère le deuxième élément de la liste (les lignes du tableur)
|
||||||
|
# Le premier element de la liste correspond à la description des feuilles excel
|
||||||
|
data: list = data[1]
|
||||||
|
|
||||||
|
# On parcourt les lignes et on les traite
|
||||||
|
erreurs: list[tuple[str, list]] = []
|
||||||
|
for num, ligne in enumerate(data):
|
||||||
|
identifiant_etud = ligne[0] # etudid/nip/ine
|
||||||
|
date_debut_str = ligne[1] # iso / fra / excel
|
||||||
|
date_fin_str = ligne[2] # iso / fra / excel
|
||||||
|
etat = ligne[3].strip().upper() # etat abs par défaut, sinon RET ou PRE
|
||||||
|
etat = etat or "ABS"
|
||||||
|
module = ligne[4] or None # code du module
|
||||||
|
moduleimpl: ModuleImpl | None = None
|
||||||
|
try:
|
||||||
|
# On récupère l'étudiant
|
||||||
|
etud: Identite = _find_etud(identifiant_etud, type_etud_identifiant)
|
||||||
|
|
||||||
|
# On vérifie que l'étudiant appartient au semestre
|
||||||
|
if formsemestre not in etud.get_formsemestres():
|
||||||
|
raise ScoValueError("Étudiant non inscrit dans le semestre")
|
||||||
|
|
||||||
|
# On transforme les dates
|
||||||
|
date_debut: datetime.datetime = _try_parse_date(date_debut_str)
|
||||||
|
date_fin: datetime.datetime = _try_parse_date(date_fin_str)
|
||||||
|
|
||||||
|
# On met les dates à la timezone du serveur
|
||||||
|
date_debut = scu.TIME_ZONE.localize(date_debut)
|
||||||
|
date_fin = scu.TIME_ZONE.localize(date_fin)
|
||||||
|
|
||||||
|
# Vérification de l'état
|
||||||
|
if etat not in ["ABS", "RET", "PRE"]:
|
||||||
|
raise ScoValueError(f"État invalide => {etat}")
|
||||||
|
|
||||||
|
etat: scu.EtatAssiduite = {
|
||||||
|
"ABS": scu.EtatAssiduite.ABSENT,
|
||||||
|
"RET": scu.EtatAssiduite.RETARD,
|
||||||
|
"PRE": scu.EtatAssiduite.PRESENT,
|
||||||
|
}.get(etat, scu.EtatAssiduite.ABSENT)
|
||||||
|
|
||||||
|
# On récupère le moduleimpl à partir du code du module et du formsemestre
|
||||||
|
if module:
|
||||||
|
moduleimpl = _get_moduleimpl_from_code(module, formsemestre)
|
||||||
|
|
||||||
|
assiduite: Assiduite = Assiduite.create_assiduite(
|
||||||
|
etud=etud,
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
moduleimpl=moduleimpl,
|
||||||
|
)
|
||||||
|
db.session.add(assiduite)
|
||||||
|
scass.simple_invalidate_cache(assiduite.to_dict())
|
||||||
|
|
||||||
|
except ScoValueError as exc:
|
||||||
|
erreurs.append((exc.args[0], [num + 1] + ligne))
|
||||||
|
except HTTPException as exc:
|
||||||
|
erreurs.append((exc.description, [num + 1] + ligne))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return erreurs
|
||||||
|
|
||||||
|
|
||||||
|
def _get_moduleimpl_from_code(
|
||||||
|
module_code: str, formsemestre: FormSemestre
|
||||||
|
) -> ModuleImpl:
|
||||||
|
query: Query = ModuleImpl.query.filter(
|
||||||
|
ModuleImpl.module.has(Module.code == module_code),
|
||||||
|
ModuleImpl.formsemestre_id == formsemestre.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
moduleimpl: ModuleImpl = query.first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
raise ScoValueError("Module non trouvé")
|
||||||
|
return moduleimpl
|
||||||
|
|
||||||
|
|
||||||
|
def _try_parse_date(date: str) -> datetime.datetime:
|
||||||
|
"""
|
||||||
|
Tente de parser une date sous différents formats
|
||||||
|
renvoie la première date valide
|
||||||
|
"""
|
||||||
|
|
||||||
|
# On tente de parser la date en iso (yyyy-mm-ddThh:mm:ss)
|
||||||
|
try:
|
||||||
|
return datetime.datetime.fromisoformat(date)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# On tente de parser la date en français (dd/mm/yyyy hh:mm:ss)
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(date, "%d/%m/%Y %H:%M:%S")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise ScoValueError("Date invalide")
|
||||||
|
|
||||||
|
|
||||||
|
def _find_etud(identifiant: str, type_identifiant: str) -> Identite:
|
||||||
|
"""
|
||||||
|
Renvoie l'étudiant correspondant à l'identifiant
|
||||||
|
"""
|
||||||
|
if type_identifiant == "etudid":
|
||||||
|
return tools.get_etud(etudid=identifiant)
|
||||||
|
elif type_identifiant == "nip":
|
||||||
|
return tools.get_etud(nip=identifiant)
|
||||||
|
elif type_identifiant == "ine":
|
||||||
|
return tools.get_etud(ine=identifiant)
|
||||||
|
else:
|
||||||
|
raise ScoValueError("Type d'identifiant invalide")
|
||||||
|
|
||||||
|
|
||||||
|
def _excel_feuille_abs(
|
||||||
|
formsemestre: FormSemestre,
|
||||||
|
groups_infos: sco_groups_view.DisplayedGroupsInfos,
|
||||||
|
hebdo_jours: list[tuple[bool, str]],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Génère un fichier excel pour la saisie des absences hebdomadaires
|
||||||
|
|
||||||
|
Colonnes :
|
||||||
|
- A : [formsemestre_id][etudid...]
|
||||||
|
- B : [nom][nom...]
|
||||||
|
- C : [prenom][prenom...]
|
||||||
|
- D : [groupes][groupe...]
|
||||||
|
- 2 colonnes (matin/aprem) par jour de la semaine
|
||||||
|
"""
|
||||||
|
|
||||||
|
ws = sco_excel.ScoExcelSheet("Saisie_ABS")
|
||||||
|
|
||||||
|
# == Préparation des données ==
|
||||||
|
lines: list[tuple] = []
|
||||||
|
|
||||||
|
for membre in groups_infos.members:
|
||||||
|
etudid = membre["etudid"]
|
||||||
|
groups = sco_groups.get_etud_groups(etudid, formsemestre_id=formsemestre.id)
|
||||||
|
grc = sco_groups.listgroups_abbrev(groups)
|
||||||
|
line = [
|
||||||
|
str(etudid),
|
||||||
|
membre["nom"].upper(),
|
||||||
|
membre["prenom"].lower().capitalize(),
|
||||||
|
membre["etat"],
|
||||||
|
grc,
|
||||||
|
]
|
||||||
|
line += ["" for _ in range(len(hebdo_jours) * 2)]
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
# == Préparation du fichier ==
|
||||||
|
|
||||||
|
# colonnes
|
||||||
|
lettres: str = "EFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
# ajuste largeurs colonnes (unite inconnue, empirique)
|
||||||
|
ws.set_column_dimension_width("A", 11.0 / 7) # codes
|
||||||
|
# ws.set_column_dimension_hidden("A", True) # codes
|
||||||
|
ws.set_column_dimension_width("B", 164.00 / 7) # noms
|
||||||
|
ws.set_column_dimension_width("C", 109.0 / 7) # prenoms
|
||||||
|
ws.set_column_dimension_width("D", 164.0 / 7) # groupes
|
||||||
|
|
||||||
|
i: int = 0
|
||||||
|
for jour in hebdo_jours:
|
||||||
|
ws.set_column_dimension_width(lettres[i], 100.0 / 7)
|
||||||
|
ws.set_column_dimension_width(lettres[i + 1], 100.0 / 7)
|
||||||
|
if jour[0]:
|
||||||
|
ws.set_column_dimension_hidden(lettres[i], True)
|
||||||
|
ws.set_column_dimension_hidden(lettres[i + 1], True)
|
||||||
|
i += 2
|
||||||
|
|
||||||
|
# fontes
|
||||||
|
font_base = sco_excel.Font(name="Arial", size=12)
|
||||||
|
font_bold = sco_excel.Font(name="Arial", bold=True)
|
||||||
|
font_italic = sco_excel.Font(
|
||||||
|
name="Arial", size=12, italic=True, color=sco_excel.COLORS.RED.value
|
||||||
|
)
|
||||||
|
font_titre = sco_excel.Font(name="Arial", bold=True, size=14)
|
||||||
|
font_purple = sco_excel.Font(name="Arial", color=sco_excel.COLORS.PURPLE.value)
|
||||||
|
font_brown = sco_excel.Font(name="Arial", color=sco_excel.COLORS.BROWN.value)
|
||||||
|
|
||||||
|
# bordures
|
||||||
|
side_thin = sco_excel.Side(border_style="thin", color=sco_excel.COLORS.BLACK.value)
|
||||||
|
border_top = sco_excel.Border(top=side_thin)
|
||||||
|
border_right = sco_excel.Border(right=side_thin)
|
||||||
|
border_sides = sco_excel.Border(left=side_thin, right=side_thin, bottom=side_thin)
|
||||||
|
|
||||||
|
# fonds
|
||||||
|
fill_light_yellow = sco_excel.PatternFill(
|
||||||
|
patternType="solid", fgColor=sco_excel.COLORS.LIGHT_YELLOW.value
|
||||||
|
)
|
||||||
|
|
||||||
|
# styles
|
||||||
|
style_titres = {"font": font_titre}
|
||||||
|
style_expl = {"font": font_italic}
|
||||||
|
|
||||||
|
style_ro = { # cells read-only
|
||||||
|
"font": font_purple,
|
||||||
|
"border": border_right,
|
||||||
|
}
|
||||||
|
style_dem = {
|
||||||
|
"font": font_brown,
|
||||||
|
"border": border_top,
|
||||||
|
}
|
||||||
|
style_nom = { # style pour nom, prenom, groupe
|
||||||
|
"font": font_base,
|
||||||
|
"border": border_top,
|
||||||
|
}
|
||||||
|
style_abs = {
|
||||||
|
"font": font_bold,
|
||||||
|
"fill": fill_light_yellow,
|
||||||
|
"border": border_sides,
|
||||||
|
}
|
||||||
|
|
||||||
|
# filtre
|
||||||
|
filter_top = 6
|
||||||
|
filter_bottom = filter_top + len(lines)
|
||||||
|
filter_left = "A"
|
||||||
|
filter_right = lettres[len(hebdo_jours) * 2 - 1]
|
||||||
|
ws.set_auto_filter(f"${filter_left}${filter_top}:${filter_right}${filter_bottom}")
|
||||||
|
|
||||||
|
# == Ecritures statiques ==
|
||||||
|
ws.append_single_cell_row(
|
||||||
|
"Saisir les assiduités dans les cases jaunes (ABS, RET, PRE)", style=style_expl
|
||||||
|
)
|
||||||
|
ws.append_single_cell_row("Ne pas modifier les cases en mauve !", style_expl)
|
||||||
|
# Nom du semestre
|
||||||
|
ws.append_single_cell_row(
|
||||||
|
scu.unescape_html(formsemestre.titre_annee()), style_titres
|
||||||
|
)
|
||||||
|
# ligne blanche
|
||||||
|
ws.append_blank_row()
|
||||||
|
|
||||||
|
# == Ecritures dynamiques ==
|
||||||
|
# Ecriture des entêtes
|
||||||
|
row = [ws.make_cell("", style=style_titres) for _ in range(4)]
|
||||||
|
for jour in hebdo_jours:
|
||||||
|
row.append(ws.make_cell(" ".join(jour[1]), style=style_titres))
|
||||||
|
row.append(ws.make_cell("", style=style_titres))
|
||||||
|
ws.append_row(row)
|
||||||
|
|
||||||
|
row = [
|
||||||
|
ws.make_cell(f"!{formsemestre.id}", style=style_ro),
|
||||||
|
ws.make_cell("Nom", style=style_titres),
|
||||||
|
ws.make_cell("Prénom", style=style_titres),
|
||||||
|
ws.make_cell("Groupe", style=style_titres),
|
||||||
|
]
|
||||||
|
|
||||||
|
for jour in hebdo_jours:
|
||||||
|
row.append(ws.make_cell("Matin", style=style_titres))
|
||||||
|
row.append(ws.make_cell("Après-Midi", style=style_titres))
|
||||||
|
|
||||||
|
ws.append_row(row)
|
||||||
|
|
||||||
|
# Ecriture des données
|
||||||
|
for line in lines:
|
||||||
|
st = style_nom
|
||||||
|
if line[3] != "I":
|
||||||
|
st = style_dem
|
||||||
|
if line[3] == "D": # demissionnaire
|
||||||
|
s = "DEM"
|
||||||
|
else:
|
||||||
|
s = line[3] # etat autre
|
||||||
|
else:
|
||||||
|
s = line[4] # groupes TD/TP/...
|
||||||
|
ws.append_row(
|
||||||
|
[
|
||||||
|
ws.make_cell("!" + line[0], style_ro), # code
|
||||||
|
ws.make_cell(line[1], st),
|
||||||
|
ws.make_cell(line[2], st),
|
||||||
|
ws.make_cell(s, st),
|
||||||
|
]
|
||||||
|
+ [ws.make_cell(" ", style_abs) for _ in range(len(hebdo_jours) * 2)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ligne blanche
|
||||||
|
ws.append_blank_row()
|
||||||
|
|
||||||
|
return ws.generate()
|
||||||
|
|
||||||
|
|
||||||
def _dateiso_to_datefr(date_iso: str) -> str:
|
def _dateiso_to_datefr(date_iso: str) -> str:
|
||||||
"""
|
"""
|
||||||
_dateiso_to_datefr Transforme une date iso en date format français
|
_dateiso_to_datefr Transforme une date iso en date format français
|
||||||
|
Loading…
Reference in New Issue
Block a user