# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# 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
#
##############################################################################
"""
# Outil de comparaison Apogée/ScoDoc (J.-M. Place, Jan 2020)
## fonctionalités
Le menu 'synchronisation avec Apogée' ne permet pas de traiter facilement les cas
où un même code étape est implementé dans des formsemestres différents.
On ajoute à la page de description des ensembles de semestres
une section permettant de faire le point sur les cas particuliers.
Cette section est composée de deux parties:
* Une partie effectif où figurent le nombre d'étudiants selon une répartition par
semestre (en ligne) et par code étape (en colonne). On ajoute également des
colonnes/lignes correspondant à des anomalies (étudiant sans code étape, sans
semestre, avec deux semestres, sans NIP, etc.).
* Une seconde partie présente la liste des étudiants. Il est possible qu'un
même nom figure deux fois dans la liste (si on a pas pu faire la correspondance
entre une inscription Apogée et un étudiant d'un semestre, par exemple).
L'activation d'un des nombres du tableau 'effectifs' restreint l'affichage de
la liste aux étudiants qui contribuent à ce nombre.
## Réalisation
Les modifications logicielles portent sur:
### La création d'une classe sco_etape_bilan.py
Cette classe compile la totalité des données:
** Liste des semestres
** Listes des étapes
** Liste des étudiants
** constitution des listes d'anomalies
Cette classe explore la suite semestres du semset.
Pour chaque semestre, elle recense les étudiants du semestre et
les codes étapes concernés, puis tous les codes étapes (toujours
en important les étudiants de l'étape via le portail).
Enfin on dispatch chaque étudiant dans une case - soit ordinaire, soit
correspondant à une anomalie.
### Modification de sco_etape_apogee_view.py
Pour insertion de l'affichage ajouté.
### Modification de sco_semset.py
Affichage proprement dit.
### Modification de sco_formsemestre.py
Modification/ajout de la méthode sem_in_semestre_scolaire pour permettre
l'inscription de semestres décalés (S1 en septembre, ...).
Le filtrage s'effectue sur la date et non plus sur la parité du semestre (1-3/2-4).
"""
import json
from flask import url_for, g
from app.scodoc.sco_portal_apogee import get_inscrits_etape
from app import log
from app.scodoc.sco_utils import annee_scolaire_debut
from app.scodoc.gen_tables import GenTable
COL_PREFIX = "COL_"
# Les indicatifs sont des marqueurs de classe CSS insérés dans la table étudiant
# et utilisés par le javascript pour permettre un filtrage de la liste étudiants
# sur un 'cas' considéré
# indicatifs
COL_CUMUL = "C9"
ROW_CUMUL = "R9"
# Constante d'anomalie
PAS_DE_NIP = "C1"
PAS_D_ETAPE = "C2"
PLUSIEURS_ETAPES = "C3"
PAS_DE_SEMESTRE = "R4"
PLUSIEURS_SEMESTRES = "R5"
NIP_NON_UNIQUE = "U"
FLAG = {
PAS_DE_NIP: "A",
PAS_D_ETAPE: "B",
PLUSIEURS_ETAPES: "C",
PAS_DE_SEMESTRE: "D",
PLUSIEURS_SEMESTRES: "E",
NIP_NON_UNIQUE: "U",
}
class DataEtudiant(object):
"""
Structure de donnée des informations pour un étudiant
"""
def __init__(self, nip="", etudid=""):
self.nip = nip
self.etudid = etudid
self.data_apogee = None
self.data_scodoc = None
self.etapes = set() # l'ensemble des étapes où il est inscrit
self.semestres = set() # l'ensemble des formsemestre_id où il est inscrit
self.tags = set() # les anomalies relevées
self.ind_row = "-" # là où il compte dans les effectifs (ligne et colonne)
self.ind_col = "-"
def add_etape(self, etape):
self.etapes.add(etape)
def add_semestre(self, formsemestre_id: int):
self.semestres.add(formsemestre_id)
def set_apogee(self, data_apogee):
self.data_apogee = data_apogee
def set_scodoc(self, data_scodoc):
self.data_scodoc = data_scodoc
def add_tag(self, tag):
self.tags.add(tag)
def set_ind_row(self, indicatif):
self.ind_row = indicatif
def set_ind_col(self, indicatif):
self.ind_col = indicatif
def get_identity(self):
"""
Calcul le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
:return: L'identité calculée
"""
if self.data_scodoc is not None:
return self.data_scodoc["nom"] + self.data_scodoc["prenom"]
else:
return self.data_apogee["nom"] + self.data_apogee["prenom"]
def _help() -> str:
return """
Tableau des effectifs
""",
self._diagtable(),
""" """,
self.display_tags(),
"""
Liste des étudiants
""",
entete_liste_etudiant(),
self.table_effectifs(),
""" """,
_help(),
]
return "\n".join(H)
def _inc_count(self, ind_row, ind_col):
if (ind_row, ind_col) not in self.repartition:
self.repartition[ind_row, ind_col] = 0
self.repartition[ind_row, ind_col] += 1
self.repartition[ROW_CUMUL, ind_col] += 1
self.repartition[ind_row, COL_CUMUL] += 1
self.repartition[ROW_CUMUL, COL_CUMUL] += 1
def _get_count(self, ind_row, ind_col):
if (ind_row, ind_col) in self.repartition:
count = self.repartition[ind_row, ind_col]
if count > 1:
comptage = f"({count} étudiants)"
else:
comptage = "(1 étudiant)"
else:
count = 0
return ""
# Ajoute l'appel à la routine javascript de filtrage (apo_semset_maq_status.js
# signature:
# function show_css(elt, all_rows, all_cols, row, col, precision)
# elt: le lien cliqué
# all_rows: la liste de toutes les lignes existantes dans le tableau répartition
# (exemple: ".Rb,.R1,.R2,.R3")
# all_cols: la liste de toutes les colonnes existantes dans le tableau répartition
# (exemple: ".Ca,.C1,.C2,.C3")
# row: la ligne sélectionnée (sélecteur css) (expl: ".R1")
# ; '*' si pas de sélection sur la ligne
# col: la (les) colonnes sélectionnées (sélecteur css) (exple: ".C2")
# ; '*' si pas de sélection sur colonne
# precision: ajout sur le titre (en général, le nombre d'étudiant)
# filtre_row: explicitation du filtre ligne éventuelle
# filtre_col: explicitation du filtre colonne évnetuelle
if ind_row == ROW_CUMUL and ind_col == COL_CUMUL:
javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str},
'*', '*',
'{comptage}',
'', ''
);"""
elif ind_row == ROW_CUMUL:
javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str},
'*', '.{ind_col}',
'{comptage}', '',
'{json.dumps(self.titres[ind_col].replace("
", " / "))[1:-1]}'
);"""
elif ind_col == COL_CUMUL:
javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str},
'.{ind_row}', '*',
' ({count} étudiants)',
'{json.dumps(self.titres[ind_row])[1:-1]}', ''
);"""
else:
javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str},
'.{ind_row}', '.{ind_col}',
'{comptage}',
'{json.dumps(self.titres[ind_row])[1:-1]}',
'{json.dumps(self.titres[ind_col].replace("
", " / "))[1:-1]}'
);"""
return f"""
{count}"""
def _diagtable(self) -> str:
"""Table avec les semestres et les effectifs"""
H = []
liste_semestres = sorted(self.semestres.keys())
liste_etapes = []
for key_etape in self.etapes:
liste_etapes.append(key_etape)
liste_etapes.sort(key=lambda key: etape_to_col(key_etape))
col_ids = []
if PAS_DE_NIP in self.tag_count:
col_ids.append(PAS_DE_NIP)
if PAS_D_ETAPE in self.tag_count:
col_ids.append(PAS_D_ETAPE)
if PLUSIEURS_ETAPES in self.tag_count:
col_ids.append(PLUSIEURS_ETAPES)
self.titres["row_title"] = "Semestre"
self.titres[PAS_DE_NIP] = "Hors Apogée (" + FLAG[PAS_DE_NIP] + ")"
self.titres[PAS_D_ETAPE] = "Sans étape (" + FLAG[PAS_D_ETAPE] + ")"
self.titres[PLUSIEURS_ETAPES] = (
"Plusieurs etapes (" + FLAG[PLUSIEURS_ETAPES] + ")"
)
for key_etape in liste_etapes:
col_id = self.indicatifs[key_etape]
col_ids.append(col_id)
self.titres[col_id] = "%s
%s" % key_to_values(key_etape)
col_ids.append(COL_CUMUL)
self.titres[COL_CUMUL] = "Total
semestre"
rows = []
for semestre in liste_semestres:
ind_row = self.indicatifs[semestre]
self.titres[ind_row] = (
"%(titre_num)s (%(formsemestre_id)s)" % self.semestres[semestre]
)
row = {
"row_title": self.link_semestre(semestre),
PAS_DE_NIP: self._get_count(ind_row, PAS_DE_NIP),
PAS_D_ETAPE: self._get_count(ind_row, PAS_D_ETAPE),
PLUSIEURS_ETAPES: self._get_count(ind_row, PLUSIEURS_ETAPES),
COL_CUMUL: self._get_count(ind_row, COL_CUMUL),
"_css_row_class": ind_row,
}
for key_etape in liste_etapes:
ind_col = self.indicatifs[key_etape]
row[ind_col] = self._get_count(ind_row, ind_col)
rows.append(row)
if PAS_DE_SEMESTRE in self.tag_count:
row = {
"row_title": "Hors semestres (" + FLAG[PAS_DE_SEMESTRE] + ")",
PAS_DE_NIP: "",
PAS_D_ETAPE: "",
PLUSIEURS_ETAPES: "",
COL_CUMUL: self._get_count(PAS_DE_SEMESTRE, COL_CUMUL),
"_css_row_class": PAS_DE_SEMESTRE,
}
for key_etape in liste_etapes:
ind_col = self.indicatifs[key_etape]
row[ind_col] = self._get_count(PAS_DE_SEMESTRE, ind_col)
rows.append(row)
if PLUSIEURS_SEMESTRES in self.tag_count:
row = {
"row_title": "Plusieurs semestres (" + FLAG[PLUSIEURS_SEMESTRES] + ")",
PAS_DE_NIP: "",
PAS_D_ETAPE: "",
PLUSIEURS_ETAPES: "",
COL_CUMUL: self._get_count(PLUSIEURS_SEMESTRES, COL_CUMUL),
"_css_row_class": PLUSIEURS_SEMESTRES,
}
for key_etape in liste_etapes:
ind_col = self.indicatifs[key_etape]
row[ind_col] = self._get_count(PLUSIEURS_SEMESTRES, ind_col)
rows.append(row)
row = {
"row_title": "Total",
PAS_DE_NIP: self._get_count(ROW_CUMUL, PAS_DE_NIP),
PAS_D_ETAPE: self._get_count(ROW_CUMUL, PAS_D_ETAPE),
PLUSIEURS_ETAPES: self._get_count(ROW_CUMUL, PLUSIEURS_ETAPES),
COL_CUMUL: self._get_count(ROW_CUMUL, COL_CUMUL),
"_css_row_class": COL_CUMUL,
}
for key_etape in liste_etapes:
ind_col = self.indicatifs[key_etape]
row[ind_col] = self._get_count(ROW_CUMUL, ind_col)
rows.append(row)
H.append(
GenTable(
rows,
col_ids,
self.titres,
html_class="repartition",
html_with_td_classes=True,
).gen(fmt="html")
)
return "\n".join(H)
def display_tags(self):
H = []
if NIP_NON_UNIQUE in self.tag_count:
H.append("
Anomalies
")
javascript = "show_tag(%s, %s, '%s');" % (
self.all_rows_str,
self.all_cols_str,
NIP_NON_UNIQUE,
)
H.append(
f"""Code(s) nip) partagé(s) par
{self.tag_count[NIP_NON_UNIQUE]}
étudiants
"""
)
return "\n".join(H)
@staticmethod
def link_etu(etudid, nom):
return '
%s' % (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
nom,
)
def link_semestre(self, semestre, short=False):
if short:
return (
'
%('
"formsemestre_id)s " % self.semestres[semestre]
)
else:
return (
'
%(titre_num)s'
" %(mois_debut)s - %(mois_fin)s)" % self.semestres[semestre]
)
def table_effectifs(self):
H = []
col_ids = ["tag", "etudiant", "prenom", "nip", "semestre", "apogee", "annee"]
titles = {
"tag": "Etat",
"etudiant": "Nom",
"prenom": "Prenom",
"nip": "code nip",
"semestre": "semestre",
"annee": "année",
"apogee": "etape",
}
rows = []
for data_etu in sorted(
list(self.etudiants.values()), key=lambda etu: etu.get_identity()
):
nip = data_etu.nip
etudid = data_etu.etudid
if data_etu.data_scodoc is None:
nom = data_etu.data_apogee["nom"]
prenom = data_etu.data_apogee["prenom"]
link = nom
else:
nom = data_etu.data_scodoc["nom"]
prenom = data_etu.data_scodoc["prenom"]
link = self.link_etu(etudid, nom)
tag = ", ".join([tag for tag in sorted(data_etu.tags)])
semestre = "
".join(
[self.link_semestre(sem, True) for sem in data_etu.semestres]
)
annees = "
".join([etape[0] for etape in data_etu.etapes])
etapes = "
".join([etape[1] for etape in data_etu.etapes])
classe = data_etu.ind_row + data_etu.ind_col
if NIP_NON_UNIQUE in data_etu.tags:
classe += " " + NIP_NON_UNIQUE
row = {
"tag": tag,
"etudiant": link,
"prenom": prenom.capitalize(),
"nip": nip,
"semestre": semestre,
"annee": annees,
"apogee": etapes,
"_css_row_class": classe,
}
rows.append(row)
H.append(
GenTable(
rows,
col_ids,
titles,
table_id="detail",
html_class="table_leftalign",
html_sortable=True,
).gen(fmt="html")
)
return "\n".join(H)
def etape_to_key(annee_apogee, etape_str):
return annee_apogee, etape_str
def key_to_values(key_etape):
return key_etape
def etape_to_col(key_etape):
return "%s@%s" % key_etape