1
0
forked from ScoDoc/ScoDoc

WIP: formulaire ajout justificatif

This commit is contained in:
Emmanuel Viennet 2023-12-12 03:05:31 +01:00
parent 99b182e1c7
commit bf4b69c9b2
16 changed files with 383 additions and 393 deletions

View File

@ -640,9 +640,9 @@ def justif_import(justif_id: int = None):
db.session.commit()
return {"filename": fname}
except ScoValueError as err:
except ScoValueError as exc:
# Si cela ne fonctionne pas on renvoie une erreur
return json_error(404, err.args[0])
return json_error(404, exc.args[0])
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["GET", "POST"])

View File

@ -26,9 +26,11 @@
"""
Formulaire ajout d'une "assiduité" sur un étudiant
Formulaire ajout d'un justificatif sur un étudiant
"""
from flask_wtf import FlaskForm
from flask_wtf.file import MultipleFileField
from wtforms import (
SelectField,
StringField,
@ -37,20 +39,15 @@ from wtforms import (
TextAreaField,
validators,
)
from wtforms.validators import DataRequired
class AjoutAssiduiteEtudForm(FlaskForm):
"Formulaire de saisie d'une assiduité pour un étudiant"
class AjoutAssiOrJustForm(FlaskForm):
"""Elements communs aux deux formulaires ajout
assiduité et justificatif
"""
error_message = "" # used to report our errors
assi_etat = RadioField(
"Signaler:",
choices=[("absent", "absence"), ("retard", "retard"), ("present", "présence")],
default="absent",
validators=[
validators.DataRequired("spécifiez le type d'évènement à signaler"),
],
)
date_debut = StringField(
"Date de début",
validators=[validators.Length(max=10)],
@ -89,14 +86,9 @@ class AjoutAssiduiteEtudForm(FlaskForm):
"id": "assi_date_fin",
},
)
modimpl = SelectField(
"Module",
choices={}, # will be populated dynamically
)
assi_raison = TextAreaField(
"Raison",
render_kw={
# "name": "assi_raison",
"id": "assi_raison",
"cols": 75,
"rows": 4,
@ -114,3 +106,37 @@ class AjoutAssiduiteEtudForm(FlaskForm):
)
submit = SubmitField("Enregistrer")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
"Formulaire de saisie d'une assiduité pour un étudiant"
assi_etat = RadioField(
"Signaler:",
choices=[("absent", "absence"), ("retard", "retard"), ("present", "présence")],
default="absent",
validators=[
validators.DataRequired("spécifiez le type d'évènement à signaler"),
],
)
modimpl = SelectField(
"Module",
choices={}, # will be populated dynamically
)
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
"Formulaire de saisie d'un justificatif pour un étudiant"
etat = SelectField(
"État du justificatif",
choices=[
("", "Choisir..."), # Placeholder
("attente", "En attente de validation"),
("non_valide", "Non valide"),
("modifie", "Modifié"),
("valide", "Valide"),
],
validators=[DataRequired(message="This field is required.")],
)
fichiers = MultipleFileField()

View File

@ -373,13 +373,13 @@ class Justificatif(db.Model):
}
return data
def __str__(self) -> str:
def __repr__(self) -> str:
"chaine pour journaux et debug (lisible par humain français)"
try:
etat_str = EtatJustificatif(self.etat).name
except ValueError:
etat_str = "Invalide"
return f"""Justificatif {etat_str} de {
return f"""Justificatif id={self.id} {etat_str} de {
self.date_debut.strftime("%d/%m/%Y %Hh%M")
} à {
self.date_fin.strftime("%d/%m/%Y %Hh%M")
@ -411,7 +411,7 @@ class Justificatif(db.Model):
db.session.add(nouv_justificatif)
log(f"create_justificatif: {etud.id} {nouv_justificatif}")
log(f"create_justificatif: etudid={etud.id} {nouv_justificatif}")
Scolog.logdb(
method="create_justificatif",
etudid=etud.id,
@ -482,8 +482,6 @@ def compute_assiduites_justified(
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
) -> list[int]:
"""
compute_assiduites_justified_faster
Args:
etudid (int): l'identifiant de l'étudiant
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés

View File

@ -122,26 +122,40 @@ def sidebar(etudid: int = None):
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, cur_sem)
nbabsnj = nbabs - nbabsjust
H.append(
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">({sco_preferences.get_preference("assi_metrique", None)})
f"""<span title="absences du { cur_sem["date_debut"] } au {
cur_sem["date_fin"] }">({
sco_preferences.get_preference("assi_metrique", None)})
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
)
H.append("<ul>")
if current_user.has_permission(Permission.AbsChange):
H.append(
f"""
<li><a href="{ url_for('assiduites.ajout_assiduite_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
<li><a href="{ url_for('assiduites.ajout_justificatif_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
<li><a href="{ url_for('assiduites.ajout_assiduite_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Ajouter</a></li>
<li><a href="{ url_for('assiduites.ajout_justificatif_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Justifier</a></li>
"""
)
if sco_preferences.get_preference("handle_billets_abs"):
H.append(
f"""<li><a href="{ url_for('absences.billets_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Billets</a></li>"""
f"""<li><a href="{ url_for('absences.billets_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Billets</a></li>"""
)
H.append(
f"""
<li><a href="{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Calendrier</a></li>
<li><a href="{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Liste</a></li>
<li><a href="{ url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Bilan</a></li>
<li><a href="{ url_for('assiduites.calendrier_assi_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Calendrier</a></li>
<li><a href="{ url_for('assiduites.liste_assiduites_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Liste</a></li>
<li><a href="{ url_for('assiduites.bilan_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Bilan</a></li>
</ul>
"""
)

