Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96

This commit is contained in:
Emmanuel Viennet 2024-01-20 19:42:26 +01:00
commit b91609950c
18 changed files with 306 additions and 407 deletions

View File

@ -151,7 +151,9 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
@as_json @as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def justificatifs_dept(dept_id: int = None, with_query: bool = False): def justificatifs_dept(dept_id: int = None, with_query: bool = False):
"""XXX TODO missing doc""" """
Renvoie tous les justificatifs d'un département (en ajoutant un champs "formsemestre" si possible)
"""
# Récupération du département et des étudiants du département # Récupération du département et des étudiants du département
dept: Departement = Departement.query.get(dept_id) dept: Departement = Departement.query.get(dept_id)

View File

@ -109,7 +109,6 @@ def do_abs_notify(
return # abort return # abort
# Vérification fréquence (pour ne pas envoyer de mails trop souvent) # Vérification fréquence (pour ne pas envoyer de mails trop souvent)
# TODO Mettre la fréquence dans les préférences assiduités
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq") abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
destinations_filtered = [] destinations_filtered = []
for email_addr in destinations: for email_addr in destinations:

View File

@ -763,5 +763,7 @@ def simple_invalidate_cache(obj: dict, etudid: str | int = None):
# Invalide les caches des tableaux de l'étudiant # Invalide les caches des tableaux de l'étudiant
sco_cache.RequeteTableauAssiduiteCache.delete_pattern( sco_cache.RequeteTableauAssiduiteCache.delete_pattern(
pattern=f"tableau-etud-{etudid}:*" pattern=f"tableau-etud-{etudid}*"
) )
# Invalide les tableaux "bilan dept"
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern=f"tableau-dept*")

View File

@ -400,7 +400,7 @@ class ValidationsSemestreCache(ScoDocCache):
class RequeteTableauAssiduiteCache(ScoDocCache): class RequeteTableauAssiduiteCache(ScoDocCache):
""" """
clé : "<titre_tableau>:<type_obj>:<show_pres>:<show_retard>>:<order_col>:<order>" clé : "<titre_tableau>:<type_obj>:<show_pres>:<show_retard>:<show_desc>:<order_col>:<order>"
Valeur = liste de dicts Valeur = liste de dicts
""" """

View File

@ -919,7 +919,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
<a class="btn" href="{ <a class="btn" href="{
url_for("assiduites.bilan_dept", url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre=formsemestre.id, formsemestre_id=formsemestre.id,
group_ids=group.id, group_ids=group.id,
)}"> )}">
<button>Justificatifs en attente</button></a> <button>Justificatifs en attente</button></a>

View File

