Compare commits
22 Commits
2212990788
...
a200be586a
Author | SHA1 | Date |
---|---|---|
Emmanuel Viennet | a200be586a | |
Emmanuel Viennet | 607604f91e | |
Emmanuel Viennet | 8eedac0f03 | |
Emmanuel Viennet | aea2204d9e | |
Emmanuel Viennet | 9c15cbe647 | |
Emmanuel Viennet | 6761f5a620 | |
Emmanuel Viennet | 69a53adb55 | |
Emmanuel Viennet | b30ea5f5fd | |
Emmanuel Viennet | 052fb3c7b9 | |
Lyanis Souidi | dbd0124c2c | |
Lyanis Souidi | e989a4ffa8 | |
Lyanis Souidi | 6ae2b0eb5f | |
Emmanuel Viennet | d7f3376103 | |
Lyanis Souidi | 677415fbfc | |
Emmanuel Viennet | 6cbeeedb1c | |
Emmanuel Viennet | 39e7ad3ad6 | |
Emmanuel Viennet | 177d38428e | |
Emmanuel Viennet | f4c1d00046 | |
Emmanuel Viennet | 86c12dee08 | |
Emmanuel Viennet | 8cf85f78a8 | |
Emmanuel Viennet | 9ec0ef27ba | |
Emmanuel Viennet | c8ac796347 |
|
@ -337,7 +337,7 @@ def assiduites_group(with_query: bool = False):
|
|||
try:
|
||||
etuds = [int(etu) for etu in etuds]
|
||||
except ValueError:
|
||||
return json_error(404, "Le champs etudids n'est pas correctement formé")
|
||||
return json_error(404, "Le champ etudids n'est pas correctement formé")
|
||||
|
||||
# Vérification que tous les étudiants sont du même département
|
||||
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, request
|
||||
from flask import g, request, Response
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
|
@ -18,7 +18,7 @@ from sqlalchemy import desc, func, or_
|
|||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import tools
|
||||
from app.but import bulletin_but_court
|
||||
|
@ -26,6 +26,7 @@ from app.decorators import scodoc, permission_required
|
|||
from app.models import (
|
||||
Admission,
|
||||
Departement,
|
||||
EtudAnnotation,
|
||||
FormSemestreInscription,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
|
@ -54,6 +55,32 @@ import app.scodoc.sco_utils as scu
|
|||
#
|
||||
|
||||
|
||||
def _get_etud_by_code(
|
||||
code_type: str, code: str, dept: Departement
|
||||
) -> tuple[bool, Response | Identite]:
|
||||
"""Get etud, using etudid, NIP or INE
|
||||
Returns True, etud if ok, or False, error response.
|
||||
"""
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code)
|
||||
elif code_type == "etudid":
|
||||
try:
|
||||
etudid = int(code)
|
||||
except ValueError:
|
||||
return False, json_error(404, "invalid etudid type")
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif code_type == "ine":
|
||||
query = Identite.query.filter_by(code_ine=code)
|
||||
else:
|
||||
return False, json_error(404, "invalid code_type")
|
||||
if dept:
|
||||
query = query.filter_by(dept_id=dept.id)
|
||||
etud = query.first()
|
||||
if etud is None:
|
||||
return False, json_error(404, message="etudiant inexistant")
|
||||
return True, etud
|
||||
|
||||
|
||||
@bp.route("/etudiants/courants", defaults={"long": False})
|
||||
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
||||
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
||||
|
@ -105,7 +132,9 @@ def etudiants_courants(long=False):
|
|||
)
|
||||
if long:
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
data = [etud.to_dict_api(restrict=restrict) for etud in etuds]
|
||||
data = [
|
||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in etuds
|
||||
]
|
||||
else:
|
||||
data = [etud.to_dict_short() for etud in etuds]
|
||||
return data
|
||||
|
@ -140,7 +169,7 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||
message="étudiant inconnu",
|
||||
)
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return etud.to_dict_api(restrict=restrict)
|
||||
return etud.to_dict_api(restrict=restrict, with_annotations=True)
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||
|
@ -253,7 +282,9 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
|||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return [etud.to_dict_api(restrict=restrict) for etud in query]
|
||||
return [
|
||||
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in query
|
||||
]
|
||||
|
||||
|
||||
@bp.route("/etudiants/name/<string:start>")
|
||||
|
@ -385,28 +416,15 @@ def bulletin(
|
|||
pdf = True
|
||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||
return json_error(404, "version invalide")
|
||||
# return f"{code_type}={code}, version={version}, pdf={pdf}"
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||
return json_error(404, "formsemestre inexistant")
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
||||
elif code_type == "etudid":
|
||||
try:
|
||||
etudid = int(code)
|
||||
except ValueError:
|
||||
return json_error(404, "invalid etudid type")
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif code_type == "ine":
|
||||
query = Identite.query.filter_by(code_ine=code, dept_id=dept.id)
|
||||
else:
|
||||
return json_error(404, "invalid code_type")
|
||||
etud = query.first()
|
||||
if etud is None:
|
||||
return json_error(404, message="etudiant inexistant")
|
||||
ok, etud = _get_etud_by_code(code_type, code, dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
|
||||
if version == "butcourt":
|
||||
if pdf:
|
||||
|
@ -558,26 +576,15 @@ def etudiant_create(force=False):
|
|||
@api_web_bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def etudiant_edit(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
):
|
||||
"""Edition des données étudiant (identité, admission, adresses)"""
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code)
|
||||
elif code_type == "etudid":
|
||||
try:
|
||||
etudid = int(code)
|
||||
except ValueError:
|
||||
return json_error(404, "invalid etudid type")
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif code_type == "ine":
|
||||
query = Identite.query.filter_by(code_ine=code)
|
||||
else:
|
||||
return json_error(404, "invalid code_type")
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
etud: Identite = query.first()
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
#
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
etud.from_dict(args)
|
||||
|
@ -600,3 +607,67 @@ def etudiant_edit(
|
|||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
r = etud.to_dict_api(restrict=restrict)
|
||||
return r
|
||||
|
||||
|
||||
@bp.route("/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"])
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"]
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit) # il faut en plus ViewEtudData
|
||||
@as_json
|
||||
def etudiant_annotation(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
):
|
||||
"""Ajout d'une annotation sur un étudiant"""
|
||||
if not current_user.has_permission(Permission.ViewEtudData):
|
||||
return json_error(403, "non autorisé (manque ViewEtudData)")
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
#
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
comment = args.get("comment", None)
|
||||
if not isinstance(comment, str):
|
||||
return json_error(404, "invalid comment (expected string)")
|
||||
if len(comment) > scu.MAX_TEXT_LEN:
|
||||
return json_error(404, "invalid comment (too large)")
|
||||
annotation = EtudAnnotation(comment=comment, author=current_user.user_name)
|
||||
etud.annotations.append(annotation)
|
||||
db.session.add(etud)
|
||||
db.session.commit()
|
||||
log(f"etudiant_annotation/{etud.id}/{annotation.id}")
|
||||
return annotation.to_dict()
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
def etudiant_annotation_delete(
|
||||
code_type: str = "etudid", code: str = None, annotation_id: int = None
|
||||
):
|
||||
"""
|
||||
Suppression d'une annotation
|
||||
"""
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
annotation = EtudAnnotation.query.filter_by(
|
||||
etudid=etud.id, id=annotation_id
|
||||
).first()
|
||||
if annotation is None:
|
||||
return json_error(404, "annotation not found")
|
||||
log(f"etudiant_annotation_delete/{etud.id}/{annotation.id}")
|
||||
db.session.delete(annotation)
|
||||
db.session.commit()
|
||||
return "ok"
|
||||
|
|
|
@ -119,9 +119,13 @@ class EtudCursusBUT:
|
|||
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
validation_rcue: ApcValidationRCUE
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
if (
|
||||
niveau is None
|
||||
or not niveau.competence.id in self.validation_par_competence_et_annee
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
|
@ -443,8 +447,24 @@ def formsemestre_warning_apc_setup(
|
|||
}">formation n'est pas associée à un référentiel de compétence.</a>
|
||||
</div>
|
||||
"""
|
||||
# Vérifie les niveaux de chaque parcours
|
||||
H = []
|
||||
# Le semestre n'a pas de parcours, mais les UE ont des parcours ?
|
||||
if not formsemestre.parcours:
|
||||
nb_ues_sans_parcours = len(
|
||||
formsemestre.formation.query_ues_parcour(None)
|
||||
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
||||
.all()
|
||||
)
|
||||
nb_ues_tot = (
|
||||
UniteEns.query.filter_by(formation=formsemestre.formation, type=UE_STANDARD)
|
||||
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
||||
.count()
|
||||
)
|
||||
if nb_ues_sans_parcours != nb_ues_tot:
|
||||
H.append(
|
||||
f"""Le semestre n'est associé à aucun parcours, mais les UEs de la formation ont des parcours"""
|
||||
)
|
||||
# Vérifie les niveaux de chaque parcours
|
||||
for parcour in formsemestre.parcours or [None]:
|
||||
annee = (formsemestre.semestre_id + 1) // 2
|
||||
niveaux_ids = {
|
||||
|
|
|
@ -256,7 +256,7 @@ def _gen_but_niveau_ue(
|
|||
return f"""<div class="but_niveau_ue {ue_class}
|
||||
{'annee_prec' if annee_prec else ''}
|
||||
">
|
||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||
<div title="{ue.titre or ''}">{ue.acronyme}</div>
|
||||
<div class="but_note with_scoplement">
|
||||
<div>{moy_ue_str}</div>
|
||||
{scoplement}
|
||||
|
|
|
@ -17,7 +17,7 @@ def UEParcoursECTSForm(ue: UniteEns) -> FlaskForm:
|
|||
pass
|
||||
|
||||
parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours
|
||||
# Initialise un champs de saisie par parcours
|
||||
# Initialise un champ de saisie par parcours
|
||||
for parcour in parcours:
|
||||
ects = ue.get_ects(parcour, only_parcours=True)
|
||||
setattr(
|
||||
|
|
|
@ -82,7 +82,7 @@ class ConfigCASForm(FlaskForm):
|
|||
|
||||
cas_attribute_id = StringField(
|
||||
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
|
||||
description="""Le champs CAS qui sera considéré comme l'id unique des
|
||||
description="""Le champ CAS qui sera considéré comme l'id unique des
|
||||
comptes utilisateurs.""",
|
||||
)
|
||||
|
||||
|
|
|
@ -77,10 +77,12 @@ class ApcValidationRCUE(db.Model):
|
|||
niveau = self.niveau()
|
||||
return niveau.annee if niveau else None
|
||||
|
||||
def niveau(self) -> ApcNiveau:
|
||||
def niveau(self) -> ApcNiveau | None:
|
||||
"""Le niveau de compétence associé à cet RCUE."""
|
||||
# Par convention, il est donné par la seconde UE
|
||||
return self.ue2.niveau_competence
|
||||
# à défaut (si l'UE a été désacciée entre temps), la première
|
||||
# et à défaut, renvoie None
|
||||
return self.ue2.niveau_competence or self.ue1.niveau_competence
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict"
|
||||
|
|
|
@ -297,7 +297,7 @@ class ScoDocSiteConfig(db.Model):
|
|||
|
||||
@classmethod
|
||||
def _get_int_field(cls, name: str, default=None) -> int:
|
||||
"""Valeur d'un champs integer"""
|
||||
"""Valeur d'un champ integer"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||
if (cfg is None) or cfg.value is None:
|
||||
return default
|
||||
|
@ -311,7 +311,7 @@ class ScoDocSiteConfig(db.Model):
|
|||
default=None,
|
||||
range_values: tuple = (),
|
||||
) -> bool:
|
||||
"""Set champs integer. True si changement."""
|
||||
"""Set champ integer. True si changement."""
|
||||
if value != cls._get_int_field(name, default=default):
|
||||
if not isinstance(value, int) or (
|
||||
range_values and (value < range_values[0]) or (value > range_values[1])
|
||||
|
|
|
@ -101,7 +101,12 @@ class Identite(models.ScoDocModel):
|
|||
adresses = db.relationship(
|
||||
"Adresse", back_populates="etud", cascade="all,delete", lazy="dynamic"
|
||||
)
|
||||
|
||||
annotations = db.relationship(
|
||||
"EtudAnnotation",
|
||||
backref="etudiant",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="dynamic",
|
||||
)
|
||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||
#
|
||||
dispense_ues = db.relationship(
|
||||
|
@ -477,9 +482,9 @@ class Identite(models.ScoDocModel):
|
|||
"civilite": self.civilite,
|
||||
"code_ine": self.code_ine or "",
|
||||
"code_nip": self.code_nip or "",
|
||||
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
||||
if self.date_naissance
|
||||
else "",
|
||||
"date_naissance": (
|
||||
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""
|
||||
),
|
||||
"dept_acronym": self.departement.acronym,
|
||||
"dept_id": self.dept_id,
|
||||
"dept_naissance": self.dept_naissance or "",
|
||||
|
@ -506,7 +511,7 @@ class Identite(models.ScoDocModel):
|
|||
d["id"] = self.id # a été écrasé par l'id de adresse
|
||||
return d
|
||||
|
||||
def to_dict_api(self, restrict=False) -> dict:
|
||||
def to_dict_api(self, restrict=False, with_annotations=False) -> dict:
|
||||
"""Représentation dictionnaire pour export API, avec adresses et admission.
|
||||
Si restrict, supprime les infos "personnelles" (boursier)
|
||||
"""
|
||||
|
@ -518,6 +523,17 @@ class Identite(models.ScoDocModel):
|
|||
e["dept_acronym"] = self.departement.acronym
|
||||
e.pop("departement", None)
|
||||
e["sort_key"] = self.sort_key
|
||||
if with_annotations:
|
||||
e["annotations"] = (
|
||||
[
|
||||
annot.to_dict()
|
||||
for annot in EtudAnnotation.query.filter_by(
|
||||
etudid=self.id
|
||||
).order_by(desc(EtudAnnotation.date))
|
||||
]
|
||||
if not restrict
|
||||
else []
|
||||
)
|
||||
if restrict:
|
||||
# Met à None les attributs protégés:
|
||||
for attr in self.protected_attrs:
|
||||
|
@ -1072,10 +1088,16 @@ class EtudAnnotation(db.Model):
|
|||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7))
|
||||
etudid = db.Column(db.Integer, db.ForeignKey(Identite.id))
|
||||
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||
comment = db.Column(db.Text)
|
||||
|
||||
def to_dict(self):
|
||||
"""Représentation dictionnaire."""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
return e
|
||||
|
||||
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.modules import Module
|
||||
|
|
|
@ -68,7 +68,7 @@ class FormSemestre(db.Model):
|
|||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
titre = db.Column(db.Text(), nullable=False)
|
||||
date_debut = db.Column(db.Date(), nullable=False)
|
||||
date_fin = db.Column(db.Date(), nullable=False)
|
||||
date_fin = db.Column(db.Date(), nullable=False) # jour inclus
|
||||
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
|
||||
"identifiant emplois du temps (unicité non imposée)"
|
||||
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
||||
|
|
|
@ -1,19 +1,44 @@
|
|||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""Affichages, debug
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
from app import log
|
||||
|
||||
PE_DEBUG = 0
|
||||
PE_DEBUG = False
|
||||
|
||||
if not PE_DEBUG:
|
||||
# log to notes.log
|
||||
def pe_print(*a, **kw):
|
||||
# kw is ignored. log always add a newline
|
||||
log(" ".join(a))
|
||||
|
||||
else:
|
||||
pe_print = print # print function
|
||||
# On stocke les logs PE dans g.scodoc_pe_log
|
||||
# pour ne pas modifier les nombreux appels à pe_print.
|
||||
def pe_start_log() -> list[str]:
|
||||
"Initialize log"
|
||||
g.scodoc_pe_log = []
|
||||
return g.scodoc_pe_log
|
||||
|
||||
|
||||
def pe_print(*a):
|
||||
"Log (or print in PE_DEBUG mode) and store in g"
|
||||
lines = getattr(g, "scodoc_pe_log")
|
||||
if lines is None:
|
||||
lines = pe_start_log()
|
||||
msg = " ".join(a)
|
||||
lines.append(msg)
|
||||
if PE_DEBUG:
|
||||
print(msg)
|
||||
else:
|
||||
log(msg)
|
||||
|
||||
|
||||
def pe_get_log() -> str:
|
||||
"Renvoie une chaîne avec tous les messages loggués"
|
||||
return "\n".join(getattr(g, "scodoc_pe_log", []))
|
||||
|
||||
|
||||
# Affichage dans le tableur pe en cas d'absence de notes
|
||||
SANS_NOTE = "-"
|
||||
NOM_STAT_GROUPE = "statistiques du groupe"
|
||||
NOM_STAT_PROMO = "statistiques de la promo"
|
||||
|
||||
|
||||
|
|
|
@ -52,7 +52,6 @@ from app.scodoc import sco_formsemestre
|
|||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
||||
|
||||
# Generated LaTeX files are encoded as:
|
||||
PE_LATEX_ENCODING = "utf-8"
|
||||
|
||||
|
@ -98,7 +97,8 @@ def calcul_age(born: datetime.date) -> int:
|
|||
return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
||||
|
||||
|
||||
def remove_accents(input_unicode_str):
|
||||
# Nota: scu.suppress_accents fait la même chose mais renvoie un str et non un bytes
|
||||
def remove_accents(input_unicode_str: str) -> bytes:
|
||||
"""Supprime les accents d'une chaine unicode"""
|
||||
nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
|
||||
only_ascii = nfkd_form.encode("ASCII", "ignore")
|
||||
|
@ -133,15 +133,15 @@ def escape_for_latex(s):
|
|||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
def list_directory_filenames(path):
|
||||
"""List of regular filenames in a directory (recursive)
|
||||
def list_directory_filenames(path: str) -> list[str]:
|
||||
"""List of regular filenames (paths) in a directory (recursive)
|
||||
Excludes files and directories begining with .
|
||||
"""
|
||||
R = []
|
||||
paths = []
|
||||
for root, dirs, files in os.walk(path, topdown=True):
|
||||
dirs[:] = [d for d in dirs if d[0] != "."]
|
||||
R += [os.path.join(root, fn) for fn in files if fn[0] != "."]
|
||||
return R
|
||||
paths += [os.path.join(root, fn) for fn in files if fn[0] != "."]
|
||||
return paths
|
||||
|
||||
|
||||
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
|
||||
|
@ -195,13 +195,15 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
|
|||
def get_annee_diplome_semestre(
|
||||
sem_base: FormSemestre | dict, nbre_sem_formation: int = 6
|
||||
) -> int:
|
||||
"""Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres)
|
||||
et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle
|
||||
sera remis le diplôme BUT des étudiants qui y sont scolarisés
|
||||
(en supposant qu'il n'y ait pas de redoublement à venir).
|
||||
"""Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT
|
||||
à 6 semestres) et connaissant le numéro du semestre, ses dates de début et de fin du
|
||||
semestre, prédit l'année à laquelle sera remis le diplôme BUT des étudiants qui y
|
||||
sont scolarisés (en supposant qu'il n'y ait pas de redoublement à venir).
|
||||
|
||||
**Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4, S6 pour des semestres décalés)
|
||||
s'étalent sur deux années civiles ; contrairement au semestre de seconde partie d'année universitaire.
|
||||
**Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4,
|
||||
S6 pour des semestres décalés)
|
||||
s'étalent sur deux années civiles ; contrairement au semestre de seconde partie
|
||||
d'année universitaire.
|
||||
|
||||
Par exemple :
|
||||
|
||||
|
@ -235,21 +237,22 @@ def get_annee_diplome_semestre(
|
|||
if (
|
||||
1 <= sem_id <= nbre_sem_formation
|
||||
): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ??
|
||||
nbreSemRestant = (
|
||||
nb_sem_restants = (
|
||||
nbre_sem_formation - sem_id
|
||||
) # nombre de semestres restant avant diplome
|
||||
nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome
|
||||
# Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé
|
||||
nb_annees_restantes = (
|
||||
nb_sem_restants // 2
|
||||
) # nombre d'annees restant avant diplome
|
||||
# Flag permettant d'activer ou désactiver un increment
|
||||
# à prendre en compte en cas de semestre décalé
|
||||
# avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon
|
||||
delta = annee_fin - annee_debut
|
||||
decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1
|
||||
decalage = nb_sem_restants % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1
|
||||
increment = decalage * (1 - delta)
|
||||
return annee_fin + nbreAnRestant + increment
|
||||
return annee_fin + nb_annees_restantes + increment
|
||||
|
||||
|
||||
def get_cosemestres_diplomants(
|
||||
annee_diplome: int
|
||||
) -> dict[int, FormSemestre]:
|
||||
def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
|
||||
"""Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``.
|
||||
|
||||
**Définition** : Un co-semestre est un semestre :
|
||||
|
@ -264,15 +267,15 @@ def get_cosemestres_diplomants(
|
|||
Returns:
|
||||
Un dictionnaire {fid: FormSemestre(fid)} contenant les cosemestres
|
||||
"""
|
||||
tousLesSems = (
|
||||
tous_les_sems = (
|
||||
sco_formsemestre.do_formsemestre_list()
|
||||
) # tous les semestres memorisés dans scodoc
|
||||
|
||||
cosemestres_fids = {
|
||||
sem["id"]
|
||||
for sem in tousLesSems
|
||||
if get_annee_diplome_semestre(sem) == annee_diplome
|
||||
}
|
||||
sem["id"]
|
||||
for sem in tous_les_sems
|
||||
if get_annee_diplome_semestre(sem) == annee_diplome
|
||||
}
|
||||
|
||||
cosemestres = {}
|
||||
for fid in cosemestres_fids:
|
||||
|
@ -281,5 +284,3 @@ def get_cosemestres_diplomants(
|
|||
cosemestres[fid] = cosem
|
||||
|
||||
return cosemestres
|
||||
|
||||
|
||||
|
|
|
@ -37,13 +37,13 @@ Created on 17/01/2024
|
|||
"""
|
||||
import pandas as pd
|
||||
|
||||
import app.pe.pe_rcs
|
||||
from app.models import FormSemestre, Identite, Formation
|
||||
from app.pe import pe_comp, pe_affichage
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.comp.res_sem import load_formsemestre_results
|
||||
|
||||
|
||||
class EtudiantsJuryPE:
|
||||
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
|
||||
|
||||
|
@ -123,20 +123,15 @@ class EtudiantsJuryPE:
|
|||
|
||||
# Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
|
||||
self.etudiants_diplomes = self.get_etudiants_diplomes()
|
||||
"""Les identités des étudiants diplômés"""
|
||||
|
||||
self.diplomes_ids = set(self.etudiants_diplomes.keys())
|
||||
"""Les identifiants des étudiants diplômés"""
|
||||
|
||||
self.etudiants_ids = set(self.identites.keys())
|
||||
"""Les identifiants des étudiants (diplômés, redoublants ou ayant abandonnés) à traiter"""
|
||||
|
||||
# Les abandons (pour debug)
|
||||
self.abandons = self.get_etudiants_redoublants_ou_reorientes()
|
||||
"""Les identités des étudiants ayant redoublés ou ayant abandonnés"""
|
||||
# Les identités des étudiants ayant redoublés ou ayant abandonnés
|
||||
|
||||
self.abandons_ids = set(self.abandons)
|
||||
"""Les identifiants des étudiants ayant redoublés ou ayant abandonnés"""
|
||||
# Les identifiants des étudiants ayant redoublés ou ayant abandonnés
|
||||
|
||||
# Synthèse
|
||||
pe_affichage.pe_print(
|
||||
|
@ -279,7 +274,7 @@ class EtudiantsJuryPE:
|
|||
semestres_significatifs = self.get_semestres_significatifs(etudid)
|
||||
|
||||
# Tri des semestres par numéro de semestre
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT+1):
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
||||
# les semestres de n°i de l'étudiant:
|
||||
semestres_i = {
|
||||
fid: sem_sig
|
||||
|
@ -288,18 +283,20 @@ class EtudiantsJuryPE:
|
|||
}
|
||||
self.cursus[etudid][f"S{i}"] = semestres_i
|
||||
|
||||
|
||||
|
||||
def get_formsemestres_terminaux_aggregat(self, aggregat: str):
|
||||
def get_formsemestres_terminaux_aggregat(
|
||||
self, aggregat: str
|
||||
) -> dict[int, FormSemestre]:
|
||||
"""Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat
|
||||
(pour l'aggrégat '3S' incluant S1+S2+S3, a pour semestre terminal S3).
|
||||
Ces formsemestres traduisent :
|
||||
|
||||
* les différents parcours des étudiants liés par exemple au choix de modalité (par ex: S1 FI + S2 FI + S3 FI
|
||||
ou S1 FI + S2 FI + S3 UFA), en renvoyant les formsemestre_id du S3 FI et du S3 UFA.
|
||||
* les éventuelles situations de redoublement (par ex pour 1 étudiant ayant redoublé sa 2ème année :
|
||||
S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en renvoyant les formsemestre_id du
|
||||
S3 (1ère session) et du S3 (2ème session)
|
||||
* les différents parcours des étudiants liés par exemple au choix de modalité
|
||||
(par ex: S1 FI + S2 FI + S3 FI ou S1 FI + S2 FI + S3 UFA), en renvoyant les
|
||||
formsemestre_id du S3 FI et du S3 UFA.
|
||||
* les éventuelles situations de redoublement (par ex pour 1 étudiant ayant
|
||||
redoublé sa 2ème année :
|
||||
S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en
|
||||
renvoyant les formsemestre_id du S3 (1ère session) et du S3 (2ème session)
|
||||
|
||||
Args:
|
||||
aggregat: L'aggrégat
|
||||
|
@ -316,7 +313,6 @@ class EtudiantsJuryPE:
|
|||
formsemestres_terminaux[fid] = trajectoire.formsemestre_final
|
||||
return formsemestres_terminaux
|
||||
|
||||
|
||||
def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
|
||||
"""Partant d'un ensemble d'étudiants,
|
||||
nombre de semestres (étapes) maximum suivis par les étudiants du jury.
|
||||
|
@ -407,7 +403,7 @@ def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
|
|||
return etudiants_ids
|
||||
|
||||
|
||||
def get_annee_diplome(etud: Identite) -> int:
|
||||
def get_annee_diplome(etud: Identite) -> int | None:
|
||||
"""L'année de diplôme prévue d'un étudiant en fonction de ses semestres
|
||||
d'inscription (pour un BUT).
|
||||
|
||||
|
@ -415,13 +411,14 @@ def get_annee_diplome(etud: Identite) -> int:
|
|||
identite: L'identité d'un étudiant
|
||||
|
||||
Returns:
|
||||
L'année prévue de sa diplômation
|
||||
L'année prévue de sa diplômation, ou None si aucun semestre
|
||||
"""
|
||||
formsemestres_apc = get_semestres_apc(etud)
|
||||
|
||||
if formsemestres_apc:
|
||||
dates_possibles_diplome = []
|
||||
"""Années de diplômation prédites en fonction des semestres (d'une formation APC) d'un étudiant"""
|
||||
# Années de diplômation prédites en fonction des semestres
|
||||
# (d'une formation APC) d'un étudiant
|
||||
for sem_base in formsemestres_apc:
|
||||
annee = pe_comp.get_annee_diplome_semestre(sem_base)
|
||||
if annee:
|
||||
|
@ -452,23 +449,26 @@ def get_semestres_apc(identite: Identite) -> list:
|
|||
def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
|
||||
"""Détermine si un étudiant a arrêté sa formation. Il peut s'agir :
|
||||
|
||||
* d'une réorientation à l'initiative du jury de semestre ou d'une démission (on pourrait
|
||||
utiliser les code NAR pour réorienté & DEM pour démissionnaire des résultats du jury renseigné dans la BDD,
|
||||
mais pas nécessaire ici)
|
||||
* d'une réorientation à l'initiative du jury de semestre ou d'une démission
|
||||
(on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire
|
||||
des résultats du jury renseigné dans la BDD, mais pas nécessaire ici)
|
||||
|
||||
* d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour autant avoir été indiqué NAR ou DEM).
|
||||
* d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour
|
||||
autant avoir été indiqué NAR ou DEM).
|
||||
|
||||
Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas dans l'un des "derniers" cosemestres
|
||||
(semestres conduisant à la même année de diplômation) connu dans Scodoc.
|
||||
Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas
|
||||
dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation)
|
||||
connu dans Scodoc.
|
||||
|
||||
Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc), l'étudiant doit appartenir à une
|
||||
instance des S5 qui conduisent à la diplomation dans l'année visée. S'il n'est que dans un S4, il a sans doute
|
||||
arrêté. A moins qu'il ne soit parti à l'étranger et là, pas de notes.
|
||||
Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc),
|
||||
l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans
|
||||
l'année visée. S'il n'est que dans un S4, il a sans doute arrêté. A moins qu'il ne soit
|
||||
parti à l'étranger et là, pas de notes.
|
||||
TODO:: Cas de l'étranger, à coder/tester
|
||||
|
||||
**Attention** : Cela suppose que toutes les instances d'un semestre donné (par ex: toutes les instances de S6
|
||||
accueillant un étudiant soient créées ; sinon les étudiants non inscrits dans un S6 seront considérés comme
|
||||
ayant abandonnés)
|
||||
**Attention** : Cela suppose que toutes les instances d'un semestre donné
|
||||
(par ex: toutes les instances de S6 accueillant un étudiant soient créées ; sinon les
|
||||
étudiants non inscrits dans un S6 seront considérés comme ayant abandonnés)
|
||||
TODO:: Peut-être à mettre en regard avec les propositions d'inscriptions d'étudiants dans un nouveau semestre
|
||||
|
||||
Pour chaque étudiant, recherche son dernier semestre en date (validé ou non) et
|
||||
|
@ -487,7 +487,6 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
|
|||
TODO:: A reprendre pour le cas des étudiants à l'étranger
|
||||
TODO:: A reprendre si BUT avec semestres décalés
|
||||
"""
|
||||
|
||||
# Les semestres APC de l'étudiant
|
||||
semestres = get_semestres_apc(etud)
|
||||
semestres_apc = {sem.semestre_id: sem for sem in semestres}
|
||||
|
|
|
@ -1,16 +1,59 @@
|
|||
from app.comp import moy_sem
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on Thu Sep 8 09:36:33 2016
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
||||
from app.pe.pe_etudiant import EtudiantsJuryPE
|
||||
from app.pe.pe_rcs import RCS, RCSsJuryPE
|
||||
from app.pe.pe_rcstag import RCSTag
|
||||
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
class RCSInterclasseTag(TableTag):
|
||||
# -------------------------------------------------------------------------------------------------------------------
|
||||
"""
|
||||
Interclasse l'ensemble des étudiants diplômés à une année
|
||||
donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S')
|
||||
en reportant :
|
||||
|
||||
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre
|
||||
le numéro de semestre de fin de l'aggrégat (indépendamment de son
|
||||
formsemestre)
|
||||
* calculant le classement sur les étudiants diplômes
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nom_rcs: str,
|
||||
|
@ -18,15 +61,6 @@ class RCSInterclasseTag(TableTag):
|
|||
rcss_jury_pe: RCSsJuryPE,
|
||||
rcss_tags: dict[tuple, RCSTag],
|
||||
):
|
||||
"""
|
||||
Interclasse l'ensemble des étudiants diplômés à une année
|
||||
donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S')
|
||||
en reportant :
|
||||
|
||||
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre le numéro de semestre de fin de l'aggrégat (indépendamment de son
|
||||
formsemestres)
|
||||
* calculant le classement sur les étudiants diplômes
|
||||
"""
|
||||
TableTag.__init__(self)
|
||||
|
||||
self.nom_rcs = nom_rcs
|
||||
|
@ -43,7 +77,8 @@ class RCSInterclasseTag(TableTag):
|
|||
for etudid in self.diplomes_ids
|
||||
}
|
||||
|
||||
# Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat
|
||||
# Les trajectoires (et leur version tagguées), en ne gardant que
|
||||
# celles associées à l'aggrégat
|
||||
self.rcss: dict[int, RCS] = {}
|
||||
"""Ensemble des trajectoires associées à l'aggrégat"""
|
||||
for trajectoire_id in rcss_jury_pe.rcss:
|
||||
|
@ -54,9 +89,7 @@ class RCSInterclasseTag(TableTag):
|
|||
self.trajectoires_taggues: dict[int, RCS] = {}
|
||||
"""Ensemble des trajectoires tagguées associées à l'aggrégat"""
|
||||
for trajectoire_id in self.rcss:
|
||||
self.trajectoires_taggues[trajectoire_id] = rcss_tags[
|
||||
trajectoire_id
|
||||
]
|
||||
self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id]
|
||||
|
||||
# Les trajectoires suivies par les étudiants du jury, en ne gardant que
|
||||
# celles associées aux diplomés
|
||||
|
|
|
@ -44,41 +44,44 @@ Created on Fri Sep 9 09:15:05 2016
|
|||
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
from zipfile import ZipFile
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE
|
||||
|
||||
from app.pe.pe_etudiant import *
|
||||
from app.pe.pe_rcs import *
|
||||
import app.pe.pe_comp as pe_comp
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant.
|
||||
from app.pe.pe_rcs import * # TODO A éviter
|
||||
from app.pe.pe_rcstag import RCSTag
|
||||
from app.pe.pe_semtag import SemestreTag
|
||||
from app.pe.pe_interclasstag import RCSInterclasseTag
|
||||
from app.pe.pe_rcstag import RCSTag
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class JuryPE(object):
|
||||
def __init__(self, diplome):
|
||||
"""
|
||||
Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
|
||||
d'une année de diplôme. De ce semestre est déduit :
|
||||
1. l'année d'obtention du DUT,
|
||||
2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
|
||||
"""
|
||||
Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
|
||||
d'une année de diplôme. De ce semestre est déduit :
|
||||
1. l'année d'obtention du DUT,
|
||||
2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
|
||||
|
||||
Args:
|
||||
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
|
||||
"""
|
||||
Args:
|
||||
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
|
||||
"""
|
||||
|
||||
def __init__(self, diplome):
|
||||
pe_affichage.pe_start_log()
|
||||
self.diplome = diplome
|
||||
"L'année du diplome"
|
||||
|
||||
self.nom_export_zip = f"Jury_PE_{self.diplome}"
|
||||
"Nom du zip où ranger les fichiers générés"
|
||||
|
||||
# Chargement des étudiants à prendre en compte Sydans le jury
|
||||
pe_affichage.pe_print(
|
||||
f"Données de poursuite d'étude générées le {time.strftime('%d/%m/%Y à %H:%M')}\n"
|
||||
)
|
||||
# Chargement des étudiants à prendre en compte dans le jury
|
||||
pe_affichage.pe_print(
|
||||
f"""*** Recherche et chargement des étudiants diplômés en {
|
||||
self.diplome}"""
|
||||
|
@ -98,6 +101,8 @@ class JuryPE(object):
|
|||
self._gen_xls_interclassements_rcss(zipfile)
|
||||
self._gen_xls_synthese_jury_par_tag(zipfile)
|
||||
self._gen_xls_synthese_par_etudiant(zipfile)
|
||||
# et le log
|
||||
self._add_log_to_zip(zipfile)
|
||||
|
||||
# Fin !!!! Tada :)
|
||||
|
||||
|
@ -161,9 +166,7 @@ class JuryPE(object):
|
|||
self.rcss.cree_rcss(self.etudiants)
|
||||
|
||||
# Génère les moyennes par tags des trajectoires
|
||||
pe_affichage.pe_print(
|
||||
"*** Calcule les moyennes par tag des RCS possibles"
|
||||
)
|
||||
pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles")
|
||||
self.rcss_tags = compute_trajectoires_tag(
|
||||
self.rcss, self.etudiants, self.sems_tags
|
||||
)
|
||||
|
@ -255,6 +258,11 @@ class JuryPE(object):
|
|||
zipfile, f"synthese_jury_{self.diplome}_par_etudiant.xlsx", output.read()
|
||||
)
|
||||
|
||||
def _add_log_to_zip(self, zipfile):
|
||||
"""Add a text file with the log messages"""
|
||||
log_data = pe_affichage.pe_get_log()
|
||||
self.add_file_to_zip(zipfile, "pe_log.txt", log_data)
|
||||
|
||||
def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""):
|
||||
"""Add a file to given zip
|
||||
All files under NOM_EXPORT_ZIP/
|
||||
|
@ -381,14 +389,13 @@ class JuryPE(object):
|
|||
champ = (descr, "", "note")
|
||||
notes_traj = moy_traj.get_notes()
|
||||
donnees.loc[etudids_communs, champ] = notes_traj.loc[
|
||||
etudids_communs]
|
||||
etudids_communs
|
||||
]
|
||||
|
||||
# Les rangs
|
||||
champ = (descr, NOM_STAT_GROUPE, "class.")
|
||||
rangs = moy_traj.get_rangs_inscrits()
|
||||
donnees.loc[etudids_communs, champ] = rangs.loc[
|
||||
etudids_communs
|
||||
]
|
||||
donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs]
|
||||
|
||||
# Les mins
|
||||
champ = (descr, NOM_STAT_GROUPE, "min")
|
||||
|
@ -402,7 +409,7 @@ class JuryPE(object):
|
|||
|
||||
# Les moys
|
||||
champ = (descr, NOM_STAT_GROUPE, "moy")
|
||||
moys = moy_traj.get_max()
|
||||
moys = moy_traj.get_moy()
|
||||
donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs]
|
||||
|
||||
# Ajoute les données d'interclassement
|
||||
|
@ -430,7 +437,7 @@ class JuryPE(object):
|
|||
|
||||
# Les moys
|
||||
champ = (descr, nom_stat_promo, "moy")
|
||||
moys = moy_interclass.get_max()
|
||||
moys = moy_interclass.get_moy()
|
||||
donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs]
|
||||
|
||||
df_synthese = df_synthese.join(donnees)
|
||||
|
@ -487,9 +494,7 @@ class JuryPE(object):
|
|||
# La trajectoire de l'étudiant sur l'aggrégat
|
||||
trajectoire = self.rcss.suivi[etudid][aggregat]
|
||||
if trajectoire:
|
||||
trajectoire_tagguee = self.rcss_tags[
|
||||
trajectoire.rcs_id
|
||||
]
|
||||
trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id]
|
||||
if tag in trajectoire_tagguee.moyennes_tags:
|
||||
# L'interclassement
|
||||
interclass = self.interclassements_taggues[aggregat]
|
||||
|
@ -512,6 +517,7 @@ class JuryPE(object):
|
|||
df.sort_values(by=[("", "", "tag")], inplace=True)
|
||||
return df
|
||||
|
||||
|
||||
def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict:
|
||||
"""Ayant connaissance des étudiants dont il faut calculer les moyennes pour
|
||||
le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres
|
||||
|
@ -534,6 +540,7 @@ def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict:
|
|||
semestres = semestres | etudiants.cursus[etudid][cle]
|
||||
return semestres
|
||||
|
||||
|
||||
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
|
||||
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.
|
||||
Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire
|
||||
|
@ -548,7 +555,7 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
|
|||
Un dictionnaire {fid: SemestreTag(fid)}
|
||||
"""
|
||||
|
||||
"""Création des semestres taggués, de type 'S1', 'S2', ..."""
|
||||
# Création des semestres taggués, de type 'S1', 'S2', ...
|
||||
pe_affichage.pe_print("*** Création des semestres taggués")
|
||||
|
||||
formsemestres = get_formsemestres_etudiants(etudiants)
|
||||
|
|
118
app/pe/pe_rcs.py
118
app/pe/pe_rcs.py
|
@ -1,5 +1,15 @@
|
|||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on 01-2024
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
import app.pe.pe_comp as pe_comp
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
|
||||
from app.models import FormSemestre
|
||||
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
|
||||
|
@ -59,39 +69,41 @@ TYPES_RCS = {
|
|||
"descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)",
|
||||
},
|
||||
}
|
||||
"""Dictionnaire détaillant les différents regroupements cohérents
|
||||
de semestres (RCS), en leur attribuant un nom et en détaillant
|
||||
le nom des semestres qu'ils regroupement et l'affichage qui en sera fait
|
||||
dans les tableurs de synthèse"""
|
||||
"""Dictionnaire détaillant les différents regroupements cohérents
|
||||
de semestres (RCS), en leur attribuant un nom et en détaillant
|
||||
le nom des semestres qu'ils regroupent et l'affichage qui en sera fait
|
||||
dans les tableurs de synthèse.
|
||||
"""
|
||||
|
||||
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS.keys() if not cle.startswith("S")]
|
||||
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")]
|
||||
TOUS_LES_RCS = list(TYPES_RCS.keys())
|
||||
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS.keys() if cle.startswith("S")]
|
||||
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")]
|
||||
|
||||
|
||||
class RCS:
|
||||
"""Modélise un ensemble de semestres d'étudiants
|
||||
associé à un type de regroupement cohérent de semestres
|
||||
donné (par ex: 'S2', '3S', '2A').
|
||||
|
||||
Si le RCS est un semestre de type Si, stocke le (ou les)
|
||||
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
|
||||
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
|
||||
|
||||
Pour le RCS de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
|
||||
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
|
||||
terminal de la trajectoire (par ex: ici un S3).
|
||||
|
||||
Ces semestres peuvent être :
|
||||
|
||||
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
|
||||
* des S1+S2+(année de césure)+S3 si césure, ...
|
||||
|
||||
Args:
|
||||
nom_rcs: Un nom du RCS (par ex: '5S')
|
||||
semestre_final: Le semestre final du RCS
|
||||
"""
|
||||
|
||||
def __init__(self, nom_rcs: str, semestre_final: FormSemestre):
|
||||
"""Modélise un ensemble de semestres d'étudiants
|
||||
associé à un type de regroupement cohérent de semestres
|
||||
donné (par ex: 'S2', '3S', '2A').
|
||||
|
||||
Si le RCS est un semestre de type Si, stocke le (ou les)
|
||||
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
|
||||
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
|
||||
|
||||
Pour le RCS de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
|
||||
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
|
||||
terminal de la trajectoire (par ex: ici un S3).
|
||||
|
||||
Ces semestres peuvent être :
|
||||
|
||||
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
|
||||
* des S1+S2+(année de césure)+S3 si césure, ...
|
||||
|
||||
Args:
|
||||
nom_rcs: Un nom du RCS (par ex: '5S')
|
||||
semestre_final: Le semestre final du RCS
|
||||
"""
|
||||
self.nom = nom_rcs
|
||||
"""Nom du RCS"""
|
||||
|
||||
|
@ -121,21 +133,22 @@ class RCS:
|
|||
semestre = self.semestres_aggreges[fid]
|
||||
noms.append(f"S{semestre.semestre_id}({fid})")
|
||||
noms = sorted(noms)
|
||||
repr = f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"
|
||||
title = f"""{self.nom} ({
|
||||
self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"""
|
||||
if verbose and noms:
|
||||
repr += " - " + "+".join(noms)
|
||||
return repr
|
||||
title += " - " + "+".join(noms)
|
||||
return title
|
||||
|
||||
|
||||
class RCSsJuryPE:
|
||||
"""Classe centralisant toutes les regroupements cohérents de
|
||||
semestres (RCS) des étudiants à prendre en compte dans un jury PE
|
||||
|
||||
Args:
|
||||
annee_diplome: L'année de diplomation
|
||||
"""
|
||||
|
||||
def __init__(self, annee_diplome: int):
|
||||
"""Classe centralisant toutes les regroupements cohérents de
|
||||
semestres (RCS) des étudiants à prendre en compte dans un jury PE
|
||||
|
||||
Args:
|
||||
annee_diplome: L'année de diplomation
|
||||
"""
|
||||
|
||||
self.annee_diplome = annee_diplome
|
||||
"""Année de diplômation"""
|
||||
|
||||
|
@ -155,7 +168,8 @@ class RCSsJuryPE:
|
|||
"""
|
||||
|
||||
for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM:
|
||||
"""L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)"""
|
||||
# L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre
|
||||
# terminal (par ex: S3) et son numéro (par ex: 3)
|
||||
noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"]
|
||||
nom_semestre_terminal = noms_semestre_de_aggregat[-1]
|
||||
|
||||
|
@ -164,17 +178,17 @@ class RCSsJuryPE:
|
|||
self.suivi[etudid] = {
|
||||
aggregat: None
|
||||
for aggregat in pe_comp.TOUS_LES_SEMESTRES
|
||||
+ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
|
||||
+ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
|
||||
}
|
||||
|
||||
"""Le formsemestre terminal (dernier en date) associé au
|
||||
semestre marquant la fin de l'aggrégat
|
||||
(par ex: son dernier S3 en date)"""
|
||||
# Le formsemestre terminal (dernier en date) associé au
|
||||
# semestre marquant la fin de l'aggrégat
|
||||
# (par ex: son dernier S3 en date)
|
||||
semestres = etudiants.cursus[etudid][nom_semestre_terminal]
|
||||
if semestres:
|
||||
formsemestre_final = get_dernier_semestre_en_date(semestres)
|
||||
|
||||
"""Ajout ou récupération de la trajectoire"""
|
||||
# Ajout ou récupération de la trajectoire
|
||||
trajectoire_id = (nom_rcs, formsemestre_final.formsemestre_id)
|
||||
if trajectoire_id not in self.rcss:
|
||||
trajectoire = RCS(nom_rcs, formsemestre_final)
|
||||
|
@ -182,21 +196,22 @@ class RCSsJuryPE:
|
|||
else:
|
||||
trajectoire = self.rcss[trajectoire_id]
|
||||
|
||||
"""La liste des semestres de l'étudiant à prendre en compte
|
||||
pour cette trajectoire"""
|
||||
# La liste des semestres de l'étudiant à prendre en compte
|
||||
# pour cette trajectoire
|
||||
semestres_a_aggreger = get_rcs_etudiant(
|
||||
etudiants.cursus[etudid], formsemestre_final, nom_rcs
|
||||
)
|
||||
|
||||
"""Ajout des semestres à la trajectoire"""
|
||||
# Ajout des semestres à la trajectoire
|
||||
trajectoire.add_semestres_a_aggreger(semestres_a_aggreger)
|
||||
|
||||
"""Mémoire la trajectoire suivie par l'étudiant"""
|
||||
# Mémoire la trajectoire suivie par l'étudiant
|
||||
self.suivi[etudid][nom_rcs] = trajectoire
|
||||
|
||||
|
||||
def get_rcs_etudiant(semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
|
||||
):
|
||||
def get_rcs_etudiant(
|
||||
semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
|
||||
) -> dict[int, FormSemestre]:
|
||||
"""Ensemble des semestres parcourus par un étudiant, connaissant
|
||||
les semestres de son cursus,
|
||||
dans le cadre du RCS visé et ayant pour semestre terminal `formsemestre_final`.
|
||||
|
@ -224,7 +239,7 @@ def get_rcs_etudiant(semestres: dict[int:FormSemestre], formsemestre_final: Form
|
|||
numero_semestre_terminal = formsemestre_final.semestre_id
|
||||
# semestres_significatifs = self.get_semestres_significatifs(etudid)
|
||||
semestres_significatifs = {}
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT+1):
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
||||
semestres_significatifs = semestres_significatifs | semestres[f"S{i}"]
|
||||
|
||||
if nom_rcs.startswith("S"): # les semestres
|
||||
|
@ -247,6 +262,7 @@ def get_rcs_etudiant(semestres: dict[int:FormSemestre], formsemestre_final: Form
|
|||
semestres_aggreges[fid] = semestre
|
||||
return semestres_aggreges
|
||||
|
||||
|
||||
def get_descr_rcs(nom_rcs: str) -> str:
|
||||
"""Renvoie la description pour les tableurs de synthèse
|
||||
Excel d'un nom de RCS"""
|
||||
|
|
|
@ -35,33 +35,30 @@ Created on Fri Sep 9 09:15:05 2016
|
|||
|
||||
@author: barasc
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import app.pe.pe_etudiant
|
||||
from app import db, log, ScoValueError
|
||||
from app.comp import res_sem, moy_ue, moy_sem
|
||||
from app.comp.moy_sem import comp_ranks_series
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app import db, ScoValueError
|
||||
from app import comp
|
||||
from app.comp.res_sem import load_formsemestre_results
|
||||
from app.models import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES, MoyenneTag
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class SemestreTag(TableTag):
|
||||
"""
|
||||
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
|
||||
accès aux moyennes par tag.
|
||||
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
|
||||
"""
|
||||
|
||||
def __init__(self, formsemestre_id: int):
|
||||
"""
|
||||
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
|
||||
accès aux moyennes par tag.
|
||||
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
|
||||
|
||||
Args:
|
||||
nom: Nom à donner au SemestreTag
|
||||
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
|
||||
"""
|
||||
TableTag.__init__(self)
|
||||
|
@ -103,27 +100,27 @@ class SemestreTag(TableTag):
|
|||
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
|
||||
noms_tags_comp = list(set(dict_ues_competences.values()))
|
||||
noms_tags_auto = ["but"] + noms_tags_comp
|
||||
self.tags = (
|
||||
noms_tags_perso + noms_tags_auto
|
||||
)
|
||||
self.tags = noms_tags_perso + noms_tags_auto
|
||||
"""Tags du semestre taggué"""
|
||||
|
||||
## Vérifie l'unicité des tags
|
||||
if len(set(self.tags)) != len(self.tags):
|
||||
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
|
||||
liste_intersection = "\n".join([f"<li><code>{tag}</code></li>" for tag in intersection])
|
||||
message = f"""Erreur dans le module PE : Un des tags saisis dans votre programme de formation
|
||||
fait parti des tags réservés. En particulier,
|
||||
votre semestre <em>{self.formsemestre.titre_annee()}</em>
|
||||
contient le(s) tag(s) réservé(s) suivant :
|
||||
liste_intersection = "\n".join(
|
||||
[f"<li><code>{tag}</code></li>" for tag in intersection]
|
||||
)
|
||||
s = "s" if len(intersection) > 0 else ""
|
||||
message = f"""Erreur dans le module PE : Un des tags saisis dans votre
|
||||
programme de formation fait parti des tags réservés. En particulier,
|
||||
votre semestre <em>{self.formsemestre.titre_annee()}</em>
|
||||
contient le{s} tag{s} réservé{s} suivant :
|
||||
<ul>
|
||||
{liste_intersection}
|
||||
</ul>
|
||||
Modifiez votre programme de formation pour le(s) supprimer. Il(s) sera(ont) automatiquement à vos documents de poursuites d'études.
|
||||
Modifiez votre programme de formation pour le{s} supprimer.
|
||||
Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études.
|
||||
"""
|
||||
raise ScoValueError(
|
||||
message
|
||||
)
|
||||
raise ScoValueError(message)
|
||||
|
||||
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
|
||||
self.moyennes_tags = {}
|
||||
|
@ -174,25 +171,25 @@ class SemestreTag(TableTag):
|
|||
La série des moyennes
|
||||
"""
|
||||
|
||||
"""Adaptation du mask de calcul des moyennes au tag visé"""
|
||||
# Adaptation du mask de calcul des moyennes au tag visé
|
||||
modimpls_mask = [
|
||||
modimpl.module.ue.type != UE_SPORT
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
]
|
||||
|
||||
"""Désactive tous les modules qui ne sont pas pris en compte pour ce tag"""
|
||||
# Désactive tous les modules qui ne sont pas pris en compte pour ce tag
|
||||
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
|
||||
if modimpl.moduleimpl_id not in tags_infos[tag]:
|
||||
modimpls_mask[i] = False
|
||||
|
||||
"""Applique la pondération des coefficients"""
|
||||
# Applique la pondération des coefficients
|
||||
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
|
||||
for modimpl_id in tags_infos[tag]:
|
||||
ponderation = tags_infos[tag][modimpl_id]["ponderation"]
|
||||
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
|
||||
|
||||
"""Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)"""
|
||||
moyennes_ues_tag = moy_ue.compute_ue_moys_apc(
|
||||
# Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)#
|
||||
moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.formsemestre.modimpls_sorted,
|
||||
|
@ -203,13 +200,13 @@ class SemestreTag(TableTag):
|
|||
block=self.formsemestre.block_moyennes,
|
||||
)
|
||||
|
||||
"""Les ects"""
|
||||
# Les ects
|
||||
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
|
||||
ue.ects for ue in self.ues if ue.type != UE_SPORT
|
||||
]
|
||||
|
||||
"""Calcule la moyenne générale dans le semestre (pondérée par le ECTS)"""
|
||||
moy_gen_tag = moy_sem.compute_sem_moys_apc_using_ects(
|
||||
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
|
||||
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
|
||||
moyennes_ues_tag,
|
||||
ects,
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
|
@ -224,11 +221,6 @@ def get_moduleimpl(modimpl_id) -> dict:
|
|||
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
||||
if modimpl:
|
||||
return modimpl
|
||||
if SemestreTag.DEBUG:
|
||||
log(
|
||||
"SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas"
|
||||
% (modimpl_id)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
|
@ -260,27 +252,26 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
|
|||
"""
|
||||
synthese_tags = {}
|
||||
|
||||
"""Instance des modules du semestre"""
|
||||
# Instance des modules du semestre
|
||||
modimpls = formsemestre.modimpls_sorted
|
||||
|
||||
for modimpl in modimpls:
|
||||
modimpl_id = modimpl.id
|
||||
|
||||
"""Liste des tags pour le module concerné"""
|
||||
# Liste des tags pour le module concerné
|
||||
tags = sco_tag_module.module_tag_list(modimpl.module.id)
|
||||
|
||||
"""Traitement des tags recensés, chacun pouvant étant de la forme
|
||||
"mathématiques", "théorie", "pe:0", "maths:2"
|
||||
"""
|
||||
# Traitement des tags recensés, chacun pouvant étant de la forme
|
||||
# "mathématiques", "théorie", "pe:0", "maths:2"
|
||||
for tag in tags:
|
||||
"""Extraction du nom du tag et du coeff de pondération"""
|
||||
# Extraction du nom du tag et du coeff de pondération
|
||||
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
|
||||
|
||||
"""Ajout d'une clé pour le tag"""
|
||||
# Ajout d'une clé pour le tag
|
||||
if tagname not in synthese_tags:
|
||||
synthese_tags[tagname] = {}
|
||||
|
||||
"""Ajout du module (modimpl) au tagname considéré"""
|
||||
# Ajout du module (modimpl) au tagname considéré
|
||||
synthese_tags[tagname][modimpl_id] = {
|
||||
"modimpl": modimpl, # les données sur le module
|
||||
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
|
||||
|
@ -298,6 +289,8 @@ def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
|
|||
"""Partant d'un formsemestre, extrait le nom des compétences associés
|
||||
à (ou aux) parcours des étudiants du formsemestre.
|
||||
|
||||
Ignore les UEs non associées à un niveau de compétence.
|
||||
|
||||
Args:
|
||||
formsemestre: Un FormSemestre
|
||||
|
||||
|
@ -310,8 +303,8 @@ def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
|
|||
|
||||
noms_competences = {}
|
||||
for ue in nt.ues:
|
||||
if ue.type != UE_SPORT:
|
||||
ordre = ue.niveau_competence.ordre
|
||||
if ue.niveau_competence and ue.type != UE_SPORT:
|
||||
# ?? inutilisé ordre = ue.niveau_competence.ordre
|
||||
nom = ue.niveau_competence.competence.titre
|
||||
noms_competences[ue.ue_id] = f"comp. {nom}"
|
||||
return noms_competences
|
||||
|
|
|
@ -102,11 +102,3 @@ def pe_view_sem_recap(formsemestre_id: int):
|
|||
download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"),
|
||||
as_attachment=True,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"pe/pe_view_sem_recap.j2",
|
||||
annee_diplome=annee_diplome,
|
||||
formsemestre=formsemestre,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
cosemestres=cosemestres,
|
||||
)
|
||||
|
|
|
@ -396,7 +396,7 @@ class TF(object):
|
|||
self.values[field] = int(self.values[field])
|
||||
except ValueError:
|
||||
msg.append(
|
||||
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
||||
f"valeur invalide ({self.values[field]}) pour le champ {field}"
|
||||
)
|
||||
ok = False
|
||||
elif typ == "float" or typ == "real":
|
||||
|
@ -404,7 +404,7 @@ class TF(object):
|
|||
self.values[field] = float(self.values[field].replace(",", "."))
|
||||
except ValueError:
|
||||
msg.append(
|
||||
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
||||
f"valeur invalide ({self.values[field]}) pour le champ {field}"
|
||||
)
|
||||
ok = False
|
||||
if ok:
|
||||
|
|
|
@ -265,7 +265,7 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu
|
|||
# log('vals=%s\n'%vals)
|
||||
except psycopg2.errors.StringDataRightTruncation as exc:
|
||||
cnx.rollback()
|
||||
raise ScoValueError("champs de texte trop long !") from exc
|
||||
raise ScoValueError("champ de texte trop long !") from exc
|
||||
except:
|
||||
cnx.rollback() # get rid of this transaction
|
||||
log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals))
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
"""
|
||||
Ecrit par Matthias Hartmann.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from pytz import UTC
|
||||
|
||||
from flask import g
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import log, db, set_sco_dept
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models import (
|
||||
Identite,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
ModuleImpl,
|
||||
ModuleImplInscription,
|
||||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
from app.models import ScoDocSiteConfig
|
||||
from flask import g
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class CountCalculator:
|
||||
|
@ -93,9 +99,9 @@ class CountCalculator:
|
|||
evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00")
|
||||
)
|
||||
|
||||
self.non_work_days: list[
|
||||
scu.NonWorkDays
|
||||
] = scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
self.non_work_days: list[scu.NonWorkDays] = (
|
||||
scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
delta_total: timedelta = datetime.combine(
|
||||
date.min, self.evening
|
||||
|
@ -371,6 +377,10 @@ def get_assiduites_stats(
|
|||
assiduites = filter_by_formsemestre(
|
||||
assiduites, Assiduite, filtered[key]
|
||||
)
|
||||
case "formsemestre_modimpls":
|
||||
assiduites = filter_by_modimpls(
|
||||
assiduites, Assiduite, filtered[key]
|
||||
)
|
||||
case "est_just":
|
||||
assiduites = filter_assiduites_by_est_just(
|
||||
assiduites, filtered[key]
|
||||
|
@ -489,7 +499,9 @@ def filter_by_formsemestre(
|
|||
formsemestre: FormSemestre,
|
||||
) -> Query:
|
||||
"""
|
||||
Filtrage d'une collection en fonction d'un formsemestre
|
||||
Filtrage d'une collection : conserve les élements tels que
|
||||
- l'étudiant est inscrit au formsemestre
|
||||
- et la plage de dates intersecte celle du formsemestre
|
||||
"""
|
||||
|
||||
if formsemestre is None:
|
||||
|
@ -504,14 +516,46 @@ def filter_by_formsemestre(
|
|||
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||
)
|
||||
|
||||
form_date_debut = formsemestre.date_debut + timedelta(days=1)
|
||||
form_date_fin = formsemestre.date_fin + timedelta(days=1)
|
||||
|
||||
collection_result = collection_result.filter(
|
||||
collection_class.date_debut >= form_date_debut
|
||||
collection_class.date_debut <= formsemestre.date_fin
|
||||
).filter(collection_class.date_fin >= formsemestre.date_debut)
|
||||
|
||||
return collection_result
|
||||
|
||||
|
||||
def filter_by_modimpls(
|
||||
collection_query: Assiduite | Justificatif,
|
||||
collection_class: Assiduite | Justificatif,
|
||||
formsemestre: FormSemestre,
|
||||
) -> Query:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduités: conserve les élements
|
||||
- si l'étudiant est inscrit au formsemestre
|
||||
- Et que l'assiduité concerne un moduleimpl de ce formsemestre
|
||||
|
||||
Ne fait rien sur les justificatifs.
|
||||
Ne fait rien si formsemestre is None
|
||||
"""
|
||||
if (collection_class != Assiduite) or (formsemestre is None):
|
||||
return collection_query
|
||||
|
||||
# restreint aux inscrits:
|
||||
collection_result = (
|
||||
collection_query.join(Identite, collection_class.etudid == Identite.id)
|
||||
.join(
|
||||
FormSemestreInscription,
|
||||
Identite.id == FormSemestreInscription.etudid,
|
||||
)
|
||||
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||
)
|
||||
|
||||
return collection_result.filter(collection_class.date_fin <= form_date_fin)
|
||||
collection_result = (
|
||||
collection_result.join(ModuleImpl)
|
||||
.join(ModuleImplInscription)
|
||||
.filter(ModuleImplInscription.etudid == collection_class.etudid)
|
||||
)
|
||||
|
||||
return collection_result
|
||||
|
||||
|
||||
def justifies(justi: Justificatif, obj: bool = False) -> list[int] | Query:
|
||||
|
|
|
@ -166,9 +166,9 @@ def process_field(
|
|||
values={pprint.pformat(cdict)}
|
||||
"""
|
||||
)
|
||||
text = f"""<para><i>format invalide: champs</i> {missing_key} <i>inexistant !</i></para>"""
|
||||
text = f"""<para><i>format invalide: champ</i> {missing_key} <i>inexistant !</i></para>"""
|
||||
scu.flash_once(
|
||||
f"Attention: format PDF invalide (champs {field}, clef {missing_key})"
|
||||
f"Attention: format PDF invalide (champ {field}, clef {missing_key})"
|
||||
)
|
||||
raise
|
||||
except: # pylint: disable=bare-except
|
||||
|
|
|
@ -126,53 +126,59 @@ def html_edit_formation_apc(
|
|||
UniteEns.type != codes_cursus.UE_SPORT,
|
||||
).first()
|
||||
H += [
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Ressources du S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle ressource",
|
||||
# matiere_parent=matiere_parent,
|
||||
modules=ressources_in_sem,
|
||||
module_type=ModuleType.RESSOURCE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else "",
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle SAÉ",
|
||||
# matiere_parent=matiere_parent,
|
||||
modules=saes_in_sem,
|
||||
module_type=ModuleType.SAE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else "",
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Autres modules (non BUT) du S{semestre_idx}",
|
||||
create_element_msg="créer un nouveau module",
|
||||
modules=other_modules_in_sem,
|
||||
module_type=ModuleType.STANDARD,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>""",
|
||||
(
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Ressources du S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle ressource",
|
||||
# matiere_parent=matiere_parent,
|
||||
modules=ressources_in_sem,
|
||||
module_type=ModuleType.RESSOURCE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else ""
|
||||
),
|
||||
(
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle SAÉ",
|
||||
# matiere_parent=matiere_parent,
|
||||
modules=saes_in_sem,
|
||||
module_type=ModuleType.SAE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else ""
|
||||
),
|
||||
(
|
||||
render_template(
|
||||
"pn/form_mods.j2",
|
||||
formation=formation,
|
||||
titre=f"Autres modules (non BUT) du S{semestre_idx}",
|
||||
create_element_msg="créer un nouveau module",
|
||||
modules=other_modules_in_sem,
|
||||
module_type=ModuleType.STANDARD,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
semestre_id=semestre_idx,
|
||||
)
|
||||
if ues_by_sem[semestre_idx].count() > 0
|
||||
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>"""
|
||||
),
|
||||
]
|
||||
|
||||
return "\n".join(H)
|
||||
|
@ -202,7 +208,7 @@ def html_ue_infos(ue):
|
|||
)
|
||||
return render_template(
|
||||
"pn/ue_infos.j2",
|
||||
titre=f"UE {ue.acronyme} {ue.titre}",
|
||||
titre=f"UE {ue.acronyme} {ue.titre or ''}",
|
||||
ue=ue,
|
||||
formsemestres=formsemestres,
|
||||
nb_etuds_valid_ue=nb_etuds_valid_ue,
|
||||
|
|
|
@ -104,7 +104,7 @@ def matiere_create(ue_id=None):
|
|||
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Création d'une matière"),
|
||||
f"""<h2>Création d'une matière dans l'UE {ue.titre} ({ue.acronyme})</h2>
|
||||
f"""<h2>Création d'une matière dans l'UE {ue.titre or ''} ({ue.acronyme})</h2>
|
||||
<p class="help">Les matières sont des groupes de modules dans une UE
|
||||
d'une formation donnée. Les matières servent surtout pour la
|
||||
présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul
|
||||
|
|
|
@ -85,7 +85,7 @@ _moduleEditor = ndb.EditableTable(
|
|||
"heures_tp": ndb.float_null_is_zero,
|
||||
"numero": ndb.int_null_is_zero,
|
||||
"coefficient": ndb.float_null_is_zero,
|
||||
"module_type": ndb.int_null_is_zero
|
||||
"module_type": ndb.int_null_is_zero,
|
||||
#'ects' : ndb.float_null_is_null
|
||||
},
|
||||
)
|
||||
|
@ -387,14 +387,16 @@ def module_edit(
|
|||
"scodoc/help/modules.j2",
|
||||
is_apc=is_apc,
|
||||
semestre_id=semestre_id,
|
||||
formsemestres=FormSemestre.query.filter(
|
||||
ModuleImpl.formsemestre_id == FormSemestre.id,
|
||||
ModuleImpl.module_id == module_id,
|
||||
)
|
||||
.order_by(FormSemestre.date_debut)
|
||||
.all()
|
||||
if not create
|
||||
else None,
|
||||
formsemestres=(
|
||||
FormSemestre.query.filter(
|
||||
ModuleImpl.formsemestre_id == FormSemestre.id,
|
||||
ModuleImpl.module_id == module_id,
|
||||
)
|
||||
.order_by(FormSemestre.date_debut)
|
||||
.all()
|
||||
if not create
|
||||
else None
|
||||
),
|
||||
create=create,
|
||||
),
|
||||
]
|
||||
|
@ -413,9 +415,11 @@ def module_edit(
|
|||
}
|
||||
if module:
|
||||
module_types |= {
|
||||
scu.ModuleType(module.module_type)
|
||||
if module.module_type
|
||||
else scu.ModuleType.STANDARD
|
||||
(
|
||||
scu.ModuleType(module.module_type)
|
||||
if module.module_type
|
||||
else scu.ModuleType.STANDARD
|
||||
)
|
||||
}
|
||||
# Numéro du module
|
||||
# cherche le numero adéquat (pour placer le module en fin de liste)
|
||||
|
@ -571,15 +575,17 @@ def module_edit(
|
|||
"input_type": "menu",
|
||||
"title": "Rattachement :" if is_apc else "Matière :",
|
||||
"explanation": (
|
||||
"UE de rattachement, utilisée notamment pour les malus"
|
||||
+ (
|
||||
" (module utilisé, ne peut pas être changé de semestre)"
|
||||
if in_use
|
||||
else ""
|
||||
(
|
||||
"UE de rattachement, utilisée notamment pour les malus"
|
||||
+ (
|
||||
" (module utilisé, ne peut pas être changé de semestre)"
|
||||
if in_use
|
||||
else ""
|
||||
)
|
||||
)
|
||||
)
|
||||
if is_apc
|
||||
else "un module appartient à une seule matière.",
|
||||
if is_apc
|
||||
else "un module appartient à une seule matière."
|
||||
),
|
||||
"labels": mat_names,
|
||||
"allowed_values": ue_mat_ids,
|
||||
"enabled": unlocked,
|
||||
|
@ -733,7 +739,7 @@ def module_edit(
|
|||
"title": f"""<span class="fontred">{scu.EMO_WARNING }
|
||||
L'UE <a class="stdlink" href="{
|
||||
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
||||
}">{ue.acronyme} {ue.titre}</a>
|
||||
}">{ue.acronyme} {ue.titre or ''}</a>
|
||||
n'est pas associée à un niveau de compétences
|
||||
</span>""",
|
||||
},
|
||||
|
@ -766,12 +772,14 @@ def module_edit(
|
|||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
descr,
|
||||
html_foot_markup=f"""<div class="sco_tag_module_edit"><span
|
||||
html_foot_markup=(
|
||||
f"""<div class="sco_tag_module_edit"><span
|
||||
class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
|
||||
>{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
|
||||
"""
|
||||
if not create
|
||||
else "",
|
||||
if not create
|
||||
else ""
|
||||
),
|
||||
initvalues=module_dict if module else {},
|
||||
submitlabel="Modifier ce module" if module else "Créer ce module",
|
||||
cancelbutton="Annuler",
|
||||
|
@ -814,7 +822,7 @@ def module_edit(
|
|||
tf[2]["matiere_id"] = matiere.id
|
||||
else:
|
||||
matiere_id = sco_edit_matiere.do_matiere_create(
|
||||
{"ue_id": ue.id, "titre": ue.titre, "numero": 1},
|
||||
{"ue_id": ue.id, "titre": ue.titre or "", "numero": 1},
|
||||
)
|
||||
tf[2]["matiere_id"] = matiere_id
|
||||
|
||||
|
|
|
@ -133,7 +133,15 @@ def do_ue_create(args, allow_empty_ue_code=False):
|
|||
break
|
||||
args["ue_code"] = code
|
||||
|
||||
# last checks
|
||||
if not args.get("acronyme"):
|
||||
raise ScoValueError("acronyme vide")
|
||||
args["coefficient"] = args.get("coefficient", None)
|
||||
if args["coefficient"] == "":
|
||||
args["coefficient"] = None
|
||||
|
||||
# create
|
||||
# XXX TODO utiliser UniteEns.create_from_dict
|
||||
ue_id = _ueEditor.create(cnx, args)
|
||||
log(f"do_ue_create: created {ue_id} with {args}")
|
||||
|
||||
|
@ -159,7 +167,7 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
|
|||
if not ue.can_be_deleted():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
f"UE (id={ue.id}, dud)",
|
||||
msg=ue.titre,
|
||||
msg=f"{ue.titre or ''} ({ue.acronyme})",
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -303,11 +311,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
|
||||
Seuls les <em>modules</em> ont des coefficients.
|
||||
</p>""",
|
||||
f"""
|
||||
(
|
||||
f"""
|
||||
<h4>UE du semestre S{ue.semestre_idx}</h4>
|
||||
"""
|
||||
if is_apc and ue
|
||||
else "",
|
||||
if is_apc and ue
|
||||
else ""
|
||||
),
|
||||
]
|
||||
|
||||
ue_types = cursus.ALLOWED_UE_TYPES
|
||||
|
@ -629,8 +639,8 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
|||
)
|
||||
if not ue.can_be_deleted():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
f"UE",
|
||||
msg=ue.titre,
|
||||
"UE",
|
||||
msg=f"{ue.titre or ''} ({ue.acronyme})",
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -641,7 +651,7 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
|||
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
f"<h2>Suppression de l'UE {ue.titre} ({ue.acronyme})</h2>",
|
||||
f"<h2>Suppression de l'UE {ue.titre or ''} ({ue.acronyme})</h2>",
|
||||
dest_url="",
|
||||
parameters={"ue_id": ue.id},
|
||||
cancel_url=url_for(
|
||||
|
@ -1442,7 +1452,7 @@ def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None
|
|||
H.append("<ul>")
|
||||
for ue in ues:
|
||||
H.append(
|
||||
f"""<li>{ue.acronyme} ({ue.titre}) dans
|
||||
f"""<li>{ue.acronyme} ({ue.titre or ''}) dans
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
|
||||
|
|
|
@ -283,7 +283,7 @@ def evaluation_create_form(
|
|||
"coef. mod.:" +str(coef_ue) if coef_ue
|
||||
else "ce module n'a pas de coef. dans cette UE"
|
||||
})</span>
|
||||
<span class="eval_coef_ue_titre">{ue.titre}</span>
|
||||
<span class="eval_coef_ue_titre">{ue.titre or ''}</span>
|
||||
""",
|
||||
"allow_null": False,
|
||||
# ok si poids nul ou coef vers l'UE nul:
|
||||
|
|
|
@ -258,13 +258,17 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
|
|||
submitlabel="Enregistrer ces validations",
|
||||
cancelbutton="Annuler",
|
||||
initvalues=initvalues,
|
||||
cssclass="tf_ext_edit_ue_validations ext_apc"
|
||||
if formsemestre.formation.is_apc()
|
||||
else "tf_ext_edit_ue_validations",
|
||||
cssclass=(
|
||||
"tf_ext_edit_ue_validations ext_apc"
|
||||
if formsemestre.formation.is_apc()
|
||||
else "tf_ext_edit_ue_validations"
|
||||
),
|
||||
# En APC, stocke les coefficients pour l'affichage de la moyenne en direct
|
||||
form_attrs=f"""data-ue_coefs='[{', '.join(str(ue.ects or 0) for ue in ues)}]'"""
|
||||
if formsemestre.formation.is_apc()
|
||||
else "",
|
||||
form_attrs=(
|
||||
f"""data-ue_coefs='[{', '.join(str(ue.ects or 0) for ue in ues)}]'"""
|
||||
if formsemestre.formation.is_apc()
|
||||
else ""
|
||||
),
|
||||
)
|
||||
if tf[0] == -1:
|
||||
return "<h4>annulation</h4>"
|
||||
|
@ -421,12 +425,18 @@ def _ue_form_description(
|
|||
"input_type": "text",
|
||||
"size": 4,
|
||||
"template": itemtemplate,
|
||||
"title": "<tt>"
|
||||
+ (f"S{ue.semestre_idx} " if ue.semestre_idx is not None else "")
|
||||
+ f"<b>{ue.acronyme}</b></tt> {ue.titre}"
|
||||
+ f" ({ue.ects} ECTS)"
|
||||
if ue.ects is not None
|
||||
else "",
|
||||
"title": (
|
||||
"<tt>"
|
||||
+ (
|
||||
f"S{ue.semestre_idx} "
|
||||
if ue.semestre_idx is not None
|
||||
else ""
|
||||
)
|
||||
+ f"<b>{ue.acronyme}</b></tt> {ue.titre or ''}"
|
||||
+ f" ({ue.ects} ECTS)"
|
||||
if ue.ects is not None
|
||||
else ""
|
||||
),
|
||||
"attributes": [coef_disabled],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -281,9 +281,11 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
|||
|
||||
menu_inscriptions = [
|
||||
{
|
||||
"title": "Gérer les inscriptions aux UE et modules"
|
||||
if formsemestre.formation.is_apc()
|
||||
else "Gérer les inscriptions aux modules",
|
||||
"title": (
|
||||
"Gérer les inscriptions aux UE et modules"
|
||||
if formsemestre.formation.is_apc()
|
||||
else "Gérer les inscriptions aux modules"
|
||||
),
|
||||
"endpoint": "notes.moduleimpl_inscriptions_stats",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
}
|
||||
|
@ -619,9 +621,9 @@ def formsemestre_description_table(
|
|||
if ue.color:
|
||||
for k in list(ue_info.keys()):
|
||||
if not k.startswith("_"):
|
||||
ue_info[
|
||||
f"_{k}_td_attrs"
|
||||
] = f'style="background-color: {ue.color} !important;"'
|
||||
ue_info[f"_{k}_td_attrs"] = (
|
||||
f'style="background-color: {ue.color} !important;"'
|
||||
)
|
||||
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
|
||||
|
@ -1050,9 +1052,11 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
|||
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
|
||||
),
|
||||
formsemestre_warning_apc_setup(formsemestre, nt),
|
||||
formsemestre_warning_etuds_sans_note(formsemestre, nt)
|
||||
if can_change_all_notes
|
||||
else "",
|
||||
(
|
||||
formsemestre_warning_etuds_sans_note(formsemestre, nt)
|
||||
if can_change_all_notes
|
||||
else ""
|
||||
),
|
||||
"""<p style="font-size: 130%"><b>Tableau de bord : </b>""",
|
||||
]
|
||||
if formsemestre.est_courant():
|
||||
|
@ -1226,7 +1230,7 @@ def formsemestre_tableau_modules(
|
|||
ue = modimpl.module.ue
|
||||
if show_ues and (prev_ue_id != ue.id):
|
||||
prev_ue_id = ue.id
|
||||
titre = ue.titre
|
||||
titre = ue.titre or ""
|
||||
if use_ue_coefs:
|
||||
titre += f""" <b>(coef. {ue.coefficient or 0.0})</b>"""
|
||||
H.append(
|
||||
|
|
|
@ -728,7 +728,9 @@ def formsemestre_recap_parcours_table(
|
|||
)
|
||||
# Dispense BUT ?
|
||||
if (etudid, ue.id) in nt.dispense_ues:
|
||||
moy_ue_txt = "❎" if (ue_status and ue_status["is_capitalized"]) else "⭕"
|
||||
moy_ue_txt = (
|
||||
"❎" if (ue_status and ue_status["is_capitalized"]) else "⭕"
|
||||
)
|
||||
explanation_ue.append("non inscrit (dispense)")
|
||||
else:
|
||||
moy_ue_txt = scu.fmt_note(moy_ue)
|
||||
|
@ -1098,7 +1100,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
|||
|
||||
ue_names = ["Choisir..."] + [
|
||||
f"""{('S'+str(ue.semestre_idx)+' : ') if ue.semestre_idx is not None else ''
|
||||
}{ue.acronyme} {ue.titre} ({ue.ue_code or ""})"""
|
||||
}{ue.acronyme} {ue.titre or ''} ({ue.ue_code or ""})"""
|
||||
for ue in ues
|
||||
]
|
||||
ue_ids = [""] + [ue.id for ue in ues]
|
||||
|
|
|
@ -319,6 +319,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
|||
<tr>
|
||||
{'<th>UE</th>' if not is_apc else ""}
|
||||
<th>Code</th>
|
||||
<th>Module</th>
|
||||
<th>Inscrits</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
@ -348,6 +349,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
|||
<td class="formsemestre_status_code">{
|
||||
modimpl.module.code or "(module sans code)"
|
||||
}</td>
|
||||
<td class="formsemestre_status_module">{modimpl.module.titre or ""}</td>
|
||||
<td class="formsemestre_status_inscrits">{
|
||||
mod_nb_inscrits[modimpl.id]}</td><td>{c_link}</td>
|
||||
</tr>
|
||||
|
|
|
@ -327,34 +327,10 @@ def fiche_etud(etudid=None):
|
|||
info["link_inscrire_ailleurs"] = ""
|
||||
|
||||
# Liste des annotations
|
||||
annotations_list = []
|
||||
annotations = EtudAnnotation.query.filter_by(etudid=etud.id).order_by(
|
||||
sa.desc(EtudAnnotation.date)
|
||||
html_annotations_list = "\n".join(
|
||||
[] if restrict_etud_data else get_html_annotations_list(etud)
|
||||
)
|
||||
for annot in annotations:
|
||||
del_link = (
|
||||
f"""<td class="annodel"><a href="{
|
||||
url_for("scolar.doSuppressAnnotation",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid, annotation_id=annot.id)}">{
|
||||
scu.icontag(
|
||||
"delete_img",
|
||||
border="0",
|
||||
alt="suppress",
|
||||
title="Supprimer cette annotation",
|
||||
)
|
||||
}</a></td>"""
|
||||
if sco_permissions_check.can_suppress_annotation(annot.id)
|
||||
else ""
|
||||
)
|
||||
|
||||
author = User.query.filter_by(user_name=annot.author).first()
|
||||
annotations_list.append(
|
||||
f"""<tr><td><span class="annodate">Le {annot.date.strftime("%d/%m/%Y") if annot.date else "?"}
|
||||
par {author.get_prenomnom() if author else "?"} :
|
||||
</span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
|
||||
"""
|
||||
)
|
||||
info["liste_annotations"] = "\n".join(annotations_list)
|
||||
# fiche admission
|
||||
infos_admission = _infos_admission(etud, restrict_etud_data)
|
||||
has_adm_notes = any(
|
||||
|
@ -442,11 +418,7 @@ def fiche_etud(etudid=None):
|
|||
</div>"""
|
||||
else:
|
||||
info["debouche_html"] = "" # pas de boite "devenir"
|
||||
#
|
||||
if info["liste_annotations"]:
|
||||
info["tit_anno"] = '<div class="fichetitre">Annotations</div>'
|
||||
else:
|
||||
info["tit_anno"] = ""
|
||||
|
||||
# Inscriptions
|
||||
info[
|
||||
"inscriptions_mkup"
|
||||
|
@ -517,7 +489,9 @@ def fiche_etud(etudid=None):
|
|||
)
|
||||
|
||||
info_naissance = (
|
||||
f"""<tr><td class="fichetitre2">Né{etud.e} le :</td><td>{info["info_naissance"]}</td></tr>"""
|
||||
f"""<tr><td class="fichetitre2">Né{etud.e} le :</td>
|
||||
<td>{info["info_naissance"]}</td></tr>
|
||||
"""
|
||||
if info["info_naissance"]
|
||||
else ""
|
||||
)
|
||||
|
@ -538,6 +512,35 @@ def fiche_etud(etudid=None):
|
|||
"""
|
||||
)
|
||||
|
||||
info["annotations_mkup"] = (
|
||||
f"""
|
||||
<div class="ficheannotations">
|
||||
<div class="fichetitre">Annotations</div>
|
||||
<table id="etudannotations">{html_annotations_list}</table>
|
||||
|
||||
<form action="doAddAnnotation" method="GET" class="noprint">
|
||||
<input type="hidden" name="etudid" value="{etudid}">
|
||||
<b>Ajouter une annotation sur {etud.nomprenom}: </b>
|
||||
<div>
|
||||
<textarea name="comment" rows="4" cols="50" value=""></textarea>
|
||||
<div style="font-size: small; font-style: italic;">
|
||||
<div>Ces annotations sont lisibles par tous les utilisateurs ayant la permission
|
||||
<tt>ViewEtudData</tt> dans ce département (souvent les enseignants et le
|
||||
secrétariat).
|
||||
</div>
|
||||
<div>L'annotation commençant par "PE:" est un avis de poursuite d'études.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="author" width=12 value="{current_user}">
|
||||
<input type="submit" value="Ajouter annotation">
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
if not restrict_etud_data
|
||||
else ""
|
||||
)
|
||||
|
||||
tmpl = (
|
||||
"""<div class="menus_etud">%(menus_etud)s</div>
|
||||
<div class="fiche_etud" id="fiche_etud"><table>
|
||||
|
@ -564,27 +567,7 @@ def fiche_etud(etudid=None):
|
|||
|
||||
%(debouche_html)s
|
||||
|
||||
<div class="ficheannotations">
|
||||
%(tit_anno)s
|
||||
<table id="etudannotations">%(liste_annotations)s</table>
|
||||
|
||||
<form action="doAddAnnotation" method="GET" class="noprint">
|
||||
<input type="hidden" name="etudid" value="%(etudid)s">
|
||||
<b>Ajouter une annotation sur %(nomprenom)s: </b>
|
||||
<table><tr>
|
||||
<tr><td><textarea name="comment" rows="4" cols="50" value=""></textarea>
|
||||
<br><font size=-1>
|
||||
<i>Ces annotations sont lisibles par tous les enseignants et le secrétariat.</i>
|
||||
<br>
|
||||
<i>L'annotation commençant par "PE:" est un avis de poursuite d'études.</i>
|
||||
</font>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<input type="hidden" name="author" width=12 value="%(authuser)s">
|
||||
<input type="submit" value="Ajouter annotation"></td></tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
%(annotations_mkup)s
|
||||
|
||||
<div class="code_nip">code NIP: %(code_nip)s</div>
|
||||
|
||||
|
@ -613,31 +596,41 @@ def fiche_etud(etudid=None):
|
|||
def _format_adresse(adresse: Adresse | None) -> dict:
|
||||
"""{ "telephonestr" : ..., "telephonemobilestr" : ... } (formats html)"""
|
||||
d = {
|
||||
"telephonestr": ("<b>Tél.:</b> " + scu.format_telephone(adresse.telephone))
|
||||
if (adresse and adresse.telephone)
|
||||
else "",
|
||||
"telephonestr": (
|
||||
("<b>Tél.:</b> " + scu.format_telephone(adresse.telephone))
|
||||
if (adresse and adresse.telephone)
|
||||
else ""
|
||||
),
|
||||
"telephonemobilestr": (
|
||||
"<b>Mobile:</b> " + scu.format_telephone(adresse.telephonemobile)
|
||||
)
|
||||
if (adresse and adresse.telephonemobile)
|
||||
else "",
|
||||
("<b>Mobile:</b> " + scu.format_telephone(adresse.telephonemobile))
|
||||
if (adresse and adresse.telephonemobile)
|
||||
else ""
|
||||
),
|
||||
# e-mail:
|
||||
"email_link": ", ".join(
|
||||
[
|
||||
f"""<a class="stdlink" href="mailto:{m}">{m}</a>"""
|
||||
for m in [adresse.email, adresse.emailperso]
|
||||
if m
|
||||
]
|
||||
)
|
||||
if adresse and (adresse.email or adresse.emailperso)
|
||||
else "",
|
||||
"domicile": (adresse.domicile or "")
|
||||
if adresse
|
||||
and (adresse.domicile or adresse.codepostaldomicile or adresse.villedomicile)
|
||||
else "<em>inconnue</em>",
|
||||
"paysdomicile": f"{sco_etud.format_pays(adresse.paysdomicile)}"
|
||||
if adresse and adresse.paysdomicile
|
||||
else "",
|
||||
"email_link": (
|
||||
", ".join(
|
||||
[
|
||||
f"""<a class="stdlink" href="mailto:{m}">{m}</a>"""
|
||||
for m in [adresse.email, adresse.emailperso]
|
||||
if m
|
||||
]
|
||||
)
|
||||
if adresse and (adresse.email or adresse.emailperso)
|
||||
else ""
|
||||
),
|
||||
"domicile": (
|
||||
(adresse.domicile or "")
|
||||
if adresse
|
||||
and (
|
||||
adresse.domicile or adresse.codepostaldomicile or adresse.villedomicile
|
||||
)
|
||||
else "<em>inconnue</em>"
|
||||
),
|
||||
"paysdomicile": (
|
||||
f"{sco_etud.format_pays(adresse.paysdomicile)}"
|
||||
if adresse and adresse.paysdomicile
|
||||
else ""
|
||||
),
|
||||
}
|
||||
d["telephones"] = (
|
||||
f"<br>{d['telephonestr']} {d['telephonemobilestr']}"
|
||||
|
@ -680,15 +673,15 @@ def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
|
|||
"info_lycee": info_lycee,
|
||||
"rapporteur": etud.admission.rapporteur if not restrict_etud_data else "",
|
||||
"rap": rap,
|
||||
"commentaire": (etud.admission.commentaire or "")
|
||||
if not restrict_etud_data
|
||||
else "",
|
||||
"classement": (etud.admission.classement or "")
|
||||
if not restrict_etud_data
|
||||
else "",
|
||||
"type_admission": (etud.admission.type_admission or "")
|
||||
if not restrict_etud_data
|
||||
else "",
|
||||
"commentaire": (
|
||||
(etud.admission.commentaire or "") if not restrict_etud_data else ""
|
||||
),
|
||||
"classement": (
|
||||
(etud.admission.classement or "") if not restrict_etud_data else ""
|
||||
),
|
||||
"type_admission": (
|
||||
(etud.admission.type_admission or "") if not restrict_etud_data else ""
|
||||
),
|
||||
"math": (etud.admission.math or "") if not restrict_etud_data else "",
|
||||
"physique": (etud.admission.physique or "") if not restrict_etud_data else "",
|
||||
"anglais": (etud.admission.anglais or "") if not restrict_etud_data else "",
|
||||
|
@ -696,6 +689,39 @@ def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
|
|||
}
|
||||
|
||||
|
||||
def get_html_annotations_list(etud: Identite) -> list[str]:
|
||||
"""Liste de chaînes html décrivant les annotations."""
|
||||
html_annotations_list = []
|
||||
annotations = EtudAnnotation.query.filter_by(etudid=etud.id).order_by(
|
||||
sa.desc(EtudAnnotation.date)
|
||||
)
|
||||
for annot in annotations:
|
||||
del_link = (
|
||||
f"""<td class="annodel"><a href="{
|
||||
url_for("scolar.doSuppressAnnotation",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etud.id, annotation_id=annot.id)}">{
|
||||
scu.icontag(
|
||||
"delete_img",
|
||||
border="0",
|
||||
alt="suppress",
|
||||
title="Supprimer cette annotation",
|
||||
)
|
||||
}</a></td>"""
|
||||
if sco_permissions_check.can_suppress_annotation(annot.id)
|
||||
else ""
|
||||
)
|
||||
|
||||
author = User.query.filter_by(user_name=annot.author).first()
|
||||
html_annotations_list.append(
|
||||
f"""<tr><td><span class="annodate">Le {
|
||||
annot.date.strftime("%d/%m/%Y") if annot.date else "?"}
|
||||
par {author.get_prenomnom() if author else "?"} :
|
||||
</span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
|
||||
"""
|
||||
)
|
||||
return html_annotations_list
|
||||
|
||||
|
||||
def menus_etud(etudid):
|
||||
"""Menu etudiant (operations sur l'etudiant)"""
|
||||
authuser = current_user
|
||||
|
|
|
@ -494,7 +494,7 @@ def _normalize_apo_fields(infolist):
|
|||
infolist: liste de dict renvoyés par le portail Apogee
|
||||
|
||||
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
|
||||
ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?'
|
||||
ajoute le champ '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' (=''), et 'prenom_etat_civil' (='') si non présent.
|
||||
"""
|
||||
|
|
|
@ -1605,7 +1605,7 @@ class BasePreferences:
|
|||
"bul_display_publication",
|
||||
{
|
||||
"initvalue": 1,
|
||||
"title": "Indique si les bulletins sont publiés",
|
||||
"title": "Afficher icône indiquant si les bulletins sont publiés",
|
||||
"explanation": "décocher si vous n'avez pas de portail étudiant publiant les bulletins",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
|
|
|
@ -342,13 +342,15 @@ def _build_page(
|
|||
"\n".join(options),
|
||||
"""</select>
|
||||
""",
|
||||
""
|
||||
if read_only
|
||||
else f"""
|
||||
(
|
||||
""
|
||||
if read_only
|
||||
else f"""
|
||||
<input type="hidden" name="formsemestre_id" value="{sem['formsemestre_id']}"/>
|
||||
<input type="submit" name="submitted" value="Appliquer les modifications"/>
|
||||
<a href="#help">aide</a>
|
||||
""",
|
||||
"""
|
||||
),
|
||||
sco_inscr_passage.etuds_select_boxes(
|
||||
etuds_by_cat,
|
||||
sel_inscrits=False,
|
||||
|
@ -356,9 +358,11 @@ def _build_page(
|
|||
base_url=base_url,
|
||||
read_only=read_only,
|
||||
),
|
||||
""
|
||||
if read_only
|
||||
else """<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>""",
|
||||
(
|
||||
""
|
||||
if read_only
|
||||
else """<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>"""
|
||||
),
|
||||
formsemestre_synchro_etuds_help(sem),
|
||||
"""</form>""",
|
||||
]
|
||||
|
@ -420,9 +424,9 @@ def list_synch(sem, annee_apogee=None):
|
|||
log(f"XXX key2etud etudid={etudid}, type {type(etudid)}")
|
||||
etud = etuds[0]
|
||||
etud["inscrit"] = is_inscrit # checkbox state
|
||||
etud[
|
||||
"datefinalisationinscription"
|
||||
] = date_finalisation_inscr_by_nip.get(key, None)
|
||||
etud["datefinalisationinscription"] = (
|
||||
date_finalisation_inscr_by_nip.get(key, None)
|
||||
)
|
||||
if key in etudsapo_ident:
|
||||
etud["etape"] = etudsapo_ident[key].get("etape", "")
|
||||
else:
|
||||
|
@ -855,7 +859,7 @@ def formsemestre_import_etud_admission(
|
|||
if import_email:
|
||||
if not "mail" in data_apo:
|
||||
raise ScoValueError(
|
||||
"la réponse portail n'a pas le champs requis 'mail'"
|
||||
"la réponse portail n'a pas le champ requis 'mail'"
|
||||
)
|
||||
if (
|
||||
adresse.email != data_apo["mail"]
|
||||
|
|
|
@ -870,7 +870,7 @@ def stripquotes(s):
|
|||
return s
|
||||
|
||||
|
||||
def suppress_accents(s):
|
||||
def suppress_accents(s: str) -> str:
|
||||
"remove accents and suppress non ascii characters from string s"
|
||||
if isinstance(s, str):
|
||||
return (
|
||||
|
@ -1018,7 +1018,7 @@ def flash_errors(form):
|
|||
"""Flashes form errors (version sommaire)"""
|
||||
for field, errors in form.errors.items():
|
||||
flash(
|
||||
"Erreur: voir le champs %s" % (getattr(form, field).label.text,),
|
||||
"Erreur: voir le champ %s" % (getattr(form, field).label.text,),
|
||||
"warning",
|
||||
)
|
||||
# see https://getbootstrap.com/docs/4.0/components/alerts/
|
||||
|
|
|
@ -401,9 +401,11 @@ class RowAssiJusti(tb.Row):
|
|||
self.add_cell(
|
||||
"entry_date",
|
||||
"Saisie le",
|
||||
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M")
|
||||
if self.ligne["entry_date"]
|
||||
else "?",
|
||||
(
|
||||
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M")
|
||||
if self.ligne["entry_date"]
|
||||
else "?"
|
||||
),
|
||||
data={"order": self.ligne["entry_date"] or ""},
|
||||
raw_content=self.ligne["entry_date"],
|
||||
classes=["small-font"],
|
||||
|
|
|
@ -682,9 +682,9 @@ class RowRecap(tb.Row):
|
|||
self.add_ue_modimpls_cols(ue, ue_status["is_capitalized"])
|
||||
|
||||
self.nb_ues_etud_parcours = len(res.etud_parcours_ues_ids(etud.id))
|
||||
ue_valid_txt = (
|
||||
ue_valid_txt_html
|
||||
) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
|
||||
ue_valid_txt = ue_valid_txt_html = (
|
||||
f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
|
||||
)
|
||||
if self.nb_ues_warning:
|
||||
ue_valid_txt_html += " " + scu.EMO_WARNING
|
||||
cell_class = ""
|
||||
|
@ -708,9 +708,9 @@ class RowRecap(tb.Row):
|
|||
# sous-classé par JuryRow pour ajouter les codes
|
||||
table: TableRecap = self.table
|
||||
formsemestre: FormSemestre = table.res.formsemestre
|
||||
table.group_titles[
|
||||
"col_ue"
|
||||
] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
|
||||
table.group_titles["col_ue"] = (
|
||||
f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
|
||||
)
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
val = (
|
||||
ue_status["moy"]
|
||||
|
@ -740,7 +740,7 @@ class RowRecap(tb.Row):
|
|||
)
|
||||
table.foot_title_row.cells[col_id].target_attrs[
|
||||
"title"
|
||||
] = f"""{ue.titre} S{ue.semestre_idx or '?'}"""
|
||||
] = f"""{ue.titre or ue.acronyme} S{ue.semestre_idx or '?'}"""
|
||||
|
||||
def add_ue_modimpls_cols(self, ue: UniteEns, is_capitalized: bool):
|
||||
"""Ajoute à row les moyennes des modules (ou ressources et SAÉs) dans l'UE"""
|
||||
|
|
|
@ -18,7 +18,14 @@ from app.scodoc import sco_utils as scu
|
|||
|
||||
class TableAssi(tb.Table):
|
||||
"""Table listant les statistiques d'assiduité des étudiants
|
||||
L'id de la ligne est etuid, et le row stocke etud.
|
||||
L'id de la ligne est etudid, et le row stocke etud.
|
||||
|
||||
On considère les assiduités entre les dates indiquées.
|
||||
|
||||
Si formsemestre_modimpls est spécifié, restreint aux assiduités associées à des
|
||||
moduleimpls de ce formsemestre.
|
||||
|
||||
Si convert_values, transforme les nombre en chaines ("12.34"), pour le html.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -26,12 +33,19 @@ class TableAssi(tb.Table):
|
|||
etuds: list[Identite] = None,
|
||||
dates: tuple[str, str] = None,
|
||||
formsemestre: FormSemestre = None,
|
||||
formsemestre_modimpls: FormSemestre | None = None,
|
||||
convert_values=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
classes = ["gt_table"]
|
||||
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
|
||||
self.formsemestre = formsemestre
|
||||
self.formsemestre_modimpls = formsemestre_modimpls
|
||||
if convert_values:
|
||||
self.fmt_num = lambda x: f"{x:2.3g}"
|
||||
else:
|
||||
self.fmt_num = lambda x: x
|
||||
super().__init__(
|
||||
row_class=RowAssi,
|
||||
classes=classes,
|
||||
|
@ -71,6 +85,7 @@ class RowAssi(tb.Row):
|
|||
def add_etud_cols(self):
|
||||
"""Ajoute les colonnes"""
|
||||
etud = self.etud
|
||||
fmt_num = self.table.fmt_num
|
||||
self.table.group_titles.update(
|
||||
{
|
||||
"etud_codes": "Codes",
|
||||
|
@ -104,12 +119,12 @@ class RowAssi(tb.Row):
|
|||
)
|
||||
stats = self._get_etud_stats(etud)
|
||||
for key, value in stats.items():
|
||||
self.add_cell(key, value[0], f"{value[1] - value[2]}", "assi_stats")
|
||||
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
|
||||
if key != "present":
|
||||
self.add_cell(
|
||||
key + "_justi",
|
||||
value[0] + " Justifiées",
|
||||
f"{value[2]}",
|
||||
fmt_num(value[2]),
|
||||
"assi_stats",
|
||||
)
|
||||
|
||||
|
@ -122,15 +137,17 @@ class RowAssi(tb.Row):
|
|||
self.add_cell(
|
||||
"justificatifs_att",
|
||||
"Justificatifs en Attente",
|
||||
f"{compte_justificatifs_att.count()}",
|
||||
fmt_num(compte_justificatifs_att.count()),
|
||||
)
|
||||
self.add_cell(
|
||||
"justificatifs", "Justificatifs", f"{compte_justificatifs.count()}"
|
||||
"justificatifs", "Justificatifs", fmt_num(compte_justificatifs.count())
|
||||
)
|
||||
|
||||
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
|
||||
"""
|
||||
Renvoie le comptage (dans la métrique du département) des différents états d'assiduité d'un étudiant
|
||||
Renvoie le comptage (dans la métrique du département) des différents états
|
||||
d'assiduité d'un étudiant.
|
||||
Considère les dates.
|
||||
|
||||
Returns :
|
||||
{
|
||||
|
@ -158,6 +175,7 @@ class RowAssi(tb.Row):
|
|||
"date_debut": self.dates[0],
|
||||
"date_fin": self.dates[1],
|
||||
"etat": "absent,present,retard", # pour tout compter d'un coup
|
||||
"formsemestre_modimpls": self.table.formsemestre_modimpls,
|
||||
"split": 1, # afin d'avoir la division des stats en état, etatjust, etatnonjust
|
||||
},
|
||||
)
|
||||
|
|
|
@ -8,6 +8,13 @@
|
|||
|
||||
{% block app_content %}
|
||||
|
||||
<style>
|
||||
label.stats_checkbox {
|
||||
font-weight: normal;
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h2>Visualisation de l'assiduité {{gr_tit|safe}}</h2>
|
||||
|
||||
<div class="stats-inputs">
|
||||
|
@ -18,6 +25,12 @@
|
|||
<button onclick="stats()">Changer</button>
|
||||
|
||||
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
|
||||
|
||||
<label class="stats_checkbox">
|
||||
<input type="checkbox" id="formsemestre_modimpls_box"> restreindre l'assiduité aux
|
||||
modules de ce semestre
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
{{tableau | safe}}
|
||||
|
@ -40,6 +53,19 @@
|
|||
window.addEventListener('load', () => {
|
||||
document.querySelector('#stats_date_debut').value = date_debut;
|
||||
document.querySelector('#stats_date_fin').value = date_fin;
|
||||
|
||||
// La checkbox pour restreindre aux modules du semestre:
|
||||
var url = new URL(window.location.href);
|
||||
var checkbox = document.getElementById('formsemestre_modimpls_box');
|
||||
checkbox.checked = url.searchParams.has('formsemestre_modimpls_id');
|
||||
checkbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
url.searchParams.set('formsemestre_modimpls_id', {{sco.formsemestre.id}});
|
||||
} else {
|
||||
url.searchParams.delete('formsemestre_modimpls_id');
|
||||
}
|
||||
window.location.href = url.href;
|
||||
});
|
||||
})
|
||||
|
||||
</script>
|
||||
|
|
|
@ -49,7 +49,7 @@ table#edt2group tbody tr.active-row {
|
|||
</div>
|
||||
|
||||
{% if ScoDocSiteConfig.get("edt_ics_group_field") %}
|
||||
<div>Les groupes sont extrait du champs <b>{{ScoDocSiteConfig.get("edt_ics_group_field")}}</b>
|
||||
<div>Les groupes sont extrait du champ <b>{{ScoDocSiteConfig.get("edt_ics_group_field")}}</b>
|
||||
à l'aide de l'expression régulière: <tt>{{ScoDocSiteConfig.get("edt_ics_group_regexp")}}</tt>
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{# Édition liste UEs APC #}
|
||||
{% for semestre_idx in semestre_ids %}
|
||||
<div class="formation_list_ues">
|
||||
<div class="formation_list_ues_titre">Unités d'Enseignement
|
||||
<div class="formation_list_ues_titre">Unités d'Enseignement
|
||||
semestre {{semestre_idx}} - {{ects_by_sem[semestre_idx] | safe}} ECTS
|
||||
</div>
|
||||
<div class="formation_list_ues_content">
|
||||
|
@ -9,14 +9,14 @@
|
|||
{% for ue in ues_by_sem[semestre_idx] %}
|
||||
<li class="notes_ue_list">
|
||||
{% if editable and not loop.first %}
|
||||
<a href="{{ url_for('notes.ue_move',
|
||||
<a href="{{ url_for('notes.ue_move',
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=0 )
|
||||
}}" class="aud">{{icons.arrow_up|safe}}</a>
|
||||
{% else %}
|
||||
{{icons.arrow_none|safe}}
|
||||
{% endif %}
|
||||
{% if editable and not loop.last %}
|
||||
<a href="{{ url_for('notes.ue_move',
|
||||
<a href="{{ url_for('notes.ue_move',
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=1 )
|
||||
}}" class="aud">{{icons.arrow_down|safe}}</a>
|
||||
{% else %}
|
||||
|
@ -24,7 +24,7 @@
|
|||
{% endif %}
|
||||
</span>
|
||||
|
||||
<a class="smallbutton" href="{{ url_for('notes.ue_delete',
|
||||
<a class="smallbutton" href="{{ url_for('notes.ue_delete',
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
||||
}}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else
|
||||
%}{{icons.delete_disabled|safe}}{% endif %}</a>
|
||||
|
@ -34,12 +34,12 @@
|
|||
ue.color if ue.color is not none else 'blue'}}"></span>
|
||||
<b>{{ue.acronyme}} <a class="discretelink" href="{{
|
||||
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}" title="{{ue.acronyme}}: {{
|
||||
('pas de compétence associée'
|
||||
if ue.niveau_competence is none
|
||||
('pas de compétence associée'
|
||||
if ue.niveau_competence is none
|
||||
else 'compétence ' + ue.niveau_competence.annee + ' ' + ue.niveau_competence.competence.titre_long)
|
||||
if ue.type == 0
|
||||
else ''
|
||||
}}">{{ue.titre}}</a>
|
||||
}}">{{ue.titre or ue.acronyme}}</a>
|
||||
</b>
|
||||
{% set virg = joiner(", ") %}
|
||||
<span class="ue_code">(
|
||||
|
@ -66,7 +66,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% if editable and not ue.is_locked() %}
|
||||
<a class="stdlink" href="{{ url_for('notes.ue_edit',
|
||||
<a class="stdlink" href="{{ url_for('notes.ue_edit',
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
|
||||
}}">modifier</a>
|
||||
{% endif %}
|
||||
|
@ -100,8 +100,8 @@
|
|||
{% if editable %}
|
||||
<ul>
|
||||
<li class="notes_ue_list notes_ue_list_add"><a class="stdlink" href="{{
|
||||
url_for('notes.ue_create',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
url_for('notes.ue_create',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation.id,
|
||||
default_semestre_idx=semestre_idx,
|
||||
)}}">ajouter une UE</a>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block app_content %}
|
||||
<!-- begin ue_infos -->
|
||||
<h2>Unité d'Enseignement {{ue.acronyme|e}} {{ue.titre}}</h2>
|
||||
<h2>Unité d'Enseignement {{ue.acronyme|e}} {{(ue.titre or '')|e}}</h2>
|
||||
<div class="ue_infos">
|
||||
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
|||
{% if loop.first %}
|
||||
<ul>
|
||||
{% endif %}
|
||||
<li><a href="{{url_for('notes.formsemestre_status',
|
||||
<li><a href="{{url_for('notes.formsemestre_status',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=sem.id )}}">{{sem.titre_mois()}}</a></li>
|
||||
{% if loop.last %}
|
||||
</ul>
|
||||
|
|
|
@ -874,9 +874,11 @@ def choix_date() -> str:
|
|||
if ok:
|
||||
return redirect(
|
||||
url_for(
|
||||
"assiduites.signal_assiduites_group"
|
||||
if request.args.get("readonly") is None
|
||||
else "assiduites.visu_assiduites_group",
|
||||
(
|
||||
"assiduites.signal_assiduites_group"
|
||||
if request.args.get("readonly") is None
|
||||
else "assiduites.visu_assiduites_group"
|
||||
),
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
|
@ -1294,13 +1296,26 @@ def etat_abs_date():
|
|||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def visu_assi_group():
|
||||
"""Visualisation de l'assiduité d'un groupe entre deux dates"""
|
||||
"""Visualisation de l'assiduité d'un groupe entre deux dates.
|
||||
Paramètres:
|
||||
- date_debut, date_fin (format ISO)
|
||||
- fmt : format d'export, html (défaut) ou xls
|
||||
- group_ids : liste des groupes
|
||||
- formsemestre_modimpls_id: id d'un formasemestre, si fournit restreint les
|
||||
comptages aux assiduités liées à des modules de ce formsemestre.
|
||||
"""
|
||||
|
||||
# Récupération des paramètres de la requête
|
||||
dates = {
|
||||
"debut": request.args.get("date_debut"),
|
||||
"fin": request.args.get("date_fin"),
|
||||
}
|
||||
formsemestre_modimpls_id = request.args.get("formsemestre_modimpls_id")
|
||||
formsemestre_modimpls = (
|
||||
None
|
||||
if formsemestre_modimpls_id is None
|
||||
else FormSemestre.get_formsemestre(formsemestre_modimpls_id)
|
||||
)
|
||||
fmt = request.args.get("fmt", "html")
|
||||
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
|
@ -1317,7 +1332,11 @@ def visu_assi_group():
|
|||
|
||||
# Génération du tableau des assiduités
|
||||
table: TableAssi = TableAssi(
|
||||
etuds=etuds, dates=list(dates.values()), formsemestre=formsemestre
|
||||
etuds=etuds,
|
||||
dates=list(dates.values()),
|
||||
formsemestre=formsemestre,
|
||||
formsemestre_modimpls=formsemestre_modimpls,
|
||||
convert_values=(fmt == "html"),
|
||||
)
|
||||
|
||||
# Export en XLS
|
||||
|
@ -2150,9 +2169,6 @@ def _module_selector_multiple(
|
|||
return render_template(
|
||||
"assiduites/widgets/moduleimpl_selector_multiple.j2",
|
||||
choices=choices,
|
||||
formsemestre_id=only_form.id
|
||||
if only_form
|
||||
else list(modimpls_by_formsemestre.keys())[0],
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None, parcours_id: int = N
|
|||
"y": 1, # 1ere ligne
|
||||
"style": "title_ue",
|
||||
"data": ue.acronyme,
|
||||
"title": ue.titre,
|
||||
"title": ue.titre or ue.acronymexs,
|
||||
}
|
||||
for (col, ue) in enumerate(ues, start=2)
|
||||
]
|
||||
|
@ -214,11 +214,13 @@ def edit_modules_ue_coefs():
|
|||
{lockicon}
|
||||
</h2>
|
||||
""",
|
||||
"""<span class="warning">Formation verrouilée car un ou plusieurs
|
||||
(
|
||||
"""<span class="warning">Formation verrouilée car un ou plusieurs
|
||||
semestres verrouillés l'utilisent.
|
||||
</span>"""
|
||||
if locked
|
||||
else "",
|
||||
if locked
|
||||
else ""
|
||||
),
|
||||
render_template(
|
||||
"pn/form_modules_ue_coefs.j2",
|
||||
formation=formation,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
"""etudiant_annotations : ajoute clé externe etudiant et moduleimpl
|
||||
|
||||
Revision ID: 2e4875004e12
|
||||
Revises: 3fa988ff8970
|
||||
Create Date: 2024-02-11 12:10:36.743212
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2e4875004e12"
|
||||
down_revision = "3fa988ff8970"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
# Supprime les annotations orphelines
|
||||
op.execute(
|
||||
"""DELETE FROM etud_annotations
|
||||
WHERE etudid NOT IN (SELECT id FROM identite);
|
||||
"""
|
||||
)
|
||||
# Ajoute clé:
|
||||
with op.batch_alter_table("etud_annotations", schema=None) as batch_op:
|
||||
batch_op.create_foreign_key(None, "identite", ["etudid"], ["id"])
|
||||
|
||||
# Et modif liée au commit 072d013590abf715395bc987fb48de49f6750527
|
||||
with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op:
|
||||
batch_op.drop_constraint(
|
||||
"notes_moduleimpl_responsable_id_fkey", type_="foreignkey"
|
||||
)
|
||||
batch_op.create_foreign_key(
|
||||
None, "user", ["responsable_id"], ["id"], ondelete="SET NULL"
|
||||
)
|
||||
|
||||
# cet index en trop trainait depuis longtemps...
|
||||
with op.batch_alter_table("assiduites", schema=None) as batch_op:
|
||||
batch_op.drop_index("ix_assiduites_user_id")
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_="foreignkey")
|
||||
batch_op.create_foreign_key(
|
||||
"notes_moduleimpl_responsable_id_fkey", "user", ["responsable_id"], ["id"]
|
||||
)
|
||||
|
||||
with op.batch_alter_table("etud_annotations", schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_="foreignkey")
|
||||
|
||||
with op.batch_alter_table("assiduites", schema=None) as batch_op:
|
||||
batch_op.create_index("ix_assiduites_user_id", ["user_id"], unique=False)
|
|
@ -1,7 +1,7 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.936"
|
||||
SCOVERSION = "9.6.939"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
|
|
@ -204,8 +204,8 @@ def test_etudiants(api_headers):
|
|||
|
||||
all_unique = True
|
||||
list_ids = [etud["id"] for etud in etud_nip]
|
||||
for id in list_ids:
|
||||
if list_ids.count(id) > 1:
|
||||
for etudid in list_ids:
|
||||
if list_ids.count(etudid) > 1:
|
||||
all_unique = False
|
||||
assert all_unique is True
|
||||
|
||||
|
@ -226,8 +226,8 @@ def test_etudiants(api_headers):
|
|||
|
||||
all_unique = True
|
||||
list_ids = [etud["id"] for etud in etud_ine]
|
||||
for id in list_ids:
|
||||
if list_ids.count(id) > 1:
|
||||
for etudid in list_ids:
|
||||
if list_ids.count(etudid) > 1:
|
||||
all_unique = False
|
||||
assert all_unique is True
|
||||
|
||||
|
@ -267,6 +267,53 @@ def test_etudiants_by_name(api_headers):
|
|||
assert etuds[0]["nom"] == "RÉGNIER"
|
||||
|
||||
|
||||
def test_etudiant_annotations(api_headers):
|
||||
"""
|
||||
Routes:
|
||||
POST /etudiant/etudid/<int:etudid>/annotation
|
||||
GET /etudiant/etudid/<int:etudid>[/long]
|
||||
"""
|
||||
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
|
||||
args = {
|
||||
"prenom": "Annoté",
|
||||
"nom": "Bach A",
|
||||
"dept": DEPT_ACRONYM,
|
||||
"civilite": "M",
|
||||
}
|
||||
etud = POST_JSON(
|
||||
"/etudiant/create",
|
||||
args,
|
||||
headers=admin_header,
|
||||
)
|
||||
assert etud["nom"] == args["nom"].upper()
|
||||
etudid = etud["id"]
|
||||
# récupère annotation (liste vide)
|
||||
etud = GET(f"/etudiant/etudid/{etudid}", headers=api_headers)
|
||||
assert etud["nom"]
|
||||
assert etud["annotations"] == []
|
||||
# ajoute annotation
|
||||
annotation = POST_JSON(
|
||||
f"/etudiant/etudid/{etudid}/annotation",
|
||||
{"comment": "annotation 1"},
|
||||
headers=admin_header,
|
||||
)
|
||||
assert annotation
|
||||
annotation_id = annotation["id"]
|
||||
etud = GET(f"/etudiant/etudid/{etudid}", headers=api_headers)
|
||||
assert len(etud["annotations"]) == 0 # pas le droit => cachée
|
||||
etud = GET(f"/etudiant/etudid/{etudid}", headers=admin_header)
|
||||
assert len(etud["annotations"]) == 1 # ok avec admin
|
||||
assert etud["annotations"][0]["comment"] == "annotation 1"
|
||||
assert etud["annotations"][0]["id"] == annotation_id
|
||||
# Supprime annotation
|
||||
POST_JSON(
|
||||
f"/etudiant/etudid/{etudid}/annotation/{annotation_id}/delete",
|
||||
headers=admin_header,
|
||||
)
|
||||
etud = GET(f"/etudiant/etudid/{etudid}", headers=api_headers)
|
||||
assert len(etud["annotations"]) == 0
|
||||
|
||||
|
||||
def test_etudiant_photo(api_headers):
|
||||
"""
|
||||
Routes : /etudiant/etudid/<int:etudid>/photo en GET et en POST
|
||||
|
|
|
@ -11,14 +11,12 @@ Usage: pytest tests/scenarios/test_scenario1_formation.py
|
|||
"""
|
||||
# code écrit par Fares Amer, mai 2021 et porté sur ScoDoc 8 en août 2021
|
||||
|
||||
import random
|
||||
|
||||
from tests.unit import sco_fake_gen
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_moduleimpl
|
||||
|
||||
|
||||
@pytest.mark.skip # test obsolete
|
||||
def test_scenario1(test_client):
|
||||
"""Applique "scenario 1"""
|
||||
run_scenario1()
|
||||
|
@ -28,7 +26,9 @@ def run_scenario1():
|
|||
G = sco_fake_gen.ScoFake(verbose=False)
|
||||
|
||||
# Lecture fichier XML local:
|
||||
with open("tests/unit/formation-exemple-1.xml") as f:
|
||||
with open(
|
||||
"tests/ressources/formations/formation-exemple-1.xml", encoding="utf8"
|
||||
) as f:
|
||||
doc = f.read()
|
||||
|
||||
# --- Création de la formation
|
||||
|
|
|
@ -15,7 +15,14 @@ from flask import current_app
|
|||
|
||||
import app
|
||||
from app import db
|
||||
from app.models import Admission, Adresse, Departement, FormSemestre, Identite
|
||||
from app.models import (
|
||||
Admission,
|
||||
Adresse,
|
||||
Departement,
|
||||
EtudAnnotation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
)
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_find_etud, sco_import_etuds
|
||||
from config import TestConfig
|
||||
|
@ -116,6 +123,32 @@ def test_etat_civil(test_client):
|
|||
assert e_d["ne"] == "(e)"
|
||||
|
||||
|
||||
def test_etud_annotations(test_client):
|
||||
"Test ajout/suppression annotations"
|
||||
dept = Departement.query.first()
|
||||
args = {"nom": "nom_a", "prenom": "prénom_a", "civilite": "M", "dept_id": dept.id}
|
||||
etud = Identite.create_etud(**args)
|
||||
db.session.flush()
|
||||
# Ajout annotations
|
||||
etud.annotations.append(EtudAnnotation(comment="annotation 1"))
|
||||
etud.annotations.append(EtudAnnotation(comment="annotation 2"))
|
||||
db.session.add(etud)
|
||||
db.session.commit()
|
||||
assert etud.annotations.count() == 2
|
||||
# Suppression de la première
|
||||
a = etud.annotations.first()
|
||||
assert a.comment == "annotation 1"
|
||||
db.session.delete(a)
|
||||
db.session.commit()
|
||||
assert db.session.get(Identite, etud.id)
|
||||
assert etud.annotations.count() == 1
|
||||
# Suppression de l'étudiant (cascade)
|
||||
a2_id = etud.annotations.first().id
|
||||
db.session.delete(etud)
|
||||
db.session.commit()
|
||||
assert db.session.get(EtudAnnotation, a2_id) is None
|
||||
|
||||
|
||||
def test_etud_legacy(test_client):
|
||||
"Test certaines fonctions scodoc7 (sco_etud)"
|
||||
dept = Departement.query.first()
|
||||
|
|
Loading…
Reference in New Issue