Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
15 changed files with 801 additions and 125 deletions
Showing only changes of commit b13e751e1a - Show all commits

View File

@ -3,8 +3,8 @@
""" """
from datetime import datetime from datetime import datetime
from app import db, log from app import db, log, g
from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription from app.models import ModuleImpl, Module, Scolog, FormSemestre, FormSemestreInscription
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.auth.models import User from app.auth.models import User
from app.scodoc import sco_abs_notification 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) sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
return nouv_assiduite 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): class Justificatif(db.Model):
""" """
@ -334,6 +371,39 @@ class Justificatif(db.Model):
) )
return nouv_justificatif 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( def is_period_conflicting(
date_debut: datetime, date_debut: datetime,

View File

@ -204,6 +204,13 @@ class EtatAssiduite(int, BiDirectionalEnum):
RETARD = 1 RETARD = 1
ABSENT = 2 ABSENT = 2
def version_lisible(self) -> str:
return {
EtatAssiduite.PRESENT: "Présence",
EtatAssiduite.ABSENT: "Absence",
EtatAssiduite.RETARD: "Retard",
}.get(self, "")
class EtatJustificatif(int, BiDirectionalEnum): class EtatJustificatif(int, BiDirectionalEnum):
"""Code des états des justificatifs""" """Code des états des justificatifs"""
@ -215,6 +222,14 @@ class EtatJustificatif(int, BiDirectionalEnum):
ATTENTE = 2 ATTENTE = 2
MODIFIE = 3 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: 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: def get_assiduites_time_config(config_type: str) -> str:
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
match config_type: match config_type:
case "matin": case "matin":
return ScoDocSiteConfig.get("assi_morning_time", "08:00:00") 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 // Ajouter le style au shadow DOM
shadow.appendChild(style); shadow.appendChild(style);
//Si une value est donnée
let value = this.getAttribute("value");
if (value != null) {
this.value = value;
}
} }
static get observedAttributes() { static get observedAttributes() {

View File

@ -5,7 +5,7 @@ from datetime import datetime
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif
from flask_sqlalchemy.query import Query, Pagination from flask_sqlalchemy.query import Query, Pagination
from sqlalchemy import union, literal, select, desc from sqlalchemy import union, literal, select, desc
from app import db from app import db, g
from flask import url_for from flask import url_for
from app import log 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. 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__( def __init__(
self, self,
*etudiants: tuple[Identite], table_data: "Data",
filtre: "Filtre" = None, filtre: "Filtre" = None,
page: int = 1, options: "Options" = None,
nb_par_page: int = None,
**kwargs, **kwargs,
) -> None: ) -> None:
""" """
@ -33,14 +32,12 @@ class ListeAssiJusti(tb.Table):
filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None. filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None.
page (int, optional): numéro de page de la pagination. Defaults to 1. 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 # Gestion du filtre, par défaut un filtre vide
self.filtre = filtre if filtre is not None else Filtre() self.filtre = filtre if filtre is not None else Filtre()
# Gestion de la pagination (par défaut page 1)
self.page: int = page # Gestion des options, par défaut un objet Options vide
self.nb_par_page: int = ( self.options = options if options is not None else Options()
nb_par_page if nb_par_page is not None else ListeAssiJusti.NB_PAR_PAGE
)
self.total_page: int = None self.total_page: int = None
@ -57,9 +54,6 @@ class ListeAssiJusti(tb.Table):
self.ajouter_lignes() self.ajouter_lignes()
def etudiant_seul(self) -> bool:
return len(self.etudiants) == 1
def ajouter_lignes(self): def ajouter_lignes(self):
# Générer les query assiduités et justificatifs # Générer les query assiduités et justificatifs
assiduites_query_etudiants: Query = None assiduites_query_etudiants: Query = None
@ -69,13 +63,21 @@ class ListeAssiJusti(tb.Table):
type_obj = self.filtre.type_obj() type_obj = self.filtre.type_obj()
if type_obj in [0, 1]: if type_obj in [0, 1]:
assiduites_query_etudiants = Assiduite.query.filter( assiduites_query_etudiants = self.table_data.assiduites_query
Assiduite.etudid.in_([e.etudid for e in self.etudiants])
# 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]: if type_obj in [0, 2]:
justificatifs_query_etudiants = Justificatif.query.filter( justificatifs_query_etudiants = self.table_data.justificatifs_query
Justificatif.etudid.in_([e.etudid for e in self.etudiants])
)
# Combinaison des requêtes # Combinaison des requêtes
@ -112,7 +114,7 @@ class ListeAssiJusti(tb.Table):
résultats paginés. résultats paginés.
""" """
return query.paginate( 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): 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é # Définir les colonnes pour la requête d'assiduité
if query_assiduite: if query_assiduite:
query_assiduite = query_assiduite.with_entities( assiduites_entities: list = [
Assiduite.assiduite_id.label("obj_id"), Assiduite.assiduite_id.label("obj_id"),
Assiduite.etudid.label("etudid"), Assiduite.etudid.label("etudid"),
Assiduite.entry_date.label("entry_date"), Assiduite.entry_date.label("entry_date"),
@ -159,12 +161,17 @@ class ListeAssiJusti(tb.Table):
literal("assiduite").label("type"), literal("assiduite").label("type"),
Assiduite.est_just.label("est_just"), Assiduite.est_just.label("est_just"),
Assiduite.user_id.label("user_id"), 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) queries.append(query_assiduite)
# Définir les colonnes pour la requête de justificatif # Définir les colonnes pour la requête de justificatif
if query_justificatif: if query_justificatif:
query_justificatif = query_justificatif.with_entities( justificatifs_entities: list = [
Justificatif.justif_id.label("obj_id"), Justificatif.justif_id.label("obj_id"),
Justificatif.etudid.label("etudid"), Justificatif.etudid.label("etudid"),
Justificatif.entry_date.label("entry_date"), 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é # donc on la met en nul car un justifcatif ne peut être justifié
literal(None).label("est_just"), literal(None).label("est_just"),
Justificatif.user_id.label("user_id"), 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) queries.append(query_justificatif)
@ -206,7 +220,7 @@ class RowAssiJusti(tb.Row):
def ajouter_colonnes(self, lien_redirection: str = None): def ajouter_colonnes(self, lien_redirection: str = None):
# Ajout de l'étudiant # Ajout de l'étudiant
self.table: ListeAssiJusti self.table: ListeAssiJusti
if not self.table.etudiant_seul(): if self.table.options.show_etu:
self._etud() self._etud()
# Type d'objet # Type d'objet
@ -218,28 +232,37 @@ class RowAssiJusti(tb.Row):
"Date de début", "Date de début",
self.ligne["date_debut"].strftime("%d/%m/%y à %H:%M"), self.ligne["date_debut"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["date_debut"]}, data={"order": self.ligne["date_debut"]},
raw_content=self.ligne["date_debut"],
) )
# Date de fin # Date de fin
self.add_cell( self.add_cell(
"date_fin", "date_fin",
"Date de fin", "Date de fin",
self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"), self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"),
raw_content=self.ligne["date_fin"],
data={"order": 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 # Date de saisie
self.add_cell( self.add_cell(
"entry_date", "entry_date",
"Saisie le", "Saisie le",
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"), self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["entry_date"]}, 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: def _type(self) -> None:
obj_type: str = "" obj_type: str = ""
is_assiduite: bool = self.ligne["type"] == "assiduite" is_assiduite: bool = self.ligne["type"] == "assiduite"
@ -297,6 +320,21 @@ class RowAssiJusti(tb.Row):
target_attrs={"class": "discretelink"}, 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: def _utilisateur(self) -> None:
utilisateur: User = User.query.get(self.ligne["user_id"]) utilisateur: User = User.query.get(self.ligne["user_id"])
@ -304,11 +342,46 @@ class RowAssiJusti(tb.Row):
"user", "user",
"Saisie par", "Saisie par",
"Inconnu" if utilisateur is None else utilisateur.get_nomprenom(), "Inconnu" if utilisateur is None else utilisateur.get_nomprenom(),
classes=["small-font"],
) )
def _actions(self) -> None: def _actions(self) -> None:
# XXX Ajouter une colonne avec les liens d'action (supprimer, modifier) url: str
pass 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: class Filtre:
@ -323,7 +396,6 @@ class Filtre:
entry_date: tuple[int, datetime] = None, entry_date: tuple[int, datetime] = None,
date_debut: tuple[int, datetime] = None, date_debut: tuple[int, datetime] = None,
date_fin: tuple[int, datetime] = None, date_fin: tuple[int, datetime] = None,
etats: list[EtatAssiduite | EtatJustificatif] = None,
) -> None: ) -> None:
""" """
__init__ Instancie un nouvel objet filtre. __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. 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: if entry_date is not None:
self.filtres["entry_date"]: tuple[int, datetime] = entry_date self.filtres["entry_date"]: tuple[int, datetime] = entry_date
@ -347,9 +419,6 @@ class Filtre:
if date_fin is not None: if date_fin is not None:
self.filtres["date_fin"]: tuple[int, datetime] = date_fin 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: def filtrage(self, query: Query, obj_class: db.Model) -> Query:
""" """
filtrage Filtre la query passée en paramètre et retourne l'objet filtré 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 int: le/les types d'objets à afficher
""" """
return self.filtres.get("type_obj", 0) 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.row_by_id: dict[str, "Row"] = {}
self.column_ids = [] self.column_ids = []
"ordered list of columns ids" "ordered list of columns ids"
self.raw_column_ids = []
"ordered list of columns ids for excel"
self.groups = [] self.groups = []
"ordered list of column groups names" "ordered list of column groups names"
self.group_titles = {} self.group_titles = {}
@ -360,6 +362,7 @@ class Row(Element):
target_attrs: dict = None, target_attrs: dict = None,
target: str = None, target: str = None,
column_classes: set[str] = None, column_classes: set[str] = None,
no_excel: bool = False,
) -> "Cell": ) -> "Cell":
"""Create cell and add it to the row. """Create cell and add it to the row.
group: groupe de colonnes group: groupe de colonnes
@ -380,10 +383,17 @@ class Row(Element):
target=target, target=target,
target_attrs=target_attrs, 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( 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": ) -> "Cell":
"""Add a cell to the row. """Add a cell to the row.
Si title est None, il doit avoir été ajouté avec table.add_title(). 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 self.cells[col_id] = cell
if col_id not in self.table.column_ids: if col_id not in self.table.column_ids:
self.table.column_ids.append(col_id) 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) self.table.insert_group(column_group)
if column_group is not None: if column_group is not None:
self.table.column_group[col_id] = column_group self.table.column_group[col_id] = column_group
@ -422,7 +435,7 @@ class Row(Element):
"""row as a dict, with only cell contents""" """row as a dict, with only cell contents"""
return { return {
col_id: self.cells.get(col_id, self.table.empty_cell).raw_content 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: def to_excel(self, sheet, style=None) -> list:

View File

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

View File

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

View File

@ -1,6 +1,10 @@
{% if scu.is_assiduites_module_forced(request.args.get('formsemestre_id', None))%} {% 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 %} {% else %}
<option value="" selected> Non spécifié </option> <option value=""> Non spécifié </option>
{% endif %} {% endif %}
{% if moduleimpl_id == "autre" %}
<option value="autre" selected> Tout module </option>
{% else %}
<option value="autre"> Tout module </option> <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 flask_login import current_user
from app import db from app import db
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.decorators import ( from app.decorators import (
@ -47,6 +48,10 @@ from app.models import (
Departement, Departement,
Evaluation, 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 assiduites_bp as bp
from app.views import ScoData 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.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -314,6 +320,15 @@ def signal_assiduites_etud():
</select> </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 # Génération de la page
return HTMLBuilder( return HTMLBuilder(
header, header,
@ -332,6 +347,7 @@ def signal_assiduites_etud():
date_fin=date_fin, date_fin=date_fin,
redirect_url=redirect_url, redirect_url=redirect_url,
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
tableau=tableau[1],
), ),
# render_template( # render_template(
# "assiduites/pages/signal_assiduites_etud.j2", # "assiduites/pages/signal_assiduites_etud.j2",
@ -1044,26 +1060,43 @@ def visu_assi_group():
) )
@bp.route("/testTableau") def _preparer_tableau(
@scodoc *etudiants: Identite,
@permission_required(Permission.ScoView) filename: str = "tableau-assiduites",
def testTableau(): afficher_etu: bool = True,
"""Visualisation de l'assiduité d'un groupe entre deux dates""" 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( Cette fontion récupère dans la requête les arguments :
"etudid", 18114
) # TODO retirer la valeur par défaut de test 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_pres: bool | str = request.args.get("show_pres", False)
show_reta: bool | str = request.args.get("show_reta", 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") nb_ligne_page: int = request.args.get("nb_ligne_page")
# Vérification de nb_ligne_page # Vérification de nb_ligne_page
try: try:
nb_ligne_page: int = int(nb_ligne_page) nb_ligne_page: int = int(nb_ligne_page)
except (ValueError, TypeError): 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) page_number: int = request.args.get("n_page", 1)
# Vérification de page_number # Vérification de page_number
@ -1072,33 +1105,177 @@ def testTableau():
except (ValueError, TypeError): except (ValueError, TypeError):
page_number = 1 page_number = 1
from app.tables.liste_assiduites import ListeAssiJusti fmt = request.args.get("fmt", "html")
table: ListeAssiJusti = ListeAssiJusti( if options is None:
Identite.get_etud(etudid), page=page_number, nb_par_page=nb_ligne_page 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"): if fmt.startswith("xls"):
return scu.send_file( return False, scu.send_file(
table.excel(), table.excel(),
filename=f"assiduite-{groups_infos.groups_filename}", filename=filename,
mime=scu.XLSX_MIMETYPE, mime=scu.XLSX_MIMETYPE,
suffix=scu.XLSX_SUFFIX, suffix=scu.XLSX_SUFFIX,
) )
return render_template( return True, render_template(
"assiduites/pages/test_assi.j2", "assiduites/widgets/tableau.j2",
sco=ScoData(),
tableau=table.html(), tableau=table.html(),
title=f"Test tableau",
total_pages=table.total_pages, total_pages=table.total_pages,
page_number=page_number, options=options,
show_pres=show_pres,
show_reta=show_reta,
nb_ligne_page=nb_ligne_page,
) )
@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") @bp.route("/SignalAssiduiteDifferee")
@scodoc @scodoc
@permission_required(Permission.AbsChange) @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 # 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" 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]] = [] modules: list[dict[str, str | int]] = []
# Récupération de l'id et d'un nom lisible pour chaque moduleimpl # Récupération de l'id et d'un nom lisible pour chaque moduleimpl
for modimpl in modimpls_list: for modimpl in modimpls_list: