refactoring NotesTabeCompat
This commit is contained in:
parent
74b432020d
commit
82d08be60d
456
app/comp/res_compat.py
Normal file
456
app/comp/res_compat.py
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Classe résultats pour compatibilité avec le code ScoDoc 7
|
||||||
|
"""
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from flask import g, flash
|
||||||
|
|
||||||
|
from app import log
|
||||||
|
from app.comp import moy_sem
|
||||||
|
from app.comp.aux_stats import StatsMoyenne
|
||||||
|
from app.comp.res_common import ResultatsSemestre
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.models import FormSemestre
|
||||||
|
from app.models import Identite
|
||||||
|
from app.models import ModuleImpl
|
||||||
|
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
||||||
|
class NotesTableCompat(ResultatsSemestre):
|
||||||
|
"""Implementation partielle de NotesTable
|
||||||
|
|
||||||
|
Les méthodes définies dans cette classe sont là
|
||||||
|
pour conserver la compatibilité abvec les codes anciens et
|
||||||
|
il n'est pas recommandé de les utiliser dans de nouveaux
|
||||||
|
développements (API malcommode et peu efficace).
|
||||||
|
"""
|
||||||
|
|
||||||
|
_cached_attrs = ResultatsSemestre._cached_attrs + (
|
||||||
|
"bonus",
|
||||||
|
"bonus_ues",
|
||||||
|
"malus",
|
||||||
|
"etud_moy_gen_ranks",
|
||||||
|
"etud_moy_gen_ranks_int",
|
||||||
|
"ue_rangs",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, formsemestre: FormSemestre):
|
||||||
|
super().__init__(formsemestre)
|
||||||
|
|
||||||
|
nb_etuds = len(self.etuds)
|
||||||
|
self.bonus = None # virtuel
|
||||||
|
self.bonus_ues = None # virtuel
|
||||||
|
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
|
||||||
|
self.mod_rangs = None # sera surchargé en Classic, mais pas en APC
|
||||||
|
"""{ modimpl_id : (rangs, effectif) }"""
|
||||||
|
self.moy_min = "NA"
|
||||||
|
self.moy_max = "NA"
|
||||||
|
self.moy_moy = "NA"
|
||||||
|
self.expr_diagnostics = ""
|
||||||
|
self.parcours = self.formsemestre.formation.get_parcours()
|
||||||
|
|
||||||
|
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
|
||||||
|
"""Liste des étudiants inscrits
|
||||||
|
order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative)
|
||||||
|
|
||||||
|
Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace
|
||||||
|
d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]`
|
||||||
|
"""
|
||||||
|
etuds = self.formsemestre.get_inscrits(
|
||||||
|
include_demdef=include_demdef, order=(order_by == "nom")
|
||||||
|
)
|
||||||
|
if order_by == "moy":
|
||||||
|
etuds.sort(
|
||||||
|
key=lambda e: (
|
||||||
|
self.etud_moy_gen_ranks_int.get(e.id, 100000),
|
||||||
|
e.sort_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return etuds
|
||||||
|
|
||||||
|
def get_etudids(self) -> list[int]:
|
||||||
|
"""(deprecated)
|
||||||
|
Liste des etudids inscrits, incluant les démissionnaires.
|
||||||
|
triée par ordre alphabetique de NOM
|
||||||
|
(à éviter: renvoie les etudids, mais est moins efficace que get_inscrits)
|
||||||
|
"""
|
||||||
|
# Note: pour avoir les inscrits non triés,
|
||||||
|
# utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
|
||||||
|
return [x["etudid"] for x in self.inscrlist]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def sem(self) -> dict:
|
||||||
|
"""le formsemestre, comme un gros et gras dict (nt.sem)"""
|
||||||
|
return self.formsemestre.get_infos_dict()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def inscrlist(self) -> list[dict]: # utilisé par PE
|
||||||
|
"""Liste des inscrits au semestre (avec DEM et DEF),
|
||||||
|
sous forme de dict etud,
|
||||||
|
classée dans l'ordre alphabétique de noms.
|
||||||
|
"""
|
||||||
|
etuds = self.formsemestre.get_inscrits(include_demdef=True, order=True)
|
||||||
|
return [e.to_dict_scodoc7() for e in etuds]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def stats_moy_gen(self):
|
||||||
|
"""Stats (moy/min/max) sur la moyenne générale"""
|
||||||
|
return StatsMoyenne(self.etud_moy_gen)
|
||||||
|
|
||||||
|
def get_ues_stat_dict(
|
||||||
|
self, filter_sport=False, check_apc_ects=True
|
||||||
|
) -> list[dict]: # was get_ues()
|
||||||
|
"""Liste des UEs, ordonnée par numero.
|
||||||
|
Si filter_sport, retire les UE de type SPORT.
|
||||||
|
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
||||||
|
"""
|
||||||
|
ues = self.formsemestre.query_ues(with_sport=not filter_sport)
|
||||||
|
ues_dict = []
|
||||||
|
for ue in ues:
|
||||||
|
d = ue.to_dict()
|
||||||
|
if ue.type != UE_SPORT:
|
||||||
|
moys = self.etud_moy_ue[ue.id]
|
||||||
|
else:
|
||||||
|
moys = None
|
||||||
|
d.update(StatsMoyenne(moys).to_dict())
|
||||||
|
ues_dict.append(d)
|
||||||
|
if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
|
||||||
|
g.checked_apc_ects = True
|
||||||
|
if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
|
||||||
|
flash(
|
||||||
|
"""Calcul moyenne générale impossible: ECTS des UE manquants !""",
|
||||||
|
category="danger",
|
||||||
|
)
|
||||||
|
return ues_dict
|
||||||
|
|
||||||
|
def get_modimpls_dict(self, ue_id=None) -> list[dict]:
|
||||||
|
"""Liste des modules pour une UE (ou toutes si ue_id==None),
|
||||||
|
triés par numéros (selon le type de formation)
|
||||||
|
"""
|
||||||
|
modimpls_dict = []
|
||||||
|
for modimpl in self.formsemestre.modimpls_sorted:
|
||||||
|
if ue_id == None or modimpl.module.ue.id == ue_id:
|
||||||
|
d = modimpl.to_dict()
|
||||||
|
# compat ScoDoc < 9.2: ajoute matières
|
||||||
|
d["mat"] = modimpl.module.matiere.to_dict()
|
||||||
|
modimpls_dict.append(d)
|
||||||
|
return modimpls_dict
|
||||||
|
|
||||||
|
def compute_rangs(self):
|
||||||
|
"""Calcule les classements
|
||||||
|
Moyenne générale: etud_moy_gen_ranks
|
||||||
|
Par UE (sauf ue bonus)
|
||||||
|
"""
|
||||||
|
(
|
||||||
|
self.etud_moy_gen_ranks,
|
||||||
|
self.etud_moy_gen_ranks_int,
|
||||||
|
) = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
||||||
|
for ue in self.formsemestre.query_ues():
|
||||||
|
moy_ue = self.etud_moy_ue[ue.id]
|
||||||
|
self.ue_rangs[ue.id] = (
|
||||||
|
moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine
|
||||||
|
int(moy_ue.count()),
|
||||||
|
)
|
||||||
|
# .count() -> nb of non NaN values
|
||||||
|
|
||||||
|
def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]:
|
||||||
|
"""Le rang de l'étudiant dans cette ue
|
||||||
|
Result: rang:str, effectif:str
|
||||||
|
"""
|
||||||
|
rangs, effectif = self.ue_rangs[ue_id]
|
||||||
|
if rangs is not None:
|
||||||
|
rang = rangs[etudid]
|
||||||
|
else:
|
||||||
|
return "", ""
|
||||||
|
return rang, effectif
|
||||||
|
|
||||||
|
def etud_check_conditions_ues(self, etudid):
|
||||||
|
"""Vrai si les conditions sur les UE sont remplies.
|
||||||
|
Ne considère que les UE ayant des notes (moyenne calculée).
|
||||||
|
(les UE sans notes ne sont pas comptées comme sous la barre)
|
||||||
|
Prend en compte les éventuelles UE capitalisées.
|
||||||
|
|
||||||
|
Pour les parcours habituels, cela revient à vérifier que
|
||||||
|
les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)
|
||||||
|
|
||||||
|
Pour les parcours non standards (LP2014), cela peut être plus compliqué.
|
||||||
|
|
||||||
|
Return: True|False, message explicatif
|
||||||
|
"""
|
||||||
|
ue_status_list = []
|
||||||
|
for ue in self.formsemestre.query_ues():
|
||||||
|
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||||
|
if ue_status:
|
||||||
|
ue_status_list.append(ue_status)
|
||||||
|
return self.parcours.check_barre_ues(ue_status_list)
|
||||||
|
|
||||||
|
def all_etuds_have_sem_decisions(self):
|
||||||
|
"""True si tous les étudiants du semestre ont une décision de jury.
|
||||||
|
Ne regarde pas les décisions d'UE.
|
||||||
|
"""
|
||||||
|
for ins in self.formsemestre.inscriptions:
|
||||||
|
if ins.etat != scu.INSCRIT:
|
||||||
|
continue # skip démissionnaires
|
||||||
|
if self.get_etud_decision_sem(ins.etudid) is None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def etud_has_decision(self, etudid):
|
||||||
|
"""True s'il y a une décision de jury pour cet étudiant"""
|
||||||
|
return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
|
||||||
|
|
||||||
|
def get_etud_decision_ues(self, etudid: int) -> dict:
|
||||||
|
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
|
||||||
|
Ne tient pas compte des UE capitalisées.
|
||||||
|
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
|
||||||
|
Ne renvoie aucune decision d'UE pour les défaillants
|
||||||
|
"""
|
||||||
|
if self.get_etud_etat(etudid) == DEF:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
if not self.validations:
|
||||||
|
self.validations = res_sem.load_formsemestre_validations(
|
||||||
|
self.formsemestre
|
||||||
|
)
|
||||||
|
return self.validations.decisions_jury_ues.get(etudid, None)
|
||||||
|
|
||||||
|
def get_etud_decision_sem(self, etudid: int) -> dict:
|
||||||
|
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||||
|
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
|
||||||
|
Si état défaillant, force le code a DEF
|
||||||
|
"""
|
||||||
|
if self.get_etud_etat(etudid) == DEF:
|
||||||
|
return {
|
||||||
|
"code": DEF,
|
||||||
|
"assidu": False,
|
||||||
|
"event_date": "",
|
||||||
|
"compense_formsemestre_id": None,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if not self.validations:
|
||||||
|
self.validations = res_sem.load_formsemestre_validations(
|
||||||
|
self.formsemestre
|
||||||
|
)
|
||||||
|
return self.validations.decisions_jury.get(etudid, None)
|
||||||
|
|
||||||
|
def get_etud_etat(self, etudid: int) -> str:
|
||||||
|
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
||||||
|
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
||||||
|
if ins is None:
|
||||||
|
return ""
|
||||||
|
return ins.etat
|
||||||
|
|
||||||
|
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
|
||||||
|
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
|
||||||
|
if not self.moyennes_matieres:
|
||||||
|
return "nd"
|
||||||
|
return (
|
||||||
|
self.moyennes_matieres[matiere_id].get(etudid, "-")
|
||||||
|
if matiere_id in self.moyennes_matieres
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||||
|
"""La moyenne de l'étudiant dans le moduleimpl
|
||||||
|
En APC, il s'agira d'une moyenne indicative sans valeur.
|
||||||
|
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError() # virtual method
|
||||||
|
|
||||||
|
def get_etud_moy_gen(self, etudid): # -> float | str
|
||||||
|
"""Moyenne générale de cet etudiant dans ce semestre.
|
||||||
|
Prend en compte les UE capitalisées.
|
||||||
|
Si apc, moyenne indicative.
|
||||||
|
Si pas de notes: 'NA'
|
||||||
|
"""
|
||||||
|
return self.etud_moy_gen[etudid]
|
||||||
|
|
||||||
|
def get_etud_ects_pot(self, etudid: int) -> dict:
|
||||||
|
"""
|
||||||
|
Un dict avec les champs
|
||||||
|
ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury)
|
||||||
|
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
|
||||||
|
|
||||||
|
Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non
|
||||||
|
encore enregistrées).
|
||||||
|
"""
|
||||||
|
# was nt.get_etud_moy_infos
|
||||||
|
# XXX pour compat nt, à remplacer ultérieurement
|
||||||
|
ues = self.get_etud_ue_validables(etudid)
|
||||||
|
ects_pot = 0.0
|
||||||
|
for ue in ues:
|
||||||
|
if (
|
||||||
|
ue.id in self.etud_moy_ue
|
||||||
|
and ue.ects is not None
|
||||||
|
and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE
|
||||||
|
):
|
||||||
|
ects_pot += ue.ects
|
||||||
|
return {
|
||||||
|
"ects_pot": ects_pot,
|
||||||
|
"ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_etud_rang(self, etudid: int):
|
||||||
|
return self.etud_moy_gen_ranks.get(etudid, 99999)
|
||||||
|
|
||||||
|
def get_etud_rang_group(self, etudid: int, group_id: int):
|
||||||
|
return (None, 0) # XXX unimplemented TODO
|
||||||
|
|
||||||
|
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
|
||||||
|
"""Liste d'informations (compat NotesTable) sur évaluations completes
|
||||||
|
de ce module.
|
||||||
|
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||||
|
"""
|
||||||
|
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
|
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||||
|
if not modimpl_results:
|
||||||
|
return [] # safeguard
|
||||||
|
evals_results = []
|
||||||
|
for e in modimpl.evaluations:
|
||||||
|
if modimpl_results.evaluations_completes_dict.get(e.id, False):
|
||||||
|
d = e.to_dict()
|
||||||
|
d["heure_debut"] = e.heure_debut # datetime.time
|
||||||
|
d["heure_fin"] = e.heure_fin
|
||||||
|
d["jour"] = e.jour # datetime
|
||||||
|
d["notes"] = {
|
||||||
|
etud.id: {
|
||||||
|
"etudid": etud.id,
|
||||||
|
"value": modimpl_results.evals_notes[e.id][etud.id],
|
||||||
|
}
|
||||||
|
for etud in self.etuds
|
||||||
|
}
|
||||||
|
d["etat"] = {
|
||||||
|
"evalattente": modimpl_results.evaluations_etat[e.id].nb_attente,
|
||||||
|
}
|
||||||
|
evals_results.append(d)
|
||||||
|
elif e.id not in modimpl_results.evaluations_completes_dict:
|
||||||
|
# ne devrait pas arriver ? XXX
|
||||||
|
log(
|
||||||
|
f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?"
|
||||||
|
)
|
||||||
|
return evals_results
|
||||||
|
|
||||||
|
def get_evaluations_etats(self):
|
||||||
|
"""[ {...evaluation et son etat...} ]"""
|
||||||
|
# TODO: à moderniser
|
||||||
|
from app.scodoc import sco_evaluations
|
||||||
|
|
||||||
|
if not hasattr(self, "_evaluations_etats"):
|
||||||
|
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
|
||||||
|
self.formsemestre.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._evaluations_etats
|
||||||
|
|
||||||
|
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
||||||
|
"""Liste des états des évaluations de ce module"""
|
||||||
|
# XXX TODO à moderniser: lent, recharge des données que l'on a déjà...
|
||||||
|
return [
|
||||||
|
e
|
||||||
|
for e in self.get_evaluations_etats()
|
||||||
|
if e["moduleimpl_id"] == moduleimpl_id
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_moduleimpls_attente(self):
|
||||||
|
"""Liste des modimpls du semestre ayant des notes en attente"""
|
||||||
|
return [
|
||||||
|
modimpl
|
||||||
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
|
if self.modimpls_results[modimpl.id].en_attente
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_mod_stats(self, moduleimpl_id: int) -> dict:
|
||||||
|
"""Stats sur les notes obtenues dans un modimpl
|
||||||
|
Vide en APC
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"moy": "-",
|
||||||
|
"max": "-",
|
||||||
|
"min": "-",
|
||||||
|
"nb_notes": "-",
|
||||||
|
"nb_missing": "-",
|
||||||
|
"nb_valid_evals": "-",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_nom_short(self, etudid):
|
||||||
|
"formatte nom d'un etud (pour table recap)"
|
||||||
|
etud = self.identdict[etudid]
|
||||||
|
return (
|
||||||
|
(etud["nom_usuel"] or etud["nom"]).upper()
|
||||||
|
+ " "
|
||||||
|
+ etud["prenom"].capitalize()[:2]
|
||||||
|
+ "."
|
||||||
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def T(self):
|
||||||
|
return self.get_table_moyennes_triees()
|
||||||
|
|
||||||
|
def get_table_moyennes_triees(self) -> list:
|
||||||
|
"""Result: liste de tuples
|
||||||
|
moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
|
||||||
|
"""
|
||||||
|
table_moyennes = []
|
||||||
|
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
||||||
|
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
||||||
|
for etudid in etuds_inscriptions:
|
||||||
|
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||||
|
if moy_gen is False:
|
||||||
|
# pas de moyenne: démissionnaire ou def
|
||||||
|
t = (
|
||||||
|
["-"]
|
||||||
|
+ ["0.00"] * len(self.ues)
|
||||||
|
+ ["NI"] * len(self.formsemestre.modimpls_sorted)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
moy_ues = []
|
||||||
|
ue_is_cap = {}
|
||||||
|
for ue in ues:
|
||||||
|
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||||
|
if ue_status:
|
||||||
|
moy_ues.append(ue_status["moy"])
|
||||||
|
ue_is_cap[ue.id] = ue_status["is_capitalized"]
|
||||||
|
else:
|
||||||
|
moy_ues.append("?")
|
||||||
|
t = [moy_gen] + list(moy_ues)
|
||||||
|
# Moyennes modules:
|
||||||
|
for modimpl in self.formsemestre.modimpls_sorted:
|
||||||
|
if ue_is_cap.get(modimpl.module.ue.id, False):
|
||||||
|
val = "-c-"
|
||||||
|
else:
|
||||||
|
val = self.get_etud_mod_moy(modimpl.id, etudid)
|
||||||
|
t.append(val)
|
||||||
|
t.append(etudid)
|
||||||
|
table_moyennes.append(t)
|
||||||
|
# tri par moyennes décroissantes,
|
||||||
|
# en laissant les démissionnaires à la fin, par ordre alphabetique
|
||||||
|
etuds = [ins.etud for ins in etuds_inscriptions.values()]
|
||||||
|
etuds.sort(key=lambda e: e.sort_key)
|
||||||
|
self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
|
||||||
|
table_moyennes.sort(key=self._row_key)
|
||||||
|
return table_moyennes
|
||||||
|
|
||||||
|
def _row_key(self, x):
|
||||||
|
"""clé de tri par moyennes décroissantes,
|
||||||
|
en laissant les demissionnaires à la fin, par ordre alphabetique.
|
||||||
|
(moy_gen, rang_alpha)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
moy = -float(x[0])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
moy = 1000.0
|
||||||
|
return (moy, self._rang_alpha[x[-1]])
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def identdict(self) -> dict:
|
||||||
|
"""{ etudid : etud_dict } pour tous les inscrits au semestre"""
|
||||||
|
return {
|
||||||
|
ins.etud.id: ins.etud.to_dict_scodoc7()
|
||||||
|
for ins in self.formsemestre.inscriptions
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user