@ -541,18 +541,6 @@ class BasePreferences:
"category": "abs", "category": "abs",
}, },
), ),
(
"abs_notify_max_freq",
{
"initvalue": 7,
"title": "Fréquence maximale de notification",
"explanation": "nb de jours minimum entre deux mails envoyés au même destinataire à propos d'un même étudiant ",
"size": 4,
"type": "int",
"convert_numbers": True,
"category": "abs",
},
),
( (
"abs_notify_abs_threshold", "abs_notify_abs_threshold",
{ {
@ -710,11 +698,23 @@ class BasePreferences:
"size": 10, "size": 10,
"title": "Seuil d'alerte des absences", "title": "Seuil d'alerte des absences",
"type": "int", "type": "int",
"explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )", "explanation": "Nombres d'absences limite avant alerte (utilisation de l'unité métrique ↑ )",
"category": "assi", "category": "assi",
"only_global": True, "only_global": True,
}, },
), ),
(
"abs_notify_max_freq",
{
"initvalue": 7,
"title": "Fréquence maximale de notification",
"explanation": "nb de jours minimum entre deux mails envoyés au même destinataire à propos d'un même étudiant ",
"size": 4,
"type": "int",
"convert_numbers": True,
"category": "abs",
},
),
# portal # portal
( (
"portal_url", "portal_url",

View File

@ -298,15 +298,11 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No
return None if convert else False return None if convert else False
def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: def localize_datetime(date: datetime.datetime) -> datetime.datetime:
"""Transforme une date sans offset en une date avec offset """Transforme une date sans offset en une date avec offset
Tente de mettre l'offset de la timezone du serveur (ex : UTC+1) Tente de mettre l'offset de la timezone du serveur (ex : UTC+1)
Si erreur, mettra l'offset UTC Si erreur, mettra l'offset UTC
TODO : vérifier puis supprimer l'auto conversion str-> datetime
""" """
if isinstance(date, str):
date = is_iso_formated(date, convert=True)
new_date: datetime.datetime = date new_date: datetime.datetime = date
if new_date.tzinfo is None: if new_date.tzinfo is None:

View File

@ -1,3 +1,4 @@
// TODO : Supprimer les fonctions non utilisées + optimiser les fonctions utilisées
// <=== CONSTANTS and GLOBALS ===> // <=== CONSTANTS and GLOBALS ===>
let url; let url;

View File

@ -140,6 +140,7 @@ class ListeAssiJusti(tb.Table):
type_obj, type_obj,
self.options.show_pres, self.options.show_pres,
self.options.show_reta, self.options.show_reta,
self.options.show_desc,
self.options.order[0], self.options.order[0],
self.options.order[1], self.options.order[1],
], ],
@ -152,12 +153,18 @@ class ListeAssiJusti(tb.Table):
assiduites_query_etudiants = self.table_data.assiduites_query assiduites_query_etudiants = self.table_data.assiduites_query
# Non affichage des présences # Non affichage des présences
if not self.options.show_pres: if (
not self.options.show_pres
and assiduites_query_etudiants is not None
):
assiduites_query_etudiants = assiduites_query_etudiants.filter( assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.PRESENT Assiduite.etat != EtatAssiduite.PRESENT
) )
# Non affichage des retards # Non affichage des retards
if not self.options.show_reta: if (
not self.options.show_reta
and assiduites_query_etudiants is not None
):
assiduites_query_etudiants = assiduites_query_etudiants.filter( assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.RETARD Assiduite.etat != EtatAssiduite.RETARD
) )
@ -266,7 +273,7 @@ class ListeAssiJusti(tb.Table):
] ]
if self.options.show_desc: if self.options.show_desc:
assiduites_entities.append(Assiduite.description.label("description")) assiduites_entities.append(Assiduite.description.label("desc"))
query_assiduite = query_assiduite.with_entities(*assiduites_entities) query_assiduite = query_assiduite.with_entities(*assiduites_entities)
queries.append(query_assiduite) queries.append(query_assiduite)
@ -288,7 +295,7 @@ class ListeAssiJusti(tb.Table):
] ]
if self.options.show_desc: if self.options.show_desc:
justificatifs_entities.append(Justificatif.raison.label("description")) justificatifs_entities.append(Justificatif.raison.label("desc"))
query_justificatif = query_justificatif.with_entities( query_justificatif = query_justificatif.with_entities(
*justificatifs_entities *justificatifs_entities
@ -466,7 +473,7 @@ class RowAssiJusti(tb.Row):
self.add_cell( self.add_cell(
"description", "description",
"Description", "Description",
self.ligne["description"] if self.ligne["description"] else "", self.ligne["desc"] if self.ligne["desc"] else "",
) )
if self.table.options.show_module: if self.table.options.show_module:
if self.ligne["type"] == "assiduite": if self.ligne["type"] == "assiduite":

View File

@ -1,187 +1,27 @@
{% include "assiduites/widgets/tableau_base.j2" %} {% extends "sco_page.j2" %}
<section class="alerte invisible"> {% block styles %}
<p>Attention, cet étudiant a trop d'absences</p> {{super()}}
</section> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock styles %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% endblock scripts %}
{% block app_content %}
<h2>Traitement de l'assiduité</h2>
<p class="help">
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
le semestre concerné (saisie par jour ou saisie différée).
</p>
<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
choisissez d'abord la personne concernée&nbsp;:</p>
<br>
{{search_etud | safe}}
<br>
{{billets | safe}}
<br>
<section class="nonvalide"> <section class="nonvalide">
<!-- Tableaux des justificatifs à valider (attente / modifié ) --> {{tableau | safe }}
<h4>Justificatifs en attente (ou modifiés)</h4>
<span class="iconline">
<a class="icon filter" onclick="filterJusti(true)"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %}
</section> </section>
{% endblock app_content %}
<div class="annee">
<span>Année scolaire 2022-2023 Changer année: </span>
<select name="" id="annee" onchange="setterAnnee(this.value)">
</select>
</div>
<div class="legende">
<h3>Gestion des justificatifs</h3>
<p>
Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu contextuel :
<ul>
<li>Détails : Affiche les détails du justificatif sélectionné</li>
<li>Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)</li>
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
</ul>
</p>
</div>
<script>
let formsemestre_id = "{{formsemestre_id}}"
let group_id = "{{group_id}}"
function getDeptJustificatifsFromPeriod(action) {
const formsemestre = formsemestre_id ? `&formsemestre_id=${formsemestre_id}` : ""
const group = group_id ? `&group_id=${group_id}` : ""
const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}${formsemestre}${group}`
async_get(
path,
(data, status) => {
if (action) {
action(data)
} else {
justificatifCallBack(data);
}
},
(data, status) => {
console.error(data, status)
errorAlert();
}
)
}
function generate(annee) {
if (annee < 1999 || annee > 2999) {
openAlertModal("Année impossible", document.createTextNode("L'année demandé n'existe pas."));
return;
}
bornes = {
deb: `${annee}-09-01T00:00`,
fin: `${annee + 1}-08-31T23:59`
}
defAnnee = annee;
loadAll();
}
function getJusti(action) {
try { getDeptJustificatifsFromPeriod(action) } catch (_) { }
}
function setterAnnee(annee) {
annee = parseInt(annee);
document.querySelector('.annee span').textContent = `Année scolaire ${annee}-${annee + 1} Changer année: `
generate(annee)
}
let defAnnee = {{ annee }};
let bornes = {
deb: `${defAnnee}-09-01T00:00`,
fin: `${defAnnee + 1}-08-31T23:59`
}
const dept_id = {{ dept_id }};
let annees = {{ annees | safe}}
annees = annees.filter((x, i) => annees.indexOf(x) === i)
window.addEventListener('load', () => {
filterJustificatifs = {
"columns": [
"formsemestre",
"etudid",
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
"etat": [
"attente",
"modifie"
],
}
}
const select = document.querySelector('#annee');
annees.forEach((a) => {
const opt = document.createElement("option");
opt.value = a + "",
opt.textContent = `${a} - ${a + 1}`;
if (a === defAnnee) {
opt.selected = true;
}
select.appendChild(opt)
})
setterAnnee(defAnnee)
})
</script>
<style>
.stats-values-item {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-direction: column;
}
.stats {
border: 1px solid #333;
padding: 5px 2px;
width: fit-content;
}
.stats-values {
display: flex;
justify-content: flex-start;
gap: 15px;
}
.stats-values-item h5 {
font-weight: bold;
text-decoration-line: underline;
}
.stats-values-part {
display: flex;
flex-direction: column;
}
.alerte {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
border-radius: 7px;
background-color: var(--color-error);
}
.alerte.invisible {
display: none;
}
.alerte p {
font-size: larger;
color: whitesmoke;
}
.suppr {
margin: 5px 0;
}
</style>

View File

@ -25,27 +25,7 @@
</section> </section>
<section class="nonvalide"> <section class="nonvalide">
<!-- Tableaux des assiduités (retard/abs) non justifiées --> {{tableau | safe }}
<h4>Absences et retards non justifiés</h4>
{# TODO Utiliser python tableau plutot que js tableau #}
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
</div>
<span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %}
<!-- Tableaux des justificatifs à valider (attente / modifié ) -->
<h4>Justificatifs en attente (ou modifiés)</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" %}
</section> </section>
<section class="suppr"> <section class="suppr">
@ -60,29 +40,6 @@
département)</p> département)</p>
<p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates, <p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
appuyer sur le bouton "Actualiser"</p> appuyer sur le bouton "Actualiser"</p>
<h3>Gestion des justificatifs</h3>
<p>
Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel :
</p>
<ul>
<li>Détails : affiche les détails du justificatif sélectionné</li>
<li>Éditer : modifie le justificatif (dates, état, ajouter/supprimer fichier, etc.)</li>
<li>Supprimer : supprime le justificatif (action irréversible)</li>
</ul>
<h3>Gestion de l'assiduité</h3>
<p>
Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel :
</p>
<ul>
<li>Détails : affiche les détails de l'élément sélectionnée</li>
<li>Editer : modifie l'élément (module, état)</li>
<li>Supprimer : supprime l'élément (action irréversible)</li>
</ul>
</div> </div>
</div> </div>
@ -275,48 +232,8 @@
window.addEventListener('load', () => { window.addEventListener('load', () => {
filterAssiduites = {
"columns": [
"entry_date",
"date_debut",
"date_fin",
"etat",
"moduleimpl_id",
"est_just"
],
"filters": {
"etat": [
"retard",
"absent"
],
"moduleimpl_id": "",
"est_just": "false"
}
};
filterJustificatifs = {
"columns": [
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
"etat": [
"attente",
"modifie"
]
}
}
document.getElementById('stats_date_fin').value = assi_date_fin; document.getElementById('stats_date_fin').value = assi_date_fin;
document.getElementById('stats_date_debut').value = assi_date_debut; document.getElementById('stats_date_debut').value = assi_date_debut;
loadAll();
stats(); stats();
}) })

View File

@ -1,5 +1,14 @@
{#
- TODO : revoir le fonctionnement de cette page (trop lente / complexe)
- Utiliser majoritairement du python
#}
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2> <h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
</div>
<h3>{{sem | safe }}</h3> <h3>{{sem | safe }}</h3>
{{diff | safe}} {{diff | safe}}

View File

@ -3,5 +3,6 @@
<div class="assiduite-period">{{date_debut}}</div> <div class="assiduite-period">{{date_debut}}</div>
<div class="assiduite-period">{{date_fin}}</div> <div class="assiduite-period">{{date_fin}}</div>
<div class="assiduite-state">État: {{etat}}</div> <div class="assiduite-state">État: {{etat}}</div>
<div class="assiduite-why">Motif: {{motif}}</div>
<div class="assiduite-user_id">{{saisie}}</div> <div class="assiduite-user_id">{{saisie}}</div>
</div> </div>

View File

@ -77,12 +77,6 @@
const duration = (endTime - startTime) / 1000 / 60; const duration = (endTime - startTime) / 1000 / 60;
const percent = (duration / (t_end * 60 - t_start * 60)) * 100 const percent = (duration / (t_end * 60 - t_start * 60)) * 100
if (percent > 100) {
console.log(start, end);
console.log(startTime, endTime)
}
return percent + "%"; return percent + "%";
} }
@ -253,13 +247,11 @@
*/ */
splitAssiduiteModal() { splitAssiduiteModal() {
//Préparation du prompt //Préparation du prompt
// TODO utiliser timepicker jquery + utiliser les bornes (t_start et t_end) const htmlPrompt = `<legend>Entrez l'heure de séparation</legend>
const htmlPrompt = `<legend>Entrez l'heure de séparation (HH:mm) :</legend> <input type="text" id="promptTime" name="appt"required style="position: relative; z-index: 100000;">`;
<input type="time" id="promptTime" name="appt"
min="08:00" max="18:00" required>`;
const fieldSet = document.createElement("fieldset"); const fieldSet = document.createElement("fieldset");
fieldSet.classList.add("fieldsplit"); fieldSet.classList.add("fieldsplit", "timepicker");
fieldSet.innerHTML = htmlPrompt; fieldSet.innerHTML = htmlPrompt;
//Callback de division //Callback de division
@ -317,11 +309,28 @@
"L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée." "L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée."
); );
openAlertModal("Attention", att, "", "var(--color-warning))"); openAlertModal("Attention", att, "", "var(--color-warning)");
} }
}; };
openPromptModal("Séparation de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)"); openPromptModal("Séparation de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)");
// Initialisation du timepicker
const deb = this.selectedAssiduite.date_debut.substring(11,16);
const fin = this.selectedAssiduite.date_fin.substring(11,16);
setTimeout(()=>{
$('#promptTime').timepicker({
timeFormat: 'HH:mm',
interval: 60 * tick_delay,
minTime: deb,
startTime: deb,
maxTime: fin,
dynamic: false,
dropdown: true,
scrollbar: false,
});
}, 100
);
} }
/** /**
@ -467,3 +476,8 @@
} }
} }
</script> </script>
<style>
.ui-timepicker-container {
z-index: 100000 !important;
}
</style>

View File

@ -157,6 +157,11 @@
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`; stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv); bubble.appendChild(stateDiv);
const motifDiv = document.createElement("div");
stateDiv.className = "assiduite-why";
stateDiv.textContent = `Motif: ${assiduite.desc?.capitalize()}`;
bubble.appendChild(motifDiv);
const userIdDiv = document.createElement("div"); const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id"; userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisie le ${formatDateModal( userIdDiv.textContent = `saisie le ${formatDateModal(

