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
5 changed files with 275 additions and 66 deletions
Showing only changes of commit 1b1b8ebdc4 - Show all commits

View File

@ -1,6 +1,6 @@
# -*- coding: UTF-8 -*
"""Gestion de l'assiduité (assiduités + justificatifs)
"""
"""Gestion de l'assiduité (assiduités + justificatifs)"""
from datetime import datetime
from flask_login import current_user
@ -336,13 +336,19 @@ class Assiduite(ScoDocModel):
"""
return get_formsemestre_from_data(self.to_dict())
def get_module(self, traduire: bool = False) -> int | str:
"TODO documenter"
def get_module(self, traduire: bool = False) -> Module | str:
"""
Retourne le module associé à l'assiduité
Si traduire est vrai, retourne le titre du module précédé du code
Sinon rentourne l'objet Module ou None
"""
if self.moduleimpl_id is not None:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
mod: Module = Module.query.get(modimpl.module_id)
if traduire:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
mod: Module = Module.query.get(modimpl.module_id)
return f"{mod.code} {mod.titre}"
return mod
elif self.external_data is not None and "module" in self.external_data:
return (

View File

@ -12,7 +12,7 @@ from sqlalchemy import desc, literal, union, asc
from app import db, g
from app.auth.models import User
from app.models import Assiduite, Identite, Justificatif
from app.models import Assiduite, Identite, Justificatif, Module
from app.scodoc.sco_utils import (
EtatAssiduite,
EtatJustificatif,
@ -534,10 +534,45 @@ class RowAssiJusti(tb.Row):
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})
if self.table.no_pagination:
mod: Module = assi.get_module(False)
code = mod.code if isinstance(mod, Module) else ""
titre = ""
if isinstance(mod, Module):
titre = mod.titre
elif isinstance(mod, str):
titre = mod
else:
titre = "Non Spécifié"
self.add_cell(
"code_module", "Code Module", code, data={"order": code}
)
self.add_cell(
"titre_module",
"Titre Module",
titre,
data={"order": titre},
)
else:
mod: Module = assi.get_module(True)
self.add_cell(
"module",
"Module",
mod,
data={"order": mod},
)
else:
self.add_cell("module", "Module", "", data={"order": ""})
if self.table.no_pagination:
self.add_cell("module", "Module", "", data={"order": ""})
else:
self.add_cell("code_module", "Code Module", "", data={"order": ""})
self.add_cell(
"titre_module",
"Titre Module",
"",
data={"order": ""},
)
def _utilisateur(self) -> None:
utilisateur: User = (

View File

@ -20,6 +20,39 @@ le semestre concerné (saisie par jour ou saisie différée).
<br>
{{billets | safe}}
<br>
<div>
<h3>Télécharger l'assiduité</h3>
<form action="{{url_for('assiduites.recup_assiduites_plage', scodoc_dept=g.scodoc_dept)}}" method="post">
<label for="datedeb">
Du&nbsp;:
<input type="text" class="datepicker" id="datedeb" name="datedeb">
</label>
<br>
<label for="datefin">
Au&nbsp;:
<input type="text" class="datepicker" id="datefin" name="datefin">
</label>
<br>
<label for="formsemestre_id">Télécharger l'assiduité de </label>
<select name="formsemestre_id" id="formsemestre_id">
<option value="">Tout le département</option>
{% for id, titre in formsemestres.items() %}
{% if formsemestre_id == id %}
<option value="{{id}}" selected>{{titre}}</option>
{% else %}
<option value="{{id}}">{{titre}}</option>
{% endif %}
{% endfor %}
</select>
<br>
<input type="submit" value="Télécharger" name="telecharger">
</form>
</div>
<br>
<section class="nonvalide">
{{tableau | safe }}

View File

@ -17,6 +17,15 @@
gap: 0.5em;
}
#actions {
flex-direction: row;
align-items: center;
margin-bottom: 5px;
}
#actions label{
margin: 0;
}
#fix {
display: flex;
flex-direction: row;
@ -49,21 +58,24 @@
}
#tableau-periode {
display: flex;
flex-direction: column;
overflow-x: scroll;
max-width: var(--sco-content-max-width);
}
#tableau-periode .pdp {
width: 2em;
height: 2em;
border-radius: 50%;
width: 5em;
border-radius: 8px;
}
.grid-table {
display: grid;
grid-template-columns: 200px repeat({{ etudiants|length }}, 1fr);
width: var(--sco-content-max-width);
.header {
background-color: #f9f9f9;
padding: 10px;
text-align: center;
border: 1px solid #ddd;
}
.cell, .header {
border: 1px solid #ddd;
padding: 10px;
@ -74,19 +86,23 @@
}
.header{
justify-content: space-between;
}
.cell{
.cell {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ddd;
padding: 10px;
text-align: center;
width: 256px;
}
.cell p{
text-align: center;
}
.sticky {
position: sticky;
left: 0;
top: 0;
background-color: #f9f9f9;
z-index: 2;
}
@ -95,12 +111,24 @@
display: block;
top: 0;
z-index: 0;
width: 100% !important;
min-width: inherit !important;
}
.assi-btns {
display: flex;
gap: 4px;
}
.pointer{
cursor: pointer;
}
.ligne{
display: flex;
gap: 1px;
}
</style>
{% endblock styles %}
@ -124,6 +152,10 @@ function afficherPDP(checked) {
} else {
gtrcontent.removeAttribute("data-pdp");
}
// On sauvegarde le choix dans le localStorage
localStorage.setItem("scodoc-signal_assiduites_diff-pdp", `${checked}`);
pdp.checked = checked;
}
/**
@ -156,7 +188,7 @@ async function nouvellePeriode(period = null) {
moduleimpl_id = period.moduleimpl_id;
}else{
//Sinon on vérifie qu'on a bien des valeurs
const text = document.createTextNode("Veuillez remplir tous les champs pour ajouter une période.")
const text = document.createTextNode("Veuillez remplir tous les champs pour ajouter une plage.")
if (date == "" || debut == "" || fin == "" || moduleimpl_id == "") {
openAlertModal(
"Erreur",
@ -168,10 +200,11 @@ async function nouvellePeriode(period = null) {
// On ajoute la nouvelle période au tableau
let periodeDiv = document.createElement("div");
periodeDiv.classList.add("cell", "sticky");
periodeDiv.classList.add("cell", "header");
periodeDiv.id = `periode-${periodId}`;
const periodP = document.createElement("p");
periodP.textContent = `Période du ${date} de ${debut} à ${fin}`;
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
// On ajoute le moduleimpl
const modP = document.createElement("p");
@ -184,7 +217,7 @@ async function nouvellePeriode(period = null) {
// On supprime toutes les cases du tableau correspondant à cette période
document
.querySelectorAll(
`.cell[data-periodeid="${periodeDiv.getAttribute("data-periodeid")}"]`
`[data-periodeid="${periodeDiv.getAttribute("data-periodeid")}"]`
)
.forEach((e) => e.remove());
// On supprime la période de la Map periodes
@ -195,11 +228,11 @@ async function nouvellePeriode(period = null) {
periodeDiv.appendChild(modP);
periodeDiv.appendChild(close);
periodeDiv.setAttribute("data-periodeid", periodId);
document.getElementById("tableau-periode").appendChild(periodeDiv);
document.getElementById("tete-table").appendChild(periodeDiv);
// On récupère les étudiants (etudids)
let etudids = [
...document.querySelectorAll("#tableau-periode .header[data-etudid]"),
...document.querySelectorAll(".ligne[data-etudid]"),
].map((e) => e.getAttribute("data-etudid"));
// On génère une date de début et de fin de la période
@ -249,7 +282,7 @@ async function nouvellePeriode(period = null) {
cell.setAttribute("data-etudid", etudid);
cell.setAttribute("data-periodeid", periodId);
cell.id = `cell-${etudid}-${periodId}`;
document.getElementById("tableau-periode").appendChild(cell);
document.querySelector(`.ligne[data-etudid="${etudid}"]`).appendChild(cell);
//Vérification inscription au module
// Si l'étudiant n'est pas inscrit, on le notifie et on passe à l'étudiant suivant
@ -265,6 +298,10 @@ async function nouvellePeriode(period = null) {
const assiduites = data[etudid];
// Si l'étudiant n'a pas d'assiduité, on crée les boutons assiduité
if (assiduites.length == 0) {
const assi_btns = document.createElement('div');
assi_btns.classList.add('assi-btns');
["present", "retard", "absent"].forEach((value) => {
const cbox = document.createElement("input");
cbox.type = "checkbox";
@ -284,8 +321,9 @@ async function nouvellePeriode(period = null) {
// Si une valeur par défaut est donnée alors on l'applique
cbox.checked = etatDef.value == value;
cell.appendChild(cbox);
assi_btns.appendChild(cbox);
});
cell.appendChild(assi_btns);
} else {
// Si une (ou plus) assiduité sont trouvée pour la période
// alors on affiche les informations de la première assiduité
@ -297,6 +335,8 @@ async function nouvellePeriode(period = null) {
.catch((error) => {
console.error("Error:", error);
});
document.getElementById("tableau-periode").classList.remove("hidden");
}
/**
* Permet de récupérer la saisie puis créer les assiduités grâce à l'api
@ -337,6 +377,7 @@ function sauvegarderAssiduites() {
}
});
}
// Une fois les assiduités générées, on les envoie à l'api
async_post(
"../../api/assiduites/create",
@ -344,7 +385,7 @@ function sauvegarderAssiduites() {
// Si la requête passe
async (data) => {
// On supprime toutes les cases du tableau pour le mettre à jour
document.querySelectorAll(".cell").forEach((e) => e.remove());
document.querySelectorAll("[data-periodeid]").forEach((e)=>e.remove())
// On recrée les périodes
// (cela permet de redemander les assiduités, donc mettre à jour les cases)
@ -402,7 +443,7 @@ function sauvegarderAssiduites() {
const period = periodes.get(periodeId);
const li = document.createElement("li");
// On affiche la période
li.textContent = `Période du ${period.date_debut.format(
li.textContent = `Plage du ${period.date_debut.format(
"DD/MM/YYYY HH:mm"
)} à ${period.date_fin.format("HH:mm")}`;
@ -474,7 +515,8 @@ if (window.forceModule) {
* - On vérifie si la date est un jour travaillé
*/
async function main() {
afficherPDP(pdp.checked);
const checked = localStorage.getItem("scodoc-signal_assiduites_diff-pdp") == "true";
afficherPDP(checked);
$("#date").on("change", async function (d) {
// On vérifie si la date est un jour travaillé
dateCouranteEstTravaillee();
@ -497,8 +539,8 @@ main();
<div id="fix">
<!-- Nouvelle période
Permet de créer une nouvelle ligne pour une nouvelle période
<!-- Nouvelle Plage
Permet de créer une nouvelle ligne pour une nouvelle Plage
(
Jour, -> datepicker
Heure de début, -> timepicker
@ -529,38 +571,34 @@ main();
{{moduleimpl_select | safe}}
</label>
<button id="add_periode" onclick="nouvellePeriode()">Ajouter une période</button>
<button id="add_periode" onclick="nouvellePeriode()">Ajouter une plage</button>
</div>
<!-- Boutons d'actions
</div>
<!-- Boutons d'actions
- Sauvegarder
- Afficher la photo de profil
- Assiduité par défaut (aucune, present, retard, absent)
--->
<br>
<div id="actions" class="flex">
<button id="save" onclick="sauvegarderAssiduites()">ENREGISTRER</button>
<label for="pdp">
Photo de profil :
<input type="checkbox" name="pdp" id="pdp" checked onclick="afficherPDP(this.checked)">
</label>
<div id="actions" class="box">
<label for="pdp">
Photo de profil :
<input type="checkbox" name="pdp" id="pdp" checked onclick="afficherPDP(this.checked)">
</label>
<label for="etatDef">
Intialiser les étudiants comme :
<select name="etatDef" id="etatDef">
<option value="">-</option>
<option value="present">présents</option>
<option value="retard">en retard</option>
<option value="absent">absents</option>
</select>
</label>
<label for="etatDef">
Assiduité par défaut :
<select name="etatDef" id="etatDef">
<option value="">Aucune</option>
<option value="present">Présence</option>
<option value="retard">Retard</option>
<option value="absent">Absence</option>
</select>
</label>
<button id="save" onclick="sauvegarderAssiduites()">Sauvegarder l'assiduité</button>
</div>
</div>
<br>
<!-- Tableau à double entrée
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
@ -570,17 +608,27 @@ main();
--->
<div id="tableau-periode" class="grid-table">
<!-- Header de la première colonne -->
<div class="header sticky">Période</div>
<!-- Headers des autres colonnes (noms des étudiants) -->
<!-- Première ligne : Plages -->
<div class="ligne" id="tete-table">
<div class="cell header sticky">Étudiants</div>
{# <div class="cell header" periode-id="X">Plage X</div> #}
</div>
{# ... #}
<hr class="hidden" id="separator">
{% for etud in etudiants %}
<div class="header etudinfo" data-etudid="{{etud.etudid}}" id="head-{{etud.etudid}}">
<img src="../../api/etudiant/etudid/{{etud.etudid}}/photo?size=small" alt="{{etud.nomprenom}}" class="pdp">
<span>{{ etud.nomprenom }}</span>
<div class="ligne" data-etudid="{{etud.etudid}}">
<div class="cell etudinfo sticky" id="head-{{etud.etudid}}">
<img src="../../api/etudiant/etudid/{{etud.etudid}}/photo?size=small" alt="{{etud.nomprenom}}" class="pdp">
<span>{{ etud.nomprenom }}</span>
</div>
{# <div class="cell" periode-id="X">Assiduité Plage 1</div> #}
</div>
{% endfor %}
<!-- Sera remplis avec les nouvelles périodes -->
</div>

View File

@ -186,6 +186,12 @@ def bilan_dept():
if not table[0]:
return table[1]
# Récupération des formsemestres (pour le menu déroulant)
formsemestres: Query = FormSemestre.get_dept_formsemestres_courants(dept)
formsemestres_choices: dict[int, str] = {
fs.id: fs.titre_annee() for fs in formsemestres
}
# Peuplement du template jinja
return render_template(
"assiduites/pages/bilan_dept.j2",
@ -193,6 +199,8 @@ def bilan_dept():
search_etud=sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud"),
billets=billets,
sco=ScoData(formsemestre=formsemestre),
formsemestres=formsemestres_choices,
formsemestre_id=None if not formsemestre else formsemestre.id,
)
@ -1565,6 +1573,85 @@ def _prepare_tableau(
)
@bp.route("/recup_assiduites_plage", methods=["POST"])
@scodoc
@permission_required(Permission.AbsChange)
def recup_assiduites_plage():
"""
Renvoie un fichier excel contenant toutes les assiduités d'une plage
La plage est définie par les valeurs "datedeb" et "datefin" du formulaire
Par défaut tous les étudiants du département sont concernés
Si le champs "formsemestre_id" est présent dans le formulaire et est non vide,
seuls les étudiants inscrits dans ce semestre sont concernés.
"""
date_deb: datetime.datetime = request.form.get("datedeb")
date_fin: datetime.datetime = request.form.get("datefin")
# Vérification des dates
try:
date_deb = datetime.datetime.strptime(date_deb, "%d/%m/%Y")
except ValueError as exc:
raise ScoValueError("date_debut invalide", dest_url=request.referrer) from exc
try:
date_fin = datetime.datetime.strptime(date_fin, "%d/%m/%Y")
except ValueError as exc:
raise ScoValueError("date_fin invalide", dest_url=request.referrer) from exc
# Récupération des étudiants
etuds: Query = []
formsemestre_id: str | None = request.form.get("formsemestre_id")
name: str = ""
if formsemestre_id is not None and formsemestre_id != "":
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
etuds = formsemestre.etuds
name = formsemestre.session_id()
else:
dept: Departement = Departement.query.get_or_404(g.scodoc_dept_id)
etuds = dept.etudiants
name = dept.acronym
# Récupération des assiduités
assiduites: Query = Assiduite.query.filter(
Assiduite.etudid.in_([etud.id for etud in etuds])
)
# Filtrage des assiduités en fonction des dates données
assiduites = scass.filter_by_date(assiduites, Assiduite, date_deb, date_fin)
table_data: liste_assi.AssiJustifData = liste_assi.AssiJustifData(
assiduites_query=assiduites,
)
options: liste_assi.AssiDisplayOptions = liste_assi.AssiDisplayOptions(
show_pres=True,
show_reta=True,
show_module=True,
show_etu=True,
)
date_deb_str: str = date_deb.strftime("%d-%m-%Y")
date_fin_str: str = date_fin.strftime("%d-%m-%Y")
filename: str = f"assiduites_{name}_{date_deb_str}_{date_fin_str}"
tableau: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti(
table_data,
options=options,
titre="tableau-dept-" + filename,
no_pagination=True,
)
return scu.send_file(
tableau.excel(),
filename=filename,
mime=scu.XLSX_MIMETYPE,
suffix=scu.XLSX_SUFFIX,
)
@bp.route("/tableau_assiduite_actions", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.AbsChange)