View File

@ -122,10 +122,10 @@ class JustificatifArchiver(BaseArchiver):
archive_name: str = None,
description: str = "",
user_id: str = None,
) -> str:
) -> tuple[str, str]:
"""
Ajoute un fichier dans une archive "justificatif" pour l'etudid donné
Retourne l'archive_name utilisé
Retourne l'archive_name utilisé et le filename
"""
if archive_name is None:
archive_id: str = self.create_obj_archive(

View File

@ -316,7 +316,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
# --- Absences
H.append(
f"""<p>
<a href="{ url_for('assiduites.calendrier_etud',
<a href="{ url_for('assiduites.calendrier_assi_etud',
scodoc_dept=g.scodoc_dept, etudid=I['etudid']) }" class="bull_link">
<b>Absences :</b>{I['nbabs']} demi-journées, dont {I['nbabsjust']} justifiées
(pendant ce semestre).

View File

@ -134,8 +134,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
if nbabs:
H.append(
f"""<p class="bul_abs">
<a href="{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept, etudid=self.infos["etudid"]) }" class="bull_link">
<b>Absences :</b> {self.infos['nbabs']} demi-journées, dont {self.infos['nbabsjust']} justifiées
<a href="{ url_for('assiduites.calendrier_assi_etud',
scodoc_dept=g.scodoc_dept,
etudid=self.infos["etudid"]) }" class="bull_link">
<b>Absences :</b> {self.infos['nbabs']} demi-journées, dont {
self.infos['nbabsjust']} justifiées
(pendant ce semestre).
</a></p>
"""

View File

@ -147,7 +147,7 @@ def evaluation_check_absences_html(
H.append(': <span class="eval_check_absences_ok">ok</span>')
H.append("</h2>")
def etudlist(etudids, linkabs=False):
def etudlist(etudids: list[int], linkabs=False):
H.append("<ul>")
if not etudids and show_ok:
H.append("<li>aucun</li>")

View File

@ -1,15 +1,15 @@
:root {
--color-present: #6bdb83;
--color-absent: #e62a11;
--color-absent-clair: #f25d4a;
--color-absent-clair: rgb(252, 151, 50);
--color-retard: #f0c865;
--color-justi: #7059ff;
--color-justi-clair: #6885e3;
--color-justi: #29b1b9;
--color-justi-clair: #48f6ff;
--color-justi-invalide: #a84476;
--color-nonwork: #badfff;
--color-absent-justi: #e65ab7;
--color-retard-justi: #ffef7a;
--color-absent-justi: #ff86d7;
--color-retard-justi: #e8c6eb;
--color-error: #e62a11;
--color-warning: #eec660;
@ -657,19 +657,35 @@ section.assi-form {
table.liste_assi td.date {
width: 140px;
}
table.liste_assi.dataTable tbody td.date-debut {
padding-left: 12px;
}
table.liste_assi td.actions {
white-space: nowrap; /* boutons horizontalement */
}
table.liste_assi td.actions a:last-child {
padding-right: 12px;
}
tr.row-assiduite td {
border-bottom: 1px solid grey;
}
table.liste_assi tbody tr td.assi-type {
padding-left: 8px;
padding-right: 4px;
}
tr.row-assiduite.absent td.assi-type {
background-color: var(--color-absent-clair);
}
tr.row-assiduite.absent.justifiee td.assi-type {
background-color: var(--color-absent-justi);
}
tr.row-assiduite.retard td.assi-type {
background-color: var(--color-retard);
}
tr.row-assiduite.present td.assi-type {
background-color: var(--color-present);
}
tr.row-justificatif.valide td.assi-type {
background-color: var(--color-justi);
}

View File

@ -247,7 +247,7 @@ class RowAssiJusti(tb.Row):
self.ligne["date_debut"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["date_debut"]},
raw_content=self.ligne["date_debut"],
column_classes={"date"},
column_classes={"date", "date-debut"},
)
# Date de fin
self.add_cell(
@ -256,7 +256,7 @@ class RowAssiJusti(tb.Row):
self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"),
raw_content=self.ligne["date_fin"],
data={"order": self.ligne["date_fin"]},
column_classes={"date"},
column_classes={"date", "date-fin"},
)
# Ajout des colonnes optionnelles
@ -280,14 +280,16 @@ class RowAssiJusti(tb.Row):
obj_type: str = ""
is_assiduite: bool = self.ligne["type"] == "assiduite"
if is_assiduite:
justifiee: str = "Justifiée" if self.ligne["est_just"] else ""
self.classes.append("row-assiduite")
self.classes.append(EtatAssiduite(self.ligne["etat"]).name.lower())
if self.ligne["est_just"]:
self.classes.append("justifiee")
etat: str = {
EtatAssiduite.PRESENT: "Présence",
EtatAssiduite.ABSENT: "Absence",
EtatAssiduite.RETARD: "Retard",
}.get(self.ligne["etat"])
justifiee: str = "Justifiée" if self.ligne["est_just"] else ""
obj_type = f"{etat} {justifiee}"
else:
self.classes.append("row-justificatif")
@ -462,12 +464,12 @@ class AssiFiltre:
type_filtrage, date = val_filtre
match (type_filtrage):
# On garde uniquement les dates supérieur au filtre
# On garde uniquement les dates supérieures au filtre
case 2:
query_filtree = query_filtree.filter(
getattr(obj_class, cle_filtre) > date
)
# On garde uniquement les dates inférieur au filtre
# On garde uniquement les dates inférieures au filtre
case 1:
query_filtree = query_filtree.filter(
getattr(obj_class, cle_filtre) < date

View File

@ -1,245 +0,0 @@
{% include "assiduites/widgets/toast.j2" %}
{% block pageContent %}
<div class="pageContent">
<h3>Justifier des absences ou retards</h3>
<section class="justi-form page">
<fieldset>
<div class="justi-row">
<div class="justi-label">
<legend for="justi_date_debut" required>Date de début</legend>
<scodoc-datetime name="justi_date_debut" id="justi_date_debut"> </scodoc-datetime>
<span>Journée entière</span> <input type="checkbox" name="justi_journee" id="justi_journee">
</div>
<div class="justi-label" id="date_fin">
<legend for="justi_date_fin" required>Date de fin</legend>
<scodoc-datetime name="justi_date_fin" id="justi_date_fin"></scodoc-datetime>
</div>
</div>
<div class="justi-row">
<div class="justi-label">
<legend for="justi_etat" required>Etat du justificatif</legend>
<select name="justi_etat" id="justi_etat">
<option value="attente" selected>En Attente de validation</option>
<option value="non_valide">Non Valide</option>
<option value="modifie">Modifié</option>
<option value="valide">Valide</option>
</select>
</div>
</div>
<div class="justi-row">
<div class="justi-label">
<legend for="justi_raison">Raison</legend>
<textarea name="justi_raison" id="justi_raison" cols="50" rows="10" maxlength="500"></textarea>
</div>
</div>
<div class="justi-row">
<div class="justi-sect">
</div>
<div class="justi-label">
<legend for="justi_fich">Importer un fichier</legend>
<input type="file" name="justi_fich" id="justi_fich" multiple>
</div>
</div>
<div class="justi-row">
<button onclick="validerFormulaire(this)">Créer le justificatif</button>
<button onclick="effacerFormulaire()">Remettre à zero</button>
</div>
</fieldset>
</section>
<section class="assi-liste">
{{tableau | safe }}
</section>
</div>
<style>
.justi-row {
margin: 5px 0;
}
.justi-form fieldset {
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.pageContent {
max-width: var(--sco-content-max-width);
margin-top: 15px;
}
.justi-label {
margin: 0 10px;
}
[required]::after {
content: "*";
color: var(--color-error);
}
</style>
<script>
function validateFields() {
const field = document.querySelector('.justi-form')
const { deb, fin } = getDates()
const date_debut = new Date(deb);
const date_fin = new Date(fin);
if (deb == "" || fin == "" || !date_debut.isValid() || !date_fin.isValid()) {
openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin valide."), "", color = "crimson");
return false;
}
if (date_fin.isBefore(date_debut)) {
openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson");
return false;
}
return true
}
function fieldsToJustificatif() {
const field = document.querySelector('.justi-form.page')
const { deb, fin } = getDates()
const etat = field.querySelector('#justi_etat').value;
const raison = field.querySelector('#justi_raison').value;
return {
date_debut: new Date(deb).toFakeIso(),
date_fin: new Date(fin).toFakeIso(),
etat: etat,
raison: raison,
}
}
function importFiles(justif_id) {
const field = document.querySelector('.justi-form')
const in_files = field.querySelector('#justi_fich');
const path = getUrl() + `/api/justificatif/${justif_id}/import`;
const requests = []
Array.from(in_files.files).forEach((f) => {
pushToast(generateToast(document.createTextNode(`Importation du fichier : ${f.name} commencée`), color = "var(--color-information)"));
const fd = new FormData();
fd.append('file', f);
requests.push(
$.ajax(
{
url: path,
type: 'POST',
data: fd,
dateType: 'json',
contentType: false,
processData: false,
success: () => {
pushToast(generateToast(document.createTextNode(`Importation du fichier : ${f.name} finie`)));
},
}
)
)
});
$.when(...requests).done(() => {
location.reload();
})
}
function validerFormulaire(btn) {
if (!validateFields()) return
const justificatif = fieldsToJustificatif();
let justif_id = null;
let couverture = null;
createJustificatif(justificatif, (data) => {
if (Object.keys(data.errors).length > 0) {
console.error(data.errors);
errorAlert();
}
if (Object.keys(data.success).length > 0) {
couverture = data.success[0].message.couverture
justif_id = data.success[0].message.justif_id;
importFiles(justif_id);
return;
}
})
btn.disabled = true;
setTimeout(() => {
btn.disabled = false;
}, 1000)
}
function effacerFormulaire() {
const field = document.querySelector('.justi-form')
field.querySelector('#justi_date_debut').value = "";
field.querySelector('#justi_date_fin').value = "";
field.querySelector('#justi_etat').value = "attente";
field.querySelector('#justi_raison').value = "";
field.querySelector('#justi_fich').value = "";
}
function dayOnly() {
const date_deb = document.getElementById("justi_date_debut");
const date_fin = document.getElementById("justi_date_fin");
if (document.getElementById('justi_journee').checked) {
date_deb.setAttribute("show", "date")
date_fin.setAttribute("show", "date")
document.querySelector(`legend[for="justi_date_fin"]`).removeAttribute("required")
} else {
date_deb.removeAttribute("show")
date_fin.removeAttribute("show")
document.querySelector(`legend[for="justi_date_fin"]`).setAttribute("required", "")
}
}
function getDates() {
const date_deb = document.querySelector(".page #justi_date_debut")
const date_fin = document.querySelector(".page #justi_date_fin")
const journee = document.querySelector('.page #justi_journee').checked
const deb = date_deb.valueAsObject.date + "T" + (journee ? assi_morning : date_deb.valueAsObject.time)
let fin = "T" + (journee ? assi_evening : date_fin.valueAsObject.time)
if (journee) {
fin = (date_fin.valueAsObject.date || date_deb.valueAsObject.date) + fin
} else {
fin = date_fin.valueAsObject.date + fin
}
return {
"deb": deb,
"fin": fin,
}
}
const etudid = {{ sco.etud.id }};
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
const assi_morning = '{{assi_morning}}';
const assi_evening = '{{assi_evening}}';
window.onload = () => {
document.getElementById('justi_journee').addEventListener('click', () => { dayOnly() });
dayOnly()
document.getElementById("justi_date_debut").valueAsObject = { time: assi_morning }
document.getElementById("justi_date_fin").valueAsObject = { time: assi_evening }
}
</script>
{% endblock pageContent %}

View File

@ -0,0 +1,105 @@
{% extends "sco_page.j2" %}
{% import 'wtf.j2' as wtf %}
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock %}
{% block app_content %}
<style>
form#ajout-justificatif-etud {
margin-bottom: 24px;
}
form#ajout-justificatif-etud > div {
margin-bottom: 16px;
}
div.submit {
margin-top: 12px;
}
div.submit > input {
margin-right: 16px;
}
</style>
<div class="tab-content">
<h2>Justifier des absences ou retards</h2>
<section class="justi-form page">
<form id="ajout-justificatif-etud" method="post">
<fieldset>
{{ form.hidden_tag() }}
{# Dates et heures #}
<div class="dates-heures">
{{ form.date_debut.label }}&nbsp;: {{ form.date_debut }}
de {{ form.heure_debut }} à {{ form.heure_fin }}
<span class="help">laisser les heures vides pour indiquer la journée entière</span>
{{ render_field_errors(form, 'date_debut') }}
{{ render_field_errors(form, 'heure_debut') }}
{{ render_field_errors(form, 'heure_fin') }}
<div>
{{ form.date_fin.label }}&nbsp;: {{ form.date_fin }}
<span class="help">si le jour de fin est différent,
les heures seront ignorées (journées complètes)</span>
{{ render_field_errors(form, 'date_fin') }}
</div>
</div>
{# Etat #}
<div>
<div>{{ form.etat.label }}</div>
{{ form.etat() }}
{{ render_field_errors(form, 'etat') }}
</div>
{# Raison #}
<div>
<div>{{ form.assi_raison.label }}</div>
{{ form.assi_raison() }}
{{ render_field_errors(form, 'assi_raison') }}
</div>
{# Fichier(s) justificatif(s) #}
<div>
<div>{{ form.fichiers.label }}</div>
{{ form.fichiers() }}
{{ render_field_errors(form, 'fichiers') }}
</div>
{# Date dépot #}
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }}
<span class="help">laisser vide pour date courante</span>
{{ render_field_errors(form, 'entry_date') }}
{# Submit #}
<div class="submit">
{{ form.submit }} {{ form.cancel }}
</div>
</fieldset>
</form>
</section>
<section class="assi-liste">
{{tableau | safe }}
</section>
</div>
{% endblock app_content %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script>
$('.timepicker').timepicker({
timeFormat: 'HH:mm',
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
minTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
maxTime: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
dynamic: false,
dropdown: true,
scrollbar: false
});
</script>
{% endblock scripts %}

View File

@ -1,5 +1,5 @@
<div>
<div class="sco_box_title">Évènements enregistrés pour cet étudiant</div>
<div class="sco_box_title">{{ titre }}</div>
<div id="options-tableau">
{% if afficher_options != false %}
<input type="checkbox" id="show_pres" name="show_pres"

View File

@ -71,7 +71,7 @@
etudid=sco.etud.id) }}">Billets</a></li>
{% endif %}
{% endif %}
<li><a href="{{ url_for('assiduites.calendrier_etud', scodoc_dept=g.scodoc_dept,
<li><a href="{{ url_for('assiduites.calendrier_assi_etud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Calendrier</a></li>
<li><a href="{{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept,
etudid=sco.etud.id) }}">Liste</a></li>

View File

@ -32,14 +32,18 @@ from flask import g, request, render_template, flash
from flask import abort, url_for, redirect, Response
from flask_login import current_user
from app import db
from app import db, log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.decorators import (
scodoc,
permission_required,
)
from app.forms.assiduite.ajout_assiduite_etud import AjoutAssiduiteEtudForm
from app.forms.assiduite.ajout_assiduite_etud import (
AjoutAssiOrJustForm,
AjoutAssiduiteEtudForm,
AjoutJustificatifEtudForm,
)
from app.models import (
Assiduite,
Departement,
@ -53,7 +57,7 @@ from app.models import (
)
from app.scodoc.codes_cursus import UE_STANDARD
from app.auth.models import User
from app.models.assiduites import get_assiduites_justif
from app.models.assiduites import get_assiduites_justif, compute_assiduites_justified
from app.tables.list_etuds import RowEtud, TableEtud
import app.tables.liste_assiduites as liste_assi
@ -63,7 +67,6 @@ from app.views import ScoData
# ---------------
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
from app.scodoc import safehtml
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_groups_view
@ -254,7 +257,7 @@ def bilan_dept():
@bp.route("/ajout_assiduite_etud", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.AbsChange)
def ajout_assiduite_etud():
def ajout_assiduite_etud() -> str:
"""
ajout_assiduite_etud Saisie d'une assiduité d'un étudiant
@ -315,12 +318,11 @@ def ajout_assiduite_etud():
liste_assi.AssiJustifData.from_etudiants(
etud,
),
filename=f"assiduite-{etudid}",
filename=f"assiduite-{etud.nom or ''}",
afficher_etu=False,
filtre=liste_assi.AssiFiltre(type_obj=1),
options=liste_assi.AssiDisplayOptions(show_module=True),
)
#
if not is_html:
return tableau
@ -336,16 +338,13 @@ def ajout_assiduite_etud():
)
def _record_assiduite_etud(
etud: Identite,
form: AjoutAssiduiteEtudForm,
) -> bool:
"""Enregistre les données du formulaire de saisie assiduité.
Returns ok if successfully recorded, else put error info in the form.
Format attendu des données du formulaire:
form.assi_etat.data : 'absent'
form.date_debut.data : '05/12/2023'
form.heure_debut.data : '09:06' (heure locale du serveur)
def _get_dates_from_assi_form(
form: AjoutAssiOrJustForm,
) -> tuple[bool, datetime.datetime, datetime.datetime, datetime.datetime]:
"""Prend les dates et heures du form, les vérifie
puis converti en deux datetime, en timezone du serveur.
Ramène ok=True si ok.
Met des messages d'erreur dans le form.
"""
ok = True
debut_jour = "00:00"
@ -384,6 +383,44 @@ def _record_assiduite_etud(
except ValueError:
form.heure_fin.errors.append("heure fin invalide")
ok = False
# Vérifie cohérence des dates/heures
dt_debut = datetime.datetime.combine(date_debut, heure_debut)
dt_fin = datetime.datetime.combine(date_fin or date_debut, heure_fin)
if dt_fin <= dt_debut:
form.errors["general_errors"] = ["Erreur: dates début/fin incohérentes"]
return False
# La date de dépot (si vide, la date actuelle)
dt_entry_date = (
datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y")
if form.entry_date.data
else None
)
# Ajoute time zone serveur
dt_debut_tz_server = scu.TIME_ZONE.localize(dt_debut)
dt_fin_tz_server = scu.TIME_ZONE.localize(dt_fin)
dt_entry_date_tz_server = (
scu.TIME_ZONE.localize(dt_entry_date) if dt_entry_date else None
)
return ok, dt_debut_tz_server, dt_fin_tz_server, dt_entry_date_tz_server
def _record_assiduite_etud(
etud: Identite,
form: AjoutAssiduiteEtudForm,
) -> bool:
"""Enregistre les données du formulaire de saisie assiduité.
Returns ok if successfully recorded, else put error info in the form.
Format attendu des données du formulaire:
form.assi_etat.data : 'absent'
form.date_debut.data : '05/12/2023'
form.heure_debut.data : '09:06' (heure locale du serveur)
"""
(
ok,
dt_debut_tz_server,
dt_fin_tz_server,
dt_entry_date_tz_server,
) = _get_dates_from_assi_form(form)
# Le module (avec "autre")
mod_data = form.modimpl.data
if mod_data:
@ -397,26 +434,10 @@ def _record_assiduite_etud(
ok = False
else:
moduleimpl_id = None
# La date de dépot (si viden date actuelle)
dt_entry_date = (
datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y")
if form.entry_date.data
else None
)
if not ok:
return False
# Vérifie cohérence des dates/heures
dt_debut = datetime.datetime.combine(date_debut, heure_debut)
dt_fin = datetime.datetime.combine(date_fin or date_debut, heure_fin)
if dt_fin <= dt_debut:
form.errors["general_errors"] = ["Erreur: dates début/fin incohérentes"]
return False
# Ajoute time zone serveur
dt_debut_tz_server = scu.TIME_ZONE.localize(dt_debut)
dt_fin_tz_server = scu.TIME_ZONE.localize(dt_fin)
dt_entry_date_tz_server = (
scu.TIME_ZONE.localize(dt_entry_date) if dt_entry_date else None
)
external_data = None
moduleimpl: ModuleImpl | None = None
match moduleimpl_id:
@ -446,27 +467,6 @@ def _record_assiduite_etud(
form.error_message = f"Erreur: {exc.args[0]}"
return False
# # Génération de la page
# return HTMLBuilder(
# header,
# _mini_timeline(),
# render_template(
# "assiduites/pages/ajout_assiduites.j2",
# sco=ScoData(etud),
# assi_limit_annee=sco_preferences.get_preference(
# "assi_limit_annee",
# dept_id=g.scodoc_dept_id,
# ),
# saisie_eval=saisie_eval,
# date_deb=date_deb,
# date_fin=date_fin,
# etud=etud,
# redirect_url=redirect_url,
# moduleimpl_id=moduleimpl_id,
# tableau=tableau,
# scu=scu,
# ),
@bp.route("/liste_assiduites_etud")
@scodoc
@ -585,7 +585,9 @@ def bilan_etud():
).build()
@bp.route("/AjoutJustificatifEtud")
@bp.route(
"/ajout_justificatif_etud", methods=["GET", "POST"]
) # was AjoutJustificatifEtud
@scodoc
@permission_required(Permission.AbsChange)
def ajout_justificatif_etud():
@ -597,63 +599,130 @@ def ajout_justificatif_etud():
Returns:
str: l'html généré
"""
# Récupération de l'étudiant concerné
etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département")
# Préparation de la page (header)
header: str = html_sco_header.sco_header(
page_title="Justificatifs",
init_qtip=True,
javascripts=[
"js/assiduites.js",
"js/date_utils.js",
],
cssstyles=CSSSTYLES
+ [
"css/assiduites.css",
],
etud = Identite.get_etud(request.args.get("etudid"))
redirect_url = url_for(
"assiduites.calendrier_assi_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
)
tableau = _prepare_tableau(
form = AjoutJustificatifEtudForm()
if form.validate_on_submit():
if form.cancel.data: # cancel button
return redirect(redirect_url)
ok = _record_justificatif_etud(etud, form)
if ok:
return redirect(redirect_url)
is_html, tableau = _prepare_tableau(
liste_assi.AssiJustifData.from_etudiants(
etud,
),
filename=f"justificatifs-{etudid}",
filename=f"justificatifs-{etud.nom or ''}",
afficher_etu=False,
filtre=liste_assi.AssiFiltre(type_obj=2),
options=liste_assi.AssiDisplayOptions(show_module=False, show_desc=True),
afficher_options=False,
titre="Justificatifs enregistrés pour cet étudiant",
)
if not tableau[0]:
return tableau[1]
if not is_html:
return tableau
# Peuplement du template jinja
return HTMLBuilder(
header,
render_template(
"assiduites/pages/ajout_justificatif.j2",
sco=ScoData(etud),
return render_template(
"assiduites/pages/ajout_justificatif_etud.j2",
assi_limit_annee=sco_preferences.get_preference(
"assi_limit_annee",
dept_id=g.scodoc_dept_id,
),
assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"),
assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"),
tableau=tableau[1],
),
).build()
etud=etud,
form=form,
page_title="Justificatifs",
redirect_url=redirect_url,
sco=ScoData(etud),
scu=scu,
tableau=tableau,
)
@bp.route("/CalendrierAssiduitesEtud")
def _record_justificatif_etud(
etud: Identite,
form: AjoutJustificatifEtudForm,
) -> bool:
"""Enregistre les données du formulaire de saisie justificatif (et ses fichiers).
Returns ok if successfully recorded, else put error info in the form.
Format attendu des données du formulaire:
form.assi_etat.data : 'absent'
form.date_debut.data : '05/12/2023'
form.heure_debut.data : '09:06' (heure locale du serveur)
"""
(
ok,
dt_debut_tz_server,
dt_fin_tz_server,
dt_entry_date_tz_server,
) = _get_dates_from_assi_form(form)
if not ok:
return False
etat = scu.EtatJustificatif.get(form.etat.data)
try:
just = Justificatif.create_justificatif(
etud,
dt_debut_tz_server,
dt_fin_tz_server,
etat=etat,
raison=form.assi_raison.data,
entry_date=dt_entry_date_tz_server,
user_id=current_user.id,
)
db.session.add(just)
if not _upload_justificatif_files(just, form):
db.session.rollback()
return False
db.session.commit()
compute_assiduites_justified(etud.id, [just])
flash("Justificatif enregistré")
return True
except ScoValueError as exc:
db.session.rollback()
form.error_message = f"Erreur: {exc.args[0]}"
return False
def _upload_justificatif_files(
just: Justificatif, form: AjoutJustificatifEtudForm
) -> bool:
"""Enregistre les fichiers du formulaire de création de justificatif"""
# Utilisation de l'archiver de justificatifs
archiver: JustificatifArchiver = JustificatifArchiver()
archive_name: str = just.fichier
try:
# On essaye de sauvegarder les fichiers
for file in form.fichiers.data or []:
archive_name, _ = archiver.save_justificatif(
just.etudiant,
filename=file.filename,
data=file.stream.read(),
archive_name=archive_name,
user_id=current_user.id,
)
flash(f"Fichier {file.filename} enregistré")
if form.fichiers.data:
# On actualise l'archive du justificatif
just.fichier = archive_name
db.session.add(just)
return True
except ScoValueError as exc:
log(f"_upload_justificatif_files: error on {file.filename} for etud {etud.id}")
form.error_message = f"Erreur sur fichier justificatif: {exc.args[0]}"
return False
@bp.route("/calendrier_assi_etud")
@scodoc
@permission_required(Permission.ScoView)
def calendrier_etud():
def calendrier_assi_etud():
"""
calendrier_etud : Affichage d'un calendrier des assiduités de l'étudiant
Affichage d'un calendrier de l'assiduité de l'étudiant
Args:
etudid (int): l'identifiant de l'étudiant
@ -1028,7 +1097,7 @@ class RowEtudWithAssi(RowEtud):
self.etat_assiduite = etat_assiduite
# remplace lien vers fiche par lien vers calendrier
self.target_url = url_for(
"assiduites.calendrier_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
"assiduites.calendrier_assi_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
)
self.target_title = f"Calendrier de {etud.nomprenom}"
@ -1206,6 +1275,7 @@ def _prepare_tableau(
filtre: liste_assi.AssiFiltre = None,
options: liste_assi.AssiDisplayOptions = None,
afficher_options: bool = True,
titre="Évènements enregistrés pour cet étudiant",
) -> tuple[bool, Response | str]:
"""
Prépare un tableau d'assiduités / justificatifs
@ -1274,6 +1344,7 @@ def _prepare_tableau(
total_pages=table.total_pages,
options=options,
afficher_options=afficher_options,
titre=titre,
)

View File

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