diff --git a/app/api/etudiants.py b/app/api/etudiants.py
index 07df3d593..e8030b019 100644
--- a/app/api/etudiants.py
+++ b/app/api/etudiants.py
@@ -8,16 +8,17 @@
API : accès aux étudiants
"""
from datetime import datetime
+from operator import attrgetter
from flask import g, request
from flask_json import as_json
from flask_login import current_user
from flask_login import login_required
-from sqlalchemy import desc, or_
+from sqlalchemy import desc, func, or_
+from sqlalchemy.dialects.postgresql import VARCHAR
import app
from app.api import api_bp as bp, api_web_bp
-from app.scodoc.sco_utils import json_error
from app.api import tools
from app.decorators import scodoc, permission_required
from app.models import (
@@ -31,6 +32,8 @@ from app.scodoc import sco_bulletins
from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc.sco_permissions import Permission
+from app.scodoc.sco_utils import json_error, suppress_accents
+
# Un exemple:
# @bp.route("/api_function/")
@@ -164,12 +167,39 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
)
if not None in allowed_depts:
# restreint aux départements autorisés:
- etuds = etuds.join(Departement).filter(
+ query = query.join(Departement).filter(
or_(Departement.acronym == acronym for acronym in allowed_depts)
)
return [etud.to_dict_api() for etud in query]
+@bp.route("/etudiants/name/")
+@api_web_bp.route("/etudiants/name/")
+@scodoc
+@permission_required(Permission.ScoView)
+@as_json
+def etudiants_by_name(start: str = "", min_len=3, limit=32):
+ """Liste des étudiants dont le nom débute par start.
+ Si start fait moins de min_len=3 caractères, liste vide.
+ La casse et les accents sont ignorés.
+ """
+ if len(start) < min_len:
+ return []
+ start = suppress_accents(start).lower()
+ query = Identite.query.filter(
+ func.lower(func.unaccent(Identite.nom, type_=VARCHAR)).ilike(start + "%")
+ )
+ allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
+ if not None in allowed_depts:
+ # restreint aux départements autorisés:
+ query = query.join(Departement).filter(
+ or_(Departement.acronym == acronym for acronym in allowed_depts)
+ )
+ etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
+ # Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
+ return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
+
+
@bp.route("/etudiant/etudid//formsemestres")
@bp.route("/etudiant/nip//formsemestres")
@bp.route("/etudiant/ine//formsemestres")
diff --git a/app/api/evaluations.py b/app/api/evaluations.py
index d5a7a3cfc..af1c40af3 100644
--- a/app/api/evaluations.py
+++ b/app/api/evaluations.py
@@ -8,7 +8,7 @@
ScoDoc 9 API : accès aux évaluations
"""
-from flask import g
+from flask import g, request
from flask_json import as_json
from flask_login import login_required
@@ -17,7 +17,7 @@ import app
from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required
from app.models import Evaluation, ModuleImpl, FormSemestre
-from app.scodoc import sco_evaluation_db
+from app.scodoc import sco_evaluation_db, sco_saisie_notes
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
@@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
@scodoc
@permission_required(Permission.ScoView)
@as_json
-def the_eval(evaluation_id: int):
+def evaluation(evaluation_id: int):
"""Description d'une évaluation.
{
@@ -93,24 +93,22 @@ def evaluations(moduleimpl_id: int):
@as_json
def evaluation_notes(evaluation_id: int):
"""
- Retourne la liste des notes à partir de l'id d'une évaluation donnée
+ Retourne la liste des notes de l'évaluation
- evaluation_id : l'id d'une évaluation
+ evaluation_id : l'id de l'évaluation
Exemple de résultat :
{
- "1": {
- "id": 1,
- "etudid": 10,
+ "11": {
+ "etudid": 11,
"evaluation_id": 1,
"value": 15.0,
"comment": "",
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
"uid": 2
},
- "2": {
- "id": 2,
- "etudid": 1,
+ "12": {
+ "etudid": 12,
"evaluation_id": 1,
"value": 12.0,
"comment": "",
@@ -128,8 +126,8 @@ def evaluation_notes(evaluation_id: int):
.filter_by(dept_id=g.scodoc_dept_id)
)
- the_eval = query.first_or_404()
- dept = the_eval.moduleimpl.formsemestre.departement
+ evaluation = query.first_or_404()
+ dept = evaluation.moduleimpl.formsemestre.departement
app.set_sco_dept(dept.acronym)
notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
@@ -137,7 +135,49 @@ def evaluation_notes(evaluation_id: int):
# "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval.
note = notes[etudid]
note["value"] = scu.fmt_note(note["value"], keep_numeric=True)
- note["note_max"] = the_eval.note_max
+ note["note_max"] = evaluation.note_max
del note["id"]
- return notes
+ # in JS, keys must be string, not integers
+ return {str(etudid): note for etudid, note in notes.items()}
+
+
+@bp.route("/evaluation//notes/set", methods=["POST"])
+@api_web_bp.route("/evaluation//notes/set", methods=["POST"])
+@login_required
+@scodoc
+@permission_required(Permission.ScoEnsView)
+@as_json
+def evaluation_set_notes(evaluation_id: int):
+ """Écriture de notes dans une évaluation.
+ The request content type should be "application/json",
+ and contains:
+ {
+ 'notes' : [ [etudid, value], ... ],
+ 'comment' : optional string
+ }
+ Result:
+ - nb_changed: nombre de notes changées
+ - nb_suppress: nombre de notes effacées
+ - etudids_with_decision: liste des etudiants dont la note a changé
+ alors qu'ils ont une décision de jury enregistrée.
+ """
+ query = Evaluation.query.filter_by(id=evaluation_id)
+ if g.scodoc_dept:
+ query = (
+ query.join(ModuleImpl)
+ .join(FormSemestre)
+ .filter_by(dept_id=g.scodoc_dept_id)
+ )
+ evaluation = query.first_or_404()
+ dept = evaluation.moduleimpl.formsemestre.departement
+ app.set_sco_dept(dept.acronym)
+ data = request.get_json(force=True) # may raise 400 Bad Request
+ notes = data.get("notes")
+ if notes is None:
+ return scu.json_error(404, "no notes")
+ if not isinstance(notes, list):
+ return scu.json_error(404, "invalid notes argument (must be a list)")
+ return sco_saisie_notes.save_notes(
+ evaluation, notes, comment=data.get("comment", "")
+ )
diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py
index 64ba29799..c9c9348bd 100644
--- a/app/comp/bonus_spo.py
+++ b/app/comp/bonus_spo.py
@@ -18,7 +18,7 @@ import pandas as pd
from flask import g
-from app.scodoc.codes_cursus import UE_SPORT
+from app.scodoc.codes_cursus import UE_SPORT, UE_STANDARD
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
from app.scodoc.sco_utils import ModuleType
@@ -740,6 +740,7 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
name = "bonus_iut1grenoble_2017"
displayed_name = "IUT de Grenoble 1"
+
# C'est un bonus "multiplicatif": on l'exprime en additif,
# sur chaque moyenne d'UE m_0
# Augmenter de 5% correspond à multiplier par a=1.05
@@ -782,6 +783,7 @@ class BonusIUTRennes1(BonusSportAdditif):
seuil_moy_gen = 10.0
proportion_point = 1 / 20.0
classic_use_bonus_ues = False
+
# S'applique aussi en classic, sur la moy. gen.
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
@@ -1336,6 +1338,7 @@ class BonusStNazaire(BonusSport):
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
amplitude = 0.01 / 4 # 4pt => 1%
factor_max = 0.1 # 10% max
+
# Modifié 2022-11-29: calculer chaque bonus
# (de 1 à 3 modules) séparément.
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
@@ -1533,6 +1536,63 @@ class BonusIUTV(BonusSportAdditif):
# c'est le bonus par défaut: aucune méthode à surcharger
+# Finalement inutile: un bonus direct est mieux adapté à leurs besoins.
+# # class BonusMastersUSPNIG(BonusSportAdditif):
+# """Calcul bonus modules optionnels (sport, culture), règle Masters de l'Institut Galilée (USPN)
+
+# Les étudiants peuvent suivre des enseignements optionnels
+# de l'USPN (sports, musique, deuxième langue, culture, etc) dans une
+# UE libre. Les points au-dessus de 10 sur 20 obtenus dans cette UE
+# libre sont ajoutés au total des points obtenus pour les UE obligatoires
+# du semestre concerné.
+# """
+
+# name = "bonus_masters__uspn_ig"
+# displayed_name = "Masters de l'Institut Galilée (USPN)"
+# proportion_point = 1.0
+# seuil_moy_gen = 10.0
+
+# def __init__(
+# self,
+# formsemestre: "FormSemestre",
+# sem_modimpl_moys: np.array,
+# ues: list,
+# modimpl_inscr_df: pd.DataFrame,
+# modimpl_coefs: np.array,
+# etud_moy_gen,
+# etud_moy_ue,
+# ):
+# # Pour ce bonus, il nous faut la somme des coefs des modules non bonus
+# # du formsemestre (et non auxquels les étudiants sont inscrits !)
+# self.sum_coefs = sum(
+# [
+# m.module.coefficient
+# for m in formsemestre.modimpls_sorted
+# if (m.module.module_type == ModuleType.STANDARD)
+# and (m.module.ue.type == UE_STANDARD)
+# ]
+# )
+# super().__init__(
+# formsemestre,
+# sem_modimpl_moys,
+# ues,
+# modimpl_inscr_df,
+# modimpl_coefs,
+# etud_moy_gen,
+# etud_moy_ue,
+# )
+# # Bonus sur la moyenne générale seulement
+# # On a dans bonus_moy_arr le bonus additif classique
+# # Sa valeur sera appliquée comme moy_gen += bonus_moy_gen
+# # or ici on veut
+# # moy_gen = (somme des notes + bonus_moy_arr) / somme des coefs
+# # moy_gen += bonus_moy_arr / somme des coefs
+
+# self.bonus_moy_gen = (
+# None if self.bonus_moy_gen is None else self.bonus_moy_gen / self.sum_coefs
+# )
+
+
def get_bonus_class_dict(start=BonusSport, d=None):
"""Dictionnaire des classes de bonus
(liste les sous-classes de BonusSport ayant un nom)
diff --git a/app/comp/res_but.py b/app/comp/res_but.py
index a7af95655..e4602327b 100644
--- a/app/comp/res_but.py
+++ b/app/comp/res_but.py
@@ -288,7 +288,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
if ref_comp is None:
return set()
if parcour_id is None:
- ues_ids = {ue.id for ue in self.ues}
+ ues_ids = {ue.id for ue in self.ues if ue.type != UE_SPORT}
else:
parcour: ApcParcours = ApcParcours.query.get(parcour_id)
annee = (self.formsemestre.semestre_id + 1) // 2
@@ -306,12 +306,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
return ues_ids
- def etud_has_decision(self, etudid):
+ def etud_has_decision(self, etudid) -> bool:
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
prend aussi en compte les autorisations de passage.
Sous-classée en BUT pour les RCUEs et années.
"""
- return (
+ return bool(
super().etud_has_decision(etudid)
or ApcValidationAnnee.query.filter_by(
formsemestre_id=self.formsemestre.id, etudid=etudid
diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py
index cbebb8356..d5e85d99d 100644
--- a/app/comp/res_compat.py
+++ b/app/comp/res_compat.py
@@ -283,12 +283,12 @@ class NotesTableCompat(ResultatsSemestre):
]
return etudids
- def etud_has_decision(self, etudid):
+ def etud_has_decision(self, etudid) -> bool:
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
prend aussi en compte les autorisations de passage.
Sous-classée en BUT pour les RCUEs et années.
"""
- return (
+ return bool(
self.get_etud_decisions_ue(etudid)
or self.get_etud_decision_sem(etudid)
or ScolarAutorisationInscription.query.filter_by(
diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py
index c2b8cdf31..5bf3e03b7 100644
--- a/app/entreprises/forms.py
+++ b/app/entreprises/forms.py
@@ -36,6 +36,7 @@ from sqlalchemy import text
from wtforms import (
BooleanField,
DateField,
+ DecimalField,
FieldList,
FormField,
HiddenField,
@@ -122,13 +123,13 @@ class EntrepriseCreationForm(FlaskForm):
origine = _build_string_field("Origine du correspondant", required=False)
notes = _build_string_field("Notes sur le correspondant", required=False)
- submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
+ submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
if EntreprisePreferences.get_check_siret() and self.siret.data != "":
siret_data = self.siret.data.strip().replace(" ", "")
@@ -248,13 +249,13 @@ class SiteCreationForm(FlaskForm):
codepostal = _build_string_field("Code postal (*)")
ville = _build_string_field("Ville (*)")
pays = _build_string_field("Pays", required=False)
- submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
+ submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
site = EntrepriseSite.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data, nom=self.nom.data
@@ -278,10 +279,10 @@ class SiteModificationForm(FlaskForm):
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
site = EntrepriseSite.query.filter(
EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data,
@@ -326,7 +327,7 @@ class OffreCreationForm(FlaskForm):
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
],
)
- submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
+ submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def __init__(self, *args, **kwargs):
@@ -344,10 +345,10 @@ class OffreCreationForm(FlaskForm):
(dept.id, dept.acronym) for dept in Departement.query.all()
]
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
if len(self.depts.data) < 1:
self.depts.errors.append("Choisir au moins un département")
@@ -392,10 +393,10 @@ class OffreModificationForm(FlaskForm):
(dept.id, dept.acronym) for dept in Departement.query.all()
]
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
if len(self.depts.data) < 1:
self.depts.errors.append("Choisir au moins un département")
@@ -442,10 +443,10 @@ class CorrespondantCreationForm(FlaskForm):
"Notes", required=False, render_kw={"class": "form-control"}
)
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
if not self.telephone.data and not self.mail.data:
msg = "Saisir un moyen de contact (mail ou téléphone)"
@@ -458,13 +459,13 @@ class CorrespondantCreationForm(FlaskForm):
class CorrespondantsCreationForm(FlaskForm):
hidden_site_id = HiddenField()
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
- submit = SubmitField("Envoyer")
+ submit = SubmitField("Enregistrer")
cancel = SubmitField("Annuler")
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
correspondant_list = []
for entry in self.correspondants.entries:
@@ -531,10 +532,10 @@ class CorrespondantModificationForm(FlaskForm):
.all()
]
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
correspondant = EntrepriseCorrespondant.query.filter(
EntrepriseCorrespondant.id != self.hidden_correspondant_id.data,
@@ -566,7 +567,7 @@ class ContactCreationForm(FlaskForm):
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
)
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
- submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
+ submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
def validate_utilisateur(self, utilisateur):
@@ -613,8 +614,9 @@ class ContactModificationForm(FlaskForm):
class StageApprentissageCreationForm(FlaskForm):
etudiant = _build_string_field(
"Étudiant (*)",
- render_kw={"placeholder": "Tapez le nom de l'étudiant"},
+ render_kw={"placeholder": "Tapez le nom de l'étudiant", "autocomplete": "off"},
)
+ etudid = HiddenField()
type_offre = SelectField(
"Type de l'offre (*)",
choices=[("Stage"), ("Alternance")],
@@ -627,12 +629,12 @@ class StageApprentissageCreationForm(FlaskForm):
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
)
notes = TextAreaField("Notes")
- submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
+ submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
+ if not super().validate(extra_validators):
validate = False
if (
@@ -646,64 +648,27 @@ class StageApprentissageCreationForm(FlaskForm):
return validate
- def validate_etudiant(self, etudiant):
- etudiant_data = etudiant.data.upper().strip()
- stm = text(
- "SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
- )
- etudiant = (
- Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
- )
+ def validate_etudid(self, field):
+ "L'etudid doit avoit été placé par le JS"
+ etudid = int(field.data) if field.data else None
+ etudiant = Identite.query.get(etudid) if etudid is not None else None
if etudiant is None:
- raise ValidationError("Champ incorrect (selectionnez dans la liste)")
+ raise ValidationError("Étudiant introuvable (sélectionnez dans la liste)")
-class StageApprentissageModificationForm(FlaskForm):
- etudiant = _build_string_field(
- "Étudiant (*)",
- render_kw={"placeholder": "Tapez le nom de l'étudiant"},
- )
- type_offre = SelectField(
- "Type de l'offre (*)",
- choices=[("Stage"), ("Alternance")],
- validators=[DataRequired(message=CHAMP_REQUIS)],
- )
- date_debut = DateField(
- "Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
- )
- date_fin = DateField(
- "Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
- )
- notes = TextAreaField("Notes")
- submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
- cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
+class FrenchFloatField(StringField):
+ "A field allowing to enter . or ,"
- def validate(self):
- validate = True
- if not FlaskForm.validate(self):
- validate = False
-
- if (
- self.date_debut.data
- and self.date_fin.data
- and self.date_debut.data > self.date_fin.data
- ):
- self.date_debut.errors.append("Les dates sont incompatibles")
- self.date_fin.errors.append("Les dates sont incompatibles")
- validate = False
-
- return validate
-
- def validate_etudiant(self, etudiant):
- etudiant_data = etudiant.data.upper().strip()
- stm = text(
- "SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
- )
- etudiant = (
- Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
- )
- if etudiant is None:
- raise ValidationError("Champ incorrect (selectionnez dans la liste)")
+ def process_formdata(self, valuelist):
+ "catch incoming data"
+ if not valuelist:
+ return
+ try:
+ value = valuelist[0].replace(",", ".")
+ self.data = float(value)
+ except ValueError as exc:
+ self.data = None
+ raise ValueError(self.gettext("Not a valid decimal value.")) from exc
class TaxeApprentissageForm(FlaskForm):
@@ -720,25 +685,26 @@ class TaxeApprentissageForm(FlaskForm):
],
default=int(datetime.now().strftime("%Y")),
)
- montant = IntegerField(
+ montant = FrenchFloatField(
"Montant (*)",
validators=[
DataRequired(message=CHAMP_REQUIS),
- NumberRange(
- min=1,
- message="Le montant doit être supérieur à 0",
- ),
+ # NumberRange(
+ # min=0.1,
+ # max=1e8,
+ # message="Le montant doit être supérieur à 0",
+ # ),
],
default=1,
)
notes = TextAreaField("Notes")
- submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
+ submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
taxe = EntrepriseTaxeApprentissage.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data, annee=self.annee.data
@@ -788,12 +754,12 @@ class EnvoiOffreForm(FlaskForm):
submit = SubmitField("Envoyer")
cancel = SubmitField("Annuler")
- def validate(self):
+ def validate(self, extra_validators=None):
validate = True
list_select = True
- if not FlaskForm.validate(self):
- validate = False
+ if not super().validate(extra_validators):
+ return False
for entry in self.responsables.entries:
if entry.data:
diff --git a/app/entreprises/models.py b/app/entreprises/models.py
index 0ad22f25f..2dc825b82 100644
--- a/app/entreprises/models.py
+++ b/app/entreprises/models.py
@@ -164,7 +164,10 @@ class EntrepriseStageApprentissage(db.Model):
entreprise_id = db.Column(
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
)
- etudid = db.Column(db.Integer)
+ etudid = db.Column(
+ db.Integer,
+ db.ForeignKey("identite.id", ondelete="CASCADE"),
+ )
type_offre = db.Column(db.Text)
date_debut = db.Column(db.Date)
date_fin = db.Column(db.Date)
@@ -180,7 +183,7 @@ class EntrepriseTaxeApprentissage(db.Model):
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
)
annee = db.Column(db.Integer)
- montant = db.Column(db.Integer)
+ montant = db.Column(db.Float)
notes = db.Column(db.Text)
diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py
index 7e9773700..f9eac4f9a 100644
--- a/app/entreprises/routes.py
+++ b/app/entreprises/routes.py
@@ -28,7 +28,6 @@ from app.entreprises.forms import (
ContactCreationForm,
ContactModificationForm,
StageApprentissageCreationForm,
- StageApprentissageModificationForm,
EnvoiOffreForm,
AjoutFichierForm,
TaxeApprentissageForm,
@@ -239,7 +238,7 @@ def delete_validation_entreprise(entreprise_id):
text=f"Non validation de la fiche entreprise ({entreprise.nom})",
)
db.session.add(log)
- flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
+ flash("L'entreprise a été supprimée de la liste des entreprises à valider.")
return redirect(url_for("entreprises.validation"))
return render_template(
"entreprises/form_confirmation.j2",
@@ -770,7 +769,7 @@ def delete_taxe_apprentissage(entreprise_id, taxe_id):
)
db.session.add(log)
db.session.commit()
- flash("La taxe d'apprentissage a été supprimé de la liste.")
+ flash("La taxe d'apprentissage a été supprimée de la liste.")
return redirect(
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
)
@@ -966,7 +965,7 @@ def delete_offre(entreprise_id, offre_id):
)
db.session.add(log)
db.session.commit()
- flash("L'offre a été supprimé de la fiche entreprise.")
+ flash("L'offre a été supprimée de la fiche entreprise.")
return redirect(
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
)
@@ -1473,7 +1472,8 @@ def delete_contact(entreprise_id, contact_id):
@permission_required(Permission.RelationsEntreprisesChange)
def add_stage_apprentissage(entreprise_id):
"""
- Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
+ Permet d'ajouter un étudiant ayant réalisé un stage ou alternance
+ sur la fiche de l'entreprise
"""
entreprise = Entreprise.query.filter_by(
id=entreprise_id, visible=True
@@ -1484,15 +1484,8 @@ def add_stage_apprentissage(entreprise_id):
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
)
if form.validate_on_submit():
- etudiant_nomcomplet = form.etudiant.data.upper().strip()
- stm = text(
- "SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
- )
- etudiant = (
- Identite.query.from_statement(stm)
- .params(nom_prenom=etudiant_nomcomplet)
- .first()
- )
+ etudid = form.etudid.data
+ etudiant = Identite.query.get_or_404(etudid)
formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data
)
@@ -1538,7 +1531,7 @@ def add_stage_apprentissage(entreprise_id):
@permission_required(Permission.RelationsEntreprisesChange)
def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
"""
- Permet de modifier un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
+ Permet de modifier un étudiant ayant réalisé un stage ou alternance sur la fiche de l'entreprise
"""
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
id=stage_apprentissage_id, entreprise_id=entreprise_id
@@ -1548,21 +1541,14 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
description=f"etudiant {stage_apprentissage.etudid} inconnue"
)
- form = StageApprentissageModificationForm()
+ form = StageApprentissageCreationForm()
if request.method == "POST" and form.cancel.data:
return redirect(
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
)
if form.validate_on_submit():
- etudiant_nomcomplet = form.etudiant.data.upper().strip()
- stm = text(
- "SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
- )
- etudiant = (
- Identite.query.from_statement(stm)
- .params(nom_prenom=etudiant_nomcomplet)
- .first()
- )
+ etudid = form.etudid.data
+ etudiant = Identite.query.get_or_404(etudid)
formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data
)
@@ -1577,6 +1563,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
formation.formsemestre.formsemestre_id if formation else None,
)
stage_apprentissage.notes = form.notes.data.strip()
+ db.session.add(stage_apprentissage)
log = EntrepriseHistorique(
authenticated_user=current_user.user_name,
entreprise_id=stage_apprentissage.entreprise_id,
@@ -1593,7 +1580,9 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
)
)
elif request.method == "GET":
- form.etudiant.data = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
+ form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
+ sco_etud.format_prenom(etudiant.prenom)}"""
+ form.etudid.data = etudiant.id
form.type_offre.data = stage_apprentissage.type_offre
form.date_debut.data = stage_apprentissage.date_debut
form.date_fin.data = stage_apprentissage.date_fin
diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index 0f5f59c25..8a6047097 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -43,8 +43,8 @@ class Identite(db.Model):
"optionnel (si present, affiché à la place du nom)"
civilite = db.Column(db.String(1), nullable=False)
- # données d'état-civil. Si présent remplace les données d'usage dans les documents officiels (bulletins, PV)
- # cf nomprenom_etat_civil()
+ # données d'état-civil. Si présent remplace les données d'usage dans les documents
+ # officiels (bulletins, PV): voir nomprenom_etat_civil()
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")
@@ -220,7 +220,7 @@ class Identite(db.Model):
}
args_dict = {}
for key, value in args.items():
- if hasattr(cls, key):
+ if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
# compat scodoc7 (mauvaise idée de l'époque)
if key in fs_empty_stored_as_nulls and value == "":
value = None
diff --git a/app/models/evaluations.py b/app/models/evaluations.py
index 3e3cd07cd..fd777b1c2 100644
--- a/app/models/evaluations.py
+++ b/app/models/evaluations.py
@@ -145,6 +145,18 @@ class Evaluation(db.Model):
db.session.add(copy)
return copy
+ def is_matin(self) -> bool:
+ "Evaluation ayant lieu le matin (faux si pas de date)"
+ heure_debut_dt = self.heure_debut or datetime.time(8, 00)
+ # 8:00 au cas ou pas d'heure (note externe?)
+ return bool(self.jour) and heure_debut_dt < datetime.time(12, 00)
+
+ def is_apresmidi(self) -> bool:
+ "Evaluation ayant lieu l'après midi (faux si pas de date)"
+ heure_debut_dt = self.heure_debut or datetime.time(8, 00)
+ # 8:00 au cas ou pas d'heure (note externe?)
+ return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00)
+
def set_default_poids(self) -> bool:
"""Initialize les poids bvers les UE à leurs valeurs par défaut
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 68399262b..24468c7fb 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -782,6 +782,8 @@ class FormSemestre(db.Model):
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
et leur nom est le code du parcours (eg "Cyber").
"""
+ if self.formation.referentiel_competence_id is None:
+ return # safety net
partition = Partition.query.filter_by(
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
).first()
@@ -805,7 +807,10 @@ class FormSemestre(db.Model):
query = (
ApcParcours.query.filter_by(code=group.group_name)
.join(ApcReferentielCompetences)
- .filter_by(dept_id=g.scodoc_dept_id)
+ .filter_by(
+ dept_id=g.scodoc_dept_id,
+ id=self.formation.referentiel_competence_id,
+ )
)
if query.count() != 1:
log(
diff --git a/app/models/modules.py b/app/models/modules.py
index 3c04f097f..ed155fdd2 100644
--- a/app/models/modules.py
+++ b/app/models/modules.py
@@ -55,7 +55,7 @@ class Module(db.Model):
secondary=parcours_modules,
lazy="subquery",
backref=db.backref("modules", lazy=True),
- order_by="ApcParcours.numero",
+ order_by="ApcParcours.numero, ApcParcours.code",
)
app_critiques = db.relationship(
diff --git a/app/models/notes.py b/app/models/notes.py
index 0f82e2865..04ceb1c0f 100644
--- a/app/models/notes.py
+++ b/app/models/notes.py
@@ -56,7 +56,7 @@ class NotesNotes(db.Model):
"pour debug"
from app.models.evaluations import Evaluation
- return f"""<{self.__class__.__name__} {self.id} v={self.value} {self.date.isoformat()
+ return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} v={self.value} {self.date.isoformat()
} {Evaluation.query.get(self.evaluation_id) if self.evaluation_id else "X" }>"""
diff --git a/app/models/ues.py b/app/models/ues.py
index 8bbdb99e5..383f20f8f 100644
--- a/app/models/ues.py
+++ b/app/models/ues.py
@@ -58,7 +58,10 @@ class UniteEns(db.Model):
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
parcours = db.relationship(
- ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
+ ApcParcours,
+ secondary="ue_parcours",
+ backref=db.backref("ues", lazy=True),
+ order_by="ApcParcours.numero, ApcParcours.code",
)
# relations
diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py
index 17e2fe2a7..5e09bf96c 100644
--- a/app/scodoc/sco_archives.py
+++ b/app/scodoc/sco_archives.py
@@ -70,7 +70,7 @@ from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Departement, FormSemestre
from app.scodoc.TrivialFormulator import TrivialFormulator
-from app.scodoc.sco_exceptions import ScoPermissionDenied
+from app.scodoc.sco_exceptions import ScoException, ScoPermissionDenied
from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_groups
@@ -125,6 +125,12 @@ class BaseArchiver(object):
if not os.path.isdir(obj_dir):
log(f"creating directory {obj_dir}")
os.mkdir(obj_dir)
+ except FileExistsError as exc:
+ raise ScoException(
+ f"""BaseArchiver error: obj_dir={obj_dir} exists={
+ os.path.exists(obj_dir)
+ } isdir={os.path.isdir(obj_dir)}"""
+ ) from exc
finally:
scu.GSL.release()
return obj_dir
diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py
index 7cdb19a0b..7bbf12b5f 100644
--- a/app/scodoc/sco_etud.py
+++ b/app/scodoc/sco_etud.py
@@ -62,7 +62,9 @@ def format_etud_ident(etud):
else:
etud["prenom_etat_civil"] = ""
etud["civilite_str"] = format_civilite(etud["civilite"])
- etud["civilite_etat_civil_str"] = format_civilite(etud["civilite_etat_civil"])
+ etud["civilite_etat_civil_str"] = format_civilite(
+ etud.get("civilite_etat_civil", "X")
+ )
# Nom à afficher:
if etud["nom_usuel"]:
etud["nom_disp"] = etud["nom_usuel"]
@@ -145,7 +147,7 @@ def format_civilite(civilite):
def format_etat_civil(etud: dict):
if etud["prenom_etat_civil"]:
- civ = {"M": "M.", "F": "Mme", "X": ""}[etud["civilite_etat_civil"]]
+ civ = {"M": "M.", "F": "Mme", "X": ""}[etud.get("civilite_etat_civil", "X")]
return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
else:
return etud["nomprenom"]
diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py
index b99af2cc3..7cbc32331 100644
--- a/app/scodoc/sco_evaluation_db.py
+++ b/app/scodoc/sco_evaluation_db.py
@@ -252,12 +252,11 @@ def do_evaluation_delete(evaluation_id):
def do_evaluation_get_all_notes(
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
):
- """Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
+ """Toutes les notes pour une évaluation: { etudid : { 'value' : value, 'date' : date ... }}
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
"""
- do_cache = (
- filter_suppressed and table == "notes_notes" and (by_uid is None)
- ) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
+ # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
+ do_cache = filter_suppressed and table == "notes_notes" and (by_uid is None)
if do_cache:
r = sco_cache.EvaluationCache.get(evaluation_id)
if r is not None:
diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index 2f2c93b55..922951379 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -433,7 +433,7 @@ def excel_simple_table(
return ws.generate()
-def excel_feuille_saisie(e, titreannee, description, lines):
+def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines):
"""Genere feuille excel pour saisie des notes.
E: evaluation (dict)
lines: liste de tuples
@@ -512,18 +512,20 @@ def excel_feuille_saisie(e, titreannee, description, lines):
# description evaluation
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
ws.append_single_cell_row(
- "Evaluation du %s (coef. %g)" % (e["jour"], e["coefficient"]), style
+ "Evaluation du %s (coef. %g)"
+ % (evaluation.jour or "sans date", evaluation.coefficient or 0.0),
+ style,
)
# ligne blanche
ws.append_blank_row()
# code et titres colonnes
ws.append_row(
[
- ws.make_cell("!%s" % e["evaluation_id"], style_ro),
+ ws.make_cell("!%s" % evaluation.id, style_ro),
ws.make_cell("Nom", style_titres),
ws.make_cell("Prénom", style_titres),
ws.make_cell("Groupe", style_titres),
- ws.make_cell("Note sur %g" % e["note_max"], style_titres),
+ ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres),
ws.make_cell("Remarque", style_titres),
]
)
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index d64bf4d9c..4961e8a21 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -1333,11 +1333,18 @@ Ceci n'est possible que si :
cancelbutton="Annuler",
)
if tf[0] == 0:
- if formsemestre_has_decisions_or_compensations(formsemestre):
+ has_decisions, message = formsemestre_has_decisions_or_compensations(
+ formsemestre
+ )
+ if has_decisions:
H.append(
- """Ce semestre ne peut pas être supprimé !
- (il y a des décisions de jury ou des compensations par d'autres semestres)
-
"""
+ f"""Ce semestre ne peut pas être supprimé !
+ il y a des décisions de jury ou des compensations par d'autres semestres:
+
+
+ """
)
else:
H.append(tf[1])
@@ -1372,32 +1379,46 @@ def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
return flask.redirect(scu.ScoURL())
-def formsemestre_has_decisions_or_compensations(formsemestre: FormSemestre):
+def formsemestre_has_decisions_or_compensations(
+ formsemestre: FormSemestre,
+) -> tuple[bool, str]:
"""True if decision de jury (sem. UE, RCUE, année) émanant de ce semestre
ou compensation de ce semestre par d'autres semestres
ou autorisations de passage.
"""
# Validations de semestre ou d'UEs
- if ScolarFormSemestreValidation.query.filter_by(
+ nb_validations = ScolarFormSemestreValidation.query.filter_by(
formsemestre_id=formsemestre.id
- ).count():
- return True
- if ScolarFormSemestreValidation.query.filter_by(
+ ).count()
+ if nb_validations:
+ return True, f"{nb_validations} validations de semestre ou d'UE"
+ nb_validations = ScolarFormSemestreValidation.query.filter_by(
compense_formsemestre_id=formsemestre.id
- ).count():
- return True
+ ).count()
+ if nb_validations:
+ return True, f"{nb_validations} compensations utilisées dans d'autres semestres"
# Autorisations d'inscription:
- if ScolarAutorisationInscription.query.filter_by(
+ nb_validations = ScolarAutorisationInscription.query.filter_by(
origin_formsemestre_id=formsemestre.id
- ).count():
- return True
+ ).count()
+ if nb_validations:
+ return (
+ True,
+ f"{nb_validations} autorisations d'inscriptions émanant de ce semestre",
+ )
# Validations d'années BUT
- if ApcValidationAnnee.query.filter_by(formsemestre_id=formsemestre.id).count():
- return True
+ nb_validations = ApcValidationAnnee.query.filter_by(
+ formsemestre_id=formsemestre.id
+ ).count()
+ if nb_validations:
+ return True, f"{nb_validations} validations d'année BUT utilisant ce semestre"
# Validations de RCUEs
- if ApcValidationRCUE.query.filter_by(formsemestre_id=formsemestre.id).count():
- return True
- return False
+ nb_validations = ApcValidationRCUE.query.filter_by(
+ formsemestre_id=formsemestre.id
+ ).count()
+ if nb_validations:
+ return True, f"{nb_validations} validations de RCUE utilisant ce semestre"
+ return False, ""
def do_formsemestre_delete(formsemestre_id):
diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py
index c12fa73a8..82744b8fb 100644
--- a/app/scodoc/sco_formsemestre_inscriptions.py
+++ b/app/scodoc/sco_formsemestre_inscriptions.py
@@ -175,9 +175,7 @@ def do_formsemestre_demission(
)
db.session.add(event)
db.session.commit()
- sco_cache.invalidate_formsemestre(
- formsemestre_id=formsemestre_id
- ) # > démission ou défaillance
+ sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
if etat_new == scu.DEMISSION:
flash("Démission enregistrée")
elif etat_new == scu.DEF:
@@ -210,7 +208,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
if nt.etud_has_decision(etudid):
raise ScoValueError(
- """désinscription impossible: l'étudiant {etud.nomprenom} a
+ f"""désinscription impossible: l'étudiant {etud.nomprenom} a
une décision de jury (la supprimer avant si nécessaire)"""
)
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index 2a911ef53..c10322de5 100644
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -600,6 +600,7 @@ def formsemestre_description_table(
formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
+ is_apc = formsemestre.formation.is_apc()
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours)
@@ -613,7 +614,7 @@ def formsemestre_description_table(
else:
ues = formsemestre.get_ues()
columns_ids += [f"ue_{ue.id}" for ue in ues]
- if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
+ if sco_preferences.get_preference("bul_show_ects", formsemestre_id) and not is_apc:
columns_ids += ["ects"]
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
if with_evals:
@@ -640,6 +641,7 @@ def formsemestre_description_table(
sum_coef = 0
sum_ects = 0
last_ue_id = None
+ formsemestre_parcours_ids = {p.id for p in formsemestre.parcours}
for modimpl in formsemestre.modimpls_sorted:
# Ligne UE avec ECTS:
ue = modimpl.module.ue
@@ -666,7 +668,7 @@ def formsemestre_description_table(
ue_info[
f"_{k}_td_attrs"
] = f'style="background-color: {ue.color} !important;"'
- if not formsemestre.formation.is_apc():
+ if not is_apc:
# n'affiche la ligne UE qu'en formation classique
# car l'UE de rattachement n'a pas d'intérêt en BUT
rows.append(ue_info)
@@ -707,8 +709,17 @@ def formsemestre_description_table(
for ue in ues:
row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
if with_parcours:
+ # Intersection des parcours du module avec ceux du formsemestre
row["parcours"] = ", ".join(
- sorted([pa.code for pa in modimpl.module.parcours])
+ [
+ pa.code
+ for pa in (
+ modimpl.module.parcours
+ if modimpl.module.parcours
+ else modimpl.formsemestre.parcours
+ )
+ if pa.id in formsemestre_parcours_ids
+ ]
)
rows.append(row)
diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py
index c693c9228..03217dc7a 100644
--- a/app/scodoc/sco_moduleimpl_inscriptions.py
+++ b/app/scodoc/sco_moduleimpl_inscriptions.py
@@ -28,7 +28,7 @@
"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
"""
import collections
-from operator import itemgetter
+from operator import attrgetter
import flask
from flask import url_for, g, request
@@ -553,8 +553,11 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
>{etud.nomprenom}"""
)
# Parcours:
- group = partition_parcours.get_etud_group(etud.id)
- parcours_name = group.group_name if group else ""
+ if partition_parcours:
+ group = partition_parcours.get_etud_group(etud.id)
+ parcours_name = group.group_name if group else ""
+ else:
+ parcours_name = ""
H.append(f"""{parcours_name} | """)
# UEs:
for ue in ues:
@@ -668,7 +671,7 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
gr.append((partition["partition_name"], grp))
#
d = []
- for (partition_name, grp) in gr:
+ for partition_name, grp in gr:
if grp:
d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
r = []
@@ -680,25 +683,25 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
return False, len(ins), " et ".join(r)
-def _fmt_etud_set(ins, max_list_size=7):
+def _fmt_etud_set(etudids, max_list_size=7) -> str:
# max_list_size est le nombre max de noms d'etudiants listés
# au delà, on indique juste le nombre, sans les noms.
- if len(ins) > max_list_size:
- return "%d étudiants" % len(ins)
+ if len(etudids) > max_list_size:
+ return f"{len(etudids)} étudiants"
etuds = []
- for etudid in ins:
- etuds.append(sco_etud.get_etud_info(etudid=etudid, filled=True)[0])
- etuds.sort(key=itemgetter("nom"))
+ for etudid in etudids:
+ etud = Identite.query.get(etudid)
+ if etud:
+ etuds.append(etud)
+
return ", ".join(
[
- '%s'
- % (
+ f"""{etud.nomprenom}"""
+ for etud in sorted(etuds, key=attrgetter("sort_key"))
]
)
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index b6a3592d6..9901dad36 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -337,17 +337,18 @@ def ficheEtud(etudid=None):
if not sco_permissions_check.can_suppress_annotation(a["id"]):
a["dellink"] = ""
else:
- a[
- "dellink"
- ] = '%s | ' % (
- etudid,
- a["id"],
- scu.icontag(
- "delete_img",
- border="0",
- alt="suppress",
- title="Supprimer cette annotation",
- ),
+ a["dellink"] = (
+ '%s | '
+ % (
+ etudid,
+ a["id"],
+ scu.icontag(
+ "delete_img",
+ border="0",
+ alt="suppress",
+ title="Supprimer cette annotation",
+ ),
+ )
)
author = sco_users.user_info(a["author"])
alist.append(
@@ -446,7 +447,7 @@ def ficheEtud(etudid=None):
info[
"inscriptions_mkup"
] = f"""
-
Parcours
{info["liste_inscriptions"]}
+
Cursus
{info["liste_inscriptions"]}
{info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]}
"""
diff --git a/app/scodoc/sco_portal_apogee.py b/app/scodoc/sco_portal_apogee.py
index 61164c616..1ec3386ea 100644
--- a/app/scodoc/sco_portal_apogee.py
+++ b/app/scodoc/sco_portal_apogee.py
@@ -489,6 +489,7 @@ def _normalize_apo_fields(infolist):
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?'
ajoute les champs 'etape' (= None) et 'prenom' ('') s'ils ne sont pas présents.
+ ajoute le champ 'civilite_etat_civil' (='X'), et 'prenom_etat_civil' (='') si non présent.
"""
for infos in infolist:
if "paiementinscription" in infos:
@@ -520,6 +521,15 @@ def _normalize_apo_fields(infolist):
if "prenom" not in infos:
infos["prenom"] = ""
+ if "civilite_etat_civil" not in infos:
+ infos["civilite_etat_civil"] = "X"
+
+ if "civilite_etat_civil" not in infos:
+ infos["civilite_etat_civil"] = "X"
+
+ if "prenom_etat_civil" not in infos:
+ infos["prenom_etat_civil"] = ""
+
return infolist
diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
index 14a103809..6e9084d0c 100644
--- a/app/scodoc/sco_saisie_notes.py
+++ b/app/scodoc/sco_saisie_notes.py
@@ -36,41 +36,49 @@ import flask
from flask import g, url_for, request
from flask_login import current_user
+from app import log
from app.auth.models import User
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
-from app.models import Evaluation, FormSemestre
-from app.models import ModuleImpl, ScolarNews
+from app.models import (
+ Evaluation,
+ FormSemestre,
+ Module,
+ ModuleImpl,
+ NotesNotes,
+ ScolarNews,
+)
from app.models.etudiants import Identite
-import app.scodoc.sco_utils as scu
-from app.scodoc.sco_utils import ModuleType
-import app.scodoc.notesdb as ndb
-from app import log
+
from app.scodoc.sco_exceptions import (
AccessDenied,
InvalidNoteValue,
NoteProcessError,
- ScoGenError,
+ ScoBugCatcher,
+ ScoException,
ScoInvalidParamError,
ScoValueError,
)
-from app.scodoc.TrivialFormulator import TrivialFormulator, TF
from app.scodoc import html_sco_header, sco_users
from app.scodoc import htmlutils
from app.scodoc import sco_abs
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
-from app.scodoc import sco_evaluations
+from app.scodoc import sco_etud
from app.scodoc import sco_evaluation_db
+from app.scodoc import sco_evaluations
from app.scodoc import sco_excel
-from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes
-from app.scodoc import sco_etud
+import app.scodoc.notesdb as ndb
+from app.scodoc.TrivialFormulator import TrivialFormulator, TF
+import app.scodoc.sco_utils as scu
+from app.scodoc.sco_utils import json_error
+from app.scodoc.sco_utils import ModuleType
def convert_note_from_string(
@@ -128,29 +136,30 @@ def _displayNote(val):
return val
-def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict):
+def _check_notes(notes: list[(int, float)], evaluation: Evaluation):
# XXX typehint : float or str
"""notes is a list of tuples (etudid, value)
mod is the module (used to ckeck type, for malus)
returns list of valid notes (etudid, float value)
- and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
+ and 4 lists of etudid: etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress
"""
- note_max = evaluation["note_max"]
- if mod["module_type"] in (
+ note_max = evaluation.note_max or 0.0
+ module: Module = evaluation.moduleimpl.module
+ if module.module_type in (
scu.ModuleType.STANDARD,
scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE,
):
note_min = scu.NOTES_MIN
- elif mod["module_type"] == ModuleType.MALUS:
+ elif module.module_type == ModuleType.MALUS:
note_min = -20.0
else:
raise ValueError("Invalid module type") # bug
- L = [] # liste (etudid, note) des notes ok (ou absent)
- invalids = [] # etudid avec notes invalides
- withoutnotes = [] # etudid sans notes (champs vides)
- absents = [] # etudid absents
- tosuppress = [] # etudids avec ancienne note à supprimer
+ valid_notes = [] # liste (etudid, note) des notes ok (ou absent)
+ etudids_invalids = [] # etudid avec notes invalides
+ etudids_without_notes = [] # etudid sans notes (champs vides)
+ etudids_absents = [] # etudid absents
+ etudid_to_suppress = [] # etudids avec ancienne note à supprimer
for etudid, note in notes:
note = str(note).strip().upper()
@@ -166,31 +175,34 @@ def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict):
note_max,
note_min=note_min,
etudid=etudid,
- absents=absents,
- tosuppress=tosuppress,
- invalids=invalids,
+ absents=etudids_absents,
+ tosuppress=etudid_to_suppress,
+ invalids=etudids_invalids,
)
if not invalid:
- L.append((etudid, value))
+ valid_notes.append((etudid, value))
else:
- withoutnotes.append(etudid)
- return L, invalids, withoutnotes, absents, tosuppress
+ etudids_without_notes.append(etudid)
+ return (
+ valid_notes,
+ etudids_invalids,
+ etudids_without_notes,
+ etudids_absents,
+ etudid_to_suppress,
+ )
def do_evaluation_upload_xls():
"""
Soumission d'un fichier XLS (evaluation_id, notefile)
"""
- authuser = current_user
vals = scu.get_request_args()
evaluation_id = int(vals["evaluation_id"])
comment = vals["comment"]
- E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
- M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
- # Check access
- # (admin, respformation, and responsable_id)
- if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]):
- raise AccessDenied("Modification des notes impossible pour %s" % authuser)
+ evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
+ # Check access (admin, respformation, and responsable_id)
+ if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
+ raise AccessDenied(f"Modification des notes impossible pour {current_user}")
#
diag, lines = sco_excel.excel_file_to_list(vals["notefile"])
try:
@@ -239,14 +251,16 @@ def do_evaluation_upload_xls():
if etudid:
notes.append((etudid, val))
ni += 1
- except:
+ except Exception as exc:
diag.append(
f"""Erreur: Ligne invalide ! (erreur ligne {ni})
{lines[ni]}"""
)
- raise InvalidNoteValue()
+ raise InvalidNoteValue() from exc
# -- check values
- L, invalids, withoutnotes, absents, _ = _check_notes(notes, E, M["module"])
- if len(invalids):
+ valid_notes, invalids, withoutnotes, absents, _ = _check_notes(
+ notes, evaluation
+ )
+ if invalids:
diag.append(
f"Erreur: la feuille contient {len(invalids)} notes invalides
"
)
@@ -258,37 +272,33 @@ def do_evaluation_upload_xls():
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
raise InvalidNoteValue()
else:
- nb_changed, nb_suppress, existing_decisions = notes_add(
- authuser, evaluation_id, L, comment
+ etudids_changed, nb_suppress, etudids_with_decisions = notes_add(
+ current_user, evaluation_id, valid_notes, comment
)
# news
- E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[
- 0
- ]
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
- mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
- mod["moduleimpl_id"] = M["moduleimpl_id"]
- mod["url"] = url_for(
+ module: Module = evaluation.moduleimpl.module
+ status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
- moduleimpl_id=mod["moduleimpl_id"],
+ moduleimpl_id=evaluation.moduleimpl_id,
_external=True,
)
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
- obj=M["moduleimpl_id"],
- text='Chargement notes dans %(titre)s' % mod,
- url=mod["url"],
+ obj=evaluation.moduleimpl_id,
+ text=f"""Chargement notes dans {
+ module.titre or module.code}""",
+ url=status_url,
max_frequency=30 * 60, # 30 minutes
)
- msg = (
- "%d notes changées (%d sans notes, %d absents, %d note supprimées)
"
- % (nb_changed, len(withoutnotes), len(absents), nb_suppress)
- )
- if existing_decisions:
- msg += """Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !
"""
- # msg += '' + str(notes) # debug
+ msg = f"""
{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, {
+ len(absents)} absents, {nb_suppress} note supprimées)
+
"""
+ if etudids_with_decisions:
+ msg += """Important: il y avait déjà des décisions de jury
+ enregistrées, qui sont peut-être à revoir suite à cette modification !
+ """
return 1, msg
except InvalidNoteValue:
@@ -310,14 +320,12 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id):
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
# Convert and check value
- L, invalids, _, _, _ = _check_notes(
- [(etud.id, value)], evaluation.to_dict(), evaluation.moduleimpl.module.to_dict()
- )
+ L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation)
if len(invalids) == 0:
- nb_changed, _, _ = notes_add(
+ etudids_changed, _, _ = notes_add(
current_user, evaluation.id, L, "Initialisation notes"
)
- if nb_changed == 1:
+ if len(etudids_changed) == 1:
return True
return False # error
@@ -352,9 +360,7 @@ def do_evaluation_set_missing(
if etudid not in notes_db: # pas de note
notes.append((etudid, value))
# Convert and check values
- L, invalids, _, _, _ = _check_notes(
- notes, evaluation.to_dict(), modimpl.module.to_dict()
- )
+ valid_notes, invalids, _, _, _ = _check_notes(notes, evaluation)
dest_url = url_for(
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id
)
@@ -372,13 +378,13 @@ def do_evaluation_set_missing(
"""
# Confirm action
if not dialog_confirmed:
- plural = len(L) > 1
+ plural = len(valid_notes) > 1
return scu.confirm_dialog(
f"""Mettre toutes les notes manquantes de l'évaluation
à la valeur {value} ?
Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
n'a été rentrée seront affectés.
- {len(L)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
+
{len(valid_notes)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
par ce changement de note.
""",
@@ -392,7 +398,7 @@ def do_evaluation_set_missing(
)
# ok
comment = "Initialisation notes manquantes"
- nb_changed, _, _ = notes_add(current_user, evaluation_id, L, comment)
+ etudids_changed, _, _ = notes_add(current_user, evaluation_id, valid_notes, comment)
# news
url = url_for(
"notes.moduleimpl_status",
@@ -408,7 +414,7 @@ def do_evaluation_set_missing(
)
return f"""
{ html_sco_header.sco_header() }
- {nb_changed} notes changées
+ {len(etudids_changed)} notes changées
-
Revenir au formulaire de saisie des notes
@@ -454,7 +460,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
)
if not dialog_confirmed:
- nb_changed, nb_suppress, existing_decisions = notes_add(
+ etudids_changed, nb_suppress, existing_decisions = notes_add(
current_user, evaluation_id, notes, do_it=False, check_inscription=False
)
msg = f"""
Confirmer la suppression des {nb_suppress} notes ?
@@ -475,14 +481,14 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
)
# modif
- nb_changed, nb_suppress, existing_decisions = notes_add(
+ etudids_changed, nb_suppress, existing_decisions = notes_add(
current_user,
evaluation_id,
notes,
comment="effacer tout",
check_inscription=False,
)
- assert nb_changed == nb_suppress
+ assert len(etudids_changed) == nb_suppress
H = [f"""
{nb_suppress} notes supprimées
"""]
if existing_decisions:
H.append(
@@ -516,7 +522,7 @@ def notes_add(
comment=None,
do_it=True,
check_inscription=True,
-) -> tuple:
+) -> tuple[list[int], int, list[int]]:
"""
Insert or update notes
notes is a list of tuples (etudid,value)
@@ -524,12 +530,12 @@ def notes_add(
WOULD be changed or suppressed.
Nota:
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
- Return tuple (nb_changed, nb_suppress, existing_decisions)
+
+ Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
"""
- now = psycopg2.Timestamp(
- *time.localtime()[:6]
- ) # datetime.datetime.now().isoformat()
- # Verifie inscription et valeur note
+ now = psycopg2.Timestamp(*time.localtime()[:6])
+
+ # Vérifie inscription et valeur note
inscrits = {
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
@@ -548,13 +554,13 @@ def notes_add(
# Met a jour la base
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
- nb_changed = 0
+ etudids_changed = []
nb_suppress = 0
- E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
- existing_decisions = (
- []
- ) # etudids pour lesquels il y a une decision de jury et que la note change
+ evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
+ formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
+ res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
+ # etudids pour lesquels il y a une decision de jury et que la note change:
+ etudids_with_decision = []
try:
for etudid, value in notes:
changed = False
@@ -562,7 +568,7 @@ def notes_add(
# nouvelle note
if value != scu.NOTES_SUPPRESS:
if do_it:
- aa = {
+ args = {
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": value,
@@ -570,13 +576,20 @@ def notes_add(
"uid": user.id,
"date": now,
}
- ndb.quote_dict(aa)
+ ndb.quote_dict(args)
+ # Note: le conflit ci-dessous peut arriver si un autre thread
+ # a modifié la base après qu'on ait lu notes_db
cursor.execute(
"""INSERT INTO notes_notes
(etudid, evaluation_id, value, comment, date, uid)
- VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)
+ VALUES
+ (%(etudid)s,%(evaluation_id)s,%(value)s,
+ %(comment)s,%(date)s,%(uid)s)
+ ON CONFLICT ON CONSTRAINT notes_notes_etudid_evaluation_id_key
+ DO UPDATE SET etudid=%(etudid)s, evaluation_id=%(evaluation_id)s,
+ value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s
""",
- aa,
+ args,
)
changed = True
else:
@@ -584,7 +597,7 @@ def notes_add(
oldval = notes_db[etudid]["value"]
if type(value) != type(oldval):
changed = True
- elif type(value) == float and (
+ elif isinstance(value, float) and (
abs(value - oldval) > scu.NOTES_PRECISION
):
changed = True
@@ -603,7 +616,7 @@ def notes_add(
""",
{"etudid": etudid, "evaluation_id": evaluation_id},
)
- aa = {
+ args = {
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": value,
@@ -611,7 +624,7 @@ def notes_add(
"comment": comment,
"uid": user.id,
}
- ndb.quote_dict(aa)
+ ndb.quote_dict(args)
if value != scu.NOTES_SUPPRESS:
if do_it:
cursor.execute(
@@ -620,52 +633,49 @@ def notes_add(
WHERE etudid = %(etudid)s
and evaluation_id = %(evaluation_id)s
""",
- aa,
+ args,
)
else: # suppression ancienne note
if do_it:
log(
- "notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
- % (evaluation_id, etudid, oldval)
+ f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={
+ etudid}, oldval={oldval}"""
)
cursor.execute(
"""DELETE FROM notes_notes
WHERE etudid = %(etudid)s
AND evaluation_id = %(evaluation_id)s
""",
- aa,
+ args,
)
# garde trace de la suppression dans l'historique:
- aa["value"] = scu.NOTES_SUPPRESS
+ args["value"] = scu.NOTES_SUPPRESS
cursor.execute(
- """INSERT INTO notes_notes_log (etudid,evaluation_id,value,comment,date,uid)
- VALUES (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
+ """INSERT INTO notes_notes_log
+ (etudid,evaluation_id,value,comment,date,uid)
+ VALUES
+ (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
""",
- aa,
+ args,
)
nb_suppress += 1
if changed:
- nb_changed += 1
- if has_existing_decision(M, E, etudid):
- existing_decisions.append(etudid)
+ etudids_changed.append(etudid)
+ if res.etud_has_decision(etudid):
+ etudids_with_decision.append(etudid)
except Exception as exc:
log("*** exception in notes_add")
if do_it:
cnx.rollback() # abort
# inval cache
- sco_cache.invalidate_formsemestre(
- formsemestre_id=M["formsemestre_id"]
- ) # > modif notes (exception)
+ sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id)
- raise # XXX
- raise ScoGenError("Erreur enregistrement note: merci de ré-essayer") from exc
+ raise ScoException from exc
if do_it:
cnx.commit()
- sco_cache.invalidate_formsemestre(
- formsemestre_id=M["formsemestre_id"]
- ) # > modif notes
+ sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id)
- return nb_changed, nb_suppress, existing_decisions
+ return etudids_changed, nb_suppress, etudids_with_decision
def saisie_notes_tableur(evaluation_id, group_ids=()):
@@ -868,44 +878,39 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
def feuille_saisie_notes(evaluation_id, group_ids=[]):
"""Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
- evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
- if not evals:
+ evaluation: Evaluation = Evaluation.query.get(evaluation_id)
+ if not evaluation:
raise ScoValueError("invalid evaluation_id")
- eval_dict = evals[0]
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=eval_dict["moduleimpl_id"])[0]
- formsemestre_id = M["formsemestre_id"]
- Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
- sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
- mod_responsable = sco_users.user_info(M["responsable_id"])
- if eval_dict["jour"]:
- indication_date = ndb.DateDMYtoISO(eval_dict["jour"])
+ modimpl = evaluation.moduleimpl
+ formsemestre = modimpl.formsemestre
+ mod_responsable = sco_users.user_info(modimpl.responsable_id)
+ if evaluation.jour:
+ indication_date = evaluation.jour.isoformat()
else:
- indication_date = scu.sanitize_filename(eval_dict["description"])[:12]
- eval_name = "%s-%s" % (Mod["code"], indication_date)
+ indication_date = scu.sanitize_filename(evaluation.description or "")[:12]
+ eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
- if eval_dict["description"]:
- evaltitre = "%s du %s" % (eval_dict["description"], eval_dict["jour"])
- else:
- evaltitre = "évaluation du %s" % eval_dict["jour"]
- description = "%s en %s (%s) resp. %s" % (
- evaltitre,
- Mod["abbrev"] or "",
- Mod["code"] or "",
- mod_responsable["prenomnom"],
+ date_str = (
+ f"""du {evaluation.jour.strftime("%d/%m/%Y")}"""
+ if evaluation.jour
+ else "(sans date)"
)
+ eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"} {date_str}"""
+
+ description = f"""{eval_titre} en {evaluation.moduleimpl.module.abbrev or ""} ({
+ evaluation.moduleimpl.module.code
+ }) resp. {mod_responsable["prenomnom"]}"""
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids=group_ids,
- formsemestre_id=formsemestre_id,
+ formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
etat=None,
)
groups = sco_groups.listgroups(groups_infos.group_ids)
gr_title_filename = sco_groups.listgroups_filename(groups)
- # gr_title = sco_groups.listgroups_abbrev(groups)
if None in [g["group_name"] for g in groups]: # tous les etudiants
getallstudents = True
- # gr_title = "tous"
gr_title_filename = "tous"
else:
getallstudents = False
@@ -917,17 +922,17 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
]
# une liste de liste de chaines: lignes de la feuille de calcul
- L = []
+ rows = []
- etuds = _get_sorted_etuds(eval_dict, etudids, formsemestre_id)
+ etuds = _get_sorted_etuds(evaluation, etudids, formsemestre.id)
for e in etuds:
etudid = e["etudid"]
- groups = sco_groups.get_etud_groups(etudid, formsemestre_id)
+ groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
grc = sco_groups.listgroups_abbrev(groups)
- L.append(
+ rows.append(
[
- "%s" % etudid,
+ str(etudid),
e["nom"].upper(),
e["prenom"].lower().capitalize(),
e["inscr"]["etat"],
@@ -937,31 +942,11 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
]
)
- filename = "notes_%s_%s" % (eval_name, gr_title_filename)
+ filename = f"notes_{eval_name}_{gr_title_filename}"
xls = sco_excel.excel_feuille_saisie(
- eval_dict, sem["titreannee"], description, lines=L
+ evaluation, formsemestre.titre_annee(), description, lines=rows
)
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
- # return sco_excel.send_excel_file(xls, filename)
-
-
-def has_existing_decision(M, E, etudid):
- """Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE
- Si oui, return True
- """
- formsemestre_id = M["formsemestre_id"]
- formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
- nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
- if nt.get_etud_decision_sem(etudid):
- return True
- dec_ues = nt.get_etud_decisions_ue(etudid)
- if dec_ues:
- mod = sco_edit_module.module_list({"module_id": M["module_id"]})[0]
- ue_id = mod["ue_id"]
- if ue_id in dec_ues:
- return True # decision pour l'UE a laquelle appartient cette evaluation
-
- return False # pas de decision de jury affectee par cette note
# -----------------------------
@@ -973,20 +958,18 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
if not isinstance(evaluation_id, int):
raise ScoInvalidParamError()
group_ids = [int(group_id) for group_id in (group_ids or [])]
- evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
- if not evals:
+ evaluation: Evaluation = Evaluation.query.get(evaluation_id)
+ if evaluation is None:
raise ScoValueError("évaluation inexistante")
- E = evals[0]
- M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
- formsemestre_id = M["formsemestre_id"]
+ modimpl = evaluation.moduleimpl
moduleimpl_status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
- moduleimpl_id=E["moduleimpl_id"],
+ moduleimpl_id=evaluation.moduleimpl_id,
)
# Check access
# (admin, respformation, and responsable_id)
- if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
+ if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
return f"""
{html_sco_header.sco_header()}
Modification des notes impossible pour {current_user.user_name}
@@ -1001,16 +984,16 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
# Informations sur les groupes à afficher:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids=group_ids,
- formsemestre_id=formsemestre_id,
+ formsemestre_id=modimpl.formsemestre_id,
select_all_when_unspecified=True,
etat=None,
)
- if E["description"]:
- page_title = 'Saisie "%s"' % E["description"]
- else:
- page_title = "Saisie des notes"
-
+ page_title = (
+ f'Saisie "{evaluation.description}"'
+ if evaluation.description
+ else "Saisie des notes"
+ )
# HTML page:
H = [
html_sco_header.sco_header(
@@ -1036,19 +1019,19 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
"id": "menu_saisie_tableur",
"endpoint": "notes.saisie_notes_tableur",
"args": {
- "evaluation_id": E["evaluation_id"],
+ "evaluation_id": evaluation.id,
"group_ids": groups_infos.group_ids,
},
},
{
"title": "Voir toutes les notes du module",
"endpoint": "notes.evaluation_listenotes",
- "args": {"moduleimpl_id": E["moduleimpl_id"]},
+ "args": {"moduleimpl_id": evaluation.moduleimpl_id},
},
{
"title": "Effacer toutes les notes de cette évaluation",
"endpoint": "notes.evaluation_suppress_alln",
- "args": {"evaluation_id": E["evaluation_id"]},
+ "args": {"evaluation_id": evaluation.id},
},
],
alone=True,
@@ -1077,7 +1060,9 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
)
# Le formulaire de saisie des notes:
- form = _form_saisie_notes(E, M, groups_infos, destination=moduleimpl_status_url)
+ form = _form_saisie_notes(
+ evaluation, modimpl, groups_infos, destination=moduleimpl_status_url
+ )
if form is None:
return flask.redirect(moduleimpl_status_url)
H.append(form)
@@ -1101,10 +1086,9 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
return "\n".join(H)
-def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
- notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
- eval_dict["evaluation_id"]
- ) # Notes existantes
+def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: int):
+ # Notes existantes
+ notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
cnx = ndb.GetDBConnexion()
etuds = []
for etudid in etudids:
@@ -1123,17 +1107,17 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
# Information sur absence (tenant compte de la demi-journée)
- jour_iso = ndb.DateDMYtoISO(eval_dict["jour"])
+ jour_iso = evaluation.jour.isoformat() if evaluation.jour else ""
warn_abs_lst = []
- if eval_dict["matin"]:
- nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=1)
- nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=1)
+ if evaluation.is_matin():
+ nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True)
+ nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True)
if nbabs:
if nbabsjust:
warn_abs_lst.append("absent justifié le matin !")
else:
warn_abs_lst.append("absent le matin !")
- if eval_dict["apresmidi"]:
+ if evaluation.is_apresmidi():
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0)
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
if nbabs:
@@ -1169,35 +1153,38 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
return etuds
-def _form_saisie_notes(E, M, groups_infos, destination=""):
+def _form_saisie_notes(
+ evaluation: Evaluation, modimpl: ModuleImpl, groups_infos, destination=""
+):
"""Formulaire HTML saisie des notes dans l'évaluation E du moduleimpl M
pour les groupes indiqués.
On charge tous les étudiants, ne seront montrés que ceux
des groupes sélectionnés grace a un filtre en javascript.
"""
- evaluation_id = E["evaluation_id"]
- formsemestre_id = M["formsemestre_id"]
-
+ formsemestre_id = modimpl.formsemestre_id
+ formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
+ res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
etudids = [
x[0]
for x in sco_groups.do_evaluation_listeetuds_groups(
- evaluation_id, getallstudents=True, include_demdef=True
+ evaluation.id, getallstudents=True, include_demdef=True
)
]
if not etudids:
return 'Aucun étudiant sélectionné !
'
- # Decisions de jury existantes ?
- decisions_jury = {etudid: has_existing_decision(M, E, etudid) for etudid in etudids}
- # Nb de decisions de jury (pour les inscrits à l'évaluation):
+ # Décisions de jury existantes ?
+ decisions_jury = {etudid: res.etud_has_decision(etudid) for etudid in etudids}
+
+ # Nb de décisions de jury (pour les inscrits à l'évaluation):
nb_decisions = sum(decisions_jury.values())
- etuds = _get_sorted_etuds(E, etudids, formsemestre_id)
+ etuds = _get_sorted_etuds(evaluation, etudids, formsemestre_id)
# Build form:
descr = [
- ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
+ ("evaluation_id", {"default": evaluation.id, "input_type": "hidden"}),
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
(
"group_ids",
@@ -1207,7 +1194,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}),
("changed", {"default": "0", "input_type": "hidden"}), # changed in JS
]
- if M["module"]["module_type"] in (
+ if modimpl.module.module_type in (
ModuleType.STANDARD,
ModuleType.RESSOURCE,
ModuleType.SAE,
@@ -1220,11 +1207,11 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
"title": "Notes ",
"cssclass": "formnote_bareme",
"readonly": True,
- "default": " / %g" % E["note_max"],
+ "default": " / %g" % evaluation.note_max,
},
)
)
- elif M["module"]["module_type"] == ModuleType.MALUS:
+ elif modimpl.module.module_type == ModuleType.MALUS:
descr.append(
(
"s3",
@@ -1238,7 +1225,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
)
)
else:
- raise ValueError("invalid module type (%s)" % M["module"]["module_type"]) # bug
+ raise ValueError(f"invalid module type ({modimpl.module.module_type})") # bug
initvalues = {}
for e in etuds:
@@ -1248,7 +1235,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
if disabled:
classdem = " etud_dem"
etud_classes.append("etud_dem")
- disabled_attr = 'disabled="%d"' % disabled
+ disabled_attr = f'disabled="{disabled}"'
else:
classdem = ""
disabled_attr = ""
@@ -1265,18 +1252,17 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
)
# Historique des saisies de notes:
- if not disabled:
- explanation = (
- '' % etudid
- + get_note_history_menu(evaluation_id, etudid)
- + ""
- )
- else:
- explanation = ""
+ explanation = (
+ ""
+ if disabled
+ else f"""{
+ get_note_history_menu(evaluation.id, etudid)
+ }"""
+ )
explanation = e["absinfo"] + explanation
# Lien modif decision de jury:
- explanation += '' % etudid
+ explanation += f''
# Valeur actuelle du champ:
initvalues["note_" + str(etudid)] = e["val"]
@@ -1330,7 +1316,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
H.append(tf.getform()) # check and init
H.append(
f"""Terminer
"""
)
@@ -1345,7 +1331,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
Mettre les notes manquantes à
-
+
@@ -1362,50 +1348,56 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
return None
-def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
- """Enregistre une note (ajax)"""
- authuser = current_user
- log(
- "save_note: evaluation_id=%s etudid=%s uid=%s value=%s"
- % (evaluation_id, etudid, authuser, value)
- )
- E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
- Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
- Mod["url"] = url_for(
+def save_notes(
+ evaluation: Evaluation, notes: list[tuple[(int, float)]], comment: str = ""
+) -> dict:
+ """Enregistre une liste de notes.
+ Vérifie que les étudiants sont bien inscrits à ce module, et que l'on a le droit.
+ Result: dict avec
+ """
+ log(f"save_note: evaluation_id={evaluation.id} uid={current_user} notes={notes}")
+ status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
- moduleimpl_id=M["moduleimpl_id"],
+ moduleimpl_id=evaluation.moduleimpl_id,
_external=True,
)
- result = {"nbchanged": 0} # JSON
# Check access: admin, respformation, or responsable_id
- if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]):
- result["status"] = "unauthorized"
+ if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
+ return json_error(403, "modification notes non autorisee pour cet utilisateur")
+ #
+ valid_notes, _, _, _, _ = _check_notes(notes, evaluation)
+ if valid_notes:
+ etudids_changed, _, etudids_with_decision = notes_add(
+ current_user, evaluation.id, valid_notes, comment=comment, do_it=True
+ )
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_NOTE,
+ obj=evaluation.moduleimpl_id,
+ text=f"""Chargement notes dans {
+ evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}""",
+ url=status_url,
+ max_frequency=30 * 60, # 30 minutes
+ )
+ result = {
+ "etudids_with_decision": etudids_with_decision,
+ "etudids_changed": etudids_changed,
+ "history_menu": {
+ etudid: get_note_history_menu(evaluation.id, etudid)
+ for etudid in etudids_changed
+ },
+ }
else:
- L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
- if L:
- nbchanged, _, existing_decisions = notes_add(
- authuser, evaluation_id, L, comment=comment, do_it=True
- )
- ScolarNews.add(
- typ=ScolarNews.NEWS_NOTE,
- obj=M["moduleimpl_id"],
- text='Chargement notes dans %(titre)s' % Mod,
- url=Mod["url"],
- max_frequency=30 * 60, # 30 minutes
- )
- result["nbchanged"] = nbchanged
- result["existing_decisions"] = existing_decisions
- if nbchanged > 0:
- result["history_menu"] = get_note_history_menu(evaluation_id, etudid)
- else:
- result["history_menu"] = "" # no update needed
- result["status"] = "ok"
- return scu.sendJSON(result)
+ result = {
+ "etudids_changed": [],
+ "etudids_with_decision": [],
+ "history_menu": [],
+ }
+
+ return result
-def get_note_history_menu(evaluation_id, etudid):
+def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
"""Menu HTML historique de la note"""
history = sco_undo_notes.get_note_history(evaluation_id, etudid)
if not history:
diff --git a/app/static/css/partition_editor.css b/app/static/css/partition_editor.css
index 086327b42..c74e3f272 100644
--- a/app/static/css/partition_editor.css
+++ b/app/static/css/partition_editor.css
@@ -354,6 +354,10 @@ body.editionActivated .filtres .nonEditable .move {
display: initial;
}
+.groupe:has(.etudiants:empty) {
+ display: none;
+}
+
/* .filtres .unselect {
background: rgba(0, 153, 204, 0.5) !important;
} */
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 7c414e181..4f922c9d9 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -3173,6 +3173,19 @@ li.tf-msg {
/* EMO_WARNING, "⚠️" */
}
+p.error {
+ font-weight: bold;
+ color: red;
+}
+
+p.error::before {
+ content: "\2049 \fe0f";
+ margin-right: 8px;
+}
+
+mark {
+ padding-right: 0px;
+}
.infop {
font-weight: normal;
diff --git a/app/static/js/etud_autocomplete.js b/app/static/js/etud_autocomplete.js
new file mode 100644
index 000000000..217707aaa
--- /dev/null
+++ b/app/static/js/etud_autocomplete.js
@@ -0,0 +1,65 @@
+
+// Mécanisme d'auto-complétion (choix) d'un étudiant
+// Il faut un champs #etudiant (text input) et à coté un champ hidden etudid qui sera rempli.
+// utilise autoComplete.js, source https://tarekraafat.github.io/autoComplete.js
+// EV 2023-06-01
+
+function etud_autocomplete_config(with_dept = false) {
+ return {
+ selector: "#etudiant",
+ placeHolder: "Nom...",
+ threshold: 3,
+ data: {
+ src: async (query) => {
+ try {
+ // Fetch Data from external Source
+ const source = await fetch(`/ScoDoc/api/etudiants/name/${query}`);
+ // Data should be an array of `Objects` or `Strings`
+ const data = await source.json();
+ return data;
+ } catch (error) {
+ return error;
+ }
+ },
+ // Data source 'Object' key to be searched
+ keys: ["nom"]
+ },
+ events: {
+ input: {
+ selection: (event) => {
+ const prenom = sco_capitalize(event.detail.selection.value.prenom);
+ const selection = with_dept ? `${event.detail.selection.value.nom} ${prenom} (${event.detail.selection.value.dept_acronym})` : `${event.detail.selection.value.nom} ${prenom}`;
+ // store etudid
+ const etudidField = document.getElementById('etudid');
+ etudidField.value = event.detail.selection.value.id;
+ autoCompleteJS.input.value = selection;
+ }
+ }
+ },
+ resultsList: {
+ element: (list, data) => {
+ if (!data.results.length) {
+ // Create "No Results" message element
+ const message = document.createElement("div");
+ // Add class to the created element
+ message.setAttribute("class", "no_result");
+ // Add message text content
+ message.innerHTML = `Pas de résultat pour "${data.query}"`;
+ // Append message element to the results list
+ list.prepend(message);
+ // Efface l'etudid
+ const etudidField = document.getElementById('etudid');
+ etudidField.value = "";
+ }
+ },
+ noResults: true,
+ },
+ resultItem: {
+ highlight: true,
+ element: (item, data) => {
+ const prenom = sco_capitalize(data.value.prenom);
+ item.innerHTML += with_dept ? ` ${prenom} (${data.value.dept_acronym})` : ` ${prenom}`;
+ },
+ },
+ }
+}
diff --git a/app/static/js/saisie_notes.js b/app/static/js/saisie_notes.js
index 0936b3d7a..7fb223bc5 100644
--- a/app/static/js/saisie_notes.js
+++ b/app/static/js/saisie_notes.js
@@ -1,132 +1,142 @@
// Formulaire saisie des notes
$().ready(function () {
+ $("#formnotes .note").bind("blur", valid_note);
- $("#formnotes .note").bind("blur", valid_note);
-
- $("#formnotes input").bind("paste", paste_text);
- $(".btn_masquer_DEM").bind("click", masquer_DEM);
-
+ $("#formnotes input").bind("paste", paste_text);
+ $(".btn_masquer_DEM").bind("click", masquer_DEM);
});
function is_valid_note(v) {
- if (!v)
- return true;
+ if (!v) return true;
- var note_min = parseFloat($("#eval_note_min").text());
- var note_max = parseFloat($("#eval_note_max").text());
+ var note_min = parseFloat($("#eval_note_min").text());
+ var note_max = parseFloat($("#eval_note_max").text());
- if (!v.match("^-?[0-9]*.?[0-9]*$")) {
- return (v == "ABS") || (v == "EXC") || (v == "SUPR") || (v == "ATT") || (v == "DEM");
- } else {
- var x = parseFloat(v);
- return (x >= note_min) && (x <= note_max);
- }
+ if (!v.match("^-?[0-9]*.?[0-9]*$")) {
+ return v == "ABS" || v == "EXC" || v == "SUPR" || v == "ATT" || v == "DEM";
+ } else {
+ var x = parseFloat(v);
+ return x >= note_min && x <= note_max;
+ }
}
function valid_note(e) {
- var v = this.value.trim().toUpperCase().replace(",", ".");
- if (is_valid_note(v)) {
- if (v && (v != $(this).attr('data-last-saved-value'))) {
- this.className = "note_valid_new";
- var etudid = $(this).attr('data-etudid');
- save_note(this, v, etudid);
- }
- } else {
- /* Saisie invalide */
- this.className = "note_invalid";
- sco_message("valeur invalide ou hors barème");
+ var v = this.value.trim().toUpperCase().replace(",", ".");
+ if (is_valid_note(v)) {
+ if (v && v != $(this).attr("data-last-saved-value")) {
+ this.className = "note_valid_new";
+ const etudid = parseInt($(this).attr("data-etudid"));
+ save_note(this, v, etudid);
}
+ } else {
+ /* Saisie invalide */
+ this.className = "note_invalid";
+ sco_message("valeur invalide ou hors barème");
+ }
}
-function save_note(elem, v, etudid) {
- var evaluation_id = $("#formnotes_evaluation_id").attr("value");
- var formsemestre_id = $("#formnotes_formsemestre_id").attr("value");
- $('#sco_msg').html("en cours...").show();
- $.post(SCO_URL + '/Notes/save_note',
- {
- 'etudid': etudid,
- 'evaluation_id': evaluation_id,
- 'value': v,
- 'comment': document.getElementById('formnotes_comment').value
+async function save_note(elem, v, etudid) {
+ let evaluation_id = $("#formnotes_evaluation_id").attr("value");
+ let formsemestre_id = $("#formnotes_formsemestre_id").attr("value");
+ $("#sco_msg").html("en cours...").show();
+ try {
+ const response = await fetch(
+ SCO_URL + "/../api/evaluation/" + evaluation_id + "/notes/set",
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
},
- function (result) {
- if (result['nbchanged'] > 0) {
- sco_message("enregistré");
- elem.className = "note_saved";
- // il y avait une decision de jury ?
- if (result.existing_decisions[0] == etudid) {
- if (v != $(elem).attr('data-orig-value')) {
- $("#jurylink_" + etudid).html('mettre à jour décision de jury');
- } else {
- $("#jurylink_" + etudid).html('');
- }
- }
- // mise a jour menu historique
- if (result['history_menu']) {
- $("#hist_" + etudid).html(result['history_menu']);
- }
- $(elem).attr('data-last-saved-value', v);
- } else {
- $('#sco_msg').html("").show();
- sco_message("valeur non enregistrée");
- }
-
- }
+ body: JSON.stringify({
+ notes: [[etudid, v]],
+ comment: document.getElementById("formnotes_comment").value,
+ }),
+ }
);
+ if (!response.ok) {
+ sco_message("Erreur: valeur non enregistrée");
+ } else {
+ const data = await response.json();
+ $("#sco_msg").hide();
+ if (data.etudids_changed.length > 0) {
+ sco_message("enregistré");
+ elem.className = "note_saved";
+ // Il y avait une decision de jury ?
+ if (data.etudids_with_decision.includes(etudid)) {
+ if (v != $(elem).attr("data-orig-value")) {
+ $("#jurylink_" + etudid).html(
+ 'mettre à jour décision de jury'
+ );
+ } else {
+ $("#jurylink_" + etudid).html("");
+ }
+ }
+ // Mise à jour menu historique
+ if (data.history_menu[etudid]) {
+ $("#hist_" + etudid).html(data.history_menu[etudid]);
+ }
+ $(elem).attr("data-last-saved-value", v);
+ }
+ }
+ } catch (error) {
+ console.error("Fetch error:", error);
+ sco_message("Erreur réseau: valeur non enregistrée");
+ }
}
function change_history(e) {
- var opt = e.selectedOptions[0];
- var val = $(opt).attr("data-note");
- var etudid = $(e).attr('data-etudid');
- // le input associé a ce menu:
- var input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
- input_elem.value = val;
- save_note(input_elem, val, etudid);
+ let opt = e.selectedOptions[0];
+ let val = $(opt).attr("data-note");
+ const etudid = parseInt($(e).attr("data-etudid"));
+ // le input associé a ce menu:
+ let input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
+ input_elem.value = val;
+ save_note(input_elem, val, etudid);
}
// Contribution S.L.: copier/coller des notes
-
function paste_text(e) {
- var event = e.originalEvent;
- event.stopPropagation();
- event.preventDefault();
- var clipb = e.originalEvent.clipboardData;
- var data = clipb.getData('Text');
- var list = data.split(/\r\n|\r|\n|\t| /g);
- var currentInput = event.currentTarget;
- var masquerDEM = document.querySelector("body").classList.contains("masquer_DEM");
+ var event = e.originalEvent;
+ event.stopPropagation();
+ event.preventDefault();
+ var clipb = e.originalEvent.clipboardData;
+ var data = clipb.getData("Text");
+ var list = data.split(/\r\n|\r|\n|\t| /g);
+ var currentInput = event.currentTarget;
+ var masquerDEM = document
+ .querySelector("body")
+ .classList.contains("masquer_DEM");
- for (var i = 0; i < list.length; i++) {
- currentInput.value = list[i];
- var evt = document.createEvent("HTMLEvents");
- evt.initEvent("blur", false, true);
- currentInput.dispatchEvent(evt);
- var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
- while (
- sibbling &&
- (
- sibbling.style.display == "none" ||
- (
- masquerDEM && sibbling.classList.contains("etud_dem")
- )
- )
- ) {
- sibbling = sibbling.nextElementSibling;
- }
- if (sibbling) {
- currentInput = sibbling.querySelector("input");
- if (!currentInput) {
- return;
- }
- } else {
- return;
- }
+ for (var i = 0; i < list.length; i++) {
+ currentInput.value = list[i];
+ var evt = document.createEvent("HTMLEvents");
+ evt.initEvent("blur", false, true);
+ currentInput.dispatchEvent(evt);
+ var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
+ while (
+ sibbling &&
+ (sibbling.style.display == "none" ||
+ (masquerDEM && sibbling.classList.contains("etud_dem")))
+ ) {
+ sibbling = sibbling.nextElementSibling;
}
+ if (sibbling) {
+ currentInput = sibbling.querySelector("input");
+ if (!currentInput) {
+ return;
+ }
+ } else {
+ return;
+ }
+ }
}
function masquer_DEM() {
- document.querySelector("body").classList.toggle("masquer_DEM");
+ document.querySelector("body").classList.toggle("masquer_DEM");
}
diff --git a/app/static/js/scodoc.js b/app/static/js/scodoc.js
index f5b9c995a..44b0352be 100644
--- a/app/static/js/scodoc.js
+++ b/app/static/js/scodoc.js
@@ -67,6 +67,10 @@ $(function () {
}
});
+function sco_capitalize(string) {
+ return string[0].toUpperCase() + string.slice(1).toLowerCase();
+}
+
// Affiche un message transitoire (duration milliseconds, 0 means infinity)
function sco_message(msg, className = "message_custom", duration = 0) {
var div = document.createElement("div");
diff --git a/app/static/libjs/autoComplete.js-10.2.7/dist/autoComplete.min.js b/app/static/libjs/autoComplete.js-10.2.7/dist/autoComplete.min.js
new file mode 100644
index 000000000..55cd004ad
--- /dev/null
+++ b/app/static/libjs/autoComplete.js-10.2.7/dist/autoComplete.min.js
@@ -0,0 +1 @@
+var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,a=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return u=e.done,e},e:function(e){a=!0,s=e},f:function(){try{u||null==n.return||n.return()}finally{if(a)throw s}}}}(n.keys);try{for(l.s();!(c=l.n()).done;)a(c.value)}catch(e){l.e(e)}finally{l.f()}}else a()})),n.filter&&(i=n.filter(i));var s=i.slice(0,t.resultsList.maxResults);t.feedback={query:e,matches:i,results:s},f("results",t)},m="aria-expanded",b="aria-activedescendant",y="aria-selected",v=function(e,n){e.feedback.selection=t({index:n},e.feedback.results[n])},g=function(e){e.isOpen||((e.wrapper||e.input).setAttribute(m,!0),e.list.removeAttribute("hidden"),e.isOpen=!0,f("open",e))},w=function(e){e.isOpen&&((e.wrapper||e.input).setAttribute(m,!1),e.input.setAttribute(b,""),e.list.setAttribute("hidden",""),e.isOpen=!1,f("close",e))},O=function(e,t){var n=t.resultItem,r=t.list.getElementsByTagName(n.tag),o=!!n.selected&&n.selected.split(" ");if(t.isOpen&&r.length){var s,u,a=t.cursor;e>=r.length&&(e=0),e<0&&(e=r.length-1),t.cursor=e,a>-1&&(r[a].removeAttribute(y),o&&(u=r[a].classList).remove.apply(u,i(o))),r[e].setAttribute(y,!0),o&&(s=r[e].classList).add.apply(s,i(o)),t.input.setAttribute(b,r[t.cursor].id),t.list.scrollTop=r[e].offsetTop-t.list.clientHeight+r[e].clientHeight+5,t.feedback.cursor=t.cursor,v(t,e),f("navigate",t)}},A=function(e){O(e.cursor+1,e)},k=function(e){O(e.cursor-1,e)},L=function(e,t,n){(n=n>=0?n:e.cursor)<0||(e.feedback.event=t,v(e,n),f("selection",e),w(e))};function j(e,n){var r=this;return new Promise((function(i,o){var s,u;return s=n||((u=e.input)instanceof HTMLInputElement||u instanceof HTMLTextAreaElement?u.value:u.innerHTML),function(e,t,n){return t?t(e):e.length>=n}(s=e.query?e.query(s):s,e.trigger,e.threshold)?d(e,s).then((function(n){try{return e.feedback instanceof Error?i():(h(s,e),e.resultsList&&function(e){var n=e.resultsList,r=e.list,i=e.resultItem,o=e.feedback,s=o.matches,u=o.results;if(e.cursor=-1,r.innerHTML="",s.length||n.noResults){var c=new DocumentFragment;u.forEach((function(e,n){var r=a(i.tag,t({id:"".concat(i.id,"_").concat(n),role:"option",innerHTML:e.match,inside:c},i.class&&{class:i.class}));i.element&&i.element(r,e)})),r.append(c),n.element&&n.element(r,o),g(e)}else w(e)}(e),c.call(r))}catch(e){return o(e)}}),o):(w(e),c.call(r));function c(){return i()}}))}var S=function(e,t){for(var n in e)for(var r in e[n])t(n,r)},T=function(e){var n,r,i,o=e.events,s=(n=function(){return j(e)},r=e.debounce,function(){clearTimeout(i),i=setTimeout((function(){return n()}),r)}),u=e.events=t({input:t({},o&&o.input)},e.resultsList&&{list:o?t({},o.list):{}}),a={input:{input:function(){s()},keydown:function(t){!function(e,t){switch(e.keyCode){case 40:case 38:e.preventDefault(),40===e.keyCode?A(t):k(t);break;case 13:t.submit||e.preventDefault(),t.cursor>=0&&L(t,e);break;case 9:t.resultsList.tabSelect&&t.cursor>=0&&L(t,e);break;case 27:t.input.value="",w(t)}}(t,e)},blur:function(){w(e)}},list:{mousedown:function(e){e.preventDefault()},click:function(t){!function(e,t){var n=t.resultItem.tag.toUpperCase(),r=Array.from(t.list.querySelectorAll(n)),i=e.target.closest(n);i&&i.nodeName===n&&L(t,e,r.indexOf(i))}(t,e)}}};S(a,(function(t,n){(e.resultsList||"input"===n)&&(u[t][n]||(u[t][n]=a[t][n]))})),S(u,(function(t,n){e[t].addEventListener(n,u[t][n])}))};function E(e){var n=this;return new Promise((function(r,i){var o,s,u;if(o=e.placeHolder,u={role:"combobox","aria-owns":(s=e.resultsList).id,"aria-haspopup":!0,"aria-expanded":!1},a(e.input,t(t({"aria-controls":s.id,"aria-autocomplete":"both"},o&&{placeholder:o}),!e.wrapper&&t({},u))),e.wrapper&&(e.wrapper=a("div",t({around:e.input,class:e.name+"_wrapper"},u))),s&&(e.list=a(s.tag,t({dest:[s.destination,s.position],id:s.id,role:"listbox",hidden:"hidden"},s.class&&{class:s.class}))),T(e),e.data.cache)return d(e).then((function(e){try{return c.call(n)}catch(e){return i(e)}}),i);function c(){return f("init",e),r()}return c.call(n)}))}function x(e){var t=e.prototype;t.init=function(){E(this)},t.start=function(e){j(this,e)},t.unInit=function(){if(this.wrapper){var e=this.wrapper.parentNode;e.insertBefore(this.input,this.wrapper),e.removeChild(this.wrapper)}var t;S((t=this).events,(function(e,n){t[e].removeEventListener(n,t.events[e][n])}))},t.open=function(){g(this)},t.close=function(){w(this)},t.goTo=function(e){O(e,this)},t.next=function(){A(this)},t.previous=function(){k(this)},t.select=function(e){L(this,null,e)},t.search=function(e,t,n){return p(e,t,n)}}return function e(t){this.options=t,this.id=e.instances=(e.instances||0)+1,this.name="autoComplete",this.wrapper=1,this.threshold=1,this.debounce=0,this.resultsList={position:"afterend",tag:"ul",maxResults:5},this.resultItem={tag:"li"},function(e){var t=e.name,r=e.options,i=e.resultsList,o=e.resultItem;for(var s in r)if("object"===n(r[s]))for(var a in e[s]||(e[s]={}),r[s])e[s][a]=r[s][a];else e[s]=r[s];e.selector=e.selector||"#"+t,i.destination=i.destination||e.selector,i.id=i.id||t+"_list_"+e.id,o.id=o.id||t+"_result",e.input=u(e.selector)}(this),x.call(this,e),E(this)}},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).autoComplete=t();
diff --git a/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.01.css b/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.01.css
new file mode 100644
index 000000000..4df567ee2
--- /dev/null
+++ b/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.01.css
@@ -0,0 +1,92 @@
+.autoComplete_wrapper {
+ display: inline-block;
+ position: relative;
+}
+
+.autoComplete_wrapper > input {
+ width: 370px;
+ height: 40px;
+ padding-left: 20px;
+ font-size: 1rem;
+ color: rgba(123, 123, 123, 1);
+ border-radius: 8px;
+ border: 0;
+ outline: none;
+ background-color: #f1f3f4;
+}
+
+.autoComplete_wrapper > input::placeholder {
+ color: rgba(123, 123, 123, 0.5);
+ transition: all 0.3s ease;
+}
+
+.autoComplete_wrapper > ul {
+ position: absolute;
+ max-height: 226px;
+ overflow-y: scroll;
+ top: 100%;
+ left: 0;
+ right: 0;
+ padding: 0;
+ margin: 0.5rem 0 0 0;
+ border-radius: 0.6rem;
+ background-color: #fff;
+ box-shadow: 0 3px 6px rgba(149, 157, 165, 0.15);
+ border: 1px solid rgba(33, 33, 33, 0.07);
+ z-index: 1000;
+ outline: none;
+}
+
+.autoComplete_wrapper > ul[hidden],
+.autoComplete_wrapper > ul:empty {
+ display: block;
+ opacity: 0;
+ transform: scale(0);
+}
+
+.autoComplete_wrapper > ul > li {
+ margin: 0.3rem;
+ padding: 0.3rem 0.5rem;
+ list-style: none;
+ text-align: left;
+ font-size: 1rem;
+ color: #212121;
+ transition: all 0.1s ease-in-out;
+ border-radius: 0.35rem;
+ background-color: rgba(255, 255, 255, 1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ transition: all 0.2s ease;
+}
+
+.autoComplete_wrapper > ul > li::selection {
+ color: rgba(#ffffff, 0);
+ background-color: rgba(#ffffff, 0);
+}
+
+.autoComplete_wrapper > ul > li:hover {
+ cursor: pointer;
+ background-color: rgba(123, 123, 123, 0.1);
+}
+
+.autoComplete_wrapper > ul > li mark {
+ background-color: transparent;
+ color: rgba(255, 122, 122, 1);
+ font-weight: bold;
+}
+
+.autoComplete_wrapper > ul > li mark::selection {
+ color: rgba(#ffffff, 0);
+ background-color: rgba(#ffffff, 0);
+}
+
+.autoComplete_wrapper > ul > li[aria-selected="true"] {
+ background-color: rgba(123, 123, 123, 0.1);
+}
+
+@media only screen and (max-width: 600px) {
+ .autoComplete_wrapper > input {
+ width: 18rem;
+ }
+}
diff --git a/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.02.css b/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.02.css
new file mode 100644
index 000000000..e9e233cc2
--- /dev/null
+++ b/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.02.css
@@ -0,0 +1,82 @@
+.autoComplete_wrapper {
+ display: inline-block;
+ position: relative;
+}
+
+.autoComplete_wrapper > input {
+ width: 370px;
+ height: 40px;
+ padding-left: 10px;
+ font-size: 1rem;
+ color: rgb(116, 116, 116);
+ border-radius: 4px;
+ border: 1px solid rgba(33, 33, 33, 0.2);
+ outline: none;
+}
+
+.autoComplete_wrapper > input::placeholder {
+ color: rgba(123, 123, 123, 0.5);
+ transition: all 0.3s ease;
+}
+
+.autoComplete_wrapper > ul {
+ position: absolute;
+ max-height: 226px;
+ overflow-y: scroll;
+ top: 100%;
+ left: 0;
+ right: 0;
+ padding: 0;
+ margin: 0.5rem 0 0 0;
+ border-radius: 4px;
+ background-color: #fff;
+ border: 1px solid rgba(33, 33, 33, 0.1);
+ z-index: 1000;
+ outline: none;
+}
+
+.autoComplete_wrapper > ul > li {
+ padding: 10px 20px;
+ list-style: none;
+ text-align: left;
+ font-size: 16px;
+ color: #212121;
+ transition: all 0.1s ease-in-out;
+ border-radius: 3px;
+ background-color: rgba(255, 255, 255, 1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ transition: all 0.2s ease;
+}
+
+.autoComplete_wrapper > ul > li::selection {
+ color: rgba(#ffffff, 0);
+ background-color: rgba(#ffffff, 0);
+}
+
+.autoComplete_wrapper > ul > li:hover {
+ cursor: pointer;
+ background-color: rgba(123, 123, 123, 0.1);
+}
+
+.autoComplete_wrapper > ul > li mark {
+ background-color: transparent;
+ color: rgba(255, 122, 122, 1);
+ font-weight: bold;
+}
+
+.autoComplete_wrapper > ul > li mark::selection {
+ color: rgba(#ffffff, 0);
+ background-color: rgba(#ffffff, 0);
+}
+
+.autoComplete_wrapper > ul > li[aria-selected="true"] {
+ background-color: rgba(123, 123, 123, 0.1);
+}
+
+@media only screen and (max-width: 600px) {
+ .autoComplete_wrapper > input {
+ width: 18rem;
+ }
+}
diff --git a/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.css b/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.css
new file mode 100644
index 000000000..3bd94d292
--- /dev/null
+++ b/app/static/libjs/autoComplete.js-10.2.7/dist/css/autoComplete.css
@@ -0,0 +1,128 @@
+.autoComplete_wrapper {
+ display: inline-block;
+ position: relative;
+}
+
+.autoComplete_wrapper > input {
+ height: 3rem;
+ width: 370px;
+ margin: 0;
+ padding: 0 2rem 0 3.2rem;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ font-size: 1rem;
+ text-overflow: ellipsis;
+ color: rgba(255, 122, 122, 0.3);
+ outline: none;
+ border-radius: 10rem;
+ border: 0.05rem solid rgba(255, 122, 122, 0.5);
+ background-image: url(./images/search.svg);
+ background-size: 1.4rem;
+ background-position: left 1.05rem top 0.8rem;
+ background-repeat: no-repeat;
+ background-origin: border-box;
+ background-color: #fff;
+ transition: all 0.4s ease;
+ -webkit-transition: all -webkit-transform 0.4s ease;
+}
+
+.autoComplete_wrapper > input::placeholder {
+ color: rgba(255, 122, 122, 0.5);
+ transition: all 0.3s ease;
+ -webkit-transition: all -webkit-transform 0.3s ease;
+}
+
+.autoComplete_wrapper > input:hover::placeholder {
+ color: rgba(255, 122, 122, 0.6);
+ transition: all 0.3s ease;
+ -webkit-transition: all -webkit-transform 0.3s ease;
+}
+
+.autoComplete_wrapper > input:focus::placeholder {
+ padding: 0.1rem 0.6rem;
+ font-size: 0.95rem;
+ color: rgba(255, 122, 122, 0.4);
+}
+
+.autoComplete_wrapper > input:focus::selection {
+ background-color: rgba(255, 122, 122, 0.15);
+}
+
+.autoComplete_wrapper > input::selection {
+ background-color: rgba(255, 122, 122, 0.15);
+}
+
+.autoComplete_wrapper > input:hover {
+ color: rgba(255, 122, 122, 0.8);
+ transition: all 0.3s ease;
+ -webkit-transition: all -webkit-transform 0.3s ease;
+}
+
+.autoComplete_wrapper > input:focus {
+ color: rgba(255, 122, 122, 1);
+ border: 0.06rem solid rgba(255, 122, 122, 0.8);
+}
+
+.autoComplete_wrapper > ul {
+ position: absolute;
+ max-height: 226px;
+ overflow-y: scroll;
+ box-sizing: border-box;
+ left: 0;
+ right: 0;
+ margin: 0.5rem 0 0 0;
+ padding: 0;
+ z-index: 1;
+ list-style: none;
+ border-radius: 0.6rem;
+ background-color: #fff;
+ border: 1px solid rgba(33, 33, 33, 0.07);
+ box-shadow: 0 3px 6px rgba(149, 157, 165, 0.15);
+ outline: none;
+ transition: opacity 0.15s ease-in-out;
+ -moz-transition: opacity 0.15s ease-in-out;
+ -webkit-transition: opacity 0.15s ease-in-out;
+}
+
+.autoComplete_wrapper > ul[hidden],
+.autoComplete_wrapper > ul:empty {
+ display: block;
+ opacity: 0;
+ transform: scale(0);
+}
+
+.autoComplete_wrapper > ul > li {
+ margin: 0.3rem;
+ padding: 0.3rem 0.5rem;
+ text-align: left;
+ font-size: 1rem;
+ color: #212121;
+ border-radius: 0.35rem;
+ background-color: rgba(255, 255, 255, 1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ transition: all 0.2s ease;
+}
+
+.autoComplete_wrapper > ul > li mark {
+ background-color: transparent;
+ color: rgba(255, 122, 122, 1);
+ font-weight: bold;
+}
+
+.autoComplete_wrapper > ul > li:hover {
+ cursor: pointer;
+ background-color: rgba(255, 122, 122, 0.15);
+}
+
+.autoComplete_wrapper > ul > li[aria-selected="true"] {
+ background-color: rgba(255, 122, 122, 0.15);
+}
+
+@media only screen and (max-width: 600px) {
+ .autoComplete_wrapper > input {
+ width: 18rem;
+ }
+}
diff --git a/app/static/libjs/autoComplete.js-10.2.7/dist/css/images/search.svg b/app/static/libjs/autoComplete.js-10.2.7/dist/css/images/search.svg
new file mode 100644
index 000000000..8063ea104
--- /dev/null
+++ b/app/static/libjs/autoComplete.js-10.2.7/dist/css/images/search.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/app/tables/recap.py b/app/tables/recap.py
index 73764ed22..5fca29127 100644
--- a/app/tables/recap.py
+++ b/app/tables/recap.py
@@ -479,17 +479,18 @@ class TableRecap(tb.Table):
for row in self.rows:
etud = row.etud
admission = etud.admission.first()
- first = True
- for cid, title in fields.items():
- cell = row.add_cell(
- cid,
- title,
- getattr(admission, cid) or "",
- "admission",
- )
- if first:
- cell.classes.append("admission_first")
- first = False
+ if admission:
+ first = True
+ for cid, title in fields.items():
+ cell = row.add_cell(
+ cid,
+ title,
+ getattr(admission, cid) or "",
+ "admission",
+ )
+ if first:
+ cell.classes.append("admission_first")
+ first = False
def add_cursus(self):
"""Ajoute colonne avec code cursus, eg 'S1 S2 S1'
diff --git a/app/templates/base.j2 b/app/templates/base.j2
index 542a161cf..1be73c172 100644
--- a/app/templates/base.j2
+++ b/app/templates/base.j2
@@ -85,7 +85,15 @@
{{ super() }}
{{ moment.include_moment() }}
{{ moment.lang(g.locale) }}
+
+
+
+
+
+
+
+
{% endblock %}
\ No newline at end of file
diff --git a/app/templates/entreprises/form_ajout_stage_apprentissage.j2 b/app/templates/entreprises/form_ajout_stage_apprentissage.j2
index ba25df2af..071e4d1fc 100644
--- a/app/templates/entreprises/form_ajout_stage_apprentissage.j2
+++ b/app/templates/entreprises/form_ajout_stage_apprentissage.j2
@@ -4,33 +4,27 @@
{% block styles %}
{{super()}}
-
-
+
{% endblock %}
{% block app_content %}
{{ title }}
-
+
(*) champs requis
{{ wtf.quick_form(form, novalidate=True) }}
+{% endblock %}
+{% block scripts %}
+{{super()}}
+
+
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/app/templates/sco_value_error.j2 b/app/templates/sco_value_error.j2
index 840d38023..94b603101 100644
--- a/app/templates/sco_value_error.j2
+++ b/app/templates/sco_value_error.j2
@@ -5,7 +5,7 @@
Erreur !
-{{ exc }}
+{{ exc | safe }}
{% if g.scodoc_dept %}
diff --git a/app/views/notes.py b/app/views/notes.py
index 47f977843..4d8c38914 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -496,7 +496,7 @@ sco_publish(
@permission_required(Permission.ScoView)
@scodoc7func
def formation_table_recap(formation_id, format="html"):
- return sco_formation_recap.formation_table_recap(formation_id, format="html")
+ return sco_formation_recap.formation_table_recap(formation_id, format=format)
sco_publish(
@@ -1874,12 +1874,6 @@ sco_publish(
Permission.ScoEnsView,
)
sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.ScoEnsView)
-sco_publish(
- "/save_note",
- sco_saisie_notes.save_note,
- Permission.ScoEnsView,
- methods=["GET", "POST"],
-)
sco_publish(
"/do_evaluation_set_missing",
sco_saisie_notes.do_evaluation_set_missing,
diff --git a/app/views/scolar.py b/app/views/scolar.py
index a53df3f31..19ec1c7d2 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -1306,6 +1306,8 @@ def _do_cancel_dem_or_def(
db.session.delete(event)
db.session.commit()
+ sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
+
flash(f"{operation_name} annulée.")
return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
@@ -1755,9 +1757,7 @@ def _etudident_create_or_edit_form(edit):
# Inval semesters with this student:
to_inval = [s["formsemestre_id"] for s in etud["sems"]]
for formsemestre_id in to_inval:
- sco_cache.invalidate_formsemestre(
- formsemestre_id=formsemestre_id
- ) # > etudident_create_or_edit
+ sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
#
return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
@@ -1833,7 +1833,7 @@ def etudident_delete(etudid, dialog_confirmed=False):
# Inval semestres où il était inscrit:
to_inval = [s["formsemestre_id"] for s in etud["sems"]]
for formsemestre_id in to_inval:
- sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) # >
+ sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
flash("Étudiant supprimé !")
return flask.redirect(scu.ScoURL())
diff --git a/migrations/versions/d84bc592584e_extension_unaccent.py b/migrations/versions/d84bc592584e_extension_unaccent.py
new file mode 100644
index 000000000..041da64ce
--- /dev/null
+++ b/migrations/versions/d84bc592584e_extension_unaccent.py
@@ -0,0 +1,77 @@
+"""Extension unaccent
+
+Revision ID: d84bc592584e
+Revises: b8df1b913c79
+Create Date: 2023-06-01 13:46:52.927951
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.orm import sessionmaker # added by ev
+
+
+# revision identifiers, used by Alembic.
+revision = "d84bc592584e"
+down_revision = "b8df1b913c79"
+branch_labels = None
+depends_on = None
+
+Session = sessionmaker()
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+
+ bind = op.get_bind()
+ session = Session(bind=bind)
+ # Ajout extension pour recherches sans accents:
+ session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
+
+ # Clé étrangère sur identite
+ session.execute(
+ sa.text(
+ """UPDATE are_stages_apprentissages
+ SET etudid = NULL
+ WHERE are_stages_apprentissages.etudid NOT IN (
+ SELECT id
+ FROM Identite
+ );
+ """
+ )
+ )
+ with op.batch_alter_table("are_stages_apprentissages", schema=None) as batch_op:
+ batch_op.create_foreign_key(
+ "are_stages_apprentissages_etudid_fkey",
+ "identite",
+ ["etudid"],
+ ["id"],
+ ondelete="CASCADE",
+ )
+
+ # Les montants de taxe en float:
+ with op.batch_alter_table("are_taxe_apprentissage", schema=None) as batch_op:
+ batch_op.alter_column(
+ "montant",
+ existing_type=sa.INTEGER(),
+ type_=sa.Float(),
+ existing_nullable=True,
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table("are_taxe_apprentissage", schema=None) as batch_op:
+ batch_op.alter_column(
+ "montant",
+ existing_type=sa.Float(),
+ type_=sa.INTEGER(),
+ existing_nullable=True,
+ )
+
+ with op.batch_alter_table("are_stages_apprentissages", schema=None) as batch_op:
+ batch_op.drop_constraint(
+ "are_stages_apprentissages_etudid_fkey", type_="foreignkey"
+ )
+
+ # ### end Alembic commands ###
diff --git a/sco_version.py b/sco_version.py
index 071863e32..5b5fa3a00 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.4.77"
+SCOVERSION = "9.4.82"
SCONAME = "ScoDoc"
diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py
index 203c587ad..56f3193d3 100644
--- a/tests/api/test_api_etudiants.py
+++ b/tests/api/test_api_etudiants.py
@@ -224,6 +224,32 @@ def test_etudiants(api_headers):
assert r.status_code == 404
+def test_etudiants_by_name(api_headers):
+ """
+ Route: /etudiants/name/
+ """
+ r = requests.get(
+ API_URL + "/etudiants/name/A",
+ headers=api_headers,
+ verify=CHECK_CERTIFICATE,
+ timeout=scu.SCO_TEST_API_TIMEOUT,
+ )
+ assert r.status_code == 200
+ etuds = r.json()
+ assert etuds == []
+ #
+ r = requests.get(
+ API_URL + "/etudiants/name/REG",
+ headers=api_headers,
+ verify=CHECK_CERTIFICATE,
+ timeout=scu.SCO_TEST_API_TIMEOUT,
+ )
+ assert r.status_code == 200
+ etuds = r.json()
+ assert len(etuds) == 1
+ assert etuds[0]["nom"] == "RÉGNIER"
+
+
def test_etudiant_formsemestres(api_headers):
"""
Route: /etudiant/etudid//formsemestres
diff --git a/tests/api/test_api_evaluations.py b/tests/api/test_api_evaluations.py
index 4a3064746..9e1de1e23 100644
--- a/tests/api/test_api_evaluations.py
+++ b/tests/api/test_api_evaluations.py
@@ -24,7 +24,7 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
from tests.api.tools_test_api import (
verify_fields,
EVALUATIONS_FIELDS,
- EVALUATION_FIELDS,
+ NOTES_FIELDS,
)
@@ -35,7 +35,7 @@ def test_evaluations(api_headers):
Route :
- /moduleimpl//evaluations
"""
- moduleimpl_id = 1
+ moduleimpl_id = 20
r = requests.get(
f"{API_URL}/moduleimpl/{moduleimpl_id}/evaluations",
headers=api_headers,
@@ -44,6 +44,7 @@ def test_evaluations(api_headers):
)
assert r.status_code == 200
list_eval = r.json()
+ assert list_eval
assert isinstance(list_eval, list)
for eval in list_eval:
assert verify_fields(eval, EVALUATIONS_FIELDS) is True
@@ -63,16 +64,14 @@ def test_evaluations(api_headers):
assert eval["moduleimpl_id"] == moduleimpl_id
-def test_evaluation_notes(
- api_headers,
-): # XXX TODO changer la boucle pour parcourir le dict sans les indices
+def test_evaluation_notes(api_headers):
"""
Test 'evaluation_notes'
Route :
- /evaluation//notes
"""
- eval_id = 1
+ eval_id = 20
r = requests.get(
f"{API_URL}/evaluation/{eval_id}/notes",
headers=api_headers,
@@ -81,14 +80,15 @@ def test_evaluation_notes(
)
assert r.status_code == 200
eval_notes = r.json()
- for i in range(1, len(eval_notes)):
- assert verify_fields(eval_notes[f"{i}"], EVALUATION_FIELDS)
- assert isinstance(eval_notes[f"{i}"]["id"], int)
- assert isinstance(eval_notes[f"{i}"]["etudid"], int)
- assert isinstance(eval_notes[f"{i}"]["evaluation_id"], int)
- assert isinstance(eval_notes[f"{i}"]["value"], float)
- assert isinstance(eval_notes[f"{i}"]["comment"], str)
- assert isinstance(eval_notes[f"{i}"]["date"], str)
- assert isinstance(eval_notes[f"{i}"]["uid"], int)
+ assert eval_notes
+ for etudid, note in eval_notes.items():
+ assert int(etudid) == note["etudid"]
+ assert verify_fields(note, NOTES_FIELDS)
+ assert isinstance(note["etudid"], int)
+ assert isinstance(note["evaluation_id"], int)
+ assert isinstance(note["value"], float)
+ assert isinstance(note["comment"], str)
+ assert isinstance(note["date"], str)
+ assert isinstance(note["uid"], int)
- assert eval_id == eval_notes[f"{i}"]["evaluation_id"]
+ assert eval_id == note["evaluation_id"]
diff --git a/tests/api/test_api_permissions.py b/tests/api/test_api_permissions.py
index 602834f1d..70d214467 100644
--- a/tests/api/test_api_permissions.py
+++ b/tests/api/test_api_permissions.py
@@ -58,6 +58,7 @@ def test_permissions(api_headers):
"nip": 1,
"partition_id": 1,
"role_name": "Ens",
+ "start": "abc",
"uid": 1,
"version": "long",
}
diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py
index b277dd281..b5aeae34f 100644
--- a/tests/api/tools_test_api.py
+++ b/tests/api/tools_test_api.py
@@ -568,8 +568,7 @@ EVALUATIONS_FIELDS = {
"visi_bulletin",
}
-EVALUATION_FIELDS = {
- "id",
+NOTES_FIELDS = {
"etudid",
"evaluation_id",
"value",
diff --git a/tests/ressources/formations/scodoc_formation_BUT_INFO_v0514.xml b/tests/ressources/formations/scodoc_formation_BUT_INFO_v0514.xml
new file mode 100644
index 000000000..dcaba6560
--- /dev/null
+++ b/tests/ressources/formations/scodoc_formation_BUT_INFO_v0514.xml
@@ -0,0 +1,554 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py
index 770fbf4e5..5472c17b6 100644
--- a/tests/unit/test_sco_basic.py
+++ b/tests/unit/test_sco_basic.py
@@ -103,14 +103,14 @@ def run_sco_basic(verbose=False) -> FormSemestre:
# --- Saisie toutes les notes de l'évaluation
for idx, etud in enumerate(etuds):
- nb_changed, nb_suppress, existing_decisions = G.create_note(
+ etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e["id"],
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
)
assert not existing_decisions
assert nb_suppress == 0
- assert nb_changed == 1
+ assert len(etudids_changed) == 1
# --- Vérifie que les notes sont prises en compte:
b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"])
@@ -136,7 +136,7 @@ def run_sco_basic(verbose=False) -> FormSemestre:
)
# Saisie les notes des 5 premiers étudiants:
for idx, etud in enumerate(etuds[:5]):
- nb_changed, nb_suppress, existing_decisions = G.create_note(
+ etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["id"],
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
@@ -158,7 +158,7 @@ def run_sco_basic(verbose=False) -> FormSemestre:
# Saisie des notes qui manquent:
for idx, etud in enumerate(etuds[5:]):
- nb_changed, nb_suppress, existing_decisions = G.create_note(
+ etudids_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["id"],
etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)],
diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py
index cd06048b0..401f33099 100644
--- a/tools/fakedatabase/create_test_api_database.py
+++ b/tools/fakedatabase/create_test_api_database.py
@@ -239,7 +239,7 @@ def create_evaluations(formsemestre: FormSemestre):
"jour": datetime.date(2022, 3, 1) + datetime.timedelta(days=modimpl.id),
"heure_debut": "8h00",
"heure_fin": "9h00",
- "description": None,
+ "description": f"Evaluation-{modimpl.module.code}",
"note_max": 20,
"coefficient": 1.0,
"visibulletin": True,