forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -372,3 +372,5 @@ def get_cosemestres_diplomants(
|
|||||||
cosemestres[fid] = cosem
|
cosemestres[fid] = cosem
|
||||||
|
|
||||||
return cosemestres
|
return cosemestres
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,14 +10,6 @@ import numpy as np
|
|||||||
|
|
||||||
|
|
||||||
class AggregatInterclasseTag(TableTag):
|
class AggregatInterclasseTag(TableTag):
|
||||||
"""Interclasse l'ensemble des étudiants diplômés à une année
|
|
||||||
donnée (celle du jury), pour un aggrégat 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
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------------------
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -27,12 +19,26 @@ class AggregatInterclasseTag(TableTag):
|
|||||||
trajectoires_jury_pe: TrajectoiresJuryPE,
|
trajectoires_jury_pe: TrajectoiresJuryPE,
|
||||||
trajectoires_taggues: dict[tuple, TrajectoireTag],
|
trajectoires_taggues: dict[tuple, TrajectoireTag],
|
||||||
):
|
):
|
||||||
# Table nommée au nom de l'aggrégat (par ex: 3S)
|
"""
|
||||||
TableTag.__init__(self, nom_aggregat)
|
Interclasse l'ensemble des étudiants diplômés à une année
|
||||||
|
donnée (celle du jury), pour un aggrégat 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)
|
||||||
|
|
||||||
|
# Le nom
|
||||||
|
self.aggregat = nom_aggregat
|
||||||
|
self.nom = self.get_repr()
|
||||||
|
|
||||||
"""Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" # TODO
|
"""Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" # TODO
|
||||||
self.diplomes_ids = etudiants.etudiants_diplomes
|
self.diplomes_ids = etudiants.etudiants_diplomes
|
||||||
self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids}
|
self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids}
|
||||||
|
# pour les exports sous forme de dataFrame
|
||||||
|
self.etudiants = {etudid: etudiants.identites[etudid].etat_civil 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.trajectoires: dict[int, Trajectoire] = {}
|
self.trajectoires: dict[int, Trajectoire] = {}
|
||||||
@ -73,9 +79,13 @@ class AggregatInterclasseTag(TableTag):
|
|||||||
"nb_inscrits": len(moy_gen_tag),
|
"nb_inscrits": len(moy_gen_tag),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Est significatif ? (aka a-t-il des tags et des notes)
|
||||||
|
self.significatif = len(self.tags_sorted) > 0
|
||||||
|
|
||||||
|
|
||||||
def get_repr(self) -> str:
|
def get_repr(self) -> str:
|
||||||
"""Une représentation textuelle"""
|
"""Une représentation textuelle"""
|
||||||
return f"Aggrégat {self.nom}"
|
return f"Aggrégat {self.aggregat}"
|
||||||
|
|
||||||
def do_taglist(self):
|
def do_taglist(self):
|
||||||
"""Synthétise les tags à partir des trajectoires_tagguées
|
"""Synthétise les tags à partir des trajectoires_tagguées
|
||||||
|
@ -121,7 +121,7 @@ class JuryPE(object):
|
|||||||
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
||||||
for formsemestretag in self.semestres_taggues.values():
|
for formsemestretag in self.semestres_taggues.values():
|
||||||
onglet = formsemestretag.nom
|
onglet = formsemestretag.nom
|
||||||
df = formsemestretag.df_semtag()
|
df = formsemestretag.df_moyennes_et_classements()
|
||||||
# écriture dans l'onglet
|
# écriture dans l'onglet
|
||||||
df.to_excel(writer, onglet, index=True, header=True)
|
df.to_excel(writer, onglet, index=True, header=True)
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
@ -149,17 +149,22 @@ class JuryPE(object):
|
|||||||
self.trajectoires, self.etudiants, self.semestres_taggues
|
self.trajectoires, self.etudiants, self.semestres_taggues
|
||||||
)
|
)
|
||||||
|
|
||||||
if pe_affichage.PE_DEBUG:
|
# Intègre le bilan des trajectoires tagguées au zip final
|
||||||
# Intègre le bilan des trajectoires tagguées au zip final
|
output = io.BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
||||||
for trajectoire_tagguee in self.trajectoires_tagguees.values():
|
for trajectoire_tagguee in self.trajectoires_tagguees.values():
|
||||||
filename = trajectoire_tagguee.get_repr().replace(" ", "_") + ".csv"
|
onglet = trajectoire_tagguee.get_repr()
|
||||||
# pe_affichage.pe_print(f" - Export csv de {filename} ")
|
df = trajectoire_tagguee.df_moyennes_et_classements()
|
||||||
self.add_file_to_zip(
|
# écriture dans l'onglet
|
||||||
zipfile,
|
df.to_excel(writer, onglet, index=True, header=True)
|
||||||
filename,
|
output.seek(0)
|
||||||
trajectoire_tagguee.str_tagtable(),
|
|
||||||
path="details_semestres",
|
self.add_file_to_zip(
|
||||||
)
|
zipfile,
|
||||||
|
f"trajectoires_taggues_{self.diplome}.xlsx",
|
||||||
|
output.read(),
|
||||||
|
path="details",
|
||||||
|
)
|
||||||
|
|
||||||
# Génère les interclassements (par promo et) par (nom d') aggrégat
|
# Génère les interclassements (par promo et) par (nom d') aggrégat
|
||||||
pe_affichage.pe_print("*** Génère les interclassements par aggrégat")
|
pe_affichage.pe_print("*** Génère les interclassements par aggrégat")
|
||||||
@ -167,17 +172,23 @@ class JuryPE(object):
|
|||||||
self.etudiants, self.trajectoires, self.trajectoires_tagguees
|
self.etudiants, self.trajectoires, self.trajectoires_tagguees
|
||||||
)
|
)
|
||||||
|
|
||||||
if pe_affichage.PE_DEBUG:
|
# Intègre le bilan des aggrégats (interclassé par promo) au zip final
|
||||||
# Intègre le bilan des aggrégats (par promo) au zip final
|
output = io.BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
||||||
for interclass_tag in self.interclassements_taggues.values():
|
for interclass_tag in self.interclassements_taggues.values():
|
||||||
filename = interclass_tag.get_repr().replace(" ", "_") + ".csv"
|
if interclass_tag.significatif: # Avec des notes
|
||||||
# pe_affichage.pe_print(f" - Export csv de {filename} ")
|
onglet = interclass_tag.get_repr()
|
||||||
self.add_file_to_zip(
|
df = interclass_tag.df_moyennes_et_classements()
|
||||||
zipfile,
|
# écriture dans l'onglet
|
||||||
filename,
|
df.to_excel(writer, onglet, index=True, header=True)
|
||||||
interclass_tag.str_tagtable(),
|
output.seek(0)
|
||||||
path="details_semestres",
|
|
||||||
)
|
self.add_file_to_zip(
|
||||||
|
zipfile,
|
||||||
|
f"interclassements_taggues_{self.diplome}.xlsx",
|
||||||
|
output.read(),
|
||||||
|
path="details",
|
||||||
|
)
|
||||||
|
|
||||||
# Synthèse des éléments du jury PE
|
# Synthèse des éléments du jury PE
|
||||||
self.synthese = self.synthetise_juryPE()
|
self.synthese = self.synthetise_juryPE()
|
||||||
@ -437,7 +448,7 @@ def compute_trajectoires_tag(
|
|||||||
nom = trajectoire.get_repr()
|
nom = trajectoire.get_repr()
|
||||||
pe_affichage.pe_print(f" --> Aggrégat {nom}")
|
pe_affichage.pe_print(f" --> Aggrégat {nom}")
|
||||||
# Trajectoire_tagguee associée
|
# Trajectoire_tagguee associée
|
||||||
trajectoire_tagguee = TrajectoireTag(nom, trajectoire, semestres_taggues)
|
trajectoire_tagguee = TrajectoireTag(trajectoire, semestres_taggues)
|
||||||
# Mémorise le résultat
|
# Mémorise le résultat
|
||||||
trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee
|
trajectoires_tagguees[trajectoire_id] = trajectoire_tagguee
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class SemestreTag(TableTag):
|
|||||||
nom: Nom à donner au SemestreTag
|
nom: Nom à donner au SemestreTag
|
||||||
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
|
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
|
||||||
"""
|
"""
|
||||||
# TableTag.__init__(self, nom=nom)
|
TableTag.__init__(self)
|
||||||
|
|
||||||
# Le semestre
|
# Le semestre
|
||||||
self.formsemestre_id = formsemestre_id
|
self.formsemestre_id = formsemestre_id
|
||||||
@ -126,7 +126,7 @@ class SemestreTag(TableTag):
|
|||||||
# Synthétise l'ensemble des moyennes dans un dataframe
|
# Synthétise l'ensemble des moyennes dans un dataframe
|
||||||
self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique
|
self.tags_sorted = sorted(self.moyennes_tags) # les tags par ordre alphabétique
|
||||||
self.notes = (
|
self.notes = (
|
||||||
self.df_tagtable()
|
self.df_notes()
|
||||||
) # Le dataframe synthétique des notes (=moyennes par tag)
|
) # Le dataframe synthétique des notes (=moyennes par tag)
|
||||||
|
|
||||||
pe_affichage.pe_print(f" => Traitement des tags {', '.join(self.tags_sorted)}")
|
pe_affichage.pe_print(f" => Traitement des tags {', '.join(self.tags_sorted)}")
|
||||||
@ -282,28 +282,7 @@ class SemestreTag(TableTag):
|
|||||||
# Sinon - pas de notes à prendre en compte
|
# Sinon - pas de notes à prendre en compte
|
||||||
return (note, coeff_norm)
|
return (note, coeff_norm)
|
||||||
|
|
||||||
def df_semtag(self):
|
|
||||||
"""Renvoie un dataframe listant toutes les moyennes,
|
|
||||||
les rangs des étudiants pour tous les tags dans le semestre taggué"""
|
|
||||||
|
|
||||||
etudiants = self.etudiants
|
|
||||||
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
|
|
||||||
|
|
||||||
for tag in self.get_all_tags():
|
|
||||||
df = df.join(self.moyennes_tags[tag]["notes"].rename(f"Moy {tag}"))
|
|
||||||
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"Class {tag}"))
|
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
# ************************************************************************
|
|
||||||
# Fonctions diverses
|
|
||||||
# ************************************************************************
|
|
||||||
|
|
||||||
|
|
||||||
# *********************************************
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
def get_moduleimpl(modimpl_id) -> dict:
|
def get_moduleimpl(modimpl_id) -> dict:
|
||||||
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
||||||
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
||||||
@ -317,7 +296,6 @@ def get_moduleimpl(modimpl_id) -> dict:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# **********************************************
|
|
||||||
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
|
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
|
||||||
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
|
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
|
||||||
le module de modimpl_id
|
le module de modimpl_id
|
||||||
|
@ -48,38 +48,13 @@ TAGS_RESERVES = ["but"]
|
|||||||
|
|
||||||
|
|
||||||
class TableTag(object):
|
class TableTag(object):
|
||||||
"""
|
|
||||||
Classe mémorisant les moyennes des étudiants à différents tags et permettant de
|
|
||||||
calculer des rangs et des statistiques.
|
|
||||||
|
|
||||||
Ses attributs sont:
|
|
||||||
|
|
||||||
* nom : Nom représentatif des données de la Table
|
def __init__(self):
|
||||||
* inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme :
|
"""Classe centralisant différentes méthodes communes aux
|
||||||
{ etudid : dictionnaire d'info extrait de Scodoc, ...}
|
SemestreTag, TrajectoireTag, AggregatInterclassTag
|
||||||
* taglist : Liste triée des noms des tags
|
|
||||||
* resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée
|
|
||||||
des coeff utilisées dans le calcul de la moyenne pondérée, sous la forme :
|
|
||||||
{ tag : { etudid: (note_moy, somme_coeff_norm),
|
|
||||||
...}
|
|
||||||
* rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme :
|
|
||||||
{ tag : {etudid: rang, ...} }
|
|
||||||
* nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags)
|
|
||||||
* statistiques : Dictionnaire donnant les statistiques (moyenne, min, max) des résultats par tag de la forme :
|
|
||||||
{ tag : (moy, min, max), ...}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, nom: str):
|
|
||||||
"""Les attributs basiques des TagTable, qui seront initialisés
|
|
||||||
dans les classes dérivées
|
|
||||||
"""
|
"""
|
||||||
self.nom = nom
|
pass
|
||||||
"""Les étudiants"""
|
|
||||||
self.etudiants = {}
|
|
||||||
"""Les moyennes par tag"""
|
|
||||||
self.moyennes_tags = {}
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------------------------------
|
||||||
def get_all_tags(self):
|
def get_all_tags(self):
|
||||||
@ -90,8 +65,21 @@ class TableTag(object):
|
|||||||
"""
|
"""
|
||||||
return sorted(self.moyennes_tags.keys())
|
return sorted(self.moyennes_tags.keys())
|
||||||
|
|
||||||
|
def df_moyennes_et_classements(self):
|
||||||
|
"""Renvoie un dataframe listant toutes les moyennes,
|
||||||
|
et les classements des étudiants pour tous les tags
|
||||||
|
"""
|
||||||
|
|
||||||
def df_tagtable(self):
|
etudiants = self.etudiants
|
||||||
|
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
|
||||||
|
|
||||||
|
for tag in self.get_all_tags():
|
||||||
|
df = df.join(self.moyennes_tags[tag]["notes"].rename(f"Moy {tag}"))
|
||||||
|
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"Class {tag}"))
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def df_notes(self):
|
||||||
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
|
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -117,4 +105,3 @@ class TableTag(object):
|
|||||||
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}"))
|
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}"))
|
||||||
|
|
||||||
return df.to_csv(sep=";")
|
return df.to_csv(sep=";")
|
||||||
|
|
||||||
|
@ -46,16 +46,17 @@ class Trajectoire:
|
|||||||
"""
|
"""
|
||||||
self.semestres_aggreges = self.semestres_aggreges | semestres
|
self.semestres_aggreges = self.semestres_aggreges | semestres
|
||||||
|
|
||||||
def get_repr(self):
|
def get_repr(self, verbose=True) -> str:
|
||||||
"""Représentation textuelle d'une trajectoire
|
"""Représentation textuelle d'une trajectoire
|
||||||
basée sur ses semestres aggrégés"""
|
basée sur ses semestres aggrégés"""
|
||||||
|
|
||||||
noms = []
|
noms = []
|
||||||
for fid in self.semestres_aggreges:
|
for fid in self.semestres_aggreges:
|
||||||
semestre = self.semestres_aggreges[fid]
|
semestre = self.semestres_aggreges[fid]
|
||||||
noms.append(f"S{semestre.semestre_id}({fid})")
|
noms.append(f"S{semestre.semestre_id}({fid})")
|
||||||
noms = sorted(noms)
|
noms = sorted(noms)
|
||||||
repr = f"{self.nom} ({self.semestre_final.formsemestre_id}) {self.semestre_final.date_fin.year}"
|
repr = f"{self.nom} ({self.semestre_final.formsemestre_id}) {self.semestre_final.date_fin.year}"
|
||||||
if noms:
|
if verbose and noms:
|
||||||
repr += " - " + "+".join(noms)
|
repr += " - " + "+".join(noms)
|
||||||
return repr
|
return repr
|
||||||
|
|
||||||
|
@ -47,30 +47,30 @@ from app.pe.pe_tabletags import TableTag
|
|||||||
|
|
||||||
|
|
||||||
class TrajectoireTag(TableTag):
|
class TrajectoireTag(TableTag):
|
||||||
"""Calcule les moyennes par tag d'une combinaison de semestres
|
|
||||||
(trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et
|
|
||||||
par un semestre terminal, pour extraire les classements par tag pour un
|
|
||||||
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
|
|
||||||
participé au semestre terminal.
|
|
||||||
|
|
||||||
Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------------------------------------------
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
nom: str,
|
|
||||||
trajectoire: Trajectoire,
|
trajectoire: Trajectoire,
|
||||||
semestres_taggues: dict[int, SemestreTag]
|
semestres_taggues: dict[int, SemestreTag]
|
||||||
):
|
):
|
||||||
""" """
|
"""Calcule les moyennes par tag d'une combinaison de semestres
|
||||||
TableTag.__init__(self, nom=nom)
|
(trajectoires), identifiée par un nom d'aggrégat (par ex: '3S') et
|
||||||
|
par un semestre terminal, pour extraire les classements par tag pour un
|
||||||
|
groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
|
||||||
|
participé au semestre terminal.
|
||||||
|
|
||||||
"""La trajectoire associée"""
|
Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
|
||||||
|
|
||||||
|
"""
|
||||||
|
TableTag.__init__(self)
|
||||||
|
|
||||||
|
# La trajectoire associée
|
||||||
self.trajectoire_id = trajectoire.trajectoire_id
|
self.trajectoire_id = trajectoire.trajectoire_id
|
||||||
self.trajectoire = trajectoire
|
self.trajectoire = trajectoire
|
||||||
|
|
||||||
|
# Le nom de la trajectoire tagguée (identique à la trajectoire)
|
||||||
|
self.nom = self.get_repr()
|
||||||
|
|
||||||
"""Le formsemestre terminal et les semestres aggrégés"""
|
"""Le formsemestre terminal et les semestres aggrégés"""
|
||||||
self.formsemestre_terminal = trajectoire.semestre_final
|
self.formsemestre_terminal = trajectoire.semestre_final
|
||||||
nt = load_formsemestre_results(self.formsemestre_terminal)
|
nt = load_formsemestre_results(self.formsemestre_terminal)
|
||||||
@ -114,10 +114,10 @@ class TrajectoireTag(TableTag):
|
|||||||
"nb_inscrits": len(moy_gen_tag),
|
"nb_inscrits": len(moy_gen_tag),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_repr(self):
|
def get_repr(self, verbose=False) -> str:
|
||||||
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
|
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
|
||||||
est basée)"""
|
est basée)"""
|
||||||
return self.trajectoire.get_repr()
|
return self.trajectoire.get_repr(verbose=verbose)
|
||||||
|
|
||||||
def compute_notes_cube(self):
|
def compute_notes_cube(self):
|
||||||
"""Construit le cube de notes (etudid x tags x semestre_aggregé)
|
"""Construit le cube de notes (etudid x tags x semestre_aggregé)
|
||||||
@ -159,8 +159,6 @@ class TrajectoireTag(TableTag):
|
|||||||
|
|
||||||
return etudids_x_tags_x_semestres
|
return etudids_x_tags_x_semestres
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def do_taglist(self):
|
def do_taglist(self):
|
||||||
"""Synthétise les tags à partir des semestres (taggués) aggrégés
|
"""Synthétise les tags à partir des semestres (taggués) aggrégés
|
||||||
|
|
||||||
@ -173,6 +171,8 @@ class TrajectoireTag(TableTag):
|
|||||||
return sorted(set(tags))
|
return sorted(set(tags))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
|
def compute_tag_moy(set_cube: np.array, etudids: list, tags: list):
|
||||||
"""Calcul de la moyenne par tag sur plusieurs semestres.
|
"""Calcul de la moyenne par tag sur plusieurs semestres.
|
||||||
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
|
La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles
|
||||||
|
Loading…
Reference in New Issue
Block a user