View File

@ -0,0 +1,20 @@
<select name="moduleimpl_select" id="moduleimpl_select">
{% with moduleimpl_id=moduleimpl_id %}
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
{% endwith %}
{% for cat, mods in choices.items() %}
<optgroup label="{{cat}}">
{% for mod in mods %}
{% if mod.moduleimpl_id == moduleimpl_id %}
<option value="{{mod.moduleimpl_id}}" selected> {{mod.name}} </option>
{% else %}
<option value="{{mod.moduleimpl_id}}"> {{mod.name}} </option>
{% endif %}
{% endfor %}
</optgroup>
{% endfor %}
</select>

View File

@ -178,62 +178,25 @@ class HTMLBuilder:
def bilan_dept(): def bilan_dept():
"""Gestionnaire assiduités, page principale""" """Gestionnaire assiduités, page principale"""
# Préparation de la page
H = [
html_sco_header.sco_header(
page_title="Saisie de l'assiduité",
javascripts=[
"js/assiduites.js",
"js/date_utils.js",
],
cssstyles=[
"css/assiduites.css",
],
),
"""<h2>Traitement de l'assiduité</h2>
<p class="help">
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
le semestre concerné (saisie par jour ou saisie différée).
</p>
""",
]
H.append(
"""<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
choisissez d'abord la personne concernée&nbsp;:</p>"""
)
# Ajout de la barre de recherche d'étudiant (redirection vers bilan etud)
H.append(sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud"))
# Gestion des billets d'absences # Gestion des billets d'absences
if current_user.has_permission( if current_user.has_permission(
Permission.AbsChange Permission.AbsChange
) and sco_preferences.get_preference("handle_billets_abs"): ) and sco_preferences.get_preference("handle_billets_abs"):
H.append( billets = f"""
f"""
<h2 style="margin-top: 30px;">Billets d'absence</h2> <h2 style="margin-top: 30px;">Billets d'absence</h2>
<ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept) <ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
}">Traitement des billets d'absence en attente</a> }">Traitement des billets d'absence en attente</a>
</li></ul> </li></ul>
""" """
) else:
billets = ""
# Récupération des années d'étude du département # Récupération du département
# (afin de sélectionner une année)
dept: Departement = Departement.query.filter_by(id=g.scodoc_dept_id).first() dept: Departement = Departement.query.filter_by(id=g.scodoc_dept_id).first()
annees: list[int] = sorted(
[f.date_debut.year for f in dept.formsemestres],
reverse=True,
)
annee = scu.annee_scolaire() # Année courante, sera utilisée par défaut
# Génération d'une liste "json" d'années
annees_str: str = "["
for ann in annees:
annees_str += f"{ann},"
annees_str += "]"
# Récupération d'un formsemestre # Récupération d'un formsemestre
# (pour n'afficher que les assiduites/justificatifs liés au formsemestre) # (pour n'afficher que les justificatifs liés au formsemestre)
formsemestre_id = request.args.get("formsemestre_id", "") formsemestre_id = request.args.get("formsemestre_id", "")
formsemestre = None
if formsemestre_id: if formsemestre_id:
try: try:
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
@ -241,19 +204,71 @@ def bilan_dept():
except AttributeError: except AttributeError:
formsemestre_id = "" formsemestre_id = ""
# Peuplement du template jinja # <=> Génération du tableau <=>
H.append(
render_template( # Récupération des étudiants du département / groupe
"assiduites/pages/bilan_dept.j2", etudids: list[int] = [etud.id for etud in dept.etudiants] # cas département
dept_id=g.scodoc_dept_id, group_ids = request.args.get("group_ids", "")
annee=annee, if group_ids and formsemestre:
annees=annees_str, groups_infos = sco_groups_view.DisplayedGroupsInfos(
formsemestre_id=formsemestre_id, group_ids.split(","),
group_id=request.args.get("group_id", ""), formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
)
if groups_infos.members:
etudids = [m["etudid"] for m in groups_infos.members]
# justificatifs (en attente ou modifiés avec les semestres associés)
justificatifs_query: Query = Justificatif.query.filter(
Justificatif.etat.in_(
[scu.EtatJustificatif.ATTENTE, scu.EtatJustificatif.MODIFIE]
), ),
Justificatif.etudid.in_(etudids),
)
# Filtrage par semestre si formsemestre_id != ""
if formsemestre:
justificatifs_query = justificatifs_query.filter(
Justificatif.date_debut >= formsemestre.date_debut,
Justificatif.date_debut <= formsemestre.date_fin,
)
data = liste_assi.AssiJustifData(
assiduites_query=None,
justificatifs_query=justificatifs_query,
)
fname: str = "Bilan Département"
cache_key: str = "tableau-dept"
titre: str = "Justificatifs en attente ou modifiés"
if formsemestre:
fname += f" {formsemestre.titre_annee()}"
cache_key += f"-{formsemestre.id}"
titre += f" {formsemestre.titre_annee()}"
if group_ids:
cache_key += f" {group_ids}"
table = _prepare_tableau(
data,
afficher_etu=True,
filename=fname,
titre=titre,
cache_key=cache_key,
)
if not table[0]:
return table[1]
# Peuplement du template jinja
return render_template(
"assiduites/pages/bilan_dept.j2",
tableau=table[1],
search_etud=sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud"),
billets=billets,
sco=ScoData(formsemestre=formsemestre),
) )
H.append(html_sco_header.sco_footer())
return "\n".join(H)
@bp.route("/ajout_assiduite_etud", methods=["GET", "POST"]) @bp.route("/ajout_assiduite_etud", methods=["GET", "POST"])
@ -601,6 +616,29 @@ def bilan_etud():
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id), sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
) )
# Récupération des assiduités et justificatifs de l'étudiant
data = liste_assi.AssiJustifData(
etud.assiduites.filter(
Assiduite.etat != scu.EtatAssiduite.PRESENT, Assiduite.est_just == False
),
etud.justificatifs.filter(
Justificatif.etat.in_(
[scu.EtatJustificatif.ATTENTE, scu.EtatJustificatif.MODIFIE]
)
),
)
table = _prepare_tableau(
data,
afficher_etu=False,
filename=f"Bilan assiduité {etud.nomprenom}",
titre="Bilan de l'assiduité de l'étudiant",
cache_key=f"tableau-etud-{etud.id}-bilan",
)
if not table[0]:
return table[1]
# Génération de la page # Génération de la page
return HTMLBuilder( return HTMLBuilder(
header, header,
@ -615,6 +653,7 @@ def bilan_etud():
"assi_limit_annee", "assi_limit_annee",
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
), ),
tableau=table[1],
), ),
).build() ).build()
@ -1599,18 +1638,7 @@ def tableau_assiduite_actions():
if obj_type == "assiduite": if obj_type == "assiduite":
# Construction du menu module # Construction du menu module
# XXX ca ne va pas car cela ne prend qu'un semestre module = _module_selector_multiple(objet.etudiant, objet.moduleimpl_id)
# TODO reprendre le menu de la page ajout_assiduite_etud
formsemestre = objet.get_formsemestre()
if formsemestre:
if objet.moduleimpl_id is not None:
module = objet.moduleimpl_id
elif objet.external_data is not None:
module = objet.external_data.get("module", "")
module = module.lower() if isinstance(module, str) else module
module = _module_selector(formsemestre, module)
else:
module = "pas de semestre correspondant"
return render_template( return render_template(
"assiduites/pages/tableau_assiduite_actions.j2", "assiduites/pages/tableau_assiduite_actions.j2",
@ -1818,7 +1846,7 @@ def signal_assiduites_diff():
) )
date_fin: datetime.date = date_deb + datetime.timedelta(days=6) date_fin: datetime.date = date_deb + datetime.timedelta(days=6)
etudiants: list[dict] = [] etudiants: list[Identite] = []
# --- Vérification de la date --- # --- Vérification de la date ---
real_date = scu.is_iso_formated(date, True).date() real_date = scu.is_iso_formated(date, True).date()
@ -1846,15 +1874,9 @@ def signal_assiduites_diff():
# Récupération des étudiants # Récupération des étudiants
etudiants.extend( etudiants.extend(
[ [Identite.get_etud(etudid=m["etudid"]) for m in groups_infos.members]
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members
]
) )
# XXX utiliser des instances d'Identite et non des dict etudiants = list(sorted(etudiants, key=lambda etud: etud.sort_key))
# puis trier avec etud.sort_key
# afin de bien prendre en compte nom usuel etc
etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
# Génération de l'HTML # Génération de l'HTML
@ -1962,9 +1984,7 @@ def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None):
"assiduites.ajout_assiduite_etud", "assiduites.ajout_assiduite_etud",
etudid=etudid, etudid=etudid,
evaluation_id=evaluation.id, evaluation_id=evaluation.id,
date_deb=evaluation.date_debut.strftime( date_deb=evaluation.date_debut.strftime("%Y-%m-%dT%H:%M:%S"),
"%Y-%m-%dT%H:%M:%S"
),
date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"), date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"),
moduleimpl_id=evaluation.moduleimpl.id, moduleimpl_id=evaluation.moduleimpl.id,
saisie_eval="true", saisie_eval="true",
@ -2234,6 +2254,32 @@ def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> s
) )
def _module_selector_multiple(
etud: Identite, moduleimpl_id: int = None, only_form: FormSemestre = None
) -> str:
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
choices = {}
for formsemestre_id in modimpls_by_formsemestre:
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
if only_form is not None and formsemestre != only_form:
continue
# indique le nom du semestre dans le menu (optgroup)
choices[formsemestre.titre_annee()] = [
{
"moduleimpl_id": m.id,
"name": 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
]
return render_template(
"assiduites/widgets/moduleimpl_selector_multiple.j2",
choices=choices,
moduleimpl_id=moduleimpl_id,
)
def _dynamic_module_selector() -> str: def _dynamic_module_selector() -> str:
""" """
_dynamic_module_selector retourne l'html/css/javascript du selecteur de module dynamique _dynamic_module_selector retourne l'html/css/javascript du selecteur de module dynamique
@ -2630,6 +2676,8 @@ def _generate_assiduite_bubble(assiduite: Assiduite) -> str:
# Récupérer informations saisie # Récupérer informations saisie
saisie: str = assiduite.get_saisie() saisie: str = assiduite.get_saisie()
motif: str = assiduite.description if assiduite.description else ""
return render_template( return render_template(
"assiduites/widgets/assiduite_bubble.j2", "assiduites/widgets/assiduite_bubble.j2",
moduleimpl=moduleimpl_infos, moduleimpl=moduleimpl_infos,
@ -2637,4 +2685,5 @@ def _generate_assiduite_bubble(assiduite: Assiduite) -> str:
date_debut=assiduite.date_debut.strftime("%d/%m/%Y %H:%M"), date_debut=assiduite.date_debut.strftime("%d/%m/%Y %H:%M"),
date_fin=assiduite.date_fin.strftime("%d/%m/%Y %H:%M"), date_fin=assiduite.date_fin.strftime("%d/%m/%Y %H:%M"),
saisie=saisie, saisie=saisie,
motif=motif,
) )

View File

@ -557,50 +557,65 @@ def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justific
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif).count() == 5 scass.filter_by_date(etud.justificatifs, Justificatif).count() == 5
), "Filtrage 'Toute Date' mauvais 1" ), "Filtrage 'Toute Date' mauvais 1"
date = scu.localize_datetime(
date = scu.localize_datetime("2022-09-01T10:00+01:00") scu.is_iso_formated("2022-09-01T10:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
== 5 == 5
), "Filtrage 'Toute Date' mauvais 2" ), "Filtrage 'Toute Date' mauvais 2"
date = scu.localize_datetime("2022-09-05T08:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T08:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
== 5 == 5
), "Filtrage 'date début' mauvais 3" ), "Filtrage 'date début' mauvais 3"
date = scu.localize_datetime("2022-09-05T08:00:01+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T08:00:01+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
== 5 == 5
), "Filtrage 'date début' mauvais 4" ), "Filtrage 'date début' mauvais 4"
date = scu.localize_datetime("2022-09-05T10:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T10:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
== 4 == 4
), "Filtrage 'date début' mauvais 5" ), "Filtrage 'date début' mauvais 5"
date = scu.localize_datetime("2022-09-01T10:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-01T10:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
== 0 == 0
), "Filtrage 'date fin' mauvais 6" ), "Filtrage 'date fin' mauvais 6"
date = scu.localize_datetime("2022-09-05T08:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T08:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
== 1 == 1
), "Filtrage 'date fin' mauvais 7" ), "Filtrage 'date fin' mauvais 7"
date = scu.localize_datetime("2022-09-05T10:00:01+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T10:00:01+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
== 2 == 2
), "Filtrage 'date fin' mauvais 8" ), "Filtrage 'date fin' mauvais 8"
date = scu.localize_datetime("2023-01-03T12:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2023-01-03T12:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
== 5 == 5
@ -624,8 +639,12 @@ def editer_supprimer_justificatif(etud: Identite):
# Modification de l'état # Modification de l'état
justi.etat = scu.EtatJustificatif.MODIFIE justi.etat = scu.EtatJustificatif.MODIFIE
# Modification du moduleimpl # Modification du moduleimpl
justi.date_debut = scu.localize_datetime("2023-02-03T11:00:01+01:00") justi.date_debut = scu.localize_datetime(
justi.date_fin = scu.localize_datetime("2023-02-03T12:00:01+01:00") scu.is_iso_formated("2023-02-03T11:00:01+01:00", convert=True)
)
justi.date_fin = scu.localize_datetime(
scu.is_iso_formated("2023-02-03T12:00:01+01:00", convert=True)
)
db.session.add(justi) db.session.add(justi)
db.session.commit() db.session.commit()
@ -639,7 +658,9 @@ def editer_supprimer_justificatif(etud: Identite):
scass.filter_by_date( scass.filter_by_date(
etud.justificatifs, etud.justificatifs,
Justificatif, Justificatif,
date_deb=scu.localize_datetime("2023-02-01T11:00:00+01:00"), date_deb=scu.localize_datetime(
scu.is_iso_formated("2023-02-01T11:00:00+01:00", convert=True)
),
).count() ).count()
== 1 == 1
), "Edition de justificatif mauvais 2" ), "Edition de justificatif mauvais 2"
@ -930,44 +951,60 @@ def verifier_comptage_et_filtrage_assiduites(
scass.filter_by_date(etu2.assiduites, Assiduite).count() == 7 scass.filter_by_date(etu2.assiduites, Assiduite).count() == 7
), "Filtrage 'Date début' mauvais 1" ), "Filtrage 'Date début' mauvais 1"
date = scu.localize_datetime("2022-09-01T10:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-01T10:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7 scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7
), "Filtrage 'Date début' mauvais 2" ), "Filtrage 'Date début' mauvais 2"
date = scu.localize_datetime("2022-09-05T10:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T10:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7 scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7
), "Filtrage 'Date début' mauvais 3" ), "Filtrage 'Date début' mauvais 3"
date = scu.localize_datetime("2022-09-05T16:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T16:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 4 scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 4
), "Filtrage 'Date début' mauvais 4" ), "Filtrage 'Date début' mauvais 4"
# Date Fin # Date Fin
date = scu.localize_datetime("2022-09-01T10:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-01T10:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 0 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 0
), "Filtrage 'Date fin' mauvais 1" ), "Filtrage 'Date fin' mauvais 1"
date = scu.localize_datetime("2022-09-05T10:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T10:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 1 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 1
), "Filtrage 'Date fin' mauvais 2" ), "Filtrage 'Date fin' mauvais 2"
date = scu.localize_datetime("2022-09-05T10:00:01+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T10:00:01+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 2 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 2
), "Filtrage 'Date fin' mauvais 3" ), "Filtrage 'Date fin' mauvais 3"
date = scu.localize_datetime("2022-09-05T16:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2022-09-05T16:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 3 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 3
), "Filtrage 'Date fin' mauvais 4" ), "Filtrage 'Date fin' mauvais 4"
date = scu.localize_datetime("2023-01-04T16:00+01:00") date = scu.localize_datetime(
scu.is_iso_formated("2023-01-04T16:00+01:00", convert=True)
)
assert ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 7 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 7
), "Filtrage 'Date fin' mauvais 5" ), "Filtrage 'Date fin' mauvais 5"