forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
@ -532,7 +532,7 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
# On créé l'assiduité
|
||||
# 200 + obj si réussi
|
||||
# 404 + message d'erreur si non réussi
|
||||
code, obj = _create_one(data, etud)
|
||||
code, obj = create_one_assiduite(data, etud)
|
||||
if code == 404:
|
||||
errors.append({"indice": i, "message": obj})
|
||||
else:
|
||||
@ -590,7 +590,7 @@ def assiduites_create():
|
||||
# route sans département
|
||||
set_sco_dept(etud.departement.acronym)
|
||||
|
||||
code, obj = _create_one(data, etud)
|
||||
code, obj = create_one_assiduite(data, etud)
|
||||
if code == 404:
|
||||
errors.append({"indice": i, "message": obj})
|
||||
else:
|
||||
@ -600,14 +600,14 @@ def assiduites_create():
|
||||
return {"errors": errors, "success": success}
|
||||
|
||||
|
||||
def _create_one(
|
||||
def create_one_assiduite(
|
||||
data: dict,
|
||||
etud: Identite,
|
||||
) -> tuple[int, object]:
|
||||
"""
|
||||
_create_one Création d'une assiduité à partir d'une représentation JSON
|
||||
create_one_assiduite: création d'une assiduité à partir d'un dict
|
||||
|
||||
Cette fonction vérifie la représentation JSON
|
||||
Cette fonction vérifie les données du dict (qui vient du JSON API ou d'ailleurs)
|
||||
|
||||
Puis crée l'assiduité si la représentation est valide.
|
||||
|
||||
@ -761,7 +761,7 @@ def assiduite_delete():
|
||||
|
||||
# Pour chaque assiduite_id on essaye de supprimer l'assiduité
|
||||
for i, assiduite_id in enumerate(assiduites_list):
|
||||
# De la même façon que "_create_one"
|
||||
# De la même façon que "create_one_assiduite"
|
||||
# Ici le code est soit 200 si réussi ou 404 si raté
|
||||
# Le message est le message d'erreur si erreur
|
||||
code, msg = _delete_one(assiduite_id)
|
||||
@ -1014,7 +1014,9 @@ def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
|
||||
else assiduite_unique.external_data
|
||||
)
|
||||
|
||||
if force and not (external_data is not None and external_data.get("module", False) != ""):
|
||||
if force and not (
|
||||
external_data is not None and external_data.get("module", False) != ""
|
||||
):
|
||||
errors.append(
|
||||
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
|
||||
)
|
||||
|
105
app/forms/assiduite/ajout_assiduite_etud.py
Normal file
105
app/forms/assiduite/ajout_assiduite_etud.py
Normal file
@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaire ajout d'une "assiduité" sur un étudiant
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
SelectField,
|
||||
StringField,
|
||||
SubmitField,
|
||||
RadioField,
|
||||
TextAreaField,
|
||||
validators,
|
||||
)
|
||||
|
||||
|
||||
class AjoutAssiduiteEtudForm(FlaskForm):
|
||||
"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"),
|
||||
],
|
||||
)
|
||||
date_debut = StringField(
|
||||
"Date de début",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "assi_date_debut",
|
||||
},
|
||||
)
|
||||
heure_debut = StringField(
|
||||
"Heure début",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_heure_debut",
|
||||
},
|
||||
)
|
||||
heure_fin = StringField(
|
||||
"Heure fin",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_heure_fin",
|
||||
},
|
||||
)
|
||||
date_fin = StringField(
|
||||
"Date de fin (si plusieurs jours)",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"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,
|
||||
"maxlength": 500,
|
||||
},
|
||||
)
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
@ -194,7 +194,8 @@ class Assiduite(db.Model):
|
||||
user_id=user_id,
|
||||
)
|
||||
db.session.add(nouv_assiduite)
|
||||
log(f"create_assiduite: {etud.id} {nouv_assiduite}")
|
||||
db.session.flush()
|
||||
log(f"create_assiduite: {etud.id} id={nouv_assiduite.id} {nouv_assiduite}")
|
||||
Scolog.logdb(
|
||||
method="create_assiduite",
|
||||
etudid=etud.id,
|
||||
@ -308,6 +309,8 @@ class Justificatif(db.Model):
|
||||
)
|
||||
|
||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
"date de création de l'élément: date de saisie"
|
||||
# pourrait devenir date de dépot au secrétariat, si différente
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
|
@ -340,6 +340,42 @@ class Identite(db.Model, models.ScoDocModel):
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def get_modimpls_by_formsemestre(
|
||||
self, annee_scolaire: int
|
||||
) -> dict[int, list["ModuleImpl"]]:
|
||||
"""Pour chaque semestre de l'année indiquée dans lequel l'étudiant
|
||||
est inscrit à des moduleimpls, liste ceux ci.
|
||||
{ formsemestre_id : [ modimpl, ... ] }
|
||||
annee_scolaire est un nombre: eg 2023
|
||||
"""
|
||||
date_debut_annee = scu.date_debut_anne_scolaire(annee_scolaire)
|
||||
date_fin_annee = scu.date_fin_anne_scolaire(annee_scolaire)
|
||||
modimpls = (
|
||||
ModuleImpl.query.join(ModuleImplInscription)
|
||||
.join(FormSemestre)
|
||||
.filter(
|
||||
(FormSemestre.date_debut <= date_fin_annee)
|
||||
& (FormSemestre.date_fin >= date_debut_annee)
|
||||
)
|
||||
.join(Identite)
|
||||
.filter_by(id=self.id)
|
||||
)
|
||||
# Tri, par semestre puis par module, suivant le type de formation:
|
||||
formsemestres = sorted(
|
||||
{m.formsemestre for m in modimpls}, key=lambda s: s.sort_key()
|
||||
)
|
||||
modimpls_by_formsemestre = {}
|
||||
for formsemestre in formsemestres:
|
||||
modimpls_sem = [m for m in modimpls if m.formsemestre_id == formsemestre.id]
|
||||
if formsemestre.formation.is_apc():
|
||||
modimpls_sem.sort(key=lambda m: m.module.sort_key_apc())
|
||||
else:
|
||||
modimpls_sem.sort(
|
||||
key=lambda m: (m.module.ue.numero or 0, m.module.numero or 0)
|
||||
)
|
||||
modimpls_by_formsemestre[formsemestre.id] = modimpls_sem
|
||||
return modimpls_by_formsemestre
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict. No other side effect.
|
||||
@ -937,3 +973,8 @@ class EtudAnnotation(db.Model):
|
||||
etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7))
|
||||
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||
comment = db.Column(db.Text)
|
||||
|
||||
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.modules import Module
|
||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||
|
@ -11,8 +11,9 @@ from operator import attrgetter
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app import db, log
|
||||
from app.models import ScoDocModel, Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||
from app.models import ScoDocModel, GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.events import Scolog
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
|
@ -129,7 +129,7 @@ def sidebar(etudid: int = None):
|
||||
if current_user.has_permission(Permission.AbsChange):
|
||||
H.append(
|
||||
f"""
|
||||
<li><a href="{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</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>
|
||||
"""
|
||||
)
|
||||
|
@ -82,7 +82,7 @@ def sco_dump_and_send_db(
|
||||
fcntl.flock(x, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except (IOError, OSError) as e:
|
||||
raise ScoValueError(
|
||||
"Un envoi de la base {db_name} est déjà en cours, re-essayer plus tard"
|
||||
f"Un envoi de la base {db_name} est déjà en cours, re-essayer plus tard"
|
||||
) from e
|
||||
|
||||
try:
|
||||
|
@ -1556,13 +1556,18 @@ def is_assiduites_module_forced(
|
||||
return retour
|
||||
|
||||
|
||||
def get_assiduites_time_config(config_type: str) -> str:
|
||||
def get_assiduites_time_config(config_type: str) -> str | int:
|
||||
"Renvoie config demandée"
|
||||
# config_type devrait être le nom de la variable de config pour rester cohérent...
|
||||
from app.models import ScoDocSiteConfig
|
||||
|
||||
match config_type:
|
||||
case "matin":
|
||||
case "matin" | "assi_morning_time":
|
||||
return ScoDocSiteConfig.get("assi_morning_time", "08:00:00")
|
||||
case "aprem":
|
||||
case "aprem" | "assi_afternoon_time":
|
||||
return ScoDocSiteConfig.get("assi_afternoon_time", "18:00:00")
|
||||
case "pivot":
|
||||
case "pivot" | "assi_lunch_time":
|
||||
return ScoDocSiteConfig.get("assi_lunch_time", "13:00:00")
|
||||
case "tick" | "assi_tick_time":
|
||||
return ScoDocSiteConfig.get("assi_tick_time", 15)
|
||||
raise ValueError(f"invalid config_type: {config_type}")
|
||||
|
@ -649,3 +649,13 @@
|
||||
font-weight: normal;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
section.assi-form {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
table.liste_assi td.date {
|
||||
width: 140px;
|
||||
}
|
||||
table.liste_assi td.actions {
|
||||
width: 70px;
|
||||
}
|
||||
|
@ -1199,7 +1199,8 @@ div.vertical_spacing_but {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
span.wtf-field ul.errors li {
|
||||
span.wtf-field ul.errors li,
|
||||
span.wtf-field-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
@ -988,8 +988,7 @@ function createAssiduiteComplete(assiduite, etudid) {
|
||||
if (data.errors["0"].message == "Module non renseigné") {
|
||||
const HTML = `
|
||||
<p>Attention, le module doit obligatoirement être renseigné.</p>
|
||||
<p>Cela vient de la configuration du semestre ou plus largement du département.</p>
|
||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
<p>Voir configuration du semestre ou du département.</p>
|
||||
`;
|
||||
|
||||
const content = document.createElement("div");
|
||||
@ -1002,7 +1001,6 @@ function createAssiduiteComplete(assiduite, etudid) {
|
||||
) {
|
||||
const HTML = `
|
||||
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
|
||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
`;
|
||||
|
||||
const content = document.createElement("div");
|
||||
@ -1015,8 +1013,8 @@ function createAssiduiteComplete(assiduite, etudid) {
|
||||
"Duplication: la période rentre en conflit avec une plage enregistrée"
|
||||
) {
|
||||
const HTML = `
|
||||
<p>L'assiduité n'a pas pu être enregistrée car une autre assiduité existe sur la période sélectionnée</p>
|
||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
<p>L'assiduité n'a pas pu être enregistrée car un autre évènement
|
||||
existe sur la période sélectionnée</p>
|
||||
`;
|
||||
|
||||
const content = document.createElement("div");
|
||||
@ -1657,7 +1655,7 @@ function getSingleEtud(etudid) {
|
||||
}
|
||||
|
||||
function isSingleEtud() {
|
||||
return location.href.includes("SignaleAssiduiteEtud");
|
||||
return location.href.includes("ajout_assiduite_etud");
|
||||
}
|
||||
|
||||
function getCurrentAssiduiteModuleImplId() {
|
||||
|
1
app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.css
vendored
Normal file
1
app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
.ui-timepicker-container{position:absolute;overflow:hidden;box-sizing:border-box}.ui-timepicker,.ui-timepicker-viewport{box-sizing:content-box;height:205px;display:block;margin:0}.ui-timepicker{list-style:none;padding:0 1px;text-align:center}.ui-timepicker-viewport{padding:0;overflow:auto;overflow-x:hidden}.ui-timepicker-standard{font-family:Verdana,Arial,sans-serif;font-size:1.1em;background-color:#FFF;border:1px solid #AAA;color:#222;margin:0;padding:2px}.ui-timepicker-standard a{border:1px solid transparent;color:#222;display:block;padding:.2em .4em;text-decoration:none}.ui-timepicker-standard .ui-state-hover{background-color:#DADADA;border:1px solid #999;font-weight:400;color:#212121}.ui-timepicker-standard .ui-menu-item{margin:0;padding:0}.ui-timepicker-corners,.ui-timepicker-corners .ui-corner-all{-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px}.ui-timepicker-hidden{display:none}.ui-timepicker-no-scrollbar .ui-timepicker{border:none}/*# sourceMappingURL=jquery.timepicker.min.css.map */
|
2
app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.js
vendored
Normal file
2
app/static/libjs/timepicker-1.3.5/jquery.timepicker.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -49,7 +49,7 @@ class ListeAssiJusti(tb.Table):
|
||||
# Instanciation de la classe parent
|
||||
super().__init__(
|
||||
row_class=RowAssiJusti,
|
||||
classes=["gt_table", "gt_left"],
|
||||
classes=["liste_assi", "gt_table", "gt_left"],
|
||||
**kwargs,
|
||||
with_foot_titles=False,
|
||||
)
|
||||
@ -101,19 +101,19 @@ class ListeAssiJusti(tb.Table):
|
||||
"""
|
||||
Applique la pagination à une requête SQLAlchemy en fonction des paramètres de la classe.
|
||||
|
||||
Cette méthode prend une requête SQLAlchemy et applique la pagination en utilisant les attributs `page` et
|
||||
`NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
||||
Cette méthode prend une requête SQLAlchemy et applique la pagination en utilisant les
|
||||
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
||||
|
||||
Args:
|
||||
query (Query): La requête SQLAlchemy à paginer. Il s'agit d'une requête qui a déjà été construite et
|
||||
qui est prête à être exécutée.
|
||||
query (Query): La requête SQLAlchemy à paginer. Il s'agit d'une requête qui a déjà
|
||||
été construite et qui est prête à être exécutée.
|
||||
|
||||
Returns:
|
||||
Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée.
|
||||
|
||||
Note:
|
||||
Cette méthode ne modifie pas la requête originale; elle renvoie plutôt un nouvel objet qui contient les
|
||||
résultats paginés.
|
||||
Cette méthode ne modifie pas la requête originale; elle renvoie plutôt un nouvel
|
||||
objet qui contient les résultats paginés.
|
||||
"""
|
||||
return query.paginate(
|
||||
page=self.options.page, per_page=self.options.nb_ligne_page, error_out=False
|
||||
@ -123,8 +123,9 @@ class ListeAssiJusti(tb.Table):
|
||||
"""
|
||||
Combine les requêtes d'assiduités et de justificatifs en une seule requête.
|
||||
|
||||
Cette fonction prend en entrée deux requêtes optionnelles, une pour les assiduités et une pour les justificatifs,
|
||||
et renvoie une requête combinée qui sélectionne un ensemble spécifique de colonnes pour chaque type d'objet.
|
||||
Cette fonction prend en entrée deux requêtes optionnelles, une pour les assiduités
|
||||
et une pour les justificatifs, et renvoie une requête combinée qui sélectionne
|
||||
un ensemble spécifique de colonnes pour chaque type d'objet.
|
||||
|
||||
Les colonnes sélectionnées sont:
|
||||
- obj_id: l'identifiant de l'objet (assiduite_id pour les assiduités, justif_id pour les justificatifs)
|
||||
@ -138,13 +139,17 @@ class ListeAssiJusti(tb.Table):
|
||||
- user_id : l'identifiant de l'utilisateur qui a signalé l'assiduité ou le justificatif
|
||||
|
||||
Args:
|
||||
query_assiduite (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy pour les assiduités.
|
||||
Si None, aucune assiduité ne sera incluse dans la requête combinée. Defaults to None.
|
||||
query_justificatif (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy pour les justificatifs.
|
||||
Si None, aucun justificatif ne sera inclus dans la requête combinée. Defaults to None.
|
||||
query_assiduite (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||
pour les assiduités.
|
||||
Si None (default), aucune assiduité ne sera incluse dans la requête combinée.
|
||||
|
||||
query_justificatif (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||
pour les justificatifs.
|
||||
Si None (default), aucun justificatif ne sera inclus dans la requête combinée.
|
||||
|
||||
Returns:
|
||||
sqlalchemy.orm.Query: Une requête combinée qui peut être exécutée pour obtenir les résultats.
|
||||
sqlalchemy.orm.Query: Une requête combinée qui peut être exécutée pour
|
||||
obtenir les résultats.
|
||||
|
||||
Raises:
|
||||
ValueError: Si aucune requête n'est fournie (les deux paramètres sont None).
|
||||
@ -220,6 +225,10 @@ class RowAssiJusti(tb.Row):
|
||||
)
|
||||
|
||||
def ajouter_colonnes(self, lien_redirection: str = None):
|
||||
# Ajout colonne actions
|
||||
if self.table.options.show_actions:
|
||||
self._actions()
|
||||
|
||||
# Ajout de l'étudiant
|
||||
self.table: ListeAssiJusti
|
||||
if self.table.options.show_etu:
|
||||
@ -235,6 +244,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"},
|
||||
)
|
||||
# Date de fin
|
||||
self.add_cell(
|
||||
@ -243,15 +253,12 @@ 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"},
|
||||
)
|
||||
|
||||
# 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()
|
||||
|
||||
@ -263,6 +270,7 @@ class RowAssiJusti(tb.Row):
|
||||
data={"order": self.ligne["entry_date"]},
|
||||
raw_content=self.ligne["entry_date"],
|
||||
classes=["small-font"],
|
||||
column_classes={"entry_date"},
|
||||
)
|
||||
|
||||
def _type(self) -> None:
|
||||
@ -338,7 +346,9 @@ class RowAssiJusti(tb.Row):
|
||||
self.add_cell("module", "Module", "", data={"order": ""})
|
||||
|
||||
def _utilisateur(self) -> None:
|
||||
utilisateur: User = User.query.get(self.ligne["user_id"])
|
||||
utilisateur: User = (
|
||||
User.query.get(self.ligne["user_id"]) if self.ligne["user_id"] else None
|
||||
)
|
||||
|
||||
self.add_cell(
|
||||
"user",
|
||||
@ -359,7 +369,7 @@ class RowAssiJusti(tb.Row):
|
||||
obj_id=self.ligne["obj_id"],
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
html.append(f'<a title="Détails" href="{url}">ℹ️</a>') # utiliser url_for
|
||||
html.append(f'<a title="Détails" href="{url}">ℹ️</a>')
|
||||
|
||||
# Modifier
|
||||
url = url_for(
|
||||
@ -369,7 +379,7 @@ class RowAssiJusti(tb.Row):
|
||||
obj_id=self.ligne["obj_id"],
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
)
|
||||
html.append(f'<a title="Modifier" href="{url}">📝</a>') # utiliser url_for
|
||||
html.append(f'<a title="Modifier" href="{url}">📝</a>')
|
||||
|
||||
# Supprimer
|
||||
url = url_for(
|
||||
@ -381,7 +391,13 @@ class RowAssiJusti(tb.Row):
|
||||
)
|
||||
html.append(f'<a title="Supprimer" href="{url}">❌</a>') # utiliser url_for
|
||||
|
||||
self.add_cell("actions", "Actions", " ".join(html), no_excel=True)
|
||||
self.add_cell(
|
||||
"actions",
|
||||
"",
|
||||
" ".join(html),
|
||||
no_excel=True,
|
||||
column_classes={"actions"},
|
||||
)
|
||||
|
||||
|
||||
class AssiFiltre:
|
||||
|
123
app/templates/assiduites/pages/ajout_assiduite_etud.j2
Normal file
123
app/templates/assiduites/pages/ajout_assiduite_etud.j2
Normal file
@ -0,0 +1,123 @@
|
||||
{# Ajout d'une "assiduité" sur un étudiant #}
|
||||
|
||||
{% 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-assiduite-etud {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
form#ajout-assiduite-etud > div {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
/* Target the container of the radio buttons */
|
||||
div.radio-assi_etat {
|
||||
font-size: 120%;
|
||||
}
|
||||
div.radio-assi_etat ul {
|
||||
list-style-type: none; /* Remove bullets */
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
/* Style each radio button and label */
|
||||
div.radio-assi_etat ul li {
|
||||
display: inline-block; /* Display radio buttons inline */
|
||||
margin-right: 16px; /* Space between each radio button */
|
||||
}
|
||||
div.radio-assi_etat input[type="radio"] + label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div.radio-assi_etat input[type="radio"]:checked + label {
|
||||
/* Style for checked state */
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="tab-content">
|
||||
<h2>Signaler une absence, retard ou présence pour {{etud.html_link_fiche()|safe}}</h2>
|
||||
|
||||
{% if 'general_errors' in form.errors %}
|
||||
<div class="wtf-error-messages">
|
||||
{% for error in form.errors['general_errors'] %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form id="ajout-assiduite-etud" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{# Type d'évènement #}
|
||||
<div class="radio-assi_etat">
|
||||
{{ form.assi_etat.label }}
|
||||
{{ form.assi_etat() }}
|
||||
</div>
|
||||
{# Dates et heures #}
|
||||
<div class="dates-heures">
|
||||
{{ form.date_debut.label }} : {{ form.date_debut }}
|
||||
de {{ form.heure_debut }} à {{ form.heure_fin }}
|
||||
<span class="help">laisser les heures vides pour signaler 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 }} : {{ 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>
|
||||
{# Menu module #}
|
||||
<div class="select-module">
|
||||
{{ form.modimpl.label }} :
|
||||
{{ form.modimpl }}
|
||||
{{ render_field_errors(form, 'modimpl') }}
|
||||
</div>
|
||||
{# Raison #}
|
||||
<div>
|
||||
<div>{{ form.assi_raison.label }}</div>
|
||||
{{ form.assi_raison() }}
|
||||
{{ render_field_errors(form, 'assi_raison') }}
|
||||
</div>
|
||||
{# Submit #}
|
||||
<div>
|
||||
{{ form.submit }} {{ form.cancel }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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 %}
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="pageContent">
|
||||
<h3>Signaler un évènement pour {{etud.html_link_fiche()|safe}}</h3>
|
||||
<h3>Signaler une absence, présence ou retard pour {{etud.html_link_fiche()|safe}}</h3>
|
||||
{% if saisie_eval %}
|
||||
<div id="saisie_eval">
|
||||
<br>
|
||||
@ -19,7 +19,10 @@
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_date_debut" required>Date de début</legend>
|
||||
<scodoc-datetime name="assi_date_debut" id="assi_date_debut"> </scodoc-datetime>
|
||||
<input type="text" name="assi_date_debut" id="assi_date_debut" size="10"
|
||||
class="datepicker">
|
||||
<input type="text" name="assi_heure_debut" id="assi_heure_debut" size="5"
|
||||
class="timepicker">
|
||||
<span>Journée entière</span> <input type="checkbox" name="assi_journee" id="assi_journee">
|
||||
</div>
|
||||
<div class="assi-label" id="date_fin">
|
||||
@ -50,7 +53,7 @@
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_raison">Raison</legend>
|
||||
<textarea name="assi_raison" id="assi_raison" cols="50" rows="10" maxlength="500"></textarea>
|
||||
<textarea name="assi_raison" id="assi_raison" cols="75" rows="4" maxlength="500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -95,7 +98,17 @@
|
||||
}
|
||||
</style>
|
||||
<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") }}",
|
||||
defaultTime: 'now',
|
||||
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||
dynamic: false,
|
||||
dropdown: true,
|
||||
scrollbar: false
|
||||
});
|
||||
|
||||
function validateFields() {
|
||||
const field = document.querySelector('.assi-form')
|
||||
|
@ -56,3 +56,13 @@
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_field_errors(form, field_name) %}
|
||||
{% if form[field_name].errors %}
|
||||
<div>
|
||||
{% for error in form[field_name].errors %}
|
||||
<span class="wtf-field-error">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
@ -62,7 +62,7 @@
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% if current_user.has_permission(sco.Permission.AbsChange) %}
|
||||
<li><a href="{{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept,
|
||||
<li><a href="{{ url_for('assiduites.ajout_assiduite_etud', scodoc_dept=g.scodoc_dept,
|
||||
etudid=sco.etud.id) }}">Ajouter</a></li>
|
||||
<li><a href="{{ url_for('assiduites.ajout_justificatif_etud', scodoc_dept=g.scodoc_dept,
|
||||
etudid=sco.etud.id) }}">Justifier</a></li>
|
||||
|
@ -32,13 +32,14 @@ from flask import abort, url_for, redirect, Response
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
|
||||
from app.api.assiduites import create_one_assiduite
|
||||
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.models import (
|
||||
FormSemestre,
|
||||
Identite,
|
||||
@ -48,6 +49,7 @@ from app.models import (
|
||||
Departement,
|
||||
Evaluation,
|
||||
)
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
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
|
||||
@ -246,19 +248,19 @@ def bilan_dept():
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
@bp.route("/SignaleAssiduiteEtud")
|
||||
@bp.route("/ajout_assiduite_etud", methods=["GEt", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
def signal_assiduites_etud():
|
||||
def ajout_assiduite_etud():
|
||||
"""
|
||||
signal_assiduites_etud Saisie de l'assiduité d'un étudiant
|
||||
ajout_assiduite_etud Saisie d'une assiduité d'un étudiant
|
||||
|
||||
Args:
|
||||
etudid (int): l'identifiant de l'étudiant
|
||||
date_deb, date_fin: heures début et fin (ISO sans timezone)
|
||||
moduleimpl_id
|
||||
evaluation_id
|
||||
saisie_eval : si présent, mode "évaluation"
|
||||
evaluation_id : si présent, mode "évaluation"
|
||||
fmt: si xls, renvoie le tableau des assiduités enregistrées
|
||||
Returns:
|
||||
str: l'html généré
|
||||
"""
|
||||
@ -266,11 +268,13 @@ def signal_assiduites_etud():
|
||||
etud = Identite.get_etud(etudid)
|
||||
|
||||
# Gestion évaluations (appel à la page depuis les évaluations)
|
||||
saisie_eval: bool = request.args.get("saisie_eval") is not None
|
||||
evaluation_id: int = request.args.get("evaluation_id")
|
||||
saisie_eval = evaluation_id is not None
|
||||
|
||||
date_deb: str = request.args.get("date_deb")
|
||||
date_fin: str = request.args.get("date_fin")
|
||||
moduleimpl_id: int = request.args.get("moduleimpl_id", "")
|
||||
evaluation_id: int = request.args.get("evaluation_id")
|
||||
|
||||
redirect_url: str = (
|
||||
"#"
|
||||
if not saisie_eval
|
||||
@ -281,21 +285,32 @@ def signal_assiduites_etud():
|
||||
)
|
||||
)
|
||||
|
||||
# Préparation de la page (Header)
|
||||
header: str = html_sco_header.sco_header(
|
||||
page_title="Saisie assiduité",
|
||||
init_qtip=True,
|
||||
javascripts=[
|
||||
"js/assiduites.js",
|
||||
"js/date_utils.js",
|
||||
"js/etud_info.js",
|
||||
],
|
||||
cssstyles=CSSSTYLES
|
||||
+ [
|
||||
"css/assiduites.css",
|
||||
],
|
||||
)
|
||||
form = AjoutAssiduiteEtudForm(request.form)
|
||||
# On dresse la liste des modules de l'année scolaire en cours
|
||||
# auxquels est inscrit l'étudiant pour peupler le menu "module"
|
||||
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
||||
choices = {
|
||||
"": [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
|
||||
}
|
||||
for formsemestre_id in modimpls_by_formsemestre:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
# indique le nom du semestre dans le menu (optgroup)
|
||||
choices[formsemestre.titre_annee()] = [
|
||||
(m.id, m.module.code)
|
||||
for m in modimpls_by_formsemestre[formsemestre_id]
|
||||
if m.module.ue.type == UE_STANDARD
|
||||
]
|
||||
form.modimpl.choices = choices
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.cancel.data: # cancel button
|
||||
return redirect(redirect_url)
|
||||
ok = _record_assiduite_etud(etud, form)
|
||||
if ok:
|
||||
flash("enregistré")
|
||||
return redirect(redirect_url)
|
||||
|
||||
# Le tableau des assiduités+justificatifs déjà en base:
|
||||
is_html, tableau = _prepare_tableau(
|
||||
liste_assi.AssiJustifData.from_etudiants(
|
||||
etud,
|
||||
@ -305,52 +320,123 @@ def signal_assiduites_etud():
|
||||
filtre=liste_assi.AssiFiltre(type_obj=1),
|
||||
options=liste_assi.AssiDisplayOptions(show_module=True),
|
||||
)
|
||||
#
|
||||
if not is_html:
|
||||
return tableau
|
||||
# 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,
|
||||
),
|
||||
assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"),
|
||||
assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"),
|
||||
saisie_eval=saisie_eval,
|
||||
date_deb=date_deb,
|
||||
date_fin=date_fin,
|
||||
etud=etud,
|
||||
redirect_url=redirect_url,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
tableau=tableau,
|
||||
),
|
||||
# render_template(
|
||||
# "assiduites/pages/signal_assiduites_etud.j2",
|
||||
# sco=ScoData(etud),
|
||||
# date=_dateiso_to_datefr(date),
|
||||
# morning=morning,
|
||||
# lunch=lunch,
|
||||
# timeline=_timeline(heures=",".join([f"'{s}'" for s in heures])),
|
||||
# afternoon=afternoon,
|
||||
# nonworkdays=_non_work_days(),
|
||||
# forcer_module=sco_preferences.get_preference(
|
||||
# "forcer_module", dept_id=g.scodoc_dept_id
|
||||
# ),
|
||||
# diff=_differee(
|
||||
# etudiants=[sco_etud.get_etud_info(etudid=etud.etudid, filled=True)[0]],
|
||||
# moduleimpl_select=select,
|
||||
# ),
|
||||
# saisie_eval=saisie_eval,
|
||||
# date_deb=date_deb,
|
||||
# date_fin=date_fin,
|
||||
# redirect_url=redirect_url,
|
||||
# moduleimpl_id=moduleimpl_id,
|
||||
# ),
|
||||
).build()
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/ajout_assiduite_etud.j2",
|
||||
etud=etud,
|
||||
form=form,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
redirect_url=redirect_url,
|
||||
sco=ScoData(etud),
|
||||
tableau=tableau,
|
||||
scu=scu,
|
||||
)
|
||||
|
||||
|
||||
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 = True
|
||||
debut_jour = "00:00"
|
||||
fin_jour = "23:59:59"
|
||||
# On commence par convertir individuellement tous les champs
|
||||
try:
|
||||
date_debut = datetime.datetime.strptime(form.date_debut.data, "%d/%m/%Y")
|
||||
except ValueError:
|
||||
form.date_debut.errors.append("date début invalide")
|
||||
ok = False
|
||||
try:
|
||||
date_fin = (
|
||||
datetime.datetime.strptime(form.date_fin.data, "%d/%m/%Y")
|
||||
if form.date_fin.data
|
||||
else None
|
||||
)
|
||||
except ValueError:
|
||||
form.date_fin.errors.append("date fin invalide")
|
||||
ok = False
|
||||
|
||||
if date_fin:
|
||||
# ignore les heures si plusieurs jours
|
||||
heure_debut = datetime.time.fromisoformat(debut_jour) # 0h
|
||||
heure_fin = datetime.time.fromisoformat(fin_jour) # minuit
|
||||
else:
|
||||
try:
|
||||
heure_debut = datetime.time.fromisoformat(
|
||||
form.heure_debut.data or debut_jour
|
||||
)
|
||||
except ValueError:
|
||||
form.heure_debut.errors.append("heure début invalide")
|
||||
ok = False
|
||||
try:
|
||||
heure_fin = datetime.time.fromisoformat(form.heure_fin.data or fin_jour)
|
||||
except ValueError:
|
||||
form.heure_fin.errors.append("heure fin invalide")
|
||||
ok = False
|
||||
# Le module (avec "autre")
|
||||
mod_data = form.modimpl.data
|
||||
if mod_data:
|
||||
if mod_data == "autre":
|
||||
moduleimpl_id = "autre"
|
||||
else:
|
||||
try:
|
||||
moduleimpl_id = int(mod_data)
|
||||
except ValueError:
|
||||
form.modimpl.error("choix de module invalide")
|
||||
ok = False
|
||||
else:
|
||||
moduleimpl_id = 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
|
||||
data = {
|
||||
"date_debut": dt_debut.isoformat(),
|
||||
"date_fin": dt_fin.isoformat(),
|
||||
"etat": form.assi_etat.data,
|
||||
"moduleimpl_id": moduleimpl_id,
|
||||
}
|
||||
ok, result = create_one_assiduite(data, etud)
|
||||
if ok == 200:
|
||||
# assiduite_id = result["assiduite_id"]
|
||||
return True
|
||||
form.errors["general_errors"] = [f"Erreur: {result}"]
|
||||
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("/ListeAssiduitesEtud")
|
||||
@ -1513,10 +1599,12 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
|
||||
# rediriger vers page saisie
|
||||
return redirect(
|
||||
url_for(
|
||||
"assiduites.signal_assiduites_etud",
|
||||
"assiduites.ajout_assiduite_etud",
|
||||
etudid=etudid,
|
||||
evaluation_id=evaluation.id,
|
||||
date_deb=evaluation.date_debut.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
date_deb=evaluation.date_debut.strftime(
|
||||
"%Y-%m-%dT%H:%M:%S"
|
||||
), # XXX TODO
|
||||
date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
moduleimpl_id=evaluation.moduleimpl.id,
|
||||
saisie_eval="true",
|
||||
@ -1540,12 +1628,14 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
|
||||
if "Duplication" in msg:
|
||||
msg = """Une autre saisie concerne déjà cette période.
|
||||
En cliquant sur continuer vous serez redirigé vers la page de
|
||||
saisie des assiduités de l'étudiant."""
|
||||
saisie de l'assiduité de l'étudiant."""
|
||||
dest: str = url_for(
|
||||
"assiduites.signal_assiduites_etud",
|
||||
"assiduites.ajout_assiduite_etud",
|
||||
etudid=etudid,
|
||||
evaluation_id=evaluation.id,
|
||||
date_deb=evaluation.date_debut.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
date_deb=evaluation.date_debut.strftime(
|
||||
"%Y-%m-%dT%H:%M:%S"
|
||||
), # XXX TODO
|
||||
date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
moduleimpl_id=evaluation.moduleimpl.id,
|
||||
saisie_eval="true",
|
||||
|
@ -535,7 +535,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
"d",
|
||||
{
|
||||
"input_type": "separator",
|
||||
"title": f"L'utilisateur sera crée dans le département {auth_dept}",
|
||||
"title": f"L'utilisateur sera créé dans le département {auth_dept}",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user