Assiduites : WIP tableaux actions (sauf modifier)

This commit is contained in:
Iziram 2023-11-24 13:58:03 +01:00
parent c0253bd05d
commit b13e751e1a
15 changed files with 801 additions and 125 deletions

View File

@ -3,8 +3,8 @@
"""
from datetime import datetime
from app import db, log
from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription
from app import db, log, g
from app.models import ModuleImpl, Module, Scolog, FormSemestre, FormSemestreInscription
from app.models.etudiants import Identite
from app.auth.models import User
from app.scodoc import sco_abs_notification
@ -204,6 +204,43 @@ class Assiduite(db.Model):
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
return nouv_assiduite
def supprimer(self):
from app.scodoc import sco_assiduites as scass
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
# route sans département
set_sco_dept(self.etudiant.departement.acronym)
obj_dict: dict = self.to_dict()
# Suppression de l'objet et LOG
log(f"delete_assidutite: {self.etudiant.id} {self}")
Scolog.logdb(
method=f"delete_assiduite",
etudid=self.etudiant.id,
msg=f"Assiduité: {self}",
)
db.session.delete(self)
# Invalidation du cache
scass.simple_invalidate_cache(obj_dict)
def get_formsemestre(self) -> FormSemestre:
return get_formsemestre_from_data(self.to_dict())
def get_module(self, traduire: bool = False) -> int | str:
if self.moduleimpl_id is not None:
if traduire:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
mod: Module = Module.query.get(modimpl.module_id)
return f"{mod.code} {mod.titre}"
elif self.external_data is not None and "module" in self.external_data:
return (
"Tout module"
if self.external_data["module"] == "Autre"
else self.external_data["module"]
)
return "Non spécifié" if traduire else None
class Justificatif(db.Model):
"""
@ -334,6 +371,39 @@ class Justificatif(db.Model):
)
return nouv_justificatif
def supprimer(self):
from app.scodoc import sco_assiduites as scass
# Récupération de l'archive du justificatif
archive_name: str = self.fichier
if archive_name is not None:
# Si elle existe : on essaye de la supprimer
archiver: JustificatifArchiver = JustificatifArchiver()
try:
archiver.delete_justificatif(self.etudiant, archive_name)
except ValueError:
pass
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
# route sans département
set_sco_dept(self.etudiant.departement.acronym)
# On invalide le cache
scass.simple_invalidate_cache(self.to_dict())
# Suppression de l'objet et LOG
log(f"delete_justificatif: {self.etudiant.id} {self}")
Scolog.logdb(
method=f"delete_justificatif",
etudid=self.etudiant.id,
msg=f"Justificatif: {self}",
)
db.session.delete(self)
# On actualise les assiduités justifiées de l'étudiant concerné
compute_assiduites_justified(
self.etudid,
Justificatif.query.filter_by(etudid=self.etudid).all(),
True,
)
def is_period_conflicting(
date_debut: datetime,

View File

@ -204,6 +204,13 @@ class EtatAssiduite(int, BiDirectionalEnum):
RETARD = 1
ABSENT = 2
def version_lisible(self) -> str:
return {
EtatAssiduite.PRESENT: "Présence",
EtatAssiduite.ABSENT: "Absence",
EtatAssiduite.RETARD: "Retard",
}.get(self, "")
class EtatJustificatif(int, BiDirectionalEnum):
"""Code des états des justificatifs"""
@ -215,6 +222,14 @@ class EtatJustificatif(int, BiDirectionalEnum):
ATTENTE = 2
MODIFIE = 3
def version_lisible(self) -> str:
return {
EtatJustificatif.VALIDE: "valide",
EtatJustificatif.ATTENTE: "soumis",
EtatJustificatif.MODIFIE: "modifié",
EtatJustificatif.NON_VALIDE: "invalide",
}.get(self, "")
def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None:
"""
@ -1480,6 +1495,7 @@ def is_assiduites_module_forced(
def get_assiduites_time_config(config_type: str) -> str:
from app.models import ScoDocSiteConfig
match config_type:
case "matin":
return ScoDocSiteConfig.get("assi_morning_time", "08:00:00")

View File

@ -448,6 +448,13 @@ class ScoDocDateTimePicker extends HTMLElement {
// Ajouter le style au shadow DOM
shadow.appendChild(style);
//Si une value est donnée
let value = this.getAttribute("value");
if (value != null) {
this.value = value;
}
}
static get observedAttributes() {

View File

@ -5,7 +5,7 @@ from datetime import datetime
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif
from flask_sqlalchemy.query import Query, Pagination
from sqlalchemy import union, literal, select, desc
from app import db
from app import db, g
from flask import url_for
from app import log
@ -16,14 +16,13 @@ class ListeAssiJusti(tb.Table):
L'affichage par défaut se fait par ordre de date de fin décroissante.
"""
NB_PAR_PAGE: int = 2
NB_PAR_PAGE: int = 25
def __init__(
self,
*etudiants: tuple[Identite],
table_data: "Data",
filtre: "Filtre" = None,
page: int = 1,
nb_par_page: int = None,
options: "Options" = None,
**kwargs,
) -> None:
"""
@ -33,14 +32,12 @@ class ListeAssiJusti(tb.Table):
filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None.
page (int, optional): numéro de page de la pagination. Defaults to 1.
"""
self.etudiants = etudiants
self.table_data: "Data" = table_data
# Gestion du filtre, par défaut un filtre vide
self.filtre = filtre if filtre is not None else Filtre()
# Gestion de la pagination (par défaut page 1)
self.page: int = page
self.nb_par_page: int = (
nb_par_page if nb_par_page is not None else ListeAssiJusti.NB_PAR_PAGE
)
# Gestion des options, par défaut un objet Options vide
self.options = options if options is not None else Options()
self.total_page: int = None
@ -57,9 +54,6 @@ class ListeAssiJusti(tb.Table):
self.ajouter_lignes()
def etudiant_seul(self) -> bool:
return len(self.etudiants) == 1
def ajouter_lignes(self):
# Générer les query assiduités et justificatifs
assiduites_query_etudiants: Query = None
@ -69,13 +63,21 @@ class ListeAssiJusti(tb.Table):
type_obj = self.filtre.type_obj()
if type_obj in [0, 1]:
assiduites_query_etudiants = Assiduite.query.filter(
Assiduite.etudid.in_([e.etudid for e in self.etudiants])
assiduites_query_etudiants = self.table_data.assiduites_query
# Non affichage des présences
if not self.options.show_pres:
assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.PRESENT
)
# Non affichage des retards
if not self.options.show_reta:
assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.RETARD
)
if type_obj in [0, 2]:
justificatifs_query_etudiants = Justificatif.query.filter(
Justificatif.etudid.in_([e.etudid for e in self.etudiants])
)
justificatifs_query_etudiants = self.table_data.justificatifs_query
# Combinaison des requêtes
@ -112,7 +114,7 @@ class ListeAssiJusti(tb.Table):
résultats paginés.
"""
return query.paginate(
page=self.page, per_page=self.nb_par_page, error_out=False
page=self.options.page, per_page=self.options.nb_ligne_page, error_out=False
)
def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None):
@ -149,7 +151,7 @@ class ListeAssiJusti(tb.Table):
# Définir les colonnes pour la requête d'assiduité
if query_assiduite:
query_assiduite = query_assiduite.with_entities(
assiduites_entities: list = [
Assiduite.assiduite_id.label("obj_id"),
Assiduite.etudid.label("etudid"),
Assiduite.entry_date.label("entry_date"),
@ -159,12 +161,17 @@ class ListeAssiJusti(tb.Table):
literal("assiduite").label("type"),
Assiduite.est_just.label("est_just"),
Assiduite.user_id.label("user_id"),
)
]
if self.options.show_desc:
assiduites_entities.append(Assiduite.description.label("description"))
query_assiduite = query_assiduite.with_entities(*assiduites_entities)
queries.append(query_assiduite)
# Définir les colonnes pour la requête de justificatif
if query_justificatif:
query_justificatif = query_justificatif.with_entities(
justificatifs_entities: list = [
Justificatif.justif_id.label("obj_id"),
Justificatif.etudid.label("etudid"),
Justificatif.entry_date.label("entry_date"),
@ -176,6 +183,13 @@ class ListeAssiJusti(tb.Table):
# donc on la met en nul car un justifcatif ne peut être justifié
literal(None).label("est_just"),
Justificatif.user_id.label("user_id"),
]
if self.options.show_desc:
justificatifs_entities.append(Justificatif.raison.label("description"))
query_justificatif = query_justificatif.with_entities(
*justificatifs_entities
)
queries.append(query_justificatif)
@ -206,7 +220,7 @@ class RowAssiJusti(tb.Row):
def ajouter_colonnes(self, lien_redirection: str = None):
# Ajout de l'étudiant
self.table: ListeAssiJusti
if not self.table.etudiant_seul():
if self.table.options.show_etu:
self._etud()
# Type d'objet
@ -218,28 +232,37 @@ class RowAssiJusti(tb.Row):
"Date de début",
self.ligne["date_debut"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["date_debut"]},
raw_content=self.ligne["date_debut"],
)
# Date de fin
self.add_cell(
"date_fin",
"Date de fin",
self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"),
raw_content=self.ligne["date_fin"],
data={"order": self.ligne["date_fin"]},
)
# Ajout des colonnes optionnelles
self._optionnelles()
# Ajout colonne actions
if self.table.options.show_actions:
self._actions()
# Ajout de l'utilisateur ayant saisie l'objet
self._utilisateur()
# Date de saisie
self.add_cell(
"entry_date",
"Saisie le",
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["entry_date"]},
raw_content=self.ligne["entry_date"],
classes=["small-font"],
)
# Ajout de l'utilisateur ayant saisie l'objet
self._utilisateur()
# Ajout colonne actions
self._actions()
def _type(self) -> None:
obj_type: str = ""
is_assiduite: bool = self.ligne["type"] == "assiduite"
@ -297,6 +320,21 @@ class RowAssiJusti(tb.Row):
target_attrs={"class": "discretelink"},
)
def _optionnelles(self) -> None:
if self.table.options.show_desc:
self.add_cell(
"description",
"Description",
self.ligne["description"] if self.ligne["description"] else "",
)
if self.table.options.show_module:
if self.ligne["type"] == "assiduite":
assi: Assiduite = Assiduite.query.get(self.ligne["obj_id"])
mod: str = assi.get_module(True)
self.add_cell("module", "Module", mod, data={"order": mod})
else:
self.add_cell("module", "Module", "", data={"order": ""})
def _utilisateur(self) -> None:
utilisateur: User = User.query.get(self.ligne["user_id"])
@ -304,11 +342,46 @@ class RowAssiJusti(tb.Row):
"user",
"Saisie par",
"Inconnu" if utilisateur is None else utilisateur.get_nomprenom(),
classes=["small-font"],
)
def _actions(self) -> None:
# XXX Ajouter une colonne avec les liens d'action (supprimer, modifier)
pass
url: str
html: list[str] = []
# Détails
url = url_for(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="details",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
html.append(f'<a href="{url}">Détails</a>') # utiliser url_for
# Modifier
url = url_for(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="modifier",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
html.append(f'<a href="{url}">Modifier</a>') # utiliser url_for
# Supprimer
url = url_for(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="supprimer",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
html.append(f'<a href="{url}">Supprimer</a>') # utiliser url_for
self.add_cell(
"actions", "Actions", "&ensp;".join(html), raw_content="test", no_excel=True
)
class Filtre:
@ -323,7 +396,6 @@ class Filtre:
entry_date: tuple[int, datetime] = None,
date_debut: tuple[int, datetime] = None,
date_fin: tuple[int, datetime] = None,
etats: list[EtatAssiduite | EtatJustificatif] = None,
) -> None:
"""
__init__ Instancie un nouvel objet filtre.
@ -336,7 +408,7 @@ class Filtre:
etats (list[int | EtatJustificatif | EtatAssiduite], optional): liste d'états valides (int | EtatJustificatif | EtatAssiduite). Defaults to None.
"""
self.filtres = {}
self.filtres = {"type_obj": type_obj}
if entry_date is not None:
self.filtres["entry_date"]: tuple[int, datetime] = entry_date
@ -347,9 +419,6 @@ class Filtre:
if date_fin is not None:
self.filtres["date_fin"]: tuple[int, datetime] = date_fin
if etats is not None:
self.filtres["etats"]: list[int | EtatJustificatif | EtatAssiduite] = etats
def filtrage(self, query: Query, obj_class: db.Model) -> Query:
"""
filtrage Filtre la query passée en paramètre et retourne l'objet filtré
@ -405,3 +474,58 @@ class Filtre:
int: le/les types d'objets à afficher
"""
return self.filtres.get("type_obj", 0)
class Options:
VRAI = ["on", "true", "t", "v", "vrai", True, 1]
def __init__(
self,
page: int = 1,
nb_ligne_page: int = None,
show_pres: str | bool = False,
show_reta: str | bool = False,
show_desc: str | bool = False,
show_etu: str | bool = True,
show_actions: str | bool = True,
show_module: str | bool = False,
):
self.page: int = page
self.nb_ligne_page: int = nb_ligne_page
self.show_pres: bool = show_pres in Options.VRAI
self.show_reta: bool = show_reta in Options.VRAI
self.show_desc: bool = show_desc in Options.VRAI
self.show_etu: bool = show_etu in Options.VRAI
self.show_actions: bool = show_actions in Options.VRAI
self.show_module: bool = show_module in Options.VRAI
def remplacer(self, **kwargs):
for k, v in kwargs.items():
if k.startswith("show_"):
self.__setattr__(k, v in Options.VRAI)
elif k in ["page", "nb_ligne_page"]:
self.__setattr__(k, int(v))
class Data:
def __init__(
self, assiduites_query: Query = None, justificatifs_query: Query = None
):
self.assiduites_query: Query = assiduites_query
self.justificatifs_query: Query = justificatifs_query
@staticmethod
def from_etudiants(*etudiants: Identite) -> "Data":
data = Data()
data.assiduites_query = Assiduite.query.filter(
Assiduite.etudid.in_([e.etudid for e in etudiants])
)
data.justificatifs_query = Justificatif.query.filter(
Justificatif.etudid.in_([e.etudid for e in etudiants])
)
return data
def get(self) -> tuple[Query, Query]:
return self.assiduites_query, self.justificatifs_query

View File

@ -84,6 +84,8 @@ class Table(Element):
self.row_by_id: dict[str, "Row"] = {}
self.column_ids = []
"ordered list of columns ids"
self.raw_column_ids = []
"ordered list of columns ids for excel"
self.groups = []
"ordered list of column groups names"
self.group_titles = {}
@ -360,6 +362,7 @@ class Row(Element):
target_attrs: dict = None,
target: str = None,
column_classes: set[str] = None,
no_excel: bool = False,
) -> "Cell":
"""Create cell and add it to the row.
group: groupe de colonnes
@ -380,10 +383,17 @@ class Row(Element):
target=target,
target_attrs=target_attrs,
)
return self.add_cell_instance(col_id, cell, column_group=group, title=title)
return self.add_cell_instance(
col_id, cell, column_group=group, title=title, no_excel=no_excel
)
def add_cell_instance(
self, col_id: str, cell: "Cell", column_group: str = None, title: str = None
self,
col_id: str,
cell: "Cell",
column_group: str = None,
title: str = None,
no_excel: bool = False,
) -> "Cell":
"""Add a cell to the row.
Si title est None, il doit avoir été ajouté avec table.add_title().
@ -392,6 +402,9 @@ class Row(Element):
self.cells[col_id] = cell
if col_id not in self.table.column_ids:
self.table.column_ids.append(col_id)
if not no_excel:
self.table.raw_column_ids.append(col_id)
self.table.insert_group(column_group)
if column_group is not None:
self.table.column_group[col_id] = column_group
@ -422,7 +435,7 @@ class Row(Element):
"""row as a dict, with only cell contents"""
return {
col_id: self.cells.get(col_id, self.table.empty_cell).raw_content
for col_id in self.table.column_ids
for col_id in self.table.raw_column_ids
}
def to_excel(self, sheet, style=None) -> list:

View File

@ -2,7 +2,6 @@
{% block pageContent %}
<div class="pageContent">
<h3>Ajouter une assiduité</h3>
{% include "assiduites/widgets/tableau_base.j2" %}
{% if saisie_eval %}
<div id="saisie_eval">
<br>
@ -63,8 +62,7 @@
</section>
<section class="liste">
<a class="icon filter" onclick="filterAssi()"></a>
{% include "assiduites/widgets/tableau_assi.j2" %}
{{tableau | safe }}
</section>
</div>
@ -141,7 +139,7 @@
let assiduite_id = null;
createAssiduiteComplete(assiduite, etudid);
loadAll();
updateTableau();
btn.disabled = true;
setTimeout(() => {
btn.disabled = false;
@ -208,7 +206,6 @@
{% endif %}
window.addEventListener("load", () => {
loadAll();
document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() });
dayOnly()

View File

@ -0,0 +1,27 @@
{% extends "sco_page.j2" %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% endblock %}
{% block app_content %}
{% if action == "modifier" %}
{% include "assiduites/widgets/tableau_actions/modifier.j2" %}
{% else%}
{% include "assiduites/widgets/tableau_actions/details.j2" %}
{% endif %}
<br>
<hr>
<br>
<a href="" id="lien-retour">Retour</a>
<script>
window.addEventListener('load', () => {
document.getElementById("lien-retour").href = document.referrer;
})
</script>
{% endblock %}

View File

@ -1,44 +0,0 @@
{% extends "sco_page.j2" %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% endblock %}
{% block app_content %}
<legend>
Options
<form action="" method="get">
<label for="show_pres">afficher les présences</label>
{% if show_pres %}
<input type="checkbox" id="show_pres" name="show_pres" checked>
{% else %}
<input type="checkbox" id="show_pres" name="show_pres">
{% endif %}
<label for="show_reta">afficher les retards</label>
{% if show_reta %}
<input type="checkbox" id="show_reta" name="show_reta" checked>
{% else %}
<input type="checkbox" id="show_reta" name="show_reta">
{% endif %}
<br>
<label for="nb_ligne_page">Nombre de ligne par page : </label>
<input type="number" name="nb_ligne_page" id="nb_ligne_page" value="{{nb_ligne_page}}">
<label for="n_page">Page n°</label>
<select name="n_page" id="n_page">
{% for n in range(1,total_pages+1) %}
<option value="{{n}}">{{n}}</option>
{% endfor %}
</select>
<br>
<input type="submit" value="valider">
</form>
</legend>
{{tableau | safe}}
{% endblock %}

View File

@ -119,11 +119,17 @@
}
{% if moduleid %}
const moduleimpl_dynamic_selector_id = "{{moduleid}}"
{% else %}
const moduleimpl_dynamic_selector_id = "moduleimpl_select"
{% endif %}
window.addEventListener("load", () => {
document.getElementById('moduleimpl_select').addEventListener('change', (el) => {
document.getElementById(moduleimpl_dynamic_selector_id).addEventListener('change', (el) => {
const assi = getCurrentAssiduite(etudid);
if (assi) {
editAssiduite(assi.assiduite_id, assi.etat, [assi]);

View File

@ -1,6 +1,8 @@
<select name="moduleimpl_select" id="moduleimpl_select">
{% with moduleimpl_id=moduleimpl_id %}
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
{% endwith %}
{% for mod in modules %}
{% if mod.moduleimpl_id == moduleimpl_id %}

View File

@ -1,6 +1,10 @@
{% if scu.is_assiduites_module_forced(request.args.get('formsemestre_id', None))%}
<option value="" selected disabled> Saisir Module</option>
<option value="" disabled> Saisir Module</option>
{% else %}
<option value="" selected> Non spécifié </option>
<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>
{% endif %}

View File

@ -0,0 +1,69 @@
<hr>
<div>
<h3>Options</h3>
<div id="options-tableau">
<label for="show_pres">afficher les présences</label>
{% if options.show_pres %}
<input type="checkbox" id="show_pres" name="show_pres" checked>
{% else %}
<input type="checkbox" id="show_pres" name="show_pres">
{% endif %}
<label for="show_reta">afficher les retards</label>
{% if options.show_reta %}
<input type="checkbox" id="show_reta" name="show_reta" checked>
{% else %}
<input type="checkbox" id="show_reta" name="show_reta">
{% endif %}
<label for="with_desc">afficher les descriptions</label>
{% if options.show_desc %}
<input type="checkbox" id="show_desc" name="show_desc" checked>
{% else %}
<input type="checkbox" id="show_desc" name="show_desc">
{% endif %}
<br>
<label for="nb_ligne_page">Nombre de ligne par page : </label>
<input type="number" name="nb_ligne_page" id="nb_ligne_page" value="{{options.nb_ligne_page}}">
<label for="n_page">Page n°</label>
<select name="n_page" id="n_page">
{% for n in range(1,total_pages+1) %}
{% if n == options.page %}
<option value="{{n}}" selected>{{n}}</option>
{% else %}
<option value="{{n}}">{{n}}</option>
{% endif %}
{% endfor %}
</select>
<br>
<button onclick="updateTableau()">valider</button>
</div>
</div>
{{tableau | safe}}
<script>
function updateTableau() {
const url = new URL(location.href);
const form = document.getElementById("options-tableau");
const formValues = form.querySelectorAll("*[name]");
formValues.forEach((el) => {
if (el.type == "checkbox") {
url.searchParams.set(el.name, el.checked)
} else {
url.searchParams.set(el.name, el.value)
}
})
location.href = url.href;
}
</script>
<style>
.small-font {
font-size: 9pt;
}
</style>

View File

@ -0,0 +1,107 @@
<h1>Détails {{type}} </h1>
<div id="informations">
<div class="info-row">
<span class="info-label">Étudiant.e concerné.e:</span> <span class="etudinfo"
id="etudid-{{objet.etudid}}">{{objet.etud_nom}}</span>
</div>
<div class="info-row">
<span class="info-label">Période concernée :</span> {{objet.date_debut}} au {{objet.date_fin}}
</div>
{% if type == "Assiduité" %}
<div class="info-row">
<span class="info-label">Module concernée :</span> {{objet.module}}
</div>
{% else %}
{% endif %}
<div class="info-row">
{% if type == "Justificatif" %}
<span class="info-label">État du justificatif :</span>
{% else %}
<span class="info-label">État de l'assiduité :</span>
{% endif %}
{{objet.etat}}
</div>
<div class="info-row">
{% if type == "Justificatif" %}
<div class="info-label">Raison:</div>
{% if objet.raison != None %}
<div class="text">{{objet.raison}}</div>
{% else %}
<div class="text">/div>
{% endif %}
{% else %}
<div class="info-label">Description:</div>
{% if objet.description != None %}
<div class="text">{{objet.description}}</div>
{% else %}
<div class="text"></div>
{% endif %}
{% endif %}
</div>
</div>
{# Affichage des justificatifs si assiduité justifiée #}
{% if type == "Assiduité" and objet.etat != "Présence" %}
<div class="info-row">
<span class="info-label">Justifiée: </span>
{% if objet.justification.est_just %}
<span class="text">Oui</span>
<div>
{% for justi in objet.justification.justificatifs %}
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}"
target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a>
{% endfor %}
</div>
{% else %}
<span class="text">Non</span>
{% endif %}
</div>
{% endif %}
{# Affichage des assiduités justifiées si justificatif valide #}
{% if type == "Justificatif" and objet.etat == "Valide" %}
<div class="info-row">
<span class="info-label">Assiduités concernées: </span>
{% if objet.justification.assiduites %}
<div>
{% for assi in objet.justification.assiduites %}
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)}}"
target="_blank" rel="noopener noreferrer">Assiduité {{assi.etat}} du {{assi.date_debut}} au
{{assi.date_fin}}</a>
{% endfor %}
</div>
{% else %}
<span class="text">Aucune</span>
{% endif %}
</div>
{% endif %}
{# Affichage des fichiers des justificatifs #}
{% if type == "Justificatif"%}
<div class="info-row">
<span class="info-label">Fichiers enregistrés: </span>
{% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div>
<div>
{% for filename in objet.justification.fichiers.filenames %}
<form method="post"
action="{{url_for('api.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}">
<button type="submit">{{filename}}</button>
</form>
{% endfor %}
</div>
{% else %}
<span class="text">Aucun</span>
{% endif %}
</div>
{% endif %}
<div class="info-row">
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div>

View File

@ -0,0 +1,107 @@
<h1>Modifier {{type}} </h1>
<form action="" method="post">
<input type="hidden" name="obj_id" value="{{obj_id}}">
{% if type == "Assiduité" %}
<input type="hidden" name="obj_type" value="assiduite">
<legend for="etat">État</legend>
<select name="etat" id="etat">
<option value="absent">Absent</option>
<option value="retard">Retard</option>
<option value="present">Présent</option>
</select>
<legend for="moduleimpl_select">Module</legend>
{{moduleimpl | safe}}
<legend for="description">Description</legend>
<textarea name="description" id="description" cols="50" rows="5">{{objet.description}}</textarea>
{% else %}
<input type="hidden" name="obj_type" value="justificatif">
<legend for="date_debut">Date de début</legend>
<scodoc-datetime name="date_debut" id="date_debut" value="{{objet.real_date_debut}}"></scodoc-datetime>
<legend for="date_fin">Date de fin</legend>
<scodoc-datetime name="date_fin" id="date_fin" value="{{objet.real_date_fin}}"></scodoc-datetime>
<legend for="etat">État</legend>
<select name="etat" id="etat">
<option value="valide">Valide</option>
<option value="non_valide">Non Valide</option>
<option value="attente">En Attente</option>
<option value="modifie">Modifié</option>
</select>
<legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea>
<legend>Fichiers</legend>
<div class="info-row">
<label class="info-label">Fichiers enregistrés: </label>
{% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div>
<ul>
{% for filename in objet.justification.fichiers.filenames %}
<li>
<script id="replace">
link = document.createElement('a');
link.textContent = "{{filename}}";
url = "{{url_for('api.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}";
link.addEventListener('click', () => { downloadFile(url) });
document.getElementById('replace').replaceWith(link);
</script>
<script id="replace2">
link = document.createElement('a');
link.textContent = "Supprimer";
link.setAttribute('data-file', "{{filename}}");
link.addEventListener('click', () => { removeFile(link) });
document.getElementById('replace2').replaceWith(link);
</script>
</li>
{% endfor %}
</ul>
{% else %}
<span class="text">Aucun</span>
{% endif %}
</div>
<br>
<label for="justi_fich">Ajouter des fichiers:</label>
<input type="file" name="justi_fich" id="justi_fich" multiple>
{% endif %}
<br>
<br>
<input type="submit" value="Valider">
</form>
<script>
function removeFile(element) {
element.toggleAttribute("data-remove")
}
function downloadFile(url) {
console.warn(url);
}
function deleteFiles(justif_id) {
const filenames = Array.from(document.querySelectorAll("*[data-remove]")).map((el) => el.getAttribute("data-file"))
obj = {
"remove": "list",
"filenames": filenames
}
console.warn(obj);
}
window.addEventListener('load', () => {
document.getElementById('etat').value = "{{objet.real_etat}}";
})
</script>

View File

@ -32,6 +32,7 @@ from flask import abort, url_for, redirect
from flask_login import current_user
from app import db
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.decorators import (
@ -47,6 +48,10 @@ from app.models import (
Departement,
Evaluation,
)
from app.auth.models import User
from app.models.assiduites import get_assiduites_justif, compute_assiduites_justified
import app.tables.liste_assiduites as liste_assi
from app.views import assiduites_bp as bp
from app.views import ScoData
@ -65,6 +70,7 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -314,6 +320,15 @@ def signal_assiduites_etud():
</select>
"""
tableau = _preparer_tableau(
etud,
filename=f"assiduite-{etudid}",
afficher_etu=False,
filtre=liste_assi.Filtre(type_obj=0),
options=liste_assi.Options(show_module=True),
)
if not tableau[0]:
return tableau[1]
# Génération de la page
return HTMLBuilder(
header,
@ -332,6 +347,7 @@ def signal_assiduites_etud():
date_fin=date_fin,
redirect_url=redirect_url,
moduleimpl_id=moduleimpl_id,
tableau=tableau[1],
),
# render_template(
# "assiduites/pages/signal_assiduites_etud.j2",
@ -1044,26 +1060,43 @@ def visu_assi_group():
)
@bp.route("/testTableau")
@scodoc
@permission_required(Permission.ScoView)
def testTableau():
"""Visualisation de l'assiduité d'un groupe entre deux dates"""
def _preparer_tableau(
*etudiants: Identite,
filename: str = "tableau-assiduites",
afficher_etu: bool = True,
filtre: liste_assi.Filtre = None,
options: liste_assi.Options = None,
) -> tuple[bool, "Response"]:
"""
_preparer_tableau prépare un tableau d'assiduités / justificatifs
etudid = request.args.get(
"etudid", 18114
) # TODO retirer la valeur par défaut de test
Cette fontion récupère dans la requête les arguments :
valeurs possibles des booléens vrais ["on", "true", "t", "v", "vrai", True, 1]
toute autre valeur est considérée comme fausse.
show_pres : bool -> Affiche les présences, par défaut False
show_reta : bool -> Affiche les retard, par défaut False
show_desc : bool -> Affiche les descriptions, par défaut False
Returns:
tuple[bool | "Reponse" ]:
- bool : Vrai si la réponse est du Text/HTML
- Reponse : du Text/HTML ou Une Reponse (téléchargement fichier)
"""
fmt = request.args.get("fmt", "html")
show_pres: bool | str = request.args.get("show_pres", False)
show_reta: bool | str = request.args.get("show_reta", False)
show_desc: bool | str = request.args.get("show_desc", False)
nb_ligne_page: int = request.args.get("nb_ligne_page")
# Vérification de nb_ligne_page
try:
nb_ligne_page: int = int(nb_ligne_page)
except (ValueError, TypeError):
nb_ligne_page = None
nb_ligne_page = liste_assi.ListeAssiJusti.NB_PAR_PAGE
page_number: int = request.args.get("n_page", 1)
# Vérification de page_number
@ -1072,33 +1105,177 @@ def testTableau():
except (ValueError, TypeError):
page_number = 1
from app.tables.liste_assiduites import ListeAssiJusti
fmt = request.args.get("fmt", "html")
table: ListeAssiJusti = ListeAssiJusti(
Identite.get_etud(etudid), page=page_number, nb_par_page=nb_ligne_page
if options is None:
options: liste_assi.Options = liste_assi.Options()
options.remplacer(
page=page_number,
nb_ligne_page=nb_ligne_page,
show_pres=show_pres,
show_reta=show_reta,
show_desc=show_desc,
show_etu=afficher_etu,
)
table: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti(
table_data=liste_assi.Data.from_etudiants(*etudiants),
options=options,
filtre=filtre,
)
if fmt.startswith("xls"):
return scu.send_file(
return False, scu.send_file(
table.excel(),
filename=f"assiduite-{groups_infos.groups_filename}",
filename=filename,
mime=scu.XLSX_MIMETYPE,
suffix=scu.XLSX_SUFFIX,
)
return render_template(
"assiduites/pages/test_assi.j2",
sco=ScoData(),
return True, render_template(
"assiduites/widgets/tableau.j2",
tableau=table.html(),
title=f"Test tableau",
total_pages=table.total_pages,
page_number=page_number,
show_pres=show_pres,
show_reta=show_reta,
nb_ligne_page=nb_ligne_page,
options=options,
)
@bp.route("/TableauAssiduiteActions", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.AbsChange)
def tableau_assiduite_actions():
obj_type: str = request.args.get("type", "assiduite")
action: str = request.args.get("action", "details")
obj_id: str = int(request.args.get("obj_id", -1))
objet: Assiduite | Justificatif
if obj_type == "assiduite":
objet: Assiduite = Assiduite.query.get_or_404(obj_id)
else:
objet: Justificatif = Justificatif.query.get_or_404(obj_id)
if action == "supprimer":
objet.supprimer()
if obj_type == "assiduite":
flash("L'assiduité a bien été supprimée")
else:
flash("Le justificatif a bien été supprimé")
return redirect(request.referrer)
if request.method == "GET":
module = ""
if obj_type == "assiduite":
formsemestre = objet.get_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)
return render_template(
"assiduites/pages/tableau_actions.j2",
sco=ScoData(etud=objet.etudiant),
type="Justificatif" if obj_type == "justificatif" else "Assiduité",
action=action,
objet=_preparer_objet(obj_type, objet),
obj_id=obj_id,
moduleimpl=module,
)
def _preparer_objet(
obj_type: str, objet: Assiduite | Justificatif, sans_gros_objet: bool = False
) -> dict:
# Préparation d'un objet pour simplifier l'affichage jinja
objet_prepare: dict = objet.to_dict()
if obj_type == "assiduite":
objet_prepare["etat"] = (
scu.EtatAssiduite(objet.etat).version_lisible().capitalize()
)
objet_prepare["real_etat"] = scu.EtatAssiduite(objet.etat).name.lower()
objet_prepare["description"] = (
"" if objet.description is None else objet.description
)
objet_prepare["description"] = objet_prepare["description"].strip()
# Gestion du module
objet_prepare["module"] = objet.get_module(True)
# Gestion justification
if not objet.est_just:
objet_prepare["justification"] = {"est_just": False}
else:
objet_prepare["justification"] = {"est_just": True, "justificatifs": []}
if not sans_gros_objet:
justificatifs: list[int] = get_assiduites_justif(
objet.assiduite_id, False
)
for justi_id in justificatifs:
justi: Justificatif = Justificatif.query.get(justi_id)
objet_prepare["justification"]["justificatifs"].append(
_preparer_objet("justificatif", justi, sans_gros_objet=True)
)
else:
objet_prepare["etat"] = (
scu.EtatJustificatif(objet.etat).version_lisible().capitalize()
)
objet_prepare["real_etat"] = scu.EtatJustificatif(objet.etat).name.lower()
objet_prepare["raison"] = "" if objet.raison is None else objet.raison
objet_prepare["raison"] = objet_prepare["raison"].strip()
objet_prepare["justification"] = {"assiduites": [], "fichiers": {}}
if not sans_gros_objet:
assiduites: list[int] = scass.justifies(objet)
for assi_id in assiduites:
assi: Assiduite = Assiduite.query.get(assi_id)
objet_prepare["justification"]["assiduites"].append(
_preparer_objet("assiduite", assi, sans_gros_objet=True)
)
# Récupération de l'archive avec l'archiver
archive_name: str = objet.fichier
filenames: list[str] = []
archiver: JustificatifArchiver = JustificatifArchiver()
if archive_name is not None:
filenames = archiver.list_justificatifs(archive_name, objet.etudiant)
objet_prepare["justification"]["fichiers"] = {
"total": len(filenames),
"filenames": [],
}
for filename in filenames:
if int(filename[1]) == current_user.id or current_user.has_permission(
Permission.AbsJustifView
):
objet_prepare["justification"]["fichiers"]["filenames"].append(
filename[0]
)
objet_prepare["date_fin"] = objet.date_fin.strftime("%d/%m/%y à %H:%M")
objet_prepare["real_date_fin"] = objet.date_fin.isoformat()
objet_prepare["date_debut"] = objet.date_debut.strftime("%d/%m/%y à %H:%M")
objet_prepare["real_date_debut"] = objet.date_debut.isoformat()
objet_prepare["entry_date"] = objet.entry_date.strftime("%d/%m/%y à %H:%M")
objet_prepare["etud_nom"] = objet.etudiant.nomprenom
if objet.user_id != None:
user: User = User.query.get(objet.user_id)
objet_prepare["saisie_par"] = user.get_nomprenom()
else:
objet_prepare["saisie_par"] = "Inconnu"
return objet_prepare
@bp.route("/SignalAssiduiteDifferee")
@scodoc
@permission_required(Permission.AbsChange)
@ -1534,12 +1711,6 @@ def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> s
# prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre
selected = "" if moduleimpl_id is not None else "selected"
# Vérification que le moduleimpl_id passé en paramètre est bien un entier
try:
moduleimpl_id = int(moduleimpl_id)
except (ValueError, TypeError):
moduleimpl_id = None
modules: list[dict[str, str | int]] = []
# Récupération de l'id et d'un nom lisible pour chaque moduleimpl
for modimpl in modimpls_list: