Assiduite: un peu de nettoyage, corrections

This commit is contained in:
Emmanuel Viennet 2024-01-23 20:09:13 +01:00
parent 2a239ab92f
commit 086b8ee191
12 changed files with 63 additions and 1187 deletions

View File

@ -5,7 +5,6 @@ from datetime import datetime
from flask_login import current_user
from flask_sqlalchemy.query import Query
from psycopg2.errors import InvalidTextRepresentation # c'est ok
from sqlalchemy.exc import DataError
from app import db, log, g, set_sco_dept
@ -256,43 +255,19 @@ class Assiduite(ScoDocModel):
def set_moduleimpl(self, moduleimpl_id: int | str):
"""Mise à jour du moduleimpl_id
Les valeurs du champs "moduleimpl_id" possibles sont :
Les valeurs du champ "moduleimpl_id" possibles sont :
- <int> (un id classique)
- <str> ("autre" ou "<id>")
- None (pas de moduleimpl_id)
- "" (pas de moduleimpl_id)
Si la valeur est "autre" il faut:
- mettre à None assiduité.moduleimpl_id
- mettre à jour assiduite.external_data["module"] = "autre"
En fonction de la configuration du semestre la valeur `None` peut-être considérée comme invalide.
En fonction de la configuration du semestre (option force_module) la valeur "" peut-être
considérée comme invalide.
- Il faudra donc vérifier que ce n'est pas le cas avant de mettre à jour l'assiduité
"""
moduleimpl: ModuleImpl = None
try:
# ne lève une erreur que si moduleimpl_id est une chaine de caractère non parsable (parseInt)
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
# moduleImpl est soit :
# - None si moduleimpl_id==None
# - None si moduleimpl_id==<int> non reconnu
# - ModuleImpl si <int|str> valide
# Vérification ModuleImpl not None (raise ScoValueError)
if moduleimpl is None and self._check_force_module(moduleimpl):
# Ici uniquement si on est autorisé à ne pas avoir de module
self.moduleimpl_id = None
return
# Vérification Inscription ModuleImpl (raise ScoValueError)
if moduleimpl.est_inscrit(self.etudiant):
self.moduleimpl_id = moduleimpl.id
else:
raise ScoValueError("L'étudiant n'est pas inscrit au module")
except (DataError, InvalidTextRepresentation) as exc:
# On arrive ici si moduleimpl_id == "autre" ou moduleimpl_id == <str> non parsé
if moduleimpl_id != "autre":
raise ScoValueError("Module non reconnu") from exc
if moduleimpl_id == "autre":
# Configuration de external_data pour Module Autre
# Si self.external_data None alors on créé un dictionnaire {"module": "autre"}
# Sinon on met à jour external_data["module"] à "autre"
@ -306,6 +281,29 @@ class Assiduite(ScoDocModel):
self.moduleimpl_id = None
# Ici pas de vérification du force module car on l'a mis dans "external_data"
return
if moduleimpl_id != "":
try:
moduleimpl_id = int(moduleimpl_id)
except ValueError as exc:
raise ScoValueError("Module non reconnu") from exc
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
# ici moduleimpl est None si non spécifié
# Vérification ModuleImpl not None (raise ScoValueError)
if moduleimpl is None:
self._check_force_module()
# Ici uniquement si on est autorisé à ne pas avoir de module
self.moduleimpl_id = None
return
# Vérification Inscription ModuleImpl (raise ScoValueError)
if moduleimpl.est_inscrit(self.etudiant):
self.moduleimpl_id = moduleimpl.id
else:
raise ScoValueError("L'étudiant n'est pas inscrit au module")
def supprime(self):
"Supprime l'assiduité. Log et commit."
@ -335,7 +333,7 @@ class Assiduite(ScoDocModel):
return get_formsemestre_from_data(self.to_dict())
def get_module(self, traduire: bool = False) -> int | str:
"TODO"
"TODO documenter"
if self.moduleimpl_id is not None:
if traduire:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
@ -364,8 +362,12 @@ class Assiduite(ScoDocModel):
return f"saisie le {date} {utilisateur}"
def _check_force_module(self, moduleimpl: ModuleImpl) -> bool:
# Vérification si module forcé
def _check_force_module(self):
"""Vérification si module forcé:
Si le module est requis, raise ScoValueError
sinon ne fait rien.
"""
# cherche le formsemestre affecté pour utiliser ses préférences
formsemestre: FormSemestre = get_formsemestre_from_data(
{
"etudid": self.etudid,
@ -373,18 +375,15 @@ class Assiduite(ScoDocModel):
"date_fin": self.date_fin,
}
)
force: bool
if formsemestre:
force = is_assiduites_module_forced(formsemestre_id=formsemestre.id)
else:
force = is_assiduites_module_forced(dept_id=self.etudiant.dept_id)
formsemestre_id = formsemestre.id if formsemestre else None
# si pas de formsemestre, utilisera les prefs globales du département
dept_id = self.etudiant.dept_id
force = is_assiduites_module_forced(
formsemestre_id=formsemestre_id, dept_id=dept_id
)
if force:
raise ScoValueError("Module non renseigné")
return True
class Justificatif(ScoDocModel):
"""

View File

@ -419,7 +419,9 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
for info in ues_cap_info[ue["ue_id"]]:
etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0]
H.append(
f"""<li class="etud"><a class="discretelink" href="{
f"""<li class="etud"><a class="discretelink etudinfo"
id="{info['etudid']}"
href="{
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,

View File

@ -147,6 +147,7 @@ def get_preference(name, formsemestre_id=None, dept_id=None):
"""Returns value of named preference.
All preferences have a sensible default value, so this
function always returns a usable value for all defined preferences names.
If dept_id is None, use current dept (g.scodoc_dept_id)
"""
return get_base_preferences(dept_id=dept_id).get(formsemestre_id, name)

View File

@ -1630,20 +1630,12 @@ def is_entreprises_enabled():
def is_assiduites_module_forced(
formsemestre_id: int = None, dept_id: int = None
) -> bool:
"""Vrai si préférence "imposer la saisie du module" sur les assiduités est vraie."""
from app.scodoc import sco_preferences
retour: bool
if dept_id is None:
dept_id = g.scodoc_dept_id
try:
retour = sco_preferences.get_preference(
"forcer_module", formsemestre_id=int(formsemestre_id)
return sco_preferences.get_preference(
"forcer_module", formsemestre_id=formsemestre_id, dept_id=dept_id
)
except (TypeError, ValueError):
retour = sco_preferences.get_preference("forcer_module", dept_id=dept_id)
return retour
def get_assiduites_time_config(config_type: str) -> str | int:

View File

@ -1,235 +0,0 @@
{% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %}
{% block pageContent %}
<div class="pageContent">
<h3>Signaler une absence, présence ou retard pour {{etud.html_link_fiche()|safe}}</h3>
{% if saisie_eval %}
<div id="saisie_eval">
<br>
<h3>
La saisie a été préconfigurée en fonction de l'évaluation. <br>
Une fois la saisie terminée, cliquez sur le lien ci-dessous
</h3>
<a href="{{redirect_url}}">retourner sur la page de l'évaluation</a>
</div>
{% endif %}
<section class="assi-form page">
<fieldset>
<div class="assi-row">
<div class="assi-label">
<legend for="assi_date_debut" required>Date de début</legend>
<input type="text" name="assi_date_debut" id="assi_date_debut" size="10"
class="datepicker">
<input type="text" name="assi_heure_debut" id="assi_heure_debut" size="5"
class="timepicker">
<span>Journée entière</span> <input type="checkbox" name="assi_journee" id="assi_journee">
</div>
<div class="assi-label" id="date_fin">
<legend for="assi_date_fin" required>Date de fin</legend>
<scodoc-datetime name="assi_date_fin" id="assi_date_fin"></scodoc-datetime>
</div>
</div>
<div class="assi-row">
<div class="assi-label">
<legend for="assi_etat" required>État de l'assiduité</legend>
<select name="assi_etat" id="assi_etat">
<option value="absent" selected>Absent</option>
<option value="retard">Retard</option>
<option value="present">Présent</option>
</select>
</div>
</div>
<div class="assi-row">
<div class="assi-label">
<legend for="assi_module" required>Module</legend>
{% with moduleid="ajout_assiduite_module_impl",label=false %}
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
{% endwith %}
</div>
</div>
<div class="assi-row">
<div class="assi-label">
<legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="75" rows="4" maxlength="500"></textarea>
</div>
</div>
<div class="assi-row">
<button onclick="validerFormulaire(this)">Enregistrer</button>
<button onclick="effacerFormulaire()">Remettre à zero</button>
</div>
</fieldset>
</section>
<section class="assi-liste">
{{tableau | safe }}
</section>
</div>
<style>
.assi-row {
margin: 5px 0;
}
.assi-form fieldset {
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.pageContent {
max-width: var(--sco-content-max-width);
margin-top: 15px;
}
.assi-label {
margin: 0 10px;
}
[required]::after {
content: "*";
color: var(--color-error);
}
</style>
{% include "sco_timepicker.j2" %}
<script>
function validateFields() {
const field = document.querySelector('.assi-form')
const { deb, fin } = getDates()
const date_debut = new Date(deb);
const date_fin = new Date(fin);
if (deb == "" || fin == "" || !date_debut.isValid() || !date_fin.isValid()) {
openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin valide."), "", color = "crimson");
return false;
}
if (date_fin.isBefore(date_debut)) {
openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson");
return false;
}
return true
}
function fieldsToAssiduite() {
const field = document.querySelector('.assi-form.page')
const { deb, fin } = getDates()
const etat = field.querySelector('#assi_etat').value;
const raison = field.querySelector('#raison').value;
const module = field.querySelector("#ajout_assiduite_module_impl").value;
return {
date_debut: new Date(deb).toFakeIso(),
date_fin: new Date(fin).toFakeIso(),
etat: etat,
description: raison,
moduleimpl_id: module,
}
}
function validerFormulaire(btn) {
if (!validateFields()) return
const assiduite = fieldsToAssiduite();
let assiduite_id = null;
createAssiduiteComplete(assiduite, etudid);
updateTableau();
btn.disabled = true;
setTimeout(() => {
btn.disabled = false;
}, 1000)
}
function effacerFormulaire() {
const field = document.querySelector('.assi-form')
field.querySelector('#assi_date_debut').value = "";
field.querySelector('#assi_date_fin').value = "";
field.querySelector('#assi_etat').value = "attente";
field.querySelector('#raison').value = "";
}
function dayOnly() {
const date_deb = document.getElementById("assi_date_debut");
const date_fin = document.getElementById("assi_date_fin");
if (document.getElementById('assi_journee').checked) {
date_deb.setAttribute("show", "date")
date_fin.setAttribute("show", "date")
document.querySelector(`legend[for="assi_date_fin"]`).removeAttribute("required")
} else {
date_deb.removeAttribute("show")
date_fin.removeAttribute("show")
document.querySelector(`legend[for="assi_date_fin"]`).setAttribute("required", "")
}
}
function getDates() {
const date_deb = document.querySelector(".page #assi_date_debut")
const date_fin = document.querySelector(".page #assi_date_fin")
const journee = document.querySelector('.page #assi_journee').checked
const deb = date_deb.valueAsObject.date + "T" + (journee ? assi_morning : date_deb.valueAsObject.time)
let fin = "T" + (journee ? assi_evening : date_fin.valueAsObject.time)
if (journee) {
fin = (date_fin.valueAsObject.date || date_deb.valueAsObject.date) + fin
} else {
fin = date_fin.valueAsObject.date + fin
}
return {
"deb": deb,
"fin": fin,
}
}
const etudid = {{ sco.etud.id }};
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
const assi_morning = '{{assi_morning}}';
const assi_evening = '{{assi_evening}}';
{% if saisie_eval %}
const saisie_eval = true;
const date_deb = "{{date_deb}}";
const date_fin = "{{date_fin}}";
const moduleimpl = {{ moduleimpl_id }};
{% else %}
const saisie_eval = false;
{% endif %}
window.addEventListener("load", () => {
document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() });
dayOnly()
if (saisie_eval) {
document.getElementById("assi_date_debut").value = Date.removeUTC(date_deb);
document.getElementById("assi_date_fin").value = Date.removeUTC(date_fin);
} else {
const today = (new Date()).format("YYYY-MM-DD");
document.getElementById("assi_date_debut").valueAsObject = { date: today, time: assi_morning }
document.getElementById("assi_date_fin").valueAsObject = { time: assi_evening }
}
document.getElementById("assi_date_debut").addEventListener("blur", (event) => {
updateSelect(null, "#ajout_assiduite_module_impl", event.target.valueAsObject.date)
})
updateSelect(saisie_eval ? moduleimpl : "", "#ajout_assiduite_module_impl", document.getElementById("assi_date_debut").valueAsObject.date);
});
</script>
{% endblock pageContent %}

View File

@ -1,94 +0,0 @@
{% block pageContent %}
<div class="pageContent">
<h3>Assiduites et justificatifs de <span class="rouge">{{sem}}</span> </h3>
{% include "assiduites/widgets/tableau_base.j2" %}
<h4>Assiduité :</h4>
<span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %}
<h4>Justificatifs :</h4>
<span class="iconline">
<a class="icon filter" onclick="filterJusti()"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %}
</div>
<script>
const formsemestre_id = {{ formsemestre_id }};
function getFormSemestreAssiduites(action) {
const path = getUrl() + `/api/assiduites/formsemestre/${formsemestre_id}`
async_get(
path,
(data, status) => {
if (action) {
action(data)
} else {
assiduiteCallBack(data);
}
},
(data, status) => {
console.error(data, status)
errorAlert();
}
)
}
function getFormSemestreJustificatifs(action) {
const path = getUrl() + `/api/justificatifs/formsemestre/${formsemestre_id}`
async_get(
path,
(data, status) => {
if (action) {
action(data)
} else {
justificatifCallBack(data);
}
},
(data, status) => {
console.error(data, status)
errorAlert();
}
)
}
function getAssi(action) {
try { getFormSemestreAssiduites(action) } catch (_) { }
}
function getJusti(action) {
try { getFormSemestreJustificatifs(action) } catch (_) { }
}
window.addEventListener('load', () => {
filterJustificatifs = {
"columns": [
"etudid",
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
}
}
filterAssiduites = {
columns: [
"etudid", "entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"
],
"filters": {
}
}
loadAll();
})
</script>
{% endblock pageContent %}

View File

@ -1,160 +0,0 @@
{# -*- mode: jinja-html -*- #}
{% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
<div id="page-assiduite-content">
{% block content %}
<h2>Signalement de l'assiduité de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
<div class="infos">
Date: <span id="datestr"></span>
<input type="text" class="datepicker" name="tl_date" id="tl_date" value="{{ date }}">
</div>
{{timeline|safe}}
<div>
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
<button class="btn" onclick="fastJustify(getCurrentAssiduite(etudid))" id="justif-rapide">Justifier</button>
</div>
<div class="btn_group">
<button class="btn" onclick="setTimeLineTimes({{morning}},{{afternoon}})">Journée</button>
<button class="btn" onclick="setTimeLineTimes({{morning}},{{lunch}})">Matin</button>
<button class="btn" onclick="setTimeLineTimes({{lunch}},{{afternoon}})">Après-midi</button>
</div>
<div class="etud_holder">
<div id="etud_row_{{sco.etud.id}}">
<div class="index"></div>
</div>
</div>
<hr>
{% if saisie_eval %}
<div id="saisie_eval">
<br>
<h3>
La saisie de l'assiduité a été préconfigurée en fonction de l'évaluation. <br>
Une fois la saisie finie, cliquez sur le lien si dessous pour revenir sur la gestion de l'évaluation
</h3>
<a href="{{redirect_url}}">retourner sur la page de l'évaluation</a>
</div>
{% endif %}
{{diff | safe}}
<div class="legende">
<h3>Explication de la timeline</h3>
<p>
Si la période indiquée par la timeline provoque un conflit d'assiduité pour un étudiant sa ligne deviendra
rouge.
<br>
Dans ce cas il faut résoudre manuellement le conflit : cliquez sur un des boutons d'assiduités pour ouvrir
le
résolveur de conflit.
<br>
Correspondance des couleurs :
</p>
<ul>
{% include "assiduites/widgets/legende_couleur.j2" %}
</ul>
<p>Vous pouvez justifier rapidement une assiduité en saisisant l'assiduité puis en appuyant sur "Justifier"</p>
<h3>Explication de la saisie différée</h3>
<p>Si la colonne n'est pas valide elle sera affichée en rouge, passez le curseur sur la colonne pour afficher
le message d'erreur</p>
<p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance
(préférence de département)</p>
<p>Modifier le module alors que des informations sont déjà enregistrées pour la période changera leur
module.</p>
<p>Il y a 4 boutons sur la colonne permettant d'enregistrer l'information pour tous les étudiants</p>
<p>Le dernier des boutons retire l'information présente.</p>
<p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p>
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la
colonne.
</p>
</div>
<!-- Ajout d'un conteneur pour le loader -->
<div class="loader-container" id="loaderContainer">
<div class="loader"></div>
</div>
<script>
const etudid = {{ sco.etud.id }};
const nonWorkDays = [{{ nonworkdays| safe }}];
setupDate(() => {
if (updateDate()) {
actualizeEtud(etudid);
updateSelect();
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
onlyAbs();
}
});
setupTimeLine(() => {
if(document.querySelector('.etud_holder .placeholder') != null){
generateAllEtudRow();
}
updateJustifyBtn();
});
window.addEventListener("DOMContentLoaded", () => {
updateDate();
getSingleEtud(etudid);
actualizeEtud(etudid);
updateSelect()
updateJustifyBtn();
})
function setTimeLineTimes(a, b) {
setPeriodValues(a, b);
updateJustifyBtn();
}
window.forceModule = "{{ forcer_module }}"
window.forceModule = window.forceModule == "True" ? true : false
const date_deb = "{{date_deb}}";
const date_fin = "{{date_fin}}";
{% if saisie_eval %}
createColumn(
date_deb,
date_fin,
{{ moduleimpl_id }}
);
window.location.href = "#saisie_eval"
getAndUpdateCol(1)
{% else %}
createColumn();
{% endif %}
</script>
<style>
.justifie {
background-color: rgb(104, 104, 252);
color: whitesmoke;
}
fieldset {
outline: none;
border: none;
}
</style>
{% endblock %}
</div>

View File

@ -1,156 +0,0 @@
<div>
{% if label != false%}
<label for="moduleimpl_select">
Module
</label>
{% else %}
{% endif %}
{% if moduleid %}
<select id="{{moduleid}}" class="dynaSelect">
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
</select>
{% else %}
<select id="moduleimpl_select" class="dynaSelect">
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
</select>
{% endif %}
<div id="saved" style="display: none;">
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
</div>
</div>
<script>
function getEtudFormSemestres() {
let semestre = {};
sync_get(getUrl() + `/api/etudiant/etudid/${etudid}/formsemestres`, (data) => {
semestre = data;
});
return semestre;
}
function filterFormSemestres(semestres, dateIso) {
const date = new Date(Date.removeUTC(dateIso));
semestres = semestres.filter((fm) => {
return date.isBetween(new Date(Date.removeUTC(fm.date_debut_iso)), new Date(Date.removeUTC(fm.date_fin_iso)), '[]');
})
return semestres;
}
function getFormSemestreProgramme(fm_id) {
let semestre = {};
sync_get(getUrl() + `/api/formsemestre/${fm_id}/programme`, (data) => {
semestre = data;
});
return semestre;
}
function getModulesImplByFormsemestre(semestres) {
const map = new Map();
semestres.forEach((fm) => {
const array = [];
const fm_p = getFormSemestreProgramme(fm.formsemestre_id);
["ressources", "saes", "modules"].forEach((r) => {
if (r in fm_p) {
fm_p[r].forEach((o) => {
array.push(getModuleInfos(o))
})
}
})
map.set(fm.titre_num, array)
})
return map;
}
function getModuleInfos(obj) {
return {
moduleimpl_id: obj.moduleimpl_id,
titre: obj.module.titre,
code: obj.module.code,
}
}
function populateSelect(sems, selected, query) {
const select = document.querySelector(query);
select.innerHTML = document.getElementById('saved').innerHTML
sems.forEach((mods, label) => {
const optGrp = document.createElement('optgroup');
optGrp.label = label
mods.forEach((obj) => {
const opt = document.createElement('option');
opt.value = obj.moduleimpl_id;
opt.textContent = `${obj.code} ${obj.titre}`
if (obj.moduleimpl_id == selected) {
opt.setAttribute('selected', 'true');
}
optGrp.appendChild(opt);
})
select.appendChild(optGrp);
})
if (selected === "autre") {
select.querySelector('option[value="autre"]').setAttribute('selected', 'true');
}
}
function updateSelect(moduleimpl_id, query = "#moduleimpl_select", dateIso = null) {
let sem = getEtudFormSemestres()
if (!dateIso) {
dateIso = getDate().format("YYYY-MM-DD")
}
sem = filterFormSemestres(sem, dateIso)
const mod = getModulesImplByFormsemestre(sem)
populateSelect(mod, moduleimpl_id, query);
}
function updateSelectedSelect(moduleimpl_id, query = "#moduleimpl_select") {
const mod_id = moduleimpl_id != null ? moduleimpl_id : ""
document.querySelector(query).value = `${mod_id}`.toLowerCase();
}
{% if moduleid %}
const moduleimpl_dynamic_selector_id = "{{moduleid}}"
{% else %}
const moduleimpl_dynamic_selector_id = "moduleimpl_select"
{% endif %}
window.addEventListener("load", () => {
document.getElementById(moduleimpl_dynamic_selector_id).addEventListener('change', (el) => {
const assi = getCurrentAssiduite(etudid);
if (assi) {
editAssiduite(assi.assiduite_id, assi.etat, [assi]);
}
})
try {
const conflicts = getAssiduitesConflict(etudid);
if (conflicts.length > 0) {
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
}
} catch { }
}, { once: true });
</script>
<style>
#moduleimpl_select {
width: 125px;
text-overflow: ellipsis;
}
</style>

View File

@ -1,10 +1,7 @@
{% if scu.is_assiduites_module_forced(request.args.get('formsemestre_id', None))%}
<option value="" disabled> Saisir Module</option>
{% if scu.is_assiduites_module_forced(formsemestre_id)%}
<option value="" disabled> Saisir Module</option>
{% else %}
<option value=""> Non spécifié </option>
{% endif %}
{% if moduleimpl_id == "autre" %}
<option value="autre" selected> Tout module </option>
{% else %}
<option value="autre"> Tout module </option>
<option value=""> Non spécifié </option>
{% endif %}
<option value="autre" {{ 'selected' if moduleimpl_id == 'autre' else '' }}>Autre module (pas dans la liste) [{{moduleimpl_id}}]</option>

View File

@ -1,465 +0,0 @@
<table id="assiduiteTable">
<thead>
<tr>
<th>
<div>
<span>Début</span>
<a class="icon order" onclick="order('date_debut', assiduiteCallBack, this)"></a>
</div>
</th>
<th>
<div>
<span>Fin</span>
<a class="icon order" onclick="order('date_fin', assiduiteCallBack, this)"></a>
</div>
</th>
<th>
<div>
<span>État</span>
<a class="icon order" onclick="order('etat', assiduiteCallBack, this)"></a>
</div>
</th>
<th>
<div>
<span>Module</span>
<a class="icon order" onclick="order('moduleimpl_id', assiduiteCallBack, this)"></a>
</div>
</th>
<th>
<div>
<span>Justifiée</span>
<a class="icon order" onclick="order('est_just', assiduiteCallBack, this)"></a>
</div>
</th>
</tr>
</thead>
<tbody id="tableBodyAssiduites">
</tbody>
</table>
<div id="paginationContainerAssiduites" class="pagination-container">
</div>
<div style="display: none;" id="cache-module">
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
</div>
<script>
const paginationContainerAssiduites = document.getElementById("paginationContainerAssiduites");
let currentPageAssiduites = 1;
let orderAssiduites = true;
let filterAssiduites = {
columns: [
"entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"
],
filters: {}
}
const tableBodyAssiduites = document.getElementById("tableBodyAssiduites");
function assiduiteCallBack(assi) {
assi = filterArray(assi, filterAssiduites.filters)
renderTableAssiduites(currentPageAssiduites, assi);
renderPaginationButtons(assi);
try { stats() } catch (_) { }
}
function renderTableAssiduites(page, assiduités) {
generateTableHead(filterAssiduites.columns, true)
tableBodyAssiduites.innerHTML = "";
const start = (page - 1) * itemsPerPage;
const end = start + itemsPerPage;
assiduités.slice(start, end).forEach((assiduite) => {
const row = document.createElement("tr");
row.setAttribute('type', "assiduite");
row.setAttribute('obj_id', assiduite.assiduite_id);
const etat = assiduite.etat.toLowerCase();
row.classList.add(`l-${etat}`);
filterAssiduites.columns.forEach((k) => {
const td = document.createElement('td');
if (k.indexOf('date') != -1) {
td.textContent = new Date(Date.removeUTC(assiduite[k])).format(`DD/MM/Y HH:mm`)
} else if (k.indexOf("module") != -1) {
td.textContent = getModuleImpl(assiduite);
} else if (k.indexOf('est_just') != -1) {
td.textContent = assiduite[k] ? "Oui" : "Non"
if (assiduite[k]) row.classList.add("est_just")
} else if (k.indexOf('etudid') != -1) {
const e = getEtudiant(assiduite.etudid);
td.innerHTML = `<a class="etudinfo" id="line-${assiduite.etudid}" href="bilan_etud?etudid=${assiduite.etudid}">${e.prenom.capitalize()} ${e.nom.toUpperCase()}</a>`;
} else {
td.textContent = assiduite[k].capitalize()
}
row.appendChild(td)
})
row.addEventListener("contextmenu", openContext);
tableBodyAssiduites.appendChild(row);
});
updateActivePaginationButton();
}
function detailAssiduites(assiduite_id) {
const path = getUrl() + `/api/assiduite/${assiduite_id}`;
async_get(
path,
(data) => {
const user = getUser(data);
const module = getModuleImpl(data);
const date_debut = new Date(Date.removeUTC(data.date_debut)).format("DD/MM/YYYY HH:mm");
const date_fin = new Date(Date.removeUTC(data.date_fin)).format("DD/MM/YYYY HH:mm");
const entry_date = new Date(Date.removeUTC(data.entry_date)).format("DD/MM/YYYY HH:mm");
const etat = data.etat.capitalize();
const desc = data.desc == null ? "" : data.desc;
const id = data.assiduite_id;
const est_just = data.est_just ? "Oui" : "Non";
const html = `
<div class="obj-detail">
<div class="obj-dates">
<div id="date_debut" class="obj-part">
<span class="obj-title">Date de début</span>
<span class="obj-content">${date_debut}</span>
</div>
<div id="date_fin" class="obj-part">
<span class="obj-title">Date de fin</span>
<span class="obj-content">${date_fin}</span>
</div>
<div id="entry_date" class="obj-part">
<span class="obj-title">Date de saisie</span>
<span class="obj-content">${entry_date}</span>
</div>
</div>
<div class="obj-mod">
<div id="module" class="obj-part">
<span class="obj-title">Module</span>
<span class="obj-content">${module}</span>
</div>
<div id="etat" class="obj-part">
<span class="obj-title">Etat</span>
<span class="obj-content">${etat}</span>
</div>
<div id="user" class="obj-part">
<span class="obj-title">par</span>
<span class="obj-content">${user}</span>
</div>
</div>
<div class="obj-rest">
<div id="est_just" class="obj-part">
<span class="obj-title">Justifié</span>
<span class="obj-content">${est_just}</span>
</div>
<div id="desc" class="obj-part">
<span class="obj-title">Description</span>
<p class="obj-content">${desc}</p>
</div>
<div id="id" class="obj-part" data-assiduite-id="${id}">
</div>
</div>
</div>
`
const el = document.createElement('div');
el.innerHTML = html;
openAlertModal("Détails", el.firstElementChild, null, "var(--color-information)")
}
)
}
function editionAssiduites(assiduite_id) {
const path = getUrl() + `/api/assiduite/${assiduite_id}`;
async_get(
path,
(data) => {
let module = data.moduleimpl_id;
if (
module == null && data.hasOwnProperty("external_data") &&
data.external_data != null &&
data.external_data.hasOwnProperty('module')
) {
module = data.external_data.module.toLowerCase();
}
const etat = data.etat;
let desc = data.desc == null ? "" : data.desc;
const html = `
<div class="assi-edit">
<div class="assi-edit-part">
<legend>État de l'assiduité</legend>
<select name="etat" id="etat">
<option value="present">Présent</option>
<option value="retard">En Retard</option>
<option value="absent">Absent</option>
</select>
</div>
<div class="assi-edit-part">
<legend>Module</legend>
<select name="module" id="module">
</select>
</div>
<div class="assi-edit-part">
<legend>Description</legend>
<textarea name="desc" id="desc" cols="50" rows="10" maxlength="500"></textarea>
</div>
</div>
`
const el = document.createElement('div')
el.innerHTML = html;
const assiEdit = el.firstElementChild;
assiEdit.querySelector('#etat').value = etat.toLowerCase();
assiEdit.querySelector('#desc').value = desc != null ? desc : "";
updateSelect(module, '#moduleimpl_select', data.date_debut.split('T')[0])
assiEdit.querySelector('#module').replaceWith(document.querySelector('#moduleimpl_select').cloneNode(true));
openPromptModal("Modification de l'assiduité", assiEdit, () => {
const prompt = document.querySelector('.assi-edit');
const etat = prompt.querySelector('#etat').value;
const desc = prompt.querySelector('#desc').value;
let module = prompt.querySelector('#moduleimpl_select').value;
let edit = {
"etat": etat,
"desc": desc,
"external_data": data.external_data
}
edit = setModuleImplId(edit, module);
fullEditAssiduites(data.assiduite_id, edit, () => {
loadAll();
})
}, () => { }, "var(--color-information)");
}
);
}
function fullEditAssiduites(assiduite_id, obj, call = () => { }) {
const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`;
async_post(
path,
obj,
call,
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
}
function filterAssi() {
let html = `
<div class="filter-body">
<h3>Affichage des colonnes:</h3>
<div class="filter-head">
<label>
Date de saisie
<input class="chk" type="checkbox" name="entry_date" id="entry_date">
</label>
<label>
Date de Début
<input class="chk" type="checkbox" name="date_debut" id="date_debut" checked>
</label>
<label>
Date de Fin
<input class="chk" type="checkbox" name="date_fin" id="date_fin" checked>
</label>
<label>
Etat
<input class="chk" type="checkbox" name="etat" id="etat" checked>
</label>
<label>
Module
<input class="chk" type="checkbox" name="moduleimpl_id" id="moduleimpl_id" checked>
</label>
<label>
Justifiée
<input class="chk" type="checkbox" name="est_just" id="est_just" checked>
</label>
</div>
<hr>
<h3>Filtrage des colonnes:</h3>
<span class="filter-line">
<span class="filter-title" for="entry_date">Date de saisie</span>
<select name="entry_date_pref" id="entry_date_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="entry_date_time" id="entry_date_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_debut">Date de début</span>
<select name="date_debut_pref" id="date_debut_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_debut_time" id="date_debut_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_fin">Date de fin</span>
<select name="date_fin_pref" id="date_fin_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_fin_time" id="date_fin_time">
</span>
<span class="filter-line">
<span class="filter-title" for="etat">Etat</span>
<input checked type="checkbox" name="etat_present" id="etat_present" class="rbtn present" value="present">
<input checked type="checkbox" name="etat_retard" id="etat_retard" class="rbtn retard" value="retard">
<input checked type="checkbox" name="etat_absent" id="etat_absent" class="rbtn absent" value="absent">
</span>
<span class="filter-line">
<span class="filter-title" for="moduleimpl_id">Module</span>
<select id="moduleimpl_id">
<option value="">Pas de filtre</option>
</select>
</span>
<span class="filter-line">
<span class="filter-title" for="est_just">Est Justifiée</span>
<select id="est_just">
<option value="">Pas de filtre</option>
<option value="true">Oui</option>
<option value="false">Non</option>
</select>
</span>
<span class="filter-line">
<span class="filter-title" for="etud">Rechercher dans les étudiants</span>
<input type="text" name="etud" id="etud" placeholder="Anne Onymous" >
</span>
</div>
`;
const span = document.createElement('span');
span.innerHTML = html
html = span.firstElementChild
const filterHead = html.querySelector('.filter-head');
filterHead.innerHTML = ""
let cols = ["etudid", "entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"];
cols.forEach((k) => {
const label = document.createElement('label')
label.classList.add('f-label')
const s = document.createElement('span');
s.textContent = columnTranslator(k);
const input = document.createElement('input');
input.classList.add('chk')
input.type = "checkbox"
input.name = k
input.id = k;
input.checked = filterAssiduites.columns.includes(k)
label.appendChild(s)
label.appendChild(input)
filterHead.appendChild(label)
})
const sl = html.querySelector('.filter-line #moduleimpl_id');
let opts = []
Object.keys(moduleimpls).forEach((k) => {
const opt = document.createElement('option');
opt.value = k == null ? "null" : k;
opt.textContent = moduleimpls[k];
opts.push(opt);
})
opts = opts.sort((a, b) => {
return a.value < b.value
})
sl.append(...opts);
// Mise à jour des filtres
Object.keys(filterAssiduites.filters).forEach((key) => {
const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement;
if (key.indexOf('date') != -1) {
l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref;
l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm");
} else if (key.indexOf('etat') != -1) {
l.querySelectorAll('input').forEach((e) => {
e.checked = filterAssiduites.filters[key].includes(e.value)
})
} else if (key.indexOf("module") != -1) {
l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key];
} else if (key.indexOf("est_just") != -1) {
l.querySelector('#est_just').value = filterAssiduites.filters[key];
} else if (key == "etud") {
l.querySelector('#etud').value = filterAssiduites.filters["etud"];
}
})
openPromptModal("Filtrage des assiduités", html, () => {
const columns = [...document.querySelectorAll('.chk')]
.map((el) => { if (el.checked) return el.id })
.filter((el) => el)
filterAssiduites.columns = columns
filterAssiduites.filters = {}
//reste des filtres
const lines = [...document.querySelectorAll('.filter-line')];
lines.forEach((l) => {
const key = l.querySelector('.filter-title').getAttribute('for');
if (key.indexOf('date') != -1) {
const pref = l.querySelector(`#${key}_pref`).value;
const time = l.querySelector(`#${key}_time`).value;
if (l.querySelector(`#${key}_time`).value != "") {
filterAssiduites.filters[key] = {
pref: pref,
time: new Date(Date.removeUTC(time))
}
}
} else if (key.indexOf('etat') != -1) {
filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value);
} else if (key.indexOf("module") != -1) {
filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value;
} else if (key.indexOf("est_just") != -1) {
filterAssiduites.filters[key] = l.querySelector('#est_just').value;
} else if (key == "etud") {
filterAssiduites.filters["etud"] = l.querySelector('#etud').value;
}
})
getAssi(assiduiteCallBack)
}, () => { }, "var(--color-primary)");
}
function downloadAssi() {
getAssi((d) => { toCSV(d, filterAssiduites) })
}
function getAssi(action) {
try { getAllAssiduitesFromEtud(etudid, action, true, true, assi_limit_annee) } catch (_) { }
}
</script>

View File

@ -2101,15 +2101,19 @@ def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> s
return render_template(
"assiduites/widgets/moduleimpl_selector.j2",
selected=selected,
formsemestre_id=formsemestre.id,
modules=modules,
moduleimpl_id=moduleimpl_id,
selected=selected,
)
def _module_selector_multiple(
etud: Identite, moduleimpl_id: int = None, only_form: FormSemestre = None
) -> str:
"""menu HTML <select> pour choix moduleimpl
Prend les semestres de l'année, sauf si only_form est indiqué.
"""
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
choices = {}
for formsemestre_id in modimpls_by_formsemestre:
@ -2129,22 +2133,13 @@ def _module_selector_multiple(
return render_template(
"assiduites/widgets/moduleimpl_selector_multiple.j2",
choices=choices,
formsemestre_id=only_form.id
if only_form
else list(modimpls_by_formsemestre.keys())[0],
moduleimpl_id=moduleimpl_id,
)
def _dynamic_module_selector() -> str:
"""
_dynamic_module_selector retourne l'html/css/javascript du selecteur de module dynamique
Returns:
str: l'html/css/javascript du selecteur de module dynamique
"""
return render_template(
"assiduites/widgets/moduleimpl_dynamic_selector.j2",
)
def _timeline(formsemestre_id: int = None, heures=None) -> str:
"""
_timeline retourne l'html de la timeline

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.82"
SCOVERSION = "9.6.83"
SCONAME = "ScoDoc"