+
+ """,
+ html_with_td_classes=True,
+ origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
+ page_title=title,
+ pdf_title=title,
+ preferences=sco_preferences.SemPreferences(),
+ rows=rows,
+ table_id="formation_table_recap",
+ titles=titles,
+ xls_columns_width={
+ "nom": 32,
+ "cursus": 12,
+ "ues": 32,
+ "niveaux": 32,
+ "decision_but": 14,
+ "diplome": 17,
+ "devenir": 8,
+ "observations": 12,
+ },
+ xls_style_base=xls_style_base,
+ )
+ return tab.make_page(format=format, javascripts=["js/etud_info.js"], init_qtip=True)
diff --git a/app/pe/pe_avislatex.py b/app/pe/pe_avislatex.py
index f4062ad9..5a507738 100644
--- a/app/pe/pe_avislatex.py
+++ b/app/pe/pe_avislatex.py
@@ -1,517 +1,517 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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)
-##############################################################################
-
-import os
-import codecs
-import re
-from app.pe import pe_tagtable
-from app.pe import pe_jurype
-from app.pe import pe_tools
-
-import app.scodoc.sco_utils as scu
-import app.scodoc.notesdb as ndb
-from app import log
-from app.scodoc.gen_tables import GenTable, SeqGenTable
-from app.scodoc import sco_preferences
-from app.scodoc import sco_etud
-
-
-DEBUG = False # Pour debug et repérage des prints à changer en Log
-
-DONNEE_MANQUANTE = (
- "" # Caractère de remplacement des données manquantes dans un avis PE
-)
-
-# ----------------------------------------------------------------------------------------
-def get_code_latex_from_modele(fichier):
- """Lit le code latex à partir d'un modèle. Renvoie une chaine unicode.
-
- Le fichier doit contenir le chemin relatif
- vers le modele : attention pas de vérification du format d'encodage
- Le fichier doit donc etre enregistré avec le même codage que ScoDoc (utf-8)
- """
- fid_latex = codecs.open(fichier, "r", encoding=scu.SCO_ENCODING)
- un_avis_latex = fid_latex.read()
- fid_latex.close()
- return un_avis_latex
-
-
-# ----------------------------------------------------------------------------------------
-def get_code_latex_from_scodoc_preference(formsemestre_id, champ="pe_avis_latex_tmpl"):
- """
- Extrait le template (ou le tag d'annotation au regard du champ fourni) des préférences LaTeX
- et s'assure qu'il est renvoyé au format unicode
- """
- template_latex = sco_preferences.get_preference(champ, formsemestre_id)
-
- return template_latex or ""
-
-
-# ----------------------------------------------------------------------------------------
-def get_tags_latex(code_latex):
- """Recherche tous les tags présents dans un code latex (ce code étant obtenu
- à la lecture d'un modèle d'avis pe).
- Ces tags sont répérés par les balises **, débutant et finissant le tag
- et sont renvoyés sous la forme d'une liste.
-
- result: liste de chaines unicode
- """
- if code_latex:
- # changé par EV: était r"([\*]{2}[a-zA-Z0-9:éèàâêëïôöù]+[\*]{2})"
- res = re.findall(r"([\*]{2}[^\t\n\r\f\v\*]+[\*]{2})", code_latex)
- return [tag[2:-2] for tag in res]
- else:
- return []
-
-
-def comp_latex_parcourstimeline(etudiant, promo, taille=17):
- """Interprète un tag dans un avis latex **parcourstimeline**
- et génère le code latex permettant de retracer le parcours d'un étudiant
- sous la forme d'une frise temporelle.
- Nota: modeles/parcourstimeline.tex doit avoir été inclu dans le préambule
-
- result: chaine unicode (EV:)
- """
- codelatexDebut = (
- """"
- \\begin{parcourstimeline}{**debut**}{**fin**}{**nbreSemestres**}{%d}
- """
- % taille
- )
-
- modeleEvent = """
- \\parcoursevent{**nosem**}{**nomsem**}{**descr**}
- """
-
- codelatexFin = """
- \\end{parcourstimeline}
- """
- reslatex = codelatexDebut
- reslatex = reslatex.replace("**debut**", etudiant["entree"])
- reslatex = reslatex.replace("**fin**", str(etudiant["promo"]))
- reslatex = reslatex.replace("**nbreSemestres**", str(etudiant["nbSemestres"]))
- # Tri du parcours par ordre croissant : de la forme descr, nom sem date-date
- parcours = etudiant["parcours"][::-1] # EV: XXX je ne comprend pas ce commentaire ?
-
- for no_sem in range(etudiant["nbSemestres"]):
- descr = modeleEvent
- nom_semestre_dans_parcours = parcours[no_sem]["nom_semestre_dans_parcours"]
- descr = descr.replace("**nosem**", str(no_sem + 1))
- if no_sem % 2 == 0:
- descr = descr.replace("**nomsem**", nom_semestre_dans_parcours)
- descr = descr.replace("**descr**", "")
- else:
- descr = descr.replace("**nomsem**", "")
- descr = descr.replace("**descr**", nom_semestre_dans_parcours)
- reslatex += descr
- reslatex += codelatexFin
- return reslatex
-
-
-# ----------------------------------------------------------------------------------------
-def interprete_tag_latex(tag):
- """Découpe les tags latex de la forme S1:groupe:dut:min et renvoie si possible
- le résultat sous la forme d'un quadruplet.
- """
- infotag = tag.split(":")
- if len(infotag) == 4:
- return (
- infotag[0].upper(),
- infotag[1].lower(),
- infotag[2].lower(),
- infotag[3].lower(),
- )
- else:
- return (None, None, None, None)
-
-
-# ----------------------------------------------------------------------------------------
-def get_code_latex_avis_etudiant(
- donnees_etudiant, un_avis_latex, annotationPE, footer_latex, prefs
-):
- """
- Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses
- donnees_etudiant contenu dans le dictionnaire de synthèse du jury PE et en suivant un
- fichier modele donné
-
- result: chaine unicode
- """
- if not donnees_etudiant or not un_avis_latex: # Cas d'un template vide
- return annotationPE if annotationPE else ""
-
- # Le template latex (corps + footer)
- code = un_avis_latex + "\n\n" + footer_latex
-
- # Recherche des tags dans le fichier
- tags_latex = get_tags_latex(code)
- if DEBUG:
- log("Les tags" + str(tags_latex))
-
- # Interprète et remplace chaque tags latex par les données numériques de l'étudiant (y compris les
- # tags "macros" tels que parcourstimeline
- for tag_latex in tags_latex:
- # les tags numériques
- valeur = DONNEE_MANQUANTE
-
- if ":" in tag_latex:
- (aggregat, groupe, tag_scodoc, champ) = interprete_tag_latex(tag_latex)
- valeur = str_from_syntheseJury(
- donnees_etudiant, aggregat, groupe, tag_scodoc, champ
- )
-
- # La macro parcourstimeline
- elif tag_latex == "parcourstimeline":
- valeur = comp_latex_parcourstimeline(
- donnees_etudiant, donnees_etudiant["promo"]
- )
-
- # Le tag annotationPE
- elif tag_latex == "annotation":
- valeur = annotationPE
-
- # Le tag bilanParTag
- elif tag_latex == "bilanParTag":
- valeur = get_bilanParTag(donnees_etudiant)
-
- # Les tags "simples": par ex. nom, prenom, civilite, ...
- else:
- if tag_latex in donnees_etudiant:
- valeur = donnees_etudiant[tag_latex]
- elif tag_latex in prefs: # les champs **NomResponsablePE**, ...
- valeur = pe_tools.escape_for_latex(prefs[tag_latex])
-
- # Vérification des pb d'encodage (debug)
- # assert isinstance(tag_latex, unicode)
- # assert isinstance(valeur, unicode)
-
- # Substitution
- code = code.replace("**" + tag_latex + "**", valeur)
- return code
-
-
-# ----------------------------------------------------------------------------------------
-def get_annotation_PE(etudid, tag_annotation_pe):
- """Renvoie l'annotation PE dans la liste de ces annotations ;
- Cette annotation est reconnue par la présence d'un tag **PE**
- (cf. .get_preferences -> pe_tag_annotation_avis_latex).
-
- Result: chaine unicode
- """
- if tag_annotation_pe:
- cnx = ndb.GetDBConnexion()
- annotations = sco_etud.etud_annotations_list(
- cnx, args={"etudid": etudid}
- ) # Les annotations de l'étudiant
- annotationsPE = []
-
- exp = re.compile(r"^" + tag_annotation_pe)
-
- for a in annotations:
- commentaire = scu.unescape_html(a["comment"])
- if exp.match(commentaire): # tag en début de commentaire ?
- a["comment_u"] = commentaire # unicode, HTML non quoté
- annotationsPE.append(
- a
- ) # sauvegarde l'annotation si elle contient le tag
-
- if annotationsPE: # Si des annotations existent, prend la plus récente
- annotationPE = sorted(annotationsPE, key=lambda a: a["date"], reverse=True)[
- 0
- ]["comment_u"]
-
- annotationPE = exp.sub(
- "", annotationPE
- ) # Suppression du tag d'annotation PE
- annotationPE = annotationPE.replace("\r", "") # Suppression des \r
- annotationPE = annotationPE.replace(
- " ", "\n\n"
- ) # Interprète les retours chariots html
- return annotationPE
- return "" # pas d'annotations
-
-
-# ----------------------------------------------------------------------------------------
-def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ):
- """Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée,
- une valeur indiquée par un champ ;
- si champ est une liste, renvoie la liste des valeurs extraites.
-
- Result: chaine unicode ou liste de chaines unicode
- """
-
- if isinstance(champ, list):
- return [
- str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, chp)
- for chp in champ
- ]
- else: # champ = str à priori
- valeur = DONNEE_MANQUANTE
- if (
- (aggregat in donnees_etudiant)
- and (groupe in donnees_etudiant[aggregat])
- and (tag_scodoc in donnees_etudiant[aggregat][groupe])
- ):
- donnees_numeriques = donnees_etudiant[aggregat][groupe][tag_scodoc]
- if champ == "rang":
- valeur = "%s/%d" % (
- donnees_numeriques[
- pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index("rang")
- ],
- donnees_numeriques[
- pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index(
- "nbinscrits"
- )
- ],
- )
- elif champ in pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS:
- indice_champ = pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index(
- champ
- )
- if (
- len(donnees_numeriques) > indice_champ
- and donnees_numeriques[indice_champ] != None
- ):
- if isinstance(
- donnees_numeriques[indice_champ], float
- ): # valeur numérique avec formattage unicode
- valeur = "%2.2f" % donnees_numeriques[indice_champ]
- else:
- valeur = "%s" % donnees_numeriques[indice_champ]
-
- return valeur
-
-
-# ----------------------------------------------------------------------------------------
-def get_bilanParTag(donnees_etudiant, groupe="groupe"):
- """Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans
- les données étudiants, ses résultats.
- result: chaine unicode
- """
-
- entete = [
- (
- agg,
- pe_jurype.JuryPE.PARCOURS[agg]["affichage_court"],
- pe_jurype.JuryPE.PARCOURS[agg]["ordre"],
- )
- for agg in pe_jurype.JuryPE.PARCOURS
- ]
- entete = sorted(entete, key=lambda t: t[2])
-
- lignes = []
- valeurs = {"note": [], "rang": []}
- for (indice_aggregat, (aggregat, intitule, _)) in enumerate(entete):
- # print("> " + aggregat)
- # listeTags = jury.get_allTagForAggregat(aggregat) # les tags de l'aggrégat
- listeTags = [
- tag for tag in donnees_etudiant[aggregat][groupe].keys() if tag != "dut"
- ] #
- for tag in listeTags:
-
- if tag not in lignes:
- lignes.append(tag)
- valeurs["note"].append(
- [""] * len(entete)
- ) # Ajout d'une ligne de données
- valeurs["rang"].append(
- [""] * len(entete)
- ) # Ajout d'une ligne de données
- indice_tag = lignes.index(tag) # l'indice de ligne du tag
-
- # print(" --- " + tag + "(" + str(indice_tag) + "," + str(indice_aggregat) + ")")
- [note, rang] = str_from_syntheseJury(
- donnees_etudiant, aggregat, groupe, tag, ["note", "rang"]
- )
- valeurs["note"][indice_tag][indice_aggregat] = "" + note + ""
- valeurs["rang"][indice_tag][indice_aggregat] = (
- ("\\textit{" + rang + "}") if note else ""
- ) # rang masqué si pas de notes
-
- code_latex = "\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n"
- code_latex += "\\hline \n"
- code_latex += (
- " & "
- + " & ".join(["\\textbf{" + intitule + "}" for (agg, intitule, _) in entete])
- + " \\\\ \n"
- )
- code_latex += "\\hline"
- code_latex += "\\hline \n"
- for (i, ligne_val) in enumerate(valeurs["note"]):
- titre = lignes[i] # règle le pb d'encodage
- code_latex += "\\textbf{" + titre + "} & " + " & ".join(ligne_val) + "\\\\ \n"
- code_latex += (
- " & "
- + " & ".join(
- ["{\\scriptsize " + clsmt + "}" for clsmt in valeurs["rang"][i]]
- )
- + "\\\\ \n"
- )
- code_latex += "\\hline \n"
- code_latex += "\\end{tabular}"
-
- return code_latex
-
-
-# ----------------------------------------------------------------------------------------
-def get_avis_poursuite_par_etudiant(
- jury, etudid, template_latex, tag_annotation_pe, footer_latex, prefs
-):
- """Renvoie un nom de fichier et le contenu de l'avis latex d'un étudiant dont l'etudid est fourni.
- result: [ chaine unicode, chaine unicode ]
- """
- if pe_tools.PE_DEBUG:
- pe_tools.pe_print(jury.syntheseJury[etudid]["nom"] + " " + str(etudid))
-
- civilite_str = jury.syntheseJury[etudid]["civilite_str"]
- nom = jury.syntheseJury[etudid]["nom"].replace(" ", "-")
- prenom = jury.syntheseJury[etudid]["prenom"].replace(" ", "-")
-
- nom_fichier = scu.sanitize_filename(
- "avis_poursuite_%s_%s_%s" % (nom, prenom, etudid)
- )
- if pe_tools.PE_DEBUG:
- pe_tools.pe_print("fichier latex =" + nom_fichier, type(nom_fichier))
-
- # Entete (commentaire)
- contenu_latex = (
- "%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + "\n"
- )
-
- # les annnotations
- annotationPE = get_annotation_PE(etudid, tag_annotation_pe=tag_annotation_pe)
- if pe_tools.PE_DEBUG:
- pe_tools.pe_print(annotationPE, type(annotationPE))
-
- # le LaTeX
- avis = get_code_latex_avis_etudiant(
- jury.syntheseJury[etudid], template_latex, annotationPE, footer_latex, prefs
- )
- # if pe_tools.PE_DEBUG: pe_tools.pe_print(avis, type(avis))
- contenu_latex += avis + "\n"
-
- return [nom_fichier, contenu_latex]
-
-
-def get_templates_from_distrib(template="avis"):
- """Récupère le template (soit un_avis.tex soit le footer.tex) à partir des fichiers mémorisés dans la distrib des avis pe (distrib local
- ou par défaut et le renvoie"""
- if template == "avis":
- pe_local_tmpl = pe_tools.PE_LOCAL_AVIS_LATEX_TMPL
- pe_default_tmpl = pe_tools.PE_DEFAULT_AVIS_LATEX_TMPL
- elif template == "footer":
- pe_local_tmpl = pe_tools.PE_LOCAL_FOOTER_TMPL
- pe_default_tmpl = pe_tools.PE_DEFAULT_FOOTER_TMPL
-
- if template in ["avis", "footer"]:
- # pas de preference pour le template: utilise fichier du serveur
- if os.path.exists(pe_local_tmpl):
- template_latex = get_code_latex_from_modele(pe_local_tmpl)
- else:
- if os.path.exists(pe_default_tmpl):
- template_latex = get_code_latex_from_modele(pe_default_tmpl)
- else:
- template_latex = "" # fallback: avis vides
- return template_latex
-
-
-# ----------------------------------------------------------------------------------------
-def table_syntheseAnnotationPE(syntheseJury, tag_annotation_pe):
- """Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant"""
- sT = SeqGenTable() # le fichier excel à générer
-
- # Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom
- donnees_tries = sorted(
- [
- (etudid, syntheseJury[etudid]["nom"] + " " + syntheseJury[etudid]["prenom"])
- for etudid in syntheseJury.keys()
- ],
- key=lambda c: c[1],
- )
- etudids = [e[0] for e in donnees_tries]
- if not etudids: # Si pas d'étudiants
- T = GenTable(
- columns_ids=["pas d'étudiants"],
- rows=[],
- titles={"pas d'étudiants": "pas d'étudiants"},
- html_sortable=True,
- xls_sheet_name="dut",
- )
- sT.add_genTable("Annotation PE", T)
- return sT
-
- # Si des étudiants
- maxParcours = max(
- [syntheseJury[etudid]["nbSemestres"] for etudid in etudids]
- ) # le nombre de semestre le + grand
-
- infos = ["civilite", "nom", "prenom", "age", "nbSemestres"]
- entete = ["etudid"]
- entete.extend(infos)
- entete.extend(["P%d" % i for i in range(1, maxParcours + 1)]) # ajout du parcours
- entete.append("Annotation PE")
- columns_ids = entete # les id et les titres de colonnes sont ici identiques
- titles = {i: i for i in columns_ids}
-
- rows = []
- for (
- etudid
- ) in etudids: # parcours des étudiants par ordre alphabétique des nom+prénom
- e = syntheseJury[etudid]
- # Les info générales:
- row = {
- "etudid": etudid,
- "civilite": e["civilite"],
- "nom": e["nom"],
- "prenom": e["prenom"],
- "age": e["age"],
- "nbSemestres": e["nbSemestres"],
- }
- # Les parcours: P1, P2, ...
- n = 1
- for p in e["parcours"]:
- row["P%d" % n] = p["titreannee"]
- n += 1
-
- # L'annotation PE
- annotationPE = get_annotation_PE(etudid, tag_annotation_pe=tag_annotation_pe)
- row["Annotation PE"] = annotationPE if annotationPE else ""
- rows.append(row)
-
- T = GenTable(
- columns_ids=columns_ids,
- rows=rows,
- titles=titles,
- html_sortable=True,
- xls_sheet_name="Annotation PE",
- )
- sT.add_genTable("Annotation PE", T)
- return sT
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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)
+##############################################################################
+
+import os
+import codecs
+import re
+from app.pe import pe_tagtable
+from app.pe import pe_jurype
+from app.pe import pe_tools
+
+import app.scodoc.sco_utils as scu
+import app.scodoc.notesdb as ndb
+from app import log
+from app.scodoc.gen_tables import GenTable, SeqGenTable
+from app.scodoc import sco_preferences
+from app.scodoc import sco_etud
+
+
+DEBUG = False # Pour debug et repérage des prints à changer en Log
+
+DONNEE_MANQUANTE = (
+ "" # Caractère de remplacement des données manquantes dans un avis PE
+)
+
+# ----------------------------------------------------------------------------------------
+def get_code_latex_from_modele(fichier):
+ """Lit le code latex à partir d'un modèle. Renvoie une chaine unicode.
+
+ Le fichier doit contenir le chemin relatif
+ vers le modele : attention pas de vérification du format d'encodage
+ Le fichier doit donc etre enregistré avec le même codage que ScoDoc (utf-8)
+ """
+ fid_latex = codecs.open(fichier, "r", encoding=scu.SCO_ENCODING)
+ un_avis_latex = fid_latex.read()
+ fid_latex.close()
+ return un_avis_latex
+
+
+# ----------------------------------------------------------------------------------------
+def get_code_latex_from_scodoc_preference(formsemestre_id, champ="pe_avis_latex_tmpl"):
+ """
+ Extrait le template (ou le tag d'annotation au regard du champ fourni) des préférences LaTeX
+ et s'assure qu'il est renvoyé au format unicode
+ """
+ template_latex = sco_preferences.get_preference(champ, formsemestre_id)
+
+ return template_latex or ""
+
+
+# ----------------------------------------------------------------------------------------
+def get_tags_latex(code_latex):
+ """Recherche tous les tags présents dans un code latex (ce code étant obtenu
+ à la lecture d'un modèle d'avis pe).
+ Ces tags sont répérés par les balises **, débutant et finissant le tag
+ et sont renvoyés sous la forme d'une liste.
+
+ result: liste de chaines unicode
+ """
+ if code_latex:
+ # changé par EV: était r"([\*]{2}[a-zA-Z0-9:éèàâêëïôöù]+[\*]{2})"
+ res = re.findall(r"([\*]{2}[^\t\n\r\f\v\*]+[\*]{2})", code_latex)
+ return [tag[2:-2] for tag in res]
+ else:
+ return []
+
+
+def comp_latex_parcourstimeline(etudiant, promo, taille=17):
+ """Interprète un tag dans un avis latex **parcourstimeline**
+ et génère le code latex permettant de retracer le parcours d'un étudiant
+ sous la forme d'une frise temporelle.
+ Nota: modeles/parcourstimeline.tex doit avoir été inclu dans le préambule
+
+ result: chaine unicode (EV:)
+ """
+ codelatexDebut = (
+ """"
+ \\begin{parcourstimeline}{**debut**}{**fin**}{**nbreSemestres**}{%d}
+ """
+ % taille
+ )
+
+ modeleEvent = """
+ \\parcoursevent{**nosem**}{**nomsem**}{**descr**}
+ """
+
+ codelatexFin = """
+ \\end{parcourstimeline}
+ """
+ reslatex = codelatexDebut
+ reslatex = reslatex.replace("**debut**", etudiant["entree"])
+ reslatex = reslatex.replace("**fin**", str(etudiant["promo"]))
+ reslatex = reslatex.replace("**nbreSemestres**", str(etudiant["nbSemestres"]))
+ # Tri du parcours par ordre croissant : de la forme descr, nom sem date-date
+ parcours = etudiant["parcours"][::-1] # EV: XXX je ne comprend pas ce commentaire ?
+
+ for no_sem in range(etudiant["nbSemestres"]):
+ descr = modeleEvent
+ nom_semestre_dans_parcours = parcours[no_sem]["nom_semestre_dans_parcours"]
+ descr = descr.replace("**nosem**", str(no_sem + 1))
+ if no_sem % 2 == 0:
+ descr = descr.replace("**nomsem**", nom_semestre_dans_parcours)
+ descr = descr.replace("**descr**", "")
+ else:
+ descr = descr.replace("**nomsem**", "")
+ descr = descr.replace("**descr**", nom_semestre_dans_parcours)
+ reslatex += descr
+ reslatex += codelatexFin
+ return reslatex
+
+
+# ----------------------------------------------------------------------------------------
+def interprete_tag_latex(tag):
+ """Découpe les tags latex de la forme S1:groupe:dut:min et renvoie si possible
+ le résultat sous la forme d'un quadruplet.
+ """
+ infotag = tag.split(":")
+ if len(infotag) == 4:
+ return (
+ infotag[0].upper(),
+ infotag[1].lower(),
+ infotag[2].lower(),
+ infotag[3].lower(),
+ )
+ else:
+ return (None, None, None, None)
+
+
+# ----------------------------------------------------------------------------------------
+def get_code_latex_avis_etudiant(
+ donnees_etudiant, un_avis_latex, annotationPE, footer_latex, prefs
+):
+ """
+ Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses
+ donnees_etudiant contenu dans le dictionnaire de synthèse du jury PE et en suivant un
+ fichier modele donné
+
+ result: chaine unicode
+ """
+ if not donnees_etudiant or not un_avis_latex: # Cas d'un template vide
+ return annotationPE if annotationPE else ""
+
+ # Le template latex (corps + footer)
+ code = un_avis_latex + "\n\n" + footer_latex
+
+ # Recherche des tags dans le fichier
+ tags_latex = get_tags_latex(code)
+ if DEBUG:
+ log("Les tags" + str(tags_latex))
+
+ # Interprète et remplace chaque tags latex par les données numériques de l'étudiant (y compris les
+ # tags "macros" tels que parcourstimeline
+ for tag_latex in tags_latex:
+ # les tags numériques
+ valeur = DONNEE_MANQUANTE
+
+ if ":" in tag_latex:
+ (aggregat, groupe, tag_scodoc, champ) = interprete_tag_latex(tag_latex)
+ valeur = str_from_syntheseJury(
+ donnees_etudiant, aggregat, groupe, tag_scodoc, champ
+ )
+
+ # La macro parcourstimeline
+ elif tag_latex == "parcourstimeline":
+ valeur = comp_latex_parcourstimeline(
+ donnees_etudiant, donnees_etudiant["promo"]
+ )
+
+ # Le tag annotationPE
+ elif tag_latex == "annotation":
+ valeur = annotationPE
+
+ # Le tag bilanParTag
+ elif tag_latex == "bilanParTag":
+ valeur = get_bilanParTag(donnees_etudiant)
+
+ # Les tags "simples": par ex. nom, prenom, civilite, ...
+ else:
+ if tag_latex in donnees_etudiant:
+ valeur = donnees_etudiant[tag_latex]
+ elif tag_latex in prefs: # les champs **NomResponsablePE**, ...
+ valeur = pe_tools.escape_for_latex(prefs[tag_latex])
+
+ # Vérification des pb d'encodage (debug)
+ # assert isinstance(tag_latex, unicode)
+ # assert isinstance(valeur, unicode)
+
+ # Substitution
+ code = code.replace("**" + tag_latex + "**", valeur)
+ return code
+
+
+# ----------------------------------------------------------------------------------------
+def get_annotation_PE(etudid, tag_annotation_pe):
+ """Renvoie l'annotation PE dans la liste de ces annotations ;
+ Cette annotation est reconnue par la présence d'un tag **PE**
+ (cf. .get_preferences -> pe_tag_annotation_avis_latex).
+
+ Result: chaine unicode
+ """
+ if tag_annotation_pe:
+ cnx = ndb.GetDBConnexion()
+ annotations = sco_etud.etud_annotations_list(
+ cnx, args={"etudid": etudid}
+ ) # Les annotations de l'étudiant
+ annotationsPE = []
+
+ exp = re.compile(r"^" + tag_annotation_pe)
+
+ for a in annotations:
+ commentaire = scu.unescape_html(a["comment"])
+ if exp.match(commentaire): # tag en début de commentaire ?
+ a["comment_u"] = commentaire # unicode, HTML non quoté
+ annotationsPE.append(
+ a
+ ) # sauvegarde l'annotation si elle contient le tag
+
+ if annotationsPE: # Si des annotations existent, prend la plus récente
+ annotationPE = sorted(annotationsPE, key=lambda a: a["date"], reverse=True)[
+ 0
+ ]["comment_u"]
+
+ annotationPE = exp.sub(
+ "", annotationPE
+ ) # Suppression du tag d'annotation PE
+ annotationPE = annotationPE.replace("\r", "") # Suppression des \r
+ annotationPE = annotationPE.replace(
+ " ", "\n\n"
+ ) # Interprète les retours chariots html
+ return annotationPE
+ return "" # pas d'annotations
+
+
+# ----------------------------------------------------------------------------------------
+def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ):
+ """Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée,
+ une valeur indiquée par un champ ;
+ si champ est une liste, renvoie la liste des valeurs extraites.
+
+ Result: chaine unicode ou liste de chaines unicode
+ """
+
+ if isinstance(champ, list):
+ return [
+ str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, chp)
+ for chp in champ
+ ]
+ else: # champ = str à priori
+ valeur = DONNEE_MANQUANTE
+ if (
+ (aggregat in donnees_etudiant)
+ and (groupe in donnees_etudiant[aggregat])
+ and (tag_scodoc in donnees_etudiant[aggregat][groupe])
+ ):
+ donnees_numeriques = donnees_etudiant[aggregat][groupe][tag_scodoc]
+ if champ == "rang":
+ valeur = "%s/%d" % (
+ donnees_numeriques[
+ pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index("rang")
+ ],
+ donnees_numeriques[
+ pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index(
+ "nbinscrits"
+ )
+ ],
+ )
+ elif champ in pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS:
+ indice_champ = pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index(
+ champ
+ )
+ if (
+ len(donnees_numeriques) > indice_champ
+ and donnees_numeriques[indice_champ] != None
+ ):
+ if isinstance(
+ donnees_numeriques[indice_champ], float
+ ): # valeur numérique avec formattage unicode
+ valeur = "%2.2f" % donnees_numeriques[indice_champ]
+ else:
+ valeur = "%s" % donnees_numeriques[indice_champ]
+
+ return valeur
+
+
+# ----------------------------------------------------------------------------------------
+def get_bilanParTag(donnees_etudiant, groupe="groupe"):
+ """Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans
+ les données étudiants, ses résultats.
+ result: chaine unicode
+ """
+
+ entete = [
+ (
+ agg,
+ pe_jurype.JuryPE.PARCOURS[agg]["affichage_court"],
+ pe_jurype.JuryPE.PARCOURS[agg]["ordre"],
+ )
+ for agg in pe_jurype.JuryPE.PARCOURS
+ ]
+ entete = sorted(entete, key=lambda t: t[2])
+
+ lignes = []
+ valeurs = {"note": [], "rang": []}
+ for (indice_aggregat, (aggregat, intitule, _)) in enumerate(entete):
+ # print("> " + aggregat)
+ # listeTags = jury.get_allTagForAggregat(aggregat) # les tags de l'aggrégat
+ listeTags = [
+ tag for tag in donnees_etudiant[aggregat][groupe].keys() if tag != "dut"
+ ] #
+ for tag in listeTags:
+
+ if tag not in lignes:
+ lignes.append(tag)
+ valeurs["note"].append(
+ [""] * len(entete)
+ ) # Ajout d'une ligne de données
+ valeurs["rang"].append(
+ [""] * len(entete)
+ ) # Ajout d'une ligne de données
+ indice_tag = lignes.index(tag) # l'indice de ligne du tag
+
+ # print(" --- " + tag + "(" + str(indice_tag) + "," + str(indice_aggregat) + ")")
+ [note, rang] = str_from_syntheseJury(
+ donnees_etudiant, aggregat, groupe, tag, ["note", "rang"]
+ )
+ valeurs["note"][indice_tag][indice_aggregat] = "" + note + ""
+ valeurs["rang"][indice_tag][indice_aggregat] = (
+ ("\\textit{" + rang + "}") if note else ""
+ ) # rang masqué si pas de notes
+
+ code_latex = "\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n"
+ code_latex += "\\hline \n"
+ code_latex += (
+ " & "
+ + " & ".join(["\\textbf{" + intitule + "}" for (agg, intitule, _) in entete])
+ + " \\\\ \n"
+ )
+ code_latex += "\\hline"
+ code_latex += "\\hline \n"
+ for (i, ligne_val) in enumerate(valeurs["note"]):
+ titre = lignes[i] # règle le pb d'encodage
+ code_latex += "\\textbf{" + titre + "} & " + " & ".join(ligne_val) + "\\\\ \n"
+ code_latex += (
+ " & "
+ + " & ".join(
+ ["{\\scriptsize " + clsmt + "}" for clsmt in valeurs["rang"][i]]
+ )
+ + "\\\\ \n"
+ )
+ code_latex += "\\hline \n"
+ code_latex += "\\end{tabular}"
+
+ return code_latex
+
+
+# ----------------------------------------------------------------------------------------
+def get_avis_poursuite_par_etudiant(
+ jury, etudid, template_latex, tag_annotation_pe, footer_latex, prefs
+):
+ """Renvoie un nom de fichier et le contenu de l'avis latex d'un étudiant dont l'etudid est fourni.
+ result: [ chaine unicode, chaine unicode ]
+ """
+ if pe_tools.PE_DEBUG:
+ pe_tools.pe_print(jury.syntheseJury[etudid]["nom"] + " " + str(etudid))
+
+ civilite_str = jury.syntheseJury[etudid]["civilite_str"]
+ nom = jury.syntheseJury[etudid]["nom"].replace(" ", "-")
+ prenom = jury.syntheseJury[etudid]["prenom"].replace(" ", "-")
+
+ nom_fichier = scu.sanitize_filename(
+ "avis_poursuite_%s_%s_%s" % (nom, prenom, etudid)
+ )
+ if pe_tools.PE_DEBUG:
+ pe_tools.pe_print("fichier latex =" + nom_fichier, type(nom_fichier))
+
+ # Entete (commentaire)
+ contenu_latex = (
+ "%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + "\n"
+ )
+
+ # les annnotations
+ annotationPE = get_annotation_PE(etudid, tag_annotation_pe=tag_annotation_pe)
+ if pe_tools.PE_DEBUG:
+ pe_tools.pe_print(annotationPE, type(annotationPE))
+
+ # le LaTeX
+ avis = get_code_latex_avis_etudiant(
+ jury.syntheseJury[etudid], template_latex, annotationPE, footer_latex, prefs
+ )
+ # if pe_tools.PE_DEBUG: pe_tools.pe_print(avis, type(avis))
+ contenu_latex += avis + "\n"
+
+ return [nom_fichier, contenu_latex]
+
+
+def get_templates_from_distrib(template="avis"):
+ """Récupère le template (soit un_avis.tex soit le footer.tex) à partir des fichiers mémorisés dans la distrib des avis pe (distrib local
+ ou par défaut et le renvoie"""
+ if template == "avis":
+ pe_local_tmpl = pe_tools.PE_LOCAL_AVIS_LATEX_TMPL
+ pe_default_tmpl = pe_tools.PE_DEFAULT_AVIS_LATEX_TMPL
+ elif template == "footer":
+ pe_local_tmpl = pe_tools.PE_LOCAL_FOOTER_TMPL
+ pe_default_tmpl = pe_tools.PE_DEFAULT_FOOTER_TMPL
+
+ if template in ["avis", "footer"]:
+ # pas de preference pour le template: utilise fichier du serveur
+ if os.path.exists(pe_local_tmpl):
+ template_latex = get_code_latex_from_modele(pe_local_tmpl)
+ else:
+ if os.path.exists(pe_default_tmpl):
+ template_latex = get_code_latex_from_modele(pe_default_tmpl)
+ else:
+ template_latex = "" # fallback: avis vides
+ return template_latex
+
+
+# ----------------------------------------------------------------------------------------
+def table_syntheseAnnotationPE(syntheseJury, tag_annotation_pe):
+ """Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant"""
+ sT = SeqGenTable() # le fichier excel à générer
+
+ # Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom
+ donnees_tries = sorted(
+ [
+ (etudid, syntheseJury[etudid]["nom"] + " " + syntheseJury[etudid]["prenom"])
+ for etudid in syntheseJury.keys()
+ ],
+ key=lambda c: c[1],
+ )
+ etudids = [e[0] for e in donnees_tries]
+ if not etudids: # Si pas d'étudiants
+ T = GenTable(
+ columns_ids=["pas d'étudiants"],
+ rows=[],
+ titles={"pas d'étudiants": "pas d'étudiants"},
+ html_sortable=True,
+ xls_sheet_name="dut",
+ )
+ sT.add_genTable("Annotation PE", T)
+ return sT
+
+ # Si des étudiants
+ maxParcours = max(
+ [syntheseJury[etudid]["nbSemestres"] for etudid in etudids]
+ ) # le nombre de semestre le + grand
+
+ infos = ["civilite", "nom", "prenom", "age", "nbSemestres"]
+ entete = ["etudid"]
+ entete.extend(infos)
+ entete.extend(["P%d" % i for i in range(1, maxParcours + 1)]) # ajout du parcours
+ entete.append("Annotation PE")
+ columns_ids = entete # les id et les titres de colonnes sont ici identiques
+ titles = {i: i for i in columns_ids}
+
+ rows = []
+ for (
+ etudid
+ ) in etudids: # parcours des étudiants par ordre alphabétique des nom+prénom
+ e = syntheseJury[etudid]
+ # Les info générales:
+ row = {
+ "etudid": etudid,
+ "civilite": e["civilite"],
+ "nom": e["nom"],
+ "prenom": e["prenom"],
+ "age": e["age"],
+ "nbSemestres": e["nbSemestres"],
+ }
+ # Les parcours: P1, P2, ...
+ n = 1
+ for p in e["parcours"]:
+ row["P%d" % n] = p["titreannee"]
+ n += 1
+
+ # L'annotation PE
+ annotationPE = get_annotation_PE(etudid, tag_annotation_pe=tag_annotation_pe)
+ row["Annotation PE"] = annotationPE if annotationPE else ""
+ rows.append(row)
+
+ T = GenTable(
+ columns_ids=columns_ids,
+ rows=rows,
+ titles=titles,
+ html_sortable=True,
+ xls_sheet_name="Annotation PE",
+ )
+ sT.add_genTable("Annotation PE", T)
+ return sT
diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py
index 79bd0307..06302cd8 100644
--- a/app/pe/pe_view.py
+++ b/app/pe/pe_view.py
@@ -1,180 +1,180 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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)
-##############################################################################
-
-
-"""ScoDoc : interface des fonctions de gestion des avis de poursuites d'étude
-
-"""
-
-from flask import send_file, request
-from app.scodoc.sco_exceptions import ScoValueError
-
-import app.scodoc.sco_utils as scu
-from app.scodoc import sco_formsemestre
-from app.scodoc import html_sco_header
-from app.scodoc import sco_preferences
-
-from app.pe import pe_tools
-from app.pe import pe_jurype
-from app.pe import pe_avislatex
-
-
-def _pe_view_sem_recap_form(formsemestre_id):
- H = [
- html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
- f"""
Génération des avis de poursuites d'études
-
- Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
- poursuites d'études.
-
- De nombreux aspects sont paramétrables:
-
- voir la documentation.
-
-
- """,
- ]
- return "\n".join(H) + html_sco_header.sco_footer()
-
-
-# called from the web, POST or GET
-def pe_view_sem_recap(
- formsemestre_id,
- avis_tmpl_file=None,
- footer_tmpl_file=None,
-):
- """Génération des avis de poursuite d'étude"""
- if request.method == "GET":
- return _pe_view_sem_recap_form(formsemestre_id)
- prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
-
- semBase = sco_formsemestre.get_formsemestre(formsemestre_id)
-
- jury = pe_jurype.JuryPE(semBase)
- # Ajout avis LaTeX au même zip:
- etudids = list(jury.syntheseJury.keys())
-
- # Récupération du template latex, du footer latex et du tag identifiant les annotations relatives aux PE
- # (chaines unicodes, html non quoté)
- template_latex = ""
- # template fourni via le formulaire Web
- if avis_tmpl_file:
- try:
- template_latex = avis_tmpl_file.read().decode("utf-8")
- except UnicodeDecodeError as e:
- raise ScoValueError(
- "Données (template) invalides (caractères non UTF8 ?)"
- ) from e
- else:
- # template indiqué dans préférences ScoDoc ?
- template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
- formsemestre_id, champ="pe_avis_latex_tmpl"
- )
-
- template_latex = template_latex.strip()
- if not template_latex:
- # pas de preference pour le template: utilise fichier du serveur
- template_latex = pe_avislatex.get_templates_from_distrib("avis")
-
- # Footer:
- footer_latex = ""
- # template fourni via le formulaire Web
- if footer_tmpl_file:
- footer_latex = footer_tmpl_file.read().decode("utf-8")
- else:
- footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
- formsemestre_id, champ="pe_avis_latex_footer"
- )
- footer_latex = footer_latex.strip()
- if not footer_latex:
- # pas de preference pour le footer: utilise fichier du serveur
- footer_latex = pe_avislatex.get_templates_from_distrib(
- "footer"
- ) # fallback: footer vides
-
- tag_annotation_pe = pe_avislatex.get_code_latex_from_scodoc_preference(
- formsemestre_id, champ="pe_tag_annotation_avis_latex"
- )
-
- # Ajout des annotations PE dans un fichier excel
- sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe)
- if sT:
- jury.add_file_to_zip(
- jury.NOM_EXPORT_ZIP + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel()
- )
-
- latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex
- for etudid in etudids:
- [nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant(
- jury,
- etudid,
- template_latex,
- tag_annotation_pe,
- footer_latex,
- prefs,
- )
- jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
- latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
-
- # Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
- doc_latex = "\n% -----\n".join(
- ["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
- )
- jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
-
- # Ajoute image, LaTeX class file(s) and modeles
- pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP)
- data = jury.get_zipped_data()
-
- return send_file(
- data,
- mimetype="application/zip",
- download_name=scu.sanitize_filename(jury.NOM_EXPORT_ZIP + ".zip"),
- as_attachment=True,
- )
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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)
+##############################################################################
+
+
+"""ScoDoc : interface des fonctions de gestion des avis de poursuites d'étude
+
+"""
+
+from flask import send_file, request
+from app.scodoc.sco_exceptions import ScoValueError
+
+import app.scodoc.sco_utils as scu
+from app.scodoc import sco_formsemestre
+from app.scodoc import html_sco_header
+from app.scodoc import sco_preferences
+
+from app.pe import pe_tools
+from app.pe import pe_jurype
+from app.pe import pe_avislatex
+
+
+def _pe_view_sem_recap_form(formsemestre_id):
+ H = [
+ html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
+ f"""
Génération des avis de poursuites d'études
+
+ Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
+ poursuites d'études.
+
+ De nombreux aspects sont paramétrables:
+
+ voir la documentation.
+
+
+ """,
+ ]
+ return "\n".join(H) + html_sco_header.sco_footer()
+
+
+# called from the web, POST or GET
+def pe_view_sem_recap(
+ formsemestre_id,
+ avis_tmpl_file=None,
+ footer_tmpl_file=None,
+):
+ """Génération des avis de poursuite d'étude"""
+ if request.method == "GET":
+ return _pe_view_sem_recap_form(formsemestre_id)
+ prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
+
+ semBase = sco_formsemestre.get_formsemestre(formsemestre_id)
+
+ jury = pe_jurype.JuryPE(semBase)
+ # Ajout avis LaTeX au même zip:
+ etudids = list(jury.syntheseJury.keys())
+
+ # Récupération du template latex, du footer latex et du tag identifiant les annotations relatives aux PE
+ # (chaines unicodes, html non quoté)
+ template_latex = ""
+ # template fourni via le formulaire Web
+ if avis_tmpl_file:
+ try:
+ template_latex = avis_tmpl_file.read().decode("utf-8")
+ except UnicodeDecodeError as e:
+ raise ScoValueError(
+ "Données (template) invalides (caractères non UTF8 ?)"
+ ) from e
+ else:
+ # template indiqué dans préférences ScoDoc ?
+ template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
+ formsemestre_id, champ="pe_avis_latex_tmpl"
+ )
+
+ template_latex = template_latex.strip()
+ if not template_latex:
+ # pas de preference pour le template: utilise fichier du serveur
+ template_latex = pe_avislatex.get_templates_from_distrib("avis")
+
+ # Footer:
+ footer_latex = ""
+ # template fourni via le formulaire Web
+ if footer_tmpl_file:
+ footer_latex = footer_tmpl_file.read().decode("utf-8")
+ else:
+ footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
+ formsemestre_id, champ="pe_avis_latex_footer"
+ )
+ footer_latex = footer_latex.strip()
+ if not footer_latex:
+ # pas de preference pour le footer: utilise fichier du serveur
+ footer_latex = pe_avislatex.get_templates_from_distrib(
+ "footer"
+ ) # fallback: footer vides
+
+ tag_annotation_pe = pe_avislatex.get_code_latex_from_scodoc_preference(
+ formsemestre_id, champ="pe_tag_annotation_avis_latex"
+ )
+
+ # Ajout des annotations PE dans un fichier excel
+ sT = pe_avislatex.table_syntheseAnnotationPE(jury.syntheseJury, tag_annotation_pe)
+ if sT:
+ jury.add_file_to_zip(
+ jury.NOM_EXPORT_ZIP + "_annotationsPE" + scu.XLSX_SUFFIX, sT.excel()
+ )
+
+ latex_pages = {} # Dictionnaire de la forme nom_fichier => contenu_latex
+ for etudid in etudids:
+ [nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant(
+ jury,
+ etudid,
+ template_latex,
+ tag_annotation_pe,
+ footer_latex,
+ prefs,
+ )
+ jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
+ latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
+
+ # Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
+ doc_latex = "\n% -----\n".join(
+ ["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
+ )
+ jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
+
+ # Ajoute image, LaTeX class file(s) and modeles
+ pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP)
+ data = jury.get_zipped_data()
+
+ return send_file(
+ data,
+ mimetype="application/zip",
+ download_name=scu.sanitize_filename(jury.NOM_EXPORT_ZIP + ".zip"),
+ as_attachment=True,
+ )
diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py
index 0920ab0e..f3c768a2 100644
--- a/app/scodoc/TrivialFormulator.py
+++ b/app/scodoc/TrivialFormulator.py
@@ -38,6 +38,9 @@ def TrivialFormulator(
html_foot_markup="",
readonly=False,
is_submitted=False,
+ title="",
+ after_table="",
+ before_table="{title}",
):
"""
form_url : URL for this form
@@ -74,7 +77,8 @@ def TrivialFormulator(
HTML elements:
input_type : 'text', 'textarea', 'password',
'radio', 'menu', 'checkbox',
- 'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
+ 'hidden', 'separator', 'table_separator',
+ 'file', 'date', 'datedmy' (avec validation),
'boolcheckbox', 'text_suggest',
'color'
(default text)
@@ -111,6 +115,9 @@ def TrivialFormulator(
html_foot_markup=html_foot_markup,
readonly=readonly,
is_submitted=is_submitted,
+ title=title,
+ after_table=after_table,
+ before_table=before_table,
)
form = t.getform()
if t.canceled():
@@ -144,6 +151,9 @@ class TF(object):
html_foot_markup="", # html snippet put at the end, just after the table
readonly=False,
is_submitted=False,
+ title="",
+ after_table="",
+ before_table="{title}",
):
self.form_url = form_url
self.values = values.copy()
@@ -165,6 +175,9 @@ class TF(object):
self.top_buttons = top_buttons
self.bottom_buttons = bottom_buttons
self.html_foot_markup = html_foot_markup
+ self.title = title
+ self.after_table = after_table
+ self.before_table = before_table
self.readonly = readonly
self.result = None
self.is_submitted = is_submitted
@@ -426,6 +439,7 @@ class TF(object):
R.append('' % self.formid)
if self.top_buttons:
R.append(buttons_markup + "")
+ R.append(self.before_table.format(title=self.title))
R.append('
')
for field, descr in self.formdescription:
if descr.get("readonly", False):
@@ -453,6 +467,16 @@ class TF(object):
etempl = separatortemplate
R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
continue
+ elif input_type == "table_separator":
+ etempl = ""
+ # Table ouverte ?
+ if len([p for p in R if "
' % klass)
@@ -786,7 +812,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
R.append(
'
%s
' % html.escape(self.values[field])
)
- elif input_type == "separator" or input_type == "hidden":
+ elif (
+ input_type == "separator"
+ or input_type == "hidden"
+ or input_type == "table_separator"
+ ):
pass
elif input_type == "file":
R.append("'%s'" % self.values[field])
diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py
index 70a6e814..409e4d13 100644
--- a/app/scodoc/html_sco_header.py
+++ b/app/scodoc/html_sco_header.py
@@ -1,321 +1,321 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""HTML Header/Footer for ScoDoc pages
-"""
-
-import html
-
-from flask import render_template
-from flask import request
-from flask_login import current_user
-
-import app.scodoc.sco_utils as scu
-from app import scodoc_flash_status_messages
-from app.scodoc import html_sidebar
-import sco_version
-
-
-# Some constants:
-
-# Multiselect menus are used on a few pages and not loaded by default
-BOOTSTRAP_MULTISELECT_JS = [
- "libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js",
- "libjs/bootstrap-multiselect/bootstrap-multiselect.js",
- "libjs/purl.js",
-]
-
-BOOTSTRAP_MULTISELECT_CSS = [
- "libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css",
- "libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css",
- "libjs/bootstrap-multiselect/bootstrap-multiselect.css",
-]
-
-
-def standard_html_header():
- """Standard HTML header for pages outside depts"""
- # not used in ZScolar, see sco_header
- return f"""
-
-ScoDoc: accueil
-
-
-
-
-
-
-
-{scu.CUSTOM_HTML_HEADER_CNX}"""
-
-
-def standard_html_footer():
- """Le pied de page HTML de la page d'accueil."""
- return f"""
-Problème de connexion (identifiant, mot de passe): contacter votre responsable ou chef de département.
",
- "authuser": current_user.user_name,
- }
- if bodyOnLoad:
- params["bodyOnLoad_mkup"] = """onload="%s" """ % bodyOnLoad
- else:
- params["bodyOnLoad_mkup"] = ""
- if no_side_bar:
- params["margin_left"] = "1em"
- else:
- params["margin_left"] = "140px"
-
- H = [
- """
-
-
-%(page_title)s
-
-
-
-"""
- % params
- ]
- # jQuery UI
- # can modify loaded theme here
- H.append(
- f'\n'
- )
- if init_google_maps:
- # It may be necessary to add an API key:
- H.append('')
-
- # Feuilles de style additionnelles:
- for cssstyle in cssstyles:
- H.append(
- f"""\n"""
- )
-
- H.append(
- f"""
-
-
-
-
-
-
-"""
- )
-
- # jQuery
- H.append(
- f"""
- """
- )
- # qTip
- if init_qtip:
- H.append(
- f"""
- """
- )
-
- H.append(
- f"""
- """
- )
- if init_google_maps:
- H.append(
- f''
- )
- if init_datatables:
- H.append(
- f"""
- """
- )
- # H.append(
- # f''
- # )
- # JS additionels
- for js in javascripts:
- H.append(f"""\n""")
-
- H.append(
- f"""
-"""
- )
- # Scripts de la page:
- if scripts:
- H.append("""""")
-
- H.append("")
-
- # Body et bandeau haut:
- H.append("""""" % params)
- H.append(scu.CUSTOM_HTML_HEADER)
- #
- if not no_side_bar:
- H.append(html_sidebar.sidebar(etudid))
- H.append("""
""")
- # En attendant le replacement complet de cette fonction,
- # inclusion ici des messages flask
- H.append(render_template("flashed_messages.html"))
- #
- # Barre menu semestre:
- H.append(formsemestre_page_title(formsemestre_id))
-
- # Avertissement si mot de passe à changer
- if user_check:
- if current_user.passwd_temp:
- H.append(
- f"""
- Attention !
- Vous avez reçu un mot de passe temporaire.
- Vous devez le changer: cliquez ici
-
""" + scu.CUSTOM_HTML_FOOTER + """"""
- )
-
-
-def html_sem_header(
- title, with_page_header=True, with_h2=True, page_title=None, **args
-):
- "Titre d'une page semestre avec lien vers tableau de bord"
- # sem now unused and thus optional...
- if with_page_header:
- h = sco_header(page_title="%s" % (page_title or title), **args)
- else:
- h = ""
- if with_h2:
- return h + f"""
{title}
"""
- else:
- return h
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""HTML Header/Footer for ScoDoc pages
+"""
+
+import html
+
+from flask import render_template
+from flask import request
+from flask_login import current_user
+
+import app.scodoc.sco_utils as scu
+from app import scodoc_flash_status_messages
+from app.scodoc import html_sidebar
+import sco_version
+
+
+# Some constants:
+
+# Multiselect menus are used on a few pages and not loaded by default
+BOOTSTRAP_MULTISELECT_JS = [
+ "libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js",
+ "libjs/bootstrap-multiselect/bootstrap-multiselect.js",
+ "libjs/purl.js",
+]
+
+BOOTSTRAP_MULTISELECT_CSS = [
+ "libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css",
+ "libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css",
+ "libjs/bootstrap-multiselect/bootstrap-multiselect.css",
+]
+
+
+def standard_html_header():
+ """Standard HTML header for pages outside depts"""
+ # not used in ZScolar, see sco_header
+ return f"""
+
+ScoDoc: accueil
+
+
+
+
+
+
+
+{scu.CUSTOM_HTML_HEADER_CNX}"""
+
+
+def standard_html_footer():
+ """Le pied de page HTML de la page d'accueil."""
+ return f"""
+Problème de connexion (identifiant, mot de passe): contacter votre responsable ou chef de département.
",
+ "authuser": current_user.user_name,
+ }
+ if bodyOnLoad:
+ params["bodyOnLoad_mkup"] = """onload="%s" """ % bodyOnLoad
+ else:
+ params["bodyOnLoad_mkup"] = ""
+ if no_side_bar:
+ params["margin_left"] = "1em"
+ else:
+ params["margin_left"] = "140px"
+
+ H = [
+ """
+
+
+%(page_title)s
+
+
+
+"""
+ % params
+ ]
+ # jQuery UI
+ # can modify loaded theme here
+ H.append(
+ f'\n'
+ )
+ if init_google_maps:
+ # It may be necessary to add an API key:
+ H.append('')
+
+ # Feuilles de style additionnelles:
+ for cssstyle in cssstyles:
+ H.append(
+ f"""\n"""
+ )
+
+ H.append(
+ f"""
+
+
+
+
+
+
+"""
+ )
+
+ # jQuery
+ H.append(
+ f"""
+ """
+ )
+ # qTip
+ if init_qtip:
+ H.append(
+ f"""
+ """
+ )
+
+ H.append(
+ f"""
+ """
+ )
+ if init_google_maps:
+ H.append(
+ f''
+ )
+ if init_datatables:
+ H.append(
+ f"""
+ """
+ )
+ # H.append(
+ # f''
+ # )
+ # JS additionels
+ for js in javascripts:
+ H.append(f"""\n""")
+
+ H.append(
+ f"""
+"""
+ )
+ # Scripts de la page:
+ if scripts:
+ H.append("""""")
+
+ H.append("")
+
+ # Body et bandeau haut:
+ H.append("""""" % params)
+ H.append(scu.CUSTOM_HTML_HEADER)
+ #
+ if not no_side_bar:
+ H.append(html_sidebar.sidebar(etudid))
+ H.append("""
""")
+ # En attendant le replacement complet de cette fonction,
+ # inclusion ici des messages flask
+ H.append(render_template("flashed_messages.html"))
+ #
+ # Barre menu semestre:
+ H.append(formsemestre_page_title(formsemestre_id))
+
+ # Avertissement si mot de passe à changer
+ if user_check:
+ if current_user.passwd_temp:
+ H.append(
+ f"""
+ Attention !
+ Vous avez reçu un mot de passe temporaire.
+ Vous devez le changer: cliquez ici
+
""" + scu.CUSTOM_HTML_FOOTER + """"""
+ )
+
+
+def html_sem_header(
+ title, with_page_header=True, with_h2=True, page_title=None, **args
+):
+ "Titre d'une page semestre avec lien vers tableau de bord"
+ # sem now unused and thus optional...
+ if with_page_header:
+ h = sco_header(page_title="%s" % (page_title or title), **args)
+ else:
+ h = ""
+ if with_h2:
+ return h + f"""
{title}
"""
+ else:
+ return h
diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py
index 87e200b5..45497ff2 100644
--- a/app/scodoc/html_sidebar.py
+++ b/app/scodoc/html_sidebar.py
@@ -1,172 +1,172 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""
-Génération de la "sidebar" (marge gauche des pages HTML)
-"""
-from flask import render_template, url_for
-from flask import g, request
-from flask_login import current_user
-
-import app.scodoc.sco_utils as scu
-from app.scodoc import sco_preferences
-from app.scodoc.sco_permissions import Permission
-from sco_version import SCOVERSION
-
-
-def sidebar_common():
- "partie commune à toutes les sidebar"
- home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
- H = [
- f"""ScoDoc {SCOVERSION}
- Accueil
-
-
- """
- )
- return "".join(H)
-
-
-def sidebar_dept():
- """Partie supérieure de la marge de gauche"""
- return render_template(
- "sidebar_dept.html",
- prefs=sco_preferences.SemPreferences(),
- )
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""
+Génération de la "sidebar" (marge gauche des pages HTML)
+"""
+from flask import render_template, url_for
+from flask import g, request
+from flask_login import current_user
+
+import app.scodoc.sco_utils as scu
+from app.scodoc import sco_preferences
+from app.scodoc.sco_permissions import Permission
+from sco_version import SCOVERSION
+
+
+def sidebar_common():
+ "partie commune à toutes les sidebar"
+ home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
+ H = [
+ f"""ScoDoc {SCOVERSION}
+ Accueil
+
+
+ """
+ )
+ return "".join(H)
+
+
+def sidebar_dept():
+ """Partie supérieure de la marge de gauche"""
+ return render_template(
+ "sidebar_dept.html",
+ prefs=sco_preferences.SemPreferences(),
+ )
diff --git a/app/scodoc/safehtml.py b/app/scodoc/safehtml.py
index a11ebf52..2988a0f3 100644
--- a/app/scodoc/safehtml.py
+++ b/app/scodoc/safehtml.py
@@ -1,80 +1,80 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-from html.parser import HTMLParser
-
-
-"""HTML sanitizing function
- used to clean user submitted HTML
- (Python 3 only)
-"""
-
-# permet de conserver les liens
-def html_to_safe_html(text, convert_br=True): # was HTML2SafeHTML
- # text = html_to_safe_html(text, valid_tags=("b", "a", "i", "br", "p"))
- # New version (jul 2021) with our own parser
- text = convert_html_to_text(text)
- if convert_br:
- return newline_to_br(text)
- else:
- return text
-
-
-def convert_html_to_text(s):
- parser = HTMLSanitizer()
- parser.feed(s)
- return parser.text
-
-
-def newline_to_br(text):
- return text.replace("\n", " ")
-
-
-class HTMLSanitizer(HTMLParser):
- def __init__(self, allowed_tags=("i", "b", "em", "br", "p"), **kwargs):
- super(HTMLSanitizer, self).__init__(**kwargs)
- self.allowed_tags = set(allowed_tags)
- self.text = ""
-
- def handle_starttag(self, tag, attrs):
- if tag in self.allowed_tags:
- self.text += "<{} {}>".format(
- tag, ", ".join(['{}="{}"'.format(k, v) for (k, v) in attrs])
- )
-
- def handle_endtag(self, tag):
- if tag in self.allowed_tags:
- self.text += "" + tag + ">"
-
- def handle_data(self, data):
- self.text += data
-
-
-if __name__ == "__main__":
- test_parser = HTMLSanitizer()
- test_parser.feed("""
Hello world grasitalique
""")
- print(test_parser.text)
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+from html.parser import HTMLParser
+
+
+"""HTML sanitizing function
+ used to clean user submitted HTML
+ (Python 3 only)
+"""
+
+# permet de conserver les liens
+def html_to_safe_html(text, convert_br=True): # was HTML2SafeHTML
+ # text = html_to_safe_html(text, valid_tags=("b", "a", "i", "br", "p"))
+ # New version (jul 2021) with our own parser
+ text = convert_html_to_text(text)
+ if convert_br:
+ return newline_to_br(text)
+ else:
+ return text
+
+
+def convert_html_to_text(s):
+ parser = HTMLSanitizer()
+ parser.feed(s)
+ return parser.text
+
+
+def newline_to_br(text):
+ return text.replace("\n", " ")
+
+
+class HTMLSanitizer(HTMLParser):
+ def __init__(self, allowed_tags=("i", "b", "em", "br", "p"), **kwargs):
+ super(HTMLSanitizer, self).__init__(**kwargs)
+ self.allowed_tags = set(allowed_tags)
+ self.text = ""
+
+ def handle_starttag(self, tag, attrs):
+ if tag in self.allowed_tags:
+ self.text += "<{} {}>".format(
+ tag, ", ".join(['{}="{}"'.format(k, v) for (k, v) in attrs])
+ )
+
+ def handle_endtag(self, tag):
+ if tag in self.allowed_tags:
+ self.text += "" + tag + ">"
+
+ def handle_data(self, data):
+ self.text += data
+
+
+if __name__ == "__main__":
+ test_parser = HTMLSanitizer()
+ test_parser.feed("""
Hello world grasitalique
""")
+ print(test_parser.text)
diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py
index b8392fcf..98faeb51 100644
--- a/app/scodoc/sco_abs_views.py
+++ b/app/scodoc/sco_abs_views.py
@@ -1,1043 +1,1043 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""Pages HTML gestion absences
- (la plupart portées du DTML)
-"""
-import datetime
-
-from flask import url_for, g, request, abort
-
-from app import log
-from app.comp import res_sem
-from app.comp.res_compat import NotesTableCompat
-from app.models import Identite, FormSemestre
-import app.scodoc.sco_utils as scu
-from app.scodoc import notesdb as ndb
-from app.scodoc.scolog import logdb
-from app.scodoc.gen_tables import GenTable
-from app.scodoc import html_sco_header
-from app.scodoc import sco_abs
-from app.scodoc import sco_etud
-from app.scodoc import sco_find_etud
-from app.scodoc import sco_formsemestre
-from app.scodoc import sco_groups
-from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_photos
-from app.scodoc import sco_preferences
-from app.scodoc.sco_exceptions import ScoValueError
-
-
-def doSignaleAbsence(
- datedebut,
- datefin,
- moduleimpl_id=None,
- demijournee=2,
- estjust=False,
- description=None,
- etudid=False,
-): # etudid implied
- """Signalement d'une absence.
-
- Args:
- datedebut: dd/mm/yyyy
- datefin: dd/mm/yyyy (non incluse)
- moduleimpl_id: module auquel imputer les absences
- demijournee: 2 si journée complète, 1 matin, 0 après-midi
- estjust: absence justifiée
- description: str
- etudid: etudiant concerné. Si non spécifié, cherche dans
- les paramètres de la requête courante.
- """
- etud = Identite.from_request(etudid)
-
- if not moduleimpl_id:
- moduleimpl_id = None
- description_abs = description
- dates = sco_abs.DateRangeISO(datedebut, datefin)
- nbadded = 0
- demijournee = int(demijournee)
- for jour in dates:
- if demijournee == 2:
- sco_abs.add_absence(
- etud.id,
- jour,
- False,
- estjust,
- description_abs,
- moduleimpl_id,
- )
- sco_abs.add_absence(
- etud.id,
- jour,
- True,
- estjust,
- description_abs,
- moduleimpl_id,
- )
- nbadded += 2
- else:
- sco_abs.add_absence(
- etud.id,
- jour,
- demijournee,
- estjust,
- description_abs,
- moduleimpl_id,
- )
- nbadded += 1
- #
- if estjust:
- J = ""
- else:
- J = "NON "
- indication_module = ""
- if moduleimpl_id and moduleimpl_id != "NULL":
- mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
- formsemestre_id = mod["formsemestre_id"]
- formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
- ues = nt.get_ues_stat_dict()
- for ue in ues:
- modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"])
- for modimpl in modimpls:
- if modimpl["moduleimpl_id"] == moduleimpl_id:
- indication_module = "dans le module %s" % (
- modimpl["module"]["code"] or "(pas de code)"
- )
- H = [
- html_sco_header.sco_header(
- page_title=f"Signalement d'une absence pour {etud.nomprenom}",
- ),
- """
importer de nouveaux étudiants
- (ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
- le tableau de bord semestre si vous souhaitez inscrire les
- étudiants importés à un semestre)
-
- """
-
- # Liste des semestres, groupés par modalités
- sems_by_mod, modalites = sco_modalites.group_sems_by_modalite(sems)
-
- H = ['
']
- for modalite in modalites:
- if len(modalites) > 1:
- H.append('
%s
' % modalite["titre"])
-
- if sems_by_mod[modalite["modalite"]]:
- cur_idx = sems_by_mod[modalite["modalite"]][0]["semestre_id"]
- for sem in sems_by_mod[modalite["modalite"]]:
- if cur_idx != sem["semestre_id"]:
- sem["trclass"] = "firstsem" # separe les groupes de semestres
- cur_idx = sem["semestre_id"]
- else:
- sem["trclass"] = ""
- sem["notes_url"] = scu.NotesURL()
- H.append(tmpl % sem)
- H.append("
")
- return "\n".join(H)
-
-
-def _sem_table_gt(sems, showcodes=False):
- """Nouvelle version de la table des semestres
- Utilise une datatables.
- """
- _style_sems(sems)
- columns_ids = (
- "lockimg",
- "semestre_id_n",
- "modalite",
- #'mois_debut',
- "dash_mois_fin",
- "titre_resp",
- "nb_inscrits",
- "etapes_apo_str",
- "elt_annee_apo",
- "elt_sem_apo",
- )
- if showcodes:
- columns_ids = ("formsemestre_id",) + columns_ids
-
- html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
- if current_user.has_permission(Permission.ScoEditApo):
- html_class += " apo_editable"
- tab = GenTable(
- titles={
- "formsemestre_id": "id",
- "semestre_id_n": "S#",
- "modalite": "",
- "mois_debut": "Début",
- "dash_mois_fin": "Année",
- "titre_resp": "Semestre",
- "nb_inscrits": "N",
- "etapes_apo_str": "Étape Apo.",
- "elt_annee_apo": "Elt. année Apo.",
- "elt_sem_apo": "Elt. sem. Apo.",
- },
- columns_ids=columns_ids,
- rows=sems,
- table_id="semlist",
- html_class_ignore_default=True,
- html_class=html_class,
- html_sortable=True,
- html_table_attrs=f"""
- data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
- data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
- data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
- """,
- html_with_td_classes=True,
- preferences=sco_preferences.SemPreferences(),
- )
-
- return tab
-
-
-def _style_sems(sems):
- """ajoute quelques attributs de présentation pour la table"""
- for sem in sems:
- sem["notes_url"] = scu.NotesURL()
- sem["_groupicon_target"] = (
- "%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
- % sem
- )
- sem["_formsemestre_id_class"] = "blacktt"
- sem["dash_mois_fin"] = ' %(anneescolaire)s' % sem
- sem["_dash_mois_fin_class"] = "datesem"
- sem["titre_resp"] = (
- """%(titre_num)s
- (%(responsable_name)s)"""
- % sem
- )
- sem["_css_row_class"] = "css_S%d css_M%s" % (
- sem["semestre_id"],
- sem["modalite"],
- )
- sem["_semestre_id_class"] = "semestre_id"
- sem["_modalite_class"] = "modalite"
- if sem["semestre_id"] == -1:
- sem["semestre_id_n"] = ""
- else:
- sem["semestre_id_n"] = sem["semestre_id"]
- # pour édition codes Apogée:
- sem[
- "_etapes_apo_str_td_attrs"
- ] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
- sem[
- "_elt_annee_apo_td_attrs"
- ] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
- sem[
- "_elt_sem_apo_td_attrs"
- ] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
-
-
-def delete_dept(dept_id: int):
- """Suppression irréversible d'un département et de tous les objets rattachés"""
- assert isinstance(dept_id, int)
-
- # Un peu complexe, merci JMP :)
- cnx = ndb.GetDBConnexion()
- cursor = cnx.cursor()
- try:
- # 1- Create temp tables to store ids
- reqs = [
- "create temp table etudids_temp as select id from identite where dept_id = %(dept_id)s",
- "create temp table formsemestres_temp as select id from notes_formsemestre where dept_id = %(dept_id)s",
- "create temp table moduleimpls_temp as select id from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
- "create temp table formations_temp as select id from notes_formations where dept_id = %(dept_id)s",
- "create temp table tags_temp as select id from notes_tags where dept_id = %(dept_id)s",
- ]
- for r in reqs:
- cursor.execute(r, {"dept_id": dept_id})
-
- # 2- Delete student-related informations
- # ordered list of tables
- etud_tables = [
- "notes_notes",
- "group_membership",
- "admissions",
- "billet_absence",
- "adresse",
- "absences",
- "notes_notes_log",
- "notes_moduleimpl_inscription",
- "itemsuivi",
- "notes_appreciations",
- "scolar_autorisation_inscription",
- "absences_notifications",
- "notes_formsemestre_inscription",
- "scolar_formsemestre_validation",
- "scolar_events",
- ]
- for table in etud_tables:
- cursor.execute(
- f"delete from {table} where etudid in (select id from etudids_temp)"
- )
-
- reqs = [
- "delete from identite where dept_id = %(dept_id)s",
- "delete from sco_prefs where dept_id = %(dept_id)s",
- "delete from notes_semset_formsemestre where formsemestre_id in (select id from formsemestres_temp)",
- "delete from notes_evaluation where moduleimpl_id in (select id from moduleimpls_temp)",
- "delete from notes_modules_enseignants where moduleimpl_id in (select id from moduleimpls_temp)",
- "delete from notes_formsemestre_uecoef where formsemestre_id in (select id from formsemestres_temp)",
- "delete from notes_formsemestre_ue_computation_expr where formsemestre_id in (select id from formsemestres_temp)",
- "delete from notes_formsemestre_responsables where formsemestre_id in (select id from formsemestres_temp)",
- "delete from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
- "delete from notes_modules_tags where tag_id in (select id from tags_temp)",
- "delete from notes_tags where dept_id = %(dept_id)s",
- "delete from notes_modules where formation_id in (select id from formations_temp)",
- "delete from notes_matieres where ue_id in (select id from notes_ue where formation_id in (select id from formations_temp))",
- "delete from notes_formsemestre_etapes where formsemestre_id in (select id from formsemestres_temp)",
- "delete from group_descr where partition_id in (select id from partition where formsemestre_id in (select id from formsemestres_temp))",
- "delete from partition where formsemestre_id in (select id from formsemestres_temp)",
- "delete from notes_formsemestre_custommenu where formsemestre_id in (select id from formsemestres_temp)",
- "delete from notes_ue where formation_id in (select id from formations_temp)",
- "delete from notes_formsemestre where dept_id = %(dept_id)s",
- "delete from scolar_news where dept_id = %(dept_id)s",
- "delete from notes_semset where dept_id = %(dept_id)s",
- "delete from notes_formations where dept_id = %(dept_id)s",
- "delete from departement where id = %(dept_id)s",
- "drop table tags_temp",
- "drop table formations_temp",
- "drop table moduleimpls_temp",
- "drop table etudids_temp",
- "drop table formsemestres_temp",
- ]
- for r in reqs:
- cursor.execute(r, {"dept_id": dept_id})
- except:
- cnx.rollback()
- finally:
- cnx.commit()
- app.clear_scodoc_cache()
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""Page accueil département (liste des semestres, etc)
+"""
+
+from flask import g, request
+from flask import url_for
+from flask_login import current_user
+
+import app
+from app.models import ScolarNews
+import app.scodoc.sco_utils as scu
+from app.scodoc.gen_tables import GenTable
+from app.scodoc.sco_permissions import Permission
+from app.scodoc import html_sco_header
+import app.scodoc.notesdb as ndb
+from app.scodoc import sco_formsemestre
+from app.scodoc import sco_formsemestre_inscriptions
+from app.scodoc import sco_modalites
+from app.scodoc import sco_preferences
+from app.scodoc import sco_users
+
+
+def index_html(showcodes=0, showsemtable=0):
+ "Page accueil département (liste des semestres)"
+ showcodes = int(showcodes)
+ showsemtable = int(showsemtable)
+ H = []
+
+ # News:
+ H.append(ScolarNews.scolar_news_summary_html())
+
+ # Avertissement de mise à jour:
+ H.append("""""")
+
+ # Liste de toutes les sessions:
+ sems = sco_formsemestre.do_formsemestre_list()
+ cursems = [] # semestres "courants"
+ othersems = [] # autres (verrouillés)
+ # icon image:
+ groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
+ emptygroupicon = scu.icontag(
+ "emptygroupicon_img", title="Pas d'inscrits", border="0"
+ )
+ lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
+ # Sélection sur l'etat du semestre
+ for sem in sems:
+ if sem["etat"] and sem["modalite"] != "EXT":
+ sem["lockimg"] = ""
+ cursems.append(sem)
+ else:
+ sem["lockimg"] = lockicon
+ othersems.append(sem)
+ # Responsable de formation:
+ sco_formsemestre.sem_set_responsable_name(sem)
+
+ if showcodes:
+ sem["tmpcode"] = f"
{sem['formsemestre_id']}
"
+ else:
+ sem["tmpcode"] = ""
+ # Nombre d'inscrits:
+ args = {"formsemestre_id": sem["formsemestre_id"]}
+ ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args)
+ nb = len(ins) # nb etudiants
+ sem["nb_inscrits"] = nb
+ if nb > 0:
+ sem["groupicon"] = groupicon
+ else:
+ sem["groupicon"] = emptygroupicon
+
+ # S'il n'y a pas d'utilisateurs dans la base, affiche message
+ if not sco_users.get_user_list(dept=g.scodoc_dept):
+ H.append(
+ """
Aucun utilisateur défini !
Pour définir des utilisateurs
+ passez par la page Utilisateurs.
+
+ Définissez au moins un utilisateur avec le rôle AdminXXX (le responsable du département XXX).
+
+ """
+ )
+
+ # Liste des formsemestres "courants"
+ if cursems:
+ H.append('
importer de nouveaux étudiants
+ (ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
+ le tableau de bord semestre si vous souhaitez inscrire les
+ étudiants importés à un semestre)
+
+ """
+
+ # Liste des semestres, groupés par modalités
+ sems_by_mod, modalites = sco_modalites.group_sems_by_modalite(sems)
+
+ H = ['
']
+ for modalite in modalites:
+ if len(modalites) > 1:
+ H.append('
%s
' % modalite["titre"])
+
+ if sems_by_mod[modalite["modalite"]]:
+ cur_idx = sems_by_mod[modalite["modalite"]][0]["semestre_id"]
+ for sem in sems_by_mod[modalite["modalite"]]:
+ if cur_idx != sem["semestre_id"]:
+ sem["trclass"] = "firstsem" # separe les groupes de semestres
+ cur_idx = sem["semestre_id"]
+ else:
+ sem["trclass"] = ""
+ sem["notes_url"] = scu.NotesURL()
+ H.append(tmpl % sem)
+ H.append("
")
+ return "\n".join(H)
+
+
+def _sem_table_gt(sems, showcodes=False):
+ """Nouvelle version de la table des semestres
+ Utilise une datatables.
+ """
+ _style_sems(sems)
+ columns_ids = (
+ "lockimg",
+ "semestre_id_n",
+ "modalite",
+ #'mois_debut',
+ "dash_mois_fin",
+ "titre_resp",
+ "nb_inscrits",
+ "etapes_apo_str",
+ "elt_annee_apo",
+ "elt_sem_apo",
+ )
+ if showcodes:
+ columns_ids = ("formsemestre_id",) + columns_ids
+
+ html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
+ if current_user.has_permission(Permission.ScoEditApo):
+ html_class += " apo_editable"
+ tab = GenTable(
+ titles={
+ "formsemestre_id": "id",
+ "semestre_id_n": "S#",
+ "modalite": "",
+ "mois_debut": "Début",
+ "dash_mois_fin": "Année",
+ "titre_resp": "Semestre",
+ "nb_inscrits": "N",
+ "etapes_apo_str": "Étape Apo.",
+ "elt_annee_apo": "Elt. année Apo.",
+ "elt_sem_apo": "Elt. sem. Apo.",
+ },
+ columns_ids=columns_ids,
+ rows=sems,
+ table_id="semlist",
+ html_class_ignore_default=True,
+ html_class=html_class,
+ html_sortable=True,
+ html_table_attrs=f"""
+ data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
+ data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
+ data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
+ """,
+ html_with_td_classes=True,
+ preferences=sco_preferences.SemPreferences(),
+ )
+
+ return tab
+
+
+def _style_sems(sems):
+ """ajoute quelques attributs de présentation pour la table"""
+ for sem in sems:
+ sem["notes_url"] = scu.NotesURL()
+ sem["_groupicon_target"] = (
+ "%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
+ % sem
+ )
+ sem["_formsemestre_id_class"] = "blacktt"
+ sem["dash_mois_fin"] = ' %(anneescolaire)s' % sem
+ sem["_dash_mois_fin_class"] = "datesem"
+ sem["titre_resp"] = (
+ """%(titre_num)s
+ (%(responsable_name)s)"""
+ % sem
+ )
+ sem["_css_row_class"] = "css_S%d css_M%s" % (
+ sem["semestre_id"],
+ sem["modalite"],
+ )
+ sem["_semestre_id_class"] = "semestre_id"
+ sem["_modalite_class"] = "modalite"
+ if sem["semestre_id"] == -1:
+ sem["semestre_id_n"] = ""
+ else:
+ sem["semestre_id_n"] = sem["semestre_id"]
+ # pour édition codes Apogée:
+ sem[
+ "_etapes_apo_str_td_attrs"
+ ] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
+ sem[
+ "_elt_annee_apo_td_attrs"
+ ] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
+ sem[
+ "_elt_sem_apo_td_attrs"
+ ] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
+
+
+def delete_dept(dept_id: int):
+ """Suppression irréversible d'un département et de tous les objets rattachés"""
+ assert isinstance(dept_id, int)
+
+ # Un peu complexe, merci JMP :)
+ cnx = ndb.GetDBConnexion()
+ cursor = cnx.cursor()
+ try:
+ # 1- Create temp tables to store ids
+ reqs = [
+ "create temp table etudids_temp as select id from identite where dept_id = %(dept_id)s",
+ "create temp table formsemestres_temp as select id from notes_formsemestre where dept_id = %(dept_id)s",
+ "create temp table moduleimpls_temp as select id from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
+ "create temp table formations_temp as select id from notes_formations where dept_id = %(dept_id)s",
+ "create temp table tags_temp as select id from notes_tags where dept_id = %(dept_id)s",
+ ]
+ for r in reqs:
+ cursor.execute(r, {"dept_id": dept_id})
+
+ # 2- Delete student-related informations
+ # ordered list of tables
+ etud_tables = [
+ "notes_notes",
+ "group_membership",
+ "admissions",
+ "billet_absence",
+ "adresse",
+ "absences",
+ "notes_notes_log",
+ "notes_moduleimpl_inscription",
+ "itemsuivi",
+ "notes_appreciations",
+ "scolar_autorisation_inscription",
+ "absences_notifications",
+ "notes_formsemestre_inscription",
+ "scolar_formsemestre_validation",
+ "scolar_events",
+ ]
+ for table in etud_tables:
+ cursor.execute(
+ f"delete from {table} where etudid in (select id from etudids_temp)"
+ )
+
+ reqs = [
+ "delete from identite where dept_id = %(dept_id)s",
+ "delete from sco_prefs where dept_id = %(dept_id)s",
+ "delete from notes_semset_formsemestre where formsemestre_id in (select id from formsemestres_temp)",
+ "delete from notes_evaluation where moduleimpl_id in (select id from moduleimpls_temp)",
+ "delete from notes_modules_enseignants where moduleimpl_id in (select id from moduleimpls_temp)",
+ "delete from notes_formsemestre_uecoef where formsemestre_id in (select id from formsemestres_temp)",
+ "delete from notes_formsemestre_ue_computation_expr where formsemestre_id in (select id from formsemestres_temp)",
+ "delete from notes_formsemestre_responsables where formsemestre_id in (select id from formsemestres_temp)",
+ "delete from notes_moduleimpl where formsemestre_id in (select id from formsemestres_temp)",
+ "delete from notes_modules_tags where tag_id in (select id from tags_temp)",
+ "delete from notes_tags where dept_id = %(dept_id)s",
+ "delete from notes_modules where formation_id in (select id from formations_temp)",
+ "delete from notes_matieres where ue_id in (select id from notes_ue where formation_id in (select id from formations_temp))",
+ "delete from notes_formsemestre_etapes where formsemestre_id in (select id from formsemestres_temp)",
+ "delete from group_descr where partition_id in (select id from partition where formsemestre_id in (select id from formsemestres_temp))",
+ "delete from partition where formsemestre_id in (select id from formsemestres_temp)",
+ "delete from notes_formsemestre_custommenu where formsemestre_id in (select id from formsemestres_temp)",
+ "delete from notes_ue where formation_id in (select id from formations_temp)",
+ "delete from notes_formsemestre where dept_id = %(dept_id)s",
+ "delete from scolar_news where dept_id = %(dept_id)s",
+ "delete from notes_semset where dept_id = %(dept_id)s",
+ "delete from notes_formations where dept_id = %(dept_id)s",
+ "delete from departement where id = %(dept_id)s",
+ "drop table tags_temp",
+ "drop table formations_temp",
+ "drop table moduleimpls_temp",
+ "drop table etudids_temp",
+ "drop table formsemestres_temp",
+ ]
+ for r in reqs:
+ cursor.execute(r, {"dept_id": dept_id})
+ except:
+ cnx.rollback()
+ finally:
+ cnx.commit()
+ app.clear_scodoc_cache()
diff --git a/app/scodoc/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py
index 5d4904e2..a2dd56bb 100644
--- a/app/scodoc/sco_etape_bilan.py
+++ b/app/scodoc/sco_etape_bilan.py
@@ -1,773 +1,773 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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 semestres (au sens ScoDoc) différents.
-
-La proposition est d'ajouter à 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 un 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.).
-
- * La 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 scp_formsemestre.py
-
-Modification/ajout de la méthode sem_in_semestre_scolaire pour permettre
-l'inscrition de semestres décalés (S1 en septembre, ...).
-Le filtrage s'effctue 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 semestres 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, semestre):
- self.semestres.add(semestre)
-
- 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():
- return """
-
Explications sur les tableaux des effectifs et liste des
- étudiants
-
Le tableau des effectifs présente le nombre d'étudiants selon deux critères:
-
-
En colonne le statut de l'étudiant par rapport à Apogée:
-
-
Hors Apogée
- (anomalie A): Le NIP de l'étudiant n'est pas connu d'apogée ou
- l'étudiant n'a pas de NIP
-
Pas d'étape(anomalie B): Le NIP de
- l'étudiant ne correspond à aucune des étapes connues pour cet ensemble de semestre. Il est
- possible qu'il soit inscrit ailleurs (dans une autre ensemble de semestres, un autre département,
- une autre composante de l'université) ou en mobilité internationale.
-
Plusieurs étapes(anomalie C):
- Les étudiants inscrits dans plusieurs étapes apogée de l'ensemble de semestres
-
Un des codes étapes connus (la liste des codes étapes connus est l'union des codes étapes
- déclarés pour chaque semestre particpant
-
Total semestre: cumul des effectifs de la ligne
-
-
-
En ligne le statut de l'étudiant par rapport à ScoDoc:
-
-
Inscription dans un des semestres de l'ensemble
-
Hors semestre(anomalie D):
- L'étudiant, bien qu'enregistré par apogée dans un des codes étapes connus, ne figure dans aucun
- des semestres de l'ensemble. On y trouve par exemple les étudiants régulièrement inscrits
- mais non présents à la rentrée (donc non enregistrés dans ScoDoc)
Note: On ne considère
- ici que les semestres de l'ensemble (l'inscription de l'étudiant dans un semestre étranger à
- l'ensemble actuel n'est pas vérifiée).
-
Plusieurs semestres(anomalie E):
- L'étudiant est enregistré dans plusieurs semestres de l'ensemble.
-
Total: cumul des effectifs de la colonne
-
-
-
(anomalie U) On présente également les cas où un même NIP est affecté
- à deux dossiers différents (Un dossier d'apogée et un dossier de ScoDoc). Un tel cas compte pour
- deux unités dans le tableau des effcetifs et engendre 2 lignes distinctes dans la liste des étudiants
Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour
- n'afficher que les étudiants correspondants
-
-
-
-
- """
-
-
-class EtapeBilan(object):
- """
- Structure de donnée représentation l'état global de la comparaison ScoDoc/Apogée
- """
-
- def __init__(self):
- self.semestres = (
- {}
- ) # Dictionnaire des formsemestres du semset (formsemestre_id -> semestre)
- self.etapes = [] # Liste des étapes apogées du semset (clé_apogée)
- # pour les descriptions qui suivents:
- # cle_etu = nip si non vide, sinon etudid
- # data_etu = { nip, etudid, data_apogee, data_scodoc }
- self.etudiants = {} # cle_etu -> data_etu
- self.keys_etu = {} # nip -> [ etudid* ]
- self.etu_semestre = {} # semestre -> { key_etu }
- self.etu_etapes = {} # etape -> { key_etu }
- self.repartition = {} # (ind_row, ind_col) -> nombre d étudiants
- self.tag_count = {} # nombre d'animalies détectées (par type d'anomalie)
-
- # on collectionne les indicatifs trouvés pour n'afficher que les indicatifs 'utiles'
- self.indicatifs = {}
- self.top_row = 0
- self.top_col = 0
- self.all_rows_ind = [PAS_DE_SEMESTRE, PLUSIEURS_SEMESTRES]
- self.all_cols_ind = [PAS_DE_NIP, PAS_D_ETAPE, PLUSIEURS_ETAPES]
- self.all_rows_str = None
- self.all_cols_str = None
- self.titres = {
- PAS_DE_NIP: "PAS_DE_NIP",
- PAS_D_ETAPE: "PAS_D_ETAPE",
- PLUSIEURS_ETAPES: "PLUSIEURS_ETAPES",
- PAS_DE_SEMESTRE: "PAS_DE_SEMESTRE",
- PLUSIEURS_SEMESTRES: "PLUSIEURS_SEMESTRES",
- NIP_NON_UNIQUE: "NIP_NON_UNIQUE",
- }
-
- def inc_tag_count(self, tag):
- if tag not in self.tag_count:
- self.tag_count[tag] = 0
- self.tag_count[tag] += 1
-
- def set_indicatif(self, item, as_row): # item = semestre ou key_etape
- if as_row:
- indicatif = "R" + chr(self.top_row + 97)
- self.all_rows_ind.append(indicatif)
- self.top_row += 1
- else:
- indicatif = "C" + chr(self.top_col + 97)
- self.all_cols_ind.append(indicatif)
- self.top_col += 1
- self.indicatifs[item] = indicatif
- if self.top_row > 26:
- log("Dépassement (plus de 26 semestres dans la table diagnostic")
- if self.top_col > 26:
- log("Dépassement (plus de 26 étapes dans la table diagnostic")
-
- def add_sem(self, semestre):
- """
- Prise en compte d'un semestre dans le bilan.
- * ajoute le semestre et les étudiants du semestre
- * ajoute les étapes du semestre et (via portail) les étudiants pour ces codes étapes
- :param semestre: Le semestre à prendre en compte
- :return: None
- """
- self.semestres[semestre["formsemestre_id"]] = semestre
- # if anneeapogee == None: # année d'inscription par défaut
- anneeapogee = str(
- annee_scolaire_debut(semestre["annee_debut"], semestre["mois_debut_ord"])
- )
- self.set_indicatif(semestre["formsemestre_id"], True)
- for etape in semestre["etapes"]:
- self.add_etape(etape.etape_vdi, anneeapogee)
-
- def add_etape(self, etape_str, anneeapogee):
- """
- Prise en compte d'une étape apogée
- :param etape_str: La clé de l'étape à prendre en compte
- :param anneeapogee: l'année de l'étape à prendre en compte
- :return: None
- """
- if etape_str != "":
- key_etape = etape_to_key(anneeapogee, etape_str)
- if key_etape not in self.etapes:
- self.etapes.append(key_etape)
- self.set_indicatif(
- key_etape, False
- ) # ajout de la colonne/indicatif supplémentaire
-
- def compute_key_etu(self, nip, etudid):
- """
- Calcul de la clé étudiant:
- * Le nip si il existe
- * sinon l'identifiant ScoDoc
- Tient à jour le dictionnaire key_etu (référentiel des étudiants)
- La problèmatique est de gérer toutes les anomalies possibles:
- - étudiant sans nip,
- - plusieurs étudiants avec le même nip,
- - etc.
- :param nip: le nip de l'étudiant
- :param etudid: l'identifiant ScoDoc
- :return: L'identifiant unique de l'étudiant
- """
- if nip not in self.keys_etu:
- self.keys_etu[nip] = []
- if etudid not in self.keys_etu[nip]:
- if etudid is None:
- if len(self.keys_etu[nip]) == 1:
- etudid = self.keys_etu[nip][0]
- else: # nip non trouvé ou utilisé par plusieurs étudiants
- self.keys_etu[nip].append(None)
- else:
- self.keys_etu[nip].append(etudid)
- return nip, etudid
-
- def register_etud_apogee(self, etud, etape):
- """
- Enregistrement des données de l'étudiant par rapport à apogée.
- L'étudiant peut avoir été déjà enregistré auparavant (par exemple connu par son semestre)
- Dans ce cas, on ne met à jour que son association à l'étape apogée
- :param etud: les données étudiant
- :param etape: l'étape apogée
- :return:
- """
- nip = etud["nip"]
- key_etu = self.compute_key_etu(nip, None)
- if key_etu not in self.etudiants:
- data = DataEtudiant(nip)
- data.set_apogee(etud)
- data.add_etape(etape)
- self.etudiants[key_etu] = data
- else:
- self.etudiants[key_etu].set_apogee(etud)
- self.etudiants[key_etu].add_etape(etape)
- return key_etu
-
- def register_etud_scodoc(self, etud, semestre):
- """
- Enregistrement de l'étudiant par rapport à son semestre
- :param etud: Les données de l'étudiant
- :param semestre: Le semestre où il est à enregistrer
- :return: la clé unique pour cet étudiant
- """
- nip = etud["code_nip"]
- etudid = etud["etudid"]
- key_etu = self.compute_key_etu(nip, etudid)
- if key_etu not in self.etudiants:
- data = DataEtudiant(nip, etudid)
- data.set_scodoc(etud)
- data.add_semestre(semestre)
- self.etudiants[key_etu] = data
- else:
- self.etudiants[key_etu].add_semestre(semestre)
- return key_etu
-
- def load_listes(self):
- """
- Inventaire complet des étudiants:
- * Pour tous les semestres d'abord
- * Puis pour toutes les étapes
- :return: None
- """
- for semestre in self.semestres:
- etuds = self.semestres[semestre]["etuds"]
- self.etu_semestre[semestre] = set()
- for etud in etuds:
- key_etu = self.register_etud_scodoc(etud, semestre)
- self.etu_semestre[semestre].add(key_etu)
-
- for key_etape in self.etapes:
- anneeapogee, etapestr = key_to_values(key_etape)
- self.etu_etapes[key_etape] = set()
- for etud in get_inscrits_etape(etapestr, anneeapogee):
- key_etu = self.register_etud_apogee(etud, key_etape)
- self.etu_etapes[key_etape].add(key_etu)
-
- def dispatch(self):
- """
- Réparti l'ensemble des étudiants selon les lignes (semestres) et les colonnes (étapes).
-
- :return: None
- """
- # Initialisation des cumuls
- self.repartition[ROW_CUMUL, COL_CUMUL] = 0
- self.repartition[PAS_DE_SEMESTRE, COL_CUMUL] = 0
- self.repartition[PLUSIEURS_SEMESTRES, COL_CUMUL] = 0
- self.repartition[ROW_CUMUL, PAS_DE_NIP] = 0
- self.repartition[ROW_CUMUL, PAS_D_ETAPE] = 0
- self.repartition[ROW_CUMUL, PLUSIEURS_ETAPES] = 0
- for semestre in self.semestres:
- self.repartition[self.indicatifs[semestre], COL_CUMUL] = 0
- for key_etape in self.etapes:
- self.repartition[ROW_CUMUL, self.indicatifs[key_etape]] = 0
-
- # recherche des nip identiques
- for nip in self.keys_etu:
- if nip != "":
- nbnips = len(self.keys_etu[nip])
- if nbnips > 1:
- for i, etudid in enumerate(self.keys_etu[nip]):
- data_etu = self.etudiants[nip, etudid]
- data_etu.add_tag(NIP_NON_UNIQUE)
- data_etu.nip = data_etu.nip + " (%d/%d)" % (i + 1, nbnips)
- self.inc_tag_count(NIP_NON_UNIQUE)
- for nip in self.keys_etu:
- for etudid in self.keys_etu[nip]:
- key_etu = (nip, etudid)
- data_etu = self.etudiants[key_etu]
- ind_col = "-"
- ind_row = "-"
-
- # calcul de la colonne
- if len(data_etu.etapes) == 1:
- ind_col = self.indicatifs[list(data_etu.etapes)[0]]
- elif nip == "":
- data_etu.add_tag(FLAG[PAS_DE_NIP])
- ind_col = PAS_DE_NIP
- elif len(data_etu.etapes) == 0:
- self.etudiants[key_etu].add_tag(FLAG[PAS_D_ETAPE])
- ind_col = PAS_D_ETAPE
- if len(data_etu.etapes) > 1:
- data_etu.add_tag(FLAG[PLUSIEURS_ETAPES])
- ind_col = PLUSIEURS_ETAPES
-
- if len(data_etu.semestres) == 1:
- ind_row = self.indicatifs[list(data_etu.semestres)[0]]
- elif len(data_etu.semestres) > 1:
- data_etu.add_tag(FLAG[PLUSIEURS_SEMESTRES])
- ind_row = PLUSIEURS_SEMESTRES
- elif len(data_etu.semestres) < 1:
- self.etudiants[key_etu].add_tag(FLAG[PAS_DE_SEMESTRE])
- ind_row = PAS_DE_SEMESTRE
-
- data_etu.set_ind_col(ind_col)
- data_etu.set_ind_row(ind_row)
- self._inc_count(ind_row, ind_col)
- self.inc_tag_count(ind_row)
- self.inc_tag_count(ind_col)
-
- def html_diagnostic(self):
- """
- affichage de l'html
- :return: Le code html à afficher
- """
- self.load_listes() # chargement des données
- self.dispatch() # analyse et répartition
- # calcul de la liste des colonnes et des lignes de la table des effectifs
- self.all_rows_str = "'" + ",".join(["." + r for r in self.all_rows_ind]) + "'"
- self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
-
- H = [
- '
Tableau des effectifs
',
- self._diagtable(),
- self.display_tags(),
- 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 = "(%d étudiants)" % count
- 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 = "doFiltrage(%s, %s, '*', '*', '%s', '%s', '%s');" % (
- self.all_rows_str,
- self.all_cols_str,
- comptage,
- "",
- "",
- )
- elif ind_row == ROW_CUMUL:
- javascript = "doFiltrage(%s, %s, '*', '.%s', '%s', '%s', '%s');" % (
- 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 = "doFiltrage(%s, %s, '.%s', '*', '%s', '%s', '%s');" % (
- self.all_rows_str,
- self.all_cols_str,
- ind_row,
- " (%d étudiants)" % count,
- json.dumps(self.titres[ind_row])[1:-1],
- "",
- )
- else:
- javascript = "doFiltrage(%s, %s, '.%s', '.%s', '%s', '%s', '%s');" % (
- 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 '%d' % (javascript, count)
-
- def _diagtable(self):
- 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] = "Pas d'é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(format="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(
- 'Code(s) nip) partagé(s) par %d étudiants '
- % (javascript, self.tag_count[NIP_NON_UNIQUE])
- )
- 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(format="html")
- )
- return "\n".join(H)
-
-
-def etape_to_key(anneeapogee, etapestr):
- return anneeapogee, etapestr
-
-
-def key_to_values(key_etape):
- return key_etape
-
-
-def etape_to_col(key_etape):
- return "%s@%s" % key_etape
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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 semestres (au sens ScoDoc) différents.
+
+La proposition est d'ajouter à 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 un 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.).
+
+ * La 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 scp_formsemestre.py
+
+Modification/ajout de la méthode sem_in_semestre_scolaire pour permettre
+l'inscrition de semestres décalés (S1 en septembre, ...).
+Le filtrage s'effctue 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 semestres 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, semestre):
+ self.semestres.add(semestre)
+
+ 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():
+ return """
+
Explications sur les tableaux des effectifs et liste des
+ étudiants
+
Le tableau des effectifs présente le nombre d'étudiants selon deux critères:
+
+
En colonne le statut de l'étudiant par rapport à Apogée:
+
+
Hors Apogée
+ (anomalie A): Le NIP de l'étudiant n'est pas connu d'apogée ou
+ l'étudiant n'a pas de NIP
+
Pas d'étape(anomalie B): Le NIP de
+ l'étudiant ne correspond à aucune des étapes connues pour cet ensemble de semestre. Il est
+ possible qu'il soit inscrit ailleurs (dans une autre ensemble de semestres, un autre département,
+ une autre composante de l'université) ou en mobilité internationale.
+
Plusieurs étapes(anomalie C):
+ Les étudiants inscrits dans plusieurs étapes apogée de l'ensemble de semestres
+
Un des codes étapes connus (la liste des codes étapes connus est l'union des codes étapes
+ déclarés pour chaque semestre particpant
+
Total semestre: cumul des effectifs de la ligne
+
+
+
En ligne le statut de l'étudiant par rapport à ScoDoc:
+
+
Inscription dans un des semestres de l'ensemble
+
Hors semestre(anomalie D):
+ L'étudiant, bien qu'enregistré par apogée dans un des codes étapes connus, ne figure dans aucun
+ des semestres de l'ensemble. On y trouve par exemple les étudiants régulièrement inscrits
+ mais non présents à la rentrée (donc non enregistrés dans ScoDoc)
Note: On ne considère
+ ici que les semestres de l'ensemble (l'inscription de l'étudiant dans un semestre étranger à
+ l'ensemble actuel n'est pas vérifiée).
+
Plusieurs semestres(anomalie E):
+ L'étudiant est enregistré dans plusieurs semestres de l'ensemble.
+
Total: cumul des effectifs de la colonne
+
+
+
(anomalie U) On présente également les cas où un même NIP est affecté
+ à deux dossiers différents (Un dossier d'apogée et un dossier de ScoDoc). Un tel cas compte pour
+ deux unités dans le tableau des effcetifs et engendre 2 lignes distinctes dans la liste des étudiants
Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour
+ n'afficher que les étudiants correspondants
+
+
+
+
+ """
+
+
+class EtapeBilan(object):
+ """
+ Structure de donnée représentation l'état global de la comparaison ScoDoc/Apogée
+ """
+
+ def __init__(self):
+ self.semestres = (
+ {}
+ ) # Dictionnaire des formsemestres du semset (formsemestre_id -> semestre)
+ self.etapes = [] # Liste des étapes apogées du semset (clé_apogée)
+ # pour les descriptions qui suivents:
+ # cle_etu = nip si non vide, sinon etudid
+ # data_etu = { nip, etudid, data_apogee, data_scodoc }
+ self.etudiants = {} # cle_etu -> data_etu
+ self.keys_etu = {} # nip -> [ etudid* ]
+ self.etu_semestre = {} # semestre -> { key_etu }
+ self.etu_etapes = {} # etape -> { key_etu }
+ self.repartition = {} # (ind_row, ind_col) -> nombre d étudiants
+ self.tag_count = {} # nombre d'animalies détectées (par type d'anomalie)
+
+ # on collectionne les indicatifs trouvés pour n'afficher que les indicatifs 'utiles'
+ self.indicatifs = {}
+ self.top_row = 0
+ self.top_col = 0
+ self.all_rows_ind = [PAS_DE_SEMESTRE, PLUSIEURS_SEMESTRES]
+ self.all_cols_ind = [PAS_DE_NIP, PAS_D_ETAPE, PLUSIEURS_ETAPES]
+ self.all_rows_str = None
+ self.all_cols_str = None
+ self.titres = {
+ PAS_DE_NIP: "PAS_DE_NIP",
+ PAS_D_ETAPE: "PAS_D_ETAPE",
+ PLUSIEURS_ETAPES: "PLUSIEURS_ETAPES",
+ PAS_DE_SEMESTRE: "PAS_DE_SEMESTRE",
+ PLUSIEURS_SEMESTRES: "PLUSIEURS_SEMESTRES",
+ NIP_NON_UNIQUE: "NIP_NON_UNIQUE",
+ }
+
+ def inc_tag_count(self, tag):
+ if tag not in self.tag_count:
+ self.tag_count[tag] = 0
+ self.tag_count[tag] += 1
+
+ def set_indicatif(self, item, as_row): # item = semestre ou key_etape
+ if as_row:
+ indicatif = "R" + chr(self.top_row + 97)
+ self.all_rows_ind.append(indicatif)
+ self.top_row += 1
+ else:
+ indicatif = "C" + chr(self.top_col + 97)
+ self.all_cols_ind.append(indicatif)
+ self.top_col += 1
+ self.indicatifs[item] = indicatif
+ if self.top_row > 26:
+ log("Dépassement (plus de 26 semestres dans la table diagnostic")
+ if self.top_col > 26:
+ log("Dépassement (plus de 26 étapes dans la table diagnostic")
+
+ def add_sem(self, semestre):
+ """
+ Prise en compte d'un semestre dans le bilan.
+ * ajoute le semestre et les étudiants du semestre
+ * ajoute les étapes du semestre et (via portail) les étudiants pour ces codes étapes
+ :param semestre: Le semestre à prendre en compte
+ :return: None
+ """
+ self.semestres[semestre["formsemestre_id"]] = semestre
+ # if anneeapogee == None: # année d'inscription par défaut
+ anneeapogee = str(
+ annee_scolaire_debut(semestre["annee_debut"], semestre["mois_debut_ord"])
+ )
+ self.set_indicatif(semestre["formsemestre_id"], True)
+ for etape in semestre["etapes"]:
+ self.add_etape(etape.etape_vdi, anneeapogee)
+
+ def add_etape(self, etape_str, anneeapogee):
+ """
+ Prise en compte d'une étape apogée
+ :param etape_str: La clé de l'étape à prendre en compte
+ :param anneeapogee: l'année de l'étape à prendre en compte
+ :return: None
+ """
+ if etape_str != "":
+ key_etape = etape_to_key(anneeapogee, etape_str)
+ if key_etape not in self.etapes:
+ self.etapes.append(key_etape)
+ self.set_indicatif(
+ key_etape, False
+ ) # ajout de la colonne/indicatif supplémentaire
+
+ def compute_key_etu(self, nip, etudid):
+ """
+ Calcul de la clé étudiant:
+ * Le nip si il existe
+ * sinon l'identifiant ScoDoc
+ Tient à jour le dictionnaire key_etu (référentiel des étudiants)
+ La problèmatique est de gérer toutes les anomalies possibles:
+ - étudiant sans nip,
+ - plusieurs étudiants avec le même nip,
+ - etc.
+ :param nip: le nip de l'étudiant
+ :param etudid: l'identifiant ScoDoc
+ :return: L'identifiant unique de l'étudiant
+ """
+ if nip not in self.keys_etu:
+ self.keys_etu[nip] = []
+ if etudid not in self.keys_etu[nip]:
+ if etudid is None:
+ if len(self.keys_etu[nip]) == 1:
+ etudid = self.keys_etu[nip][0]
+ else: # nip non trouvé ou utilisé par plusieurs étudiants
+ self.keys_etu[nip].append(None)
+ else:
+ self.keys_etu[nip].append(etudid)
+ return nip, etudid
+
+ def register_etud_apogee(self, etud, etape):
+ """
+ Enregistrement des données de l'étudiant par rapport à apogée.
+ L'étudiant peut avoir été déjà enregistré auparavant (par exemple connu par son semestre)
+ Dans ce cas, on ne met à jour que son association à l'étape apogée
+ :param etud: les données étudiant
+ :param etape: l'étape apogée
+ :return:
+ """
+ nip = etud["nip"]
+ key_etu = self.compute_key_etu(nip, None)
+ if key_etu not in self.etudiants:
+ data = DataEtudiant(nip)
+ data.set_apogee(etud)
+ data.add_etape(etape)
+ self.etudiants[key_etu] = data
+ else:
+ self.etudiants[key_etu].set_apogee(etud)
+ self.etudiants[key_etu].add_etape(etape)
+ return key_etu
+
+ def register_etud_scodoc(self, etud, semestre):
+ """
+ Enregistrement de l'étudiant par rapport à son semestre
+ :param etud: Les données de l'étudiant
+ :param semestre: Le semestre où il est à enregistrer
+ :return: la clé unique pour cet étudiant
+ """
+ nip = etud["code_nip"]
+ etudid = etud["etudid"]
+ key_etu = self.compute_key_etu(nip, etudid)
+ if key_etu not in self.etudiants:
+ data = DataEtudiant(nip, etudid)
+ data.set_scodoc(etud)
+ data.add_semestre(semestre)
+ self.etudiants[key_etu] = data
+ else:
+ self.etudiants[key_etu].add_semestre(semestre)
+ return key_etu
+
+ def load_listes(self):
+ """
+ Inventaire complet des étudiants:
+ * Pour tous les semestres d'abord
+ * Puis pour toutes les étapes
+ :return: None
+ """
+ for semestre in self.semestres:
+ etuds = self.semestres[semestre]["etuds"]
+ self.etu_semestre[semestre] = set()
+ for etud in etuds:
+ key_etu = self.register_etud_scodoc(etud, semestre)
+ self.etu_semestre[semestre].add(key_etu)
+
+ for key_etape in self.etapes:
+ anneeapogee, etapestr = key_to_values(key_etape)
+ self.etu_etapes[key_etape] = set()
+ for etud in get_inscrits_etape(etapestr, anneeapogee):
+ key_etu = self.register_etud_apogee(etud, key_etape)
+ self.etu_etapes[key_etape].add(key_etu)
+
+ def dispatch(self):
+ """
+ Réparti l'ensemble des étudiants selon les lignes (semestres) et les colonnes (étapes).
+
+ :return: None
+ """
+ # Initialisation des cumuls
+ self.repartition[ROW_CUMUL, COL_CUMUL] = 0
+ self.repartition[PAS_DE_SEMESTRE, COL_CUMUL] = 0
+ self.repartition[PLUSIEURS_SEMESTRES, COL_CUMUL] = 0
+ self.repartition[ROW_CUMUL, PAS_DE_NIP] = 0
+ self.repartition[ROW_CUMUL, PAS_D_ETAPE] = 0
+ self.repartition[ROW_CUMUL, PLUSIEURS_ETAPES] = 0
+ for semestre in self.semestres:
+ self.repartition[self.indicatifs[semestre], COL_CUMUL] = 0
+ for key_etape in self.etapes:
+ self.repartition[ROW_CUMUL, self.indicatifs[key_etape]] = 0
+
+ # recherche des nip identiques
+ for nip in self.keys_etu:
+ if nip != "":
+ nbnips = len(self.keys_etu[nip])
+ if nbnips > 1:
+ for i, etudid in enumerate(self.keys_etu[nip]):
+ data_etu = self.etudiants[nip, etudid]
+ data_etu.add_tag(NIP_NON_UNIQUE)
+ data_etu.nip = data_etu.nip + " (%d/%d)" % (i + 1, nbnips)
+ self.inc_tag_count(NIP_NON_UNIQUE)
+ for nip in self.keys_etu:
+ for etudid in self.keys_etu[nip]:
+ key_etu = (nip, etudid)
+ data_etu = self.etudiants[key_etu]
+ ind_col = "-"
+ ind_row = "-"
+
+ # calcul de la colonne
+ if len(data_etu.etapes) == 1:
+ ind_col = self.indicatifs[list(data_etu.etapes)[0]]
+ elif nip == "":
+ data_etu.add_tag(FLAG[PAS_DE_NIP])
+ ind_col = PAS_DE_NIP
+ elif len(data_etu.etapes) == 0:
+ self.etudiants[key_etu].add_tag(FLAG[PAS_D_ETAPE])
+ ind_col = PAS_D_ETAPE
+ if len(data_etu.etapes) > 1:
+ data_etu.add_tag(FLAG[PLUSIEURS_ETAPES])
+ ind_col = PLUSIEURS_ETAPES
+
+ if len(data_etu.semestres) == 1:
+ ind_row = self.indicatifs[list(data_etu.semestres)[0]]
+ elif len(data_etu.semestres) > 1:
+ data_etu.add_tag(FLAG[PLUSIEURS_SEMESTRES])
+ ind_row = PLUSIEURS_SEMESTRES
+ elif len(data_etu.semestres) < 1:
+ self.etudiants[key_etu].add_tag(FLAG[PAS_DE_SEMESTRE])
+ ind_row = PAS_DE_SEMESTRE
+
+ data_etu.set_ind_col(ind_col)
+ data_etu.set_ind_row(ind_row)
+ self._inc_count(ind_row, ind_col)
+ self.inc_tag_count(ind_row)
+ self.inc_tag_count(ind_col)
+
+ def html_diagnostic(self):
+ """
+ affichage de l'html
+ :return: Le code html à afficher
+ """
+ self.load_listes() # chargement des données
+ self.dispatch() # analyse et répartition
+ # calcul de la liste des colonnes et des lignes de la table des effectifs
+ self.all_rows_str = "'" + ",".join(["." + r for r in self.all_rows_ind]) + "'"
+ self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
+
+ H = [
+ '
Tableau des effectifs
',
+ self._diagtable(),
+ self.display_tags(),
+ 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 = "(%d étudiants)" % count
+ 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 = "doFiltrage(%s, %s, '*', '*', '%s', '%s', '%s');" % (
+ self.all_rows_str,
+ self.all_cols_str,
+ comptage,
+ "",
+ "",
+ )
+ elif ind_row == ROW_CUMUL:
+ javascript = "doFiltrage(%s, %s, '*', '.%s', '%s', '%s', '%s');" % (
+ 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 = "doFiltrage(%s, %s, '.%s', '*', '%s', '%s', '%s');" % (
+ self.all_rows_str,
+ self.all_cols_str,
+ ind_row,
+ " (%d étudiants)" % count,
+ json.dumps(self.titres[ind_row])[1:-1],
+ "",
+ )
+ else:
+ javascript = "doFiltrage(%s, %s, '.%s', '.%s', '%s', '%s', '%s');" % (
+ 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 '%d' % (javascript, count)
+
+ def _diagtable(self):
+ 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] = "Pas d'é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(format="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(
+ 'Code(s) nip) partagé(s) par %d étudiants '
+ % (javascript, self.tag_count[NIP_NON_UNIQUE])
+ )
+ 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(format="html")
+ )
+ return "\n".join(H)
+
+
+def etape_to_key(anneeapogee, etapestr):
+ return anneeapogee, etapestr
+
+
+def key_to_values(key_etape):
+ return key_etape
+
+
+def etape_to_col(key_etape):
+ return "%s@%s" % key_etape
diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py
index bb869d5a..632f5b72 100644
--- a/app/scodoc/sco_etud.py
+++ b/app/scodoc/sco_etud.py
@@ -907,7 +907,7 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
if etud["villelycee"]:
etud["ilycee"] += " (%s)" % etud.get("villelycee", "")
- etud["ilycee"] += " "
+ etud["ilycee"] += " "
else:
if etud.get("codelycee"):
etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py
index 5779892d..21e10f53 100644
--- a/app/scodoc/sco_evaluation_check_abs.py
+++ b/app/scodoc/sco_evaluation_check_abs.py
@@ -1,257 +1,257 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""Vérification des absences à une évaluation
-"""
-from flask import url_for, g
-
-import app.scodoc.sco_utils as scu
-import app.scodoc.notesdb as ndb
-from app.scodoc import html_sco_header
-from app.scodoc import sco_abs
-from app.scodoc import sco_etud
-from app.scodoc import sco_evaluations
-from app.scodoc import sco_evaluation_db
-from app.scodoc import sco_formsemestre
-from app.scodoc import sco_groups
-from app.scodoc import sco_moduleimpl
-
-# matin et/ou après-midi ?
-def _eval_demijournee(E):
- "1 si matin, 0 si apres midi, 2 si toute la journee"
- am, pm = False, False
- if E["heure_debut"] < "13:00":
- am = True
- if E["heure_fin"] > "13:00":
- pm = True
- if am and pm:
- demijournee = 2
- elif am:
- demijournee = 1
- else:
- demijournee = 0
- pm = True
- return am, pm, demijournee
-
-
-def evaluation_check_absences(evaluation_id):
- """Vérifie les absences au moment de cette évaluation.
- Cas incohérents que l'on peut rencontrer pour chaque étudiant:
- note et absent
- ABS et pas noté absent
- ABS et absent justifié
- EXC et pas noté absent
- EXC et pas justifie
- Ramene 3 listes d'etudid
- """
- E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
- if not E["jour"]:
- return [], [], [], [], [] # evaluation sans date
-
- am, pm, demijournee = _eval_demijournee(E)
-
- # Liste les absences à ce moment:
- A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
- As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
- NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
- NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
- Just = sco_abs.list_abs_jour(
- ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
- )
- Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
-
- # Les notes:
- notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
- ValButAbs = [] # une note mais noté absent
- AbsNonSignalee = [] # note ABS mais pas noté absent
- ExcNonSignalee = [] # note EXC mais pas noté absent
- ExcNonJust = [] # note EXC mais absent non justifie
- AbsButExc = [] # note ABS mais justifié
- for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
- evaluation_id, getallstudents=True
- ):
- if etudid in notes_db:
- val = notes_db[etudid]["value"]
- if (
- val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
- ) and etudid in As:
- # note valide et absent
- ValButAbs.append(etudid)
- if val is None and not etudid in As:
- # absent mais pas signale comme tel
- AbsNonSignalee.append(etudid)
- if val == scu.NOTES_NEUTRALISE and not etudid in As:
- # Neutralisé mais pas signale absent
- ExcNonSignalee.append(etudid)
- if val == scu.NOTES_NEUTRALISE and etudid in NJs:
- # EXC mais pas justifié
- ExcNonJust.append(etudid)
- if val is None and etudid in Justs:
- # ABS mais justificatif
- AbsButExc.append(etudid)
-
- return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
-
-
-def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
- """Affiche état vérification absences d'une évaluation"""
-
- E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
- am, pm, demijournee = _eval_demijournee(E)
-
- (
- ValButAbs,
- AbsNonSignalee,
- ExcNonSignalee,
- ExcNonJust,
- AbsButExc,
- ) = evaluation_check_absences(evaluation_id)
-
- if with_header:
- H = [
- html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
- sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
- """
Vérification de la cohérence entre les notes saisies et les absences signalées.
""",
- ]
- else:
- # pas de header, mais un titre
- H = [
- """
%s du %s """
- % (E["description"], E["jour"])
- ]
- if (
- not ValButAbs
- and not AbsNonSignalee
- and not ExcNonSignalee
- and not ExcNonJust
- ):
- H.append(': ok')
- H.append("
Etudiants ayant une note alors qu'ils sont signalés absents:
"
- )
- etudlist(ValButAbs)
-
- if AbsNonSignalee or show_ok:
- H.append(
- """
Etudiants avec note "ABS" alors qu'ils ne sont pas signalés absents:
"""
- )
- etudlist(AbsNonSignalee, linkabs=True)
-
- if ExcNonSignalee or show_ok:
- H.append(
- """
Etudiants avec note "EXC" alors qu'ils ne sont pas signalés absents:
"""
- )
- etudlist(ExcNonSignalee)
-
- if ExcNonJust or show_ok:
- H.append(
- """
Etudiants avec note "EXC" alors qu'ils sont absents non justifiés:
"""
- )
- etudlist(ExcNonJust)
-
- if AbsButExc or show_ok:
- H.append(
- """
Etudiants avec note "ABS" alors qu'ils ont une justification:
"""
- )
- etudlist(AbsButExc)
-
- if with_header:
- H.append(html_sco_header.sco_footer())
- return "\n".join(H)
-
-
-def formsemestre_check_absences_html(formsemestre_id):
- """Affiche etat verification absences pour toutes les evaluations du semestre !"""
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- H = [
- html_sco_header.html_sem_header(
- "Vérification absences aux évaluations de ce semestre",
- ),
- """
Vérification de la cohérence entre les notes saisies et les absences signalées.
- Sont listés tous les modules avec des évaluations. Aucune action n'est effectuée:
- il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
-
""",
- ]
- # Modules, dans l'ordre
- Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
- for M in Mlist:
- evals = sco_evaluation_db.do_evaluation_list(
- {"moduleimpl_id": M["moduleimpl_id"]}
- )
- if evals:
- H.append(
- '
'
- % (
- M["moduleimpl_id"],
- M["module"]["code"] or "",
- M["module"]["abbrev"] or "",
- )
- )
- for E in evals:
- H.append(
- evaluation_check_absences_html(
- E["evaluation_id"],
- with_header=False,
- show_ok=False,
- )
- )
- if evals:
- H.append("
")
- H.append(html_sco_header.sco_footer())
- return "\n".join(H)
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""Vérification des absences à une évaluation
+"""
+from flask import url_for, g
+
+import app.scodoc.sco_utils as scu
+import app.scodoc.notesdb as ndb
+from app.scodoc import html_sco_header
+from app.scodoc import sco_abs
+from app.scodoc import sco_etud
+from app.scodoc import sco_evaluations
+from app.scodoc import sco_evaluation_db
+from app.scodoc import sco_formsemestre
+from app.scodoc import sco_groups
+from app.scodoc import sco_moduleimpl
+
+# matin et/ou après-midi ?
+def _eval_demijournee(E):
+ "1 si matin, 0 si apres midi, 2 si toute la journee"
+ am, pm = False, False
+ if E["heure_debut"] < "13:00":
+ am = True
+ if E["heure_fin"] > "13:00":
+ pm = True
+ if am and pm:
+ demijournee = 2
+ elif am:
+ demijournee = 1
+ else:
+ demijournee = 0
+ pm = True
+ return am, pm, demijournee
+
+
+def evaluation_check_absences(evaluation_id):
+ """Vérifie les absences au moment de cette évaluation.
+ Cas incohérents que l'on peut rencontrer pour chaque étudiant:
+ note et absent
+ ABS et pas noté absent
+ ABS et absent justifié
+ EXC et pas noté absent
+ EXC et pas justifie
+ Ramene 3 listes d'etudid
+ """
+ E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+ if not E["jour"]:
+ return [], [], [], [], [] # evaluation sans date
+
+ am, pm, demijournee = _eval_demijournee(E)
+
+ # Liste les absences à ce moment:
+ A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
+ As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
+ NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
+ NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
+ Just = sco_abs.list_abs_jour(
+ ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
+ )
+ Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
+
+ # Les notes:
+ notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
+ ValButAbs = [] # une note mais noté absent
+ AbsNonSignalee = [] # note ABS mais pas noté absent
+ ExcNonSignalee = [] # note EXC mais pas noté absent
+ ExcNonJust = [] # note EXC mais absent non justifie
+ AbsButExc = [] # note ABS mais justifié
+ for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
+ evaluation_id, getallstudents=True
+ ):
+ if etudid in notes_db:
+ val = notes_db[etudid]["value"]
+ if (
+ val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
+ ) and etudid in As:
+ # note valide et absent
+ ValButAbs.append(etudid)
+ if val is None and not etudid in As:
+ # absent mais pas signale comme tel
+ AbsNonSignalee.append(etudid)
+ if val == scu.NOTES_NEUTRALISE and not etudid in As:
+ # Neutralisé mais pas signale absent
+ ExcNonSignalee.append(etudid)
+ if val == scu.NOTES_NEUTRALISE and etudid in NJs:
+ # EXC mais pas justifié
+ ExcNonJust.append(etudid)
+ if val is None and etudid in Justs:
+ # ABS mais justificatif
+ AbsButExc.append(etudid)
+
+ return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
+
+
+def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
+ """Affiche état vérification absences d'une évaluation"""
+
+ E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+ am, pm, demijournee = _eval_demijournee(E)
+
+ (
+ ValButAbs,
+ AbsNonSignalee,
+ ExcNonSignalee,
+ ExcNonJust,
+ AbsButExc,
+ ) = evaluation_check_absences(evaluation_id)
+
+ if with_header:
+ H = [
+ html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
+ sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
+ """
Vérification de la cohérence entre les notes saisies et les absences signalées.
""",
+ ]
+ else:
+ # pas de header, mais un titre
+ H = [
+ """
%s du %s """
+ % (E["description"], E["jour"])
+ ]
+ if (
+ not ValButAbs
+ and not AbsNonSignalee
+ and not ExcNonSignalee
+ and not ExcNonJust
+ ):
+ H.append(': ok')
+ H.append("
Etudiants ayant une note alors qu'ils sont signalés absents:
"
+ )
+ etudlist(ValButAbs)
+
+ if AbsNonSignalee or show_ok:
+ H.append(
+ """
Etudiants avec note "ABS" alors qu'ils ne sont pas signalés absents:
"""
+ )
+ etudlist(AbsNonSignalee, linkabs=True)
+
+ if ExcNonSignalee or show_ok:
+ H.append(
+ """
Etudiants avec note "EXC" alors qu'ils ne sont pas signalés absents:
"""
+ )
+ etudlist(ExcNonSignalee)
+
+ if ExcNonJust or show_ok:
+ H.append(
+ """
Etudiants avec note "EXC" alors qu'ils sont absents non justifiés:
"""
+ )
+ etudlist(ExcNonJust)
+
+ if AbsButExc or show_ok:
+ H.append(
+ """
Etudiants avec note "ABS" alors qu'ils ont une justification:
"""
+ )
+ etudlist(AbsButExc)
+
+ if with_header:
+ H.append(html_sco_header.sco_footer())
+ return "\n".join(H)
+
+
+def formsemestre_check_absences_html(formsemestre_id):
+ """Affiche etat verification absences pour toutes les evaluations du semestre !"""
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ H = [
+ html_sco_header.html_sem_header(
+ "Vérification absences aux évaluations de ce semestre",
+ ),
+ """
Vérification de la cohérence entre les notes saisies et les absences signalées.
+ Sont listés tous les modules avec des évaluations. Aucune action n'est effectuée:
+ il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
+
""",
+ ]
+ # Modules, dans l'ordre
+ Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+ for M in Mlist:
+ evals = sco_evaluation_db.do_evaluation_list(
+ {"moduleimpl_id": M["moduleimpl_id"]}
+ )
+ if evals:
+ H.append(
+ '
'
+ % (
+ M["moduleimpl_id"],
+ M["module"]["code"] or "",
+ M["module"]["abbrev"] or "",
+ )
+ )
+ for E in evals:
+ H.append(
+ evaluation_check_absences_html(
+ E["evaluation_id"],
+ with_header=False,
+ show_ok=False,
+ )
+ )
+ if evals:
+ H.append("
")
+ H.append(html_sco_header.sco_footer())
+ return "\n".join(H)
diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py
index 581e0add..163daf74 100644
--- a/app/scodoc/sco_find_etud.py
+++ b/app/scodoc/sco_find_etud.py
@@ -1,411 +1,411 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""Recherche d'étudiants
-"""
-import flask
-from flask import url_for, g, request
-from flask_login import current_user
-
-import app
-from app.models import Departement
-import app.scodoc.sco_utils as scu
-import app.scodoc.notesdb as ndb
-from app.scodoc.gen_tables import GenTable
-from app.scodoc import html_sco_header
-from app.scodoc import sco_etud
-from app.scodoc import sco_groups
-from app.scodoc.sco_exceptions import ScoException
-from app.scodoc.sco_permissions import Permission
-from app.scodoc import sco_preferences
-
-
-def form_search_etud(
- dest_url=None,
- parameters=None,
- parameters_keys=None,
- title="Rechercher un étudiant par nom : ",
- add_headers=False, # complete page
-):
- "form recherche par nom"
- H = []
- H.append(
- f"""")
-
- if add_headers:
- return (
- html_sco_header.sco_header(page_title="Choix d'un étudiant")
- + "\n".join(H)
- + html_sco_header.sco_footer()
- )
- else:
- return "\n".join(H)
-
-
-def search_etud_in_dept(expnom=""):
- """Page recherche d'un etudiant.
-
- Affiche la fiche de l'étudiant, ou, si la recherche donne plusieurs résultats,
- la liste des étudiants correspondants.
- Appelée par:
- - boite de recherche barre latérale gauche.
- - choix d'un étudiant à inscrire (en POST avec dest_url et parameters_keys)
-
- Args:
- expnom: string, regexp sur le nom ou un code_nip ou un etudid
- """
- if isinstance(expnom, int) or len(expnom) > 1:
- try:
- etudid = int(expnom)
- except ValueError:
- etudid = None
- if etudid is not None:
- etuds = sco_etud.get_etud_info(filled=True, etudid=expnom)
- if (etudid is None) or len(etuds) != 1:
- expnom_str = str(expnom)
- if scu.is_valid_code_nip(expnom_str):
- etuds = search_etuds_infos(code_nip=expnom_str)
- else:
- etuds = search_etuds_infos(expnom=expnom_str)
- else:
- etuds = [] # si expnom est trop court, n'affiche rien
-
- if request.method == "POST":
- vals = request.form
- elif request.method == "GET":
- vals = request.args
- else:
- vals = {}
-
- url_args = {"scodoc_dept": g.scodoc_dept}
- if "dest_url" in vals:
- endpoint = vals["dest_url"]
- else:
- endpoint = "scolar.ficheEtud"
- if "parameters_keys" in vals:
- for key in vals["parameters_keys"].split(","):
- url_args[key] = vals[key]
-
- if len(etuds) == 1:
- # va directement a la fiche
- url_args["etudid"] = etuds[0]["etudid"]
- return flask.redirect(url_for(endpoint, **url_args))
-
- H = [
- html_sco_header.sco_header(
- page_title="Recherche d'un étudiant",
- no_side_bar=False,
- init_qtip=True,
- javascripts=["js/etud_info.js"],
- )
- ]
- if len(etuds) == 0 and len(etuds) <= 1:
- H.append("""
chercher un étudiant:
""")
- else:
- H.append(
- f"""
{len(etuds)} résultats pour "{expnom}": choisissez un étudiant:
"""
- )
- H.append(
- form_search_etud(
- dest_url=endpoint,
- parameters=vals.get("parameters"),
- parameters_keys=vals.get("parameters_keys"),
- title="Autre recherche",
- )
- )
- if len(etuds) > 0:
- # Choix dans la liste des résultats:
- for e in etuds:
- url_args["etudid"] = e["etudid"]
- target = url_for(endpoint, **url_args)
- e["_nomprenom_target"] = target
- e["inscription_target"] = target
- e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
- sco_groups.etud_add_group_infos(
- e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
- )
-
- tab = GenTable(
- columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
- titles={
- "nomprenom": "Étudiant",
- "code_nip": "NIP",
- "inscription": "Inscription",
- "groupes": "Groupes",
- },
- rows=etuds,
- html_sortable=True,
- html_class="table_leftalign",
- preferences=sco_preferences.SemPreferences(),
- )
- H.append(tab.html())
- if len(etuds) > 20: # si la page est grande
- H.append(
- form_search_etud(
- dest_url=endpoint,
- parameters=vals.get("parameters"),
- parameters_keys=vals.get("parameters_keys"),
- title="Autre recherche",
- )
- )
- else:
- H.append('
Aucun résultat pour "%s".
' % expnom)
- H.append(
- """
La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant. Saisir au moins deux caractères.
"""
- )
- return "\n".join(H) + html_sco_header.sco_footer()
-
-
-# Was chercheEtudsInfo()
-def search_etuds_infos(expnom=None, code_nip=None):
- """recherche les étudiants correspondants à expnom ou au code_nip
- et ramene liste de mappings utilisables en DTML.
- """
- may_be_nip = scu.is_valid_code_nip(expnom)
- cnx = ndb.GetDBConnexion()
- if expnom and not may_be_nip:
- expnom = expnom.upper() # les noms dans la BD sont en uppercase
- try:
- etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
- except ScoException:
- etuds = []
- else:
- code_nip = code_nip or expnom
- if code_nip:
- etuds = sco_etud.etudident_list(cnx, args={"code_nip": str(code_nip)})
- else:
- etuds = []
- sco_etud.fill_etuds_info(etuds)
- return etuds
-
-
-def search_etud_by_name(term: str) -> list:
- """Recherche noms étudiants par début du nom, pour autocomplete
- Accepte aussi un début de code NIP (au moins 6 caractères)
- Renvoie une liste de dicts
- { "label" : "", "value" : etudid }
- """
- may_be_nip = scu.is_valid_code_nip(term)
- # term = term.upper() # conserve les accents
- term = term.upper()
- if (
- not scu.ALPHANUM_EXP.match(term) # n'autorise pas les caractères spéciaux
- and not may_be_nip
- ):
- data = []
- else:
- if may_be_nip:
- r = ndb.SimpleDictFetch(
- """SELECT nom, prenom, code_nip
- FROM identite
- WHERE
- dept_id = %(dept_id)s
- AND code_nip LIKE %(beginning)s
- ORDER BY nom
- """,
- {"beginning": term + "%", "dept_id": g.scodoc_dept_id},
- )
- data = [
- {
- "label": "%s %s %s"
- % (x["code_nip"], x["nom"], sco_etud.format_prenom(x["prenom"])),
- "value": x["code_nip"],
- }
- for x in r
- ]
- else:
- r = ndb.SimpleDictFetch(
- """SELECT id AS etudid, nom, prenom
- FROM identite
- WHERE
- dept_id = %(dept_id)s
- AND nom LIKE %(beginning)s
- ORDER BY nom
- """,
- {"beginning": term + "%", "dept_id": g.scodoc_dept_id},
- )
-
- data = [
- {
- "label": "%s %s" % (x["nom"], sco_etud.format_prenom(x["prenom"])),
- "value": x["etudid"],
- }
- for x in r
- ]
- return data
-
-
-# ---------- Recherche sur plusieurs département
-
-
-def search_etud_in_accessible_depts(expnom=None, code_nip=None):
- """
- result is a list of (sorted) etuds, one list per dept.
- """
- result = []
- accessible_depts = []
- depts = Departement.query.filter_by(visible=True).all()
- for dept in depts:
- if current_user.has_permission(Permission.ScoView, dept=dept.acronym):
- if expnom or code_nip:
- accessible_depts.append(dept.acronym)
- app.set_sco_dept(dept.acronym)
- etuds = search_etuds_infos(expnom=expnom, code_nip=code_nip)
- else:
- etuds = []
- result.append(etuds)
- return result, accessible_depts
-
-
-def table_etud_in_accessible_depts(expnom=None):
- """
- Page avec table étudiants trouvés, dans tous les departements.
- Attention: nous sommes ici au niveau de ScoDoc, pas dans un département
- """
- result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom)
- H = [
- """
""",
- """
Recherche multi-département de "%s"
""" % expnom,
- ]
- for etuds in result:
- if etuds:
- dept_id = etuds[0]["dept"]
- # H.append('
- """
- )
- return (
- html_sco_header.scodoc_top_html_header(page_title="Choix d'un étudiant")
- + "\n".join(H)
- + html_sco_header.standard_html_footer()
- )
-
-
-def search_inscr_etud_by_nip(code_nip, format="json"):
- """Recherche multi-departement d'un étudiant par son code NIP
- Seuls les départements accessibles par l'utilisateur sont cherchés.
-
- Renvoie une liste des inscriptions de l'étudiants dans tout ScoDoc:
- code_nip, nom, prenom, civilite_str, dept, formsemestre_id, date_debut_sem, date_fin_sem
- """
- result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
-
- T = []
- for etuds in result:
- if etuds:
- dept_id = etuds[0]["dept"]
- for e in etuds:
- for sem in e["sems"]:
- T.append(
- {
- "dept": dept_id,
- "etudid": e["etudid"],
- "code_nip": e["code_nip"],
- "civilite_str": e["civilite_str"],
- "nom": e["nom"],
- "prenom": e["prenom"],
- "formsemestre_id": sem["formsemestre_id"],
- "date_debut_iso": sem["date_debut_iso"],
- "date_fin_iso": sem["date_fin_iso"],
- }
- )
-
- columns_ids = (
- "dept",
- "etudid",
- "code_nip",
- "civilite_str",
- "nom",
- "prenom",
- "formsemestre_id",
- "date_debut_iso",
- "date_fin_iso",
- )
- tab = GenTable(columns_ids=columns_ids, rows=T)
-
- return tab.make_page(format=format, with_html_headers=False, publish=True)
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""Recherche d'étudiants
+"""
+import flask
+from flask import url_for, g, request
+from flask_login import current_user
+
+import app
+from app.models import Departement
+import app.scodoc.sco_utils as scu
+import app.scodoc.notesdb as ndb
+from app.scodoc.gen_tables import GenTable
+from app.scodoc import html_sco_header
+from app.scodoc import sco_etud
+from app.scodoc import sco_groups
+from app.scodoc.sco_exceptions import ScoException
+from app.scodoc.sco_permissions import Permission
+from app.scodoc import sco_preferences
+
+
+def form_search_etud(
+ dest_url=None,
+ parameters=None,
+ parameters_keys=None,
+ title="Rechercher un étudiant par nom : ",
+ add_headers=False, # complete page
+):
+ "form recherche par nom"
+ H = []
+ H.append(
+ f"""")
+
+ if add_headers:
+ return (
+ html_sco_header.sco_header(page_title="Choix d'un étudiant")
+ + "\n".join(H)
+ + html_sco_header.sco_footer()
+ )
+ else:
+ return "\n".join(H)
+
+
+def search_etud_in_dept(expnom=""):
+ """Page recherche d'un etudiant.
+
+ Affiche la fiche de l'étudiant, ou, si la recherche donne plusieurs résultats,
+ la liste des étudiants correspondants.
+ Appelée par:
+ - boite de recherche barre latérale gauche.
+ - choix d'un étudiant à inscrire (en POST avec dest_url et parameters_keys)
+
+ Args:
+ expnom: string, regexp sur le nom ou un code_nip ou un etudid
+ """
+ if isinstance(expnom, int) or len(expnom) > 1:
+ try:
+ etudid = int(expnom)
+ except ValueError:
+ etudid = None
+ if etudid is not None:
+ etuds = sco_etud.get_etud_info(filled=True, etudid=expnom)
+ if (etudid is None) or len(etuds) != 1:
+ expnom_str = str(expnom)
+ if scu.is_valid_code_nip(expnom_str):
+ etuds = search_etuds_infos(code_nip=expnom_str)
+ else:
+ etuds = search_etuds_infos(expnom=expnom_str)
+ else:
+ etuds = [] # si expnom est trop court, n'affiche rien
+
+ if request.method == "POST":
+ vals = request.form
+ elif request.method == "GET":
+ vals = request.args
+ else:
+ vals = {}
+
+ url_args = {"scodoc_dept": g.scodoc_dept}
+ if "dest_url" in vals:
+ endpoint = vals["dest_url"]
+ else:
+ endpoint = "scolar.ficheEtud"
+ if "parameters_keys" in vals:
+ for key in vals["parameters_keys"].split(","):
+ url_args[key] = vals[key]
+
+ if len(etuds) == 1:
+ # va directement a la fiche
+ url_args["etudid"] = etuds[0]["etudid"]
+ return flask.redirect(url_for(endpoint, **url_args))
+
+ H = [
+ html_sco_header.sco_header(
+ page_title="Recherche d'un étudiant",
+ no_side_bar=False,
+ init_qtip=True,
+ javascripts=["js/etud_info.js"],
+ )
+ ]
+ if len(etuds) == 0 and len(etuds) <= 1:
+ H.append("""
chercher un étudiant:
""")
+ else:
+ H.append(
+ f"""
{len(etuds)} résultats pour "{expnom}": choisissez un étudiant:
"""
+ )
+ H.append(
+ form_search_etud(
+ dest_url=endpoint,
+ parameters=vals.get("parameters"),
+ parameters_keys=vals.get("parameters_keys"),
+ title="Autre recherche",
+ )
+ )
+ if len(etuds) > 0:
+ # Choix dans la liste des résultats:
+ for e in etuds:
+ url_args["etudid"] = e["etudid"]
+ target = url_for(endpoint, **url_args)
+ e["_nomprenom_target"] = target
+ e["inscription_target"] = target
+ e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
+ sco_groups.etud_add_group_infos(
+ e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
+ )
+
+ tab = GenTable(
+ columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
+ titles={
+ "nomprenom": "Étudiant",
+ "code_nip": "NIP",
+ "inscription": "Inscription",
+ "groupes": "Groupes",
+ },
+ rows=etuds,
+ html_sortable=True,
+ html_class="table_leftalign",
+ preferences=sco_preferences.SemPreferences(),
+ )
+ H.append(tab.html())
+ if len(etuds) > 20: # si la page est grande
+ H.append(
+ form_search_etud(
+ dest_url=endpoint,
+ parameters=vals.get("parameters"),
+ parameters_keys=vals.get("parameters_keys"),
+ title="Autre recherche",
+ )
+ )
+ else:
+ H.append('
Aucun résultat pour "%s".
' % expnom)
+ H.append(
+ """
La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant. Saisir au moins deux caractères.
"""
+ )
+ return "\n".join(H) + html_sco_header.sco_footer()
+
+
+# Was chercheEtudsInfo()
+def search_etuds_infos(expnom=None, code_nip=None):
+ """recherche les étudiants correspondants à expnom ou au code_nip
+ et ramene liste de mappings utilisables en DTML.
+ """
+ may_be_nip = scu.is_valid_code_nip(expnom)
+ cnx = ndb.GetDBConnexion()
+ if expnom and not may_be_nip:
+ expnom = expnom.upper() # les noms dans la BD sont en uppercase
+ try:
+ etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
+ except ScoException:
+ etuds = []
+ else:
+ code_nip = code_nip or expnom
+ if code_nip:
+ etuds = sco_etud.etudident_list(cnx, args={"code_nip": str(code_nip)})
+ else:
+ etuds = []
+ sco_etud.fill_etuds_info(etuds)
+ return etuds
+
+
+def search_etud_by_name(term: str) -> list:
+ """Recherche noms étudiants par début du nom, pour autocomplete
+ Accepte aussi un début de code NIP (au moins 6 caractères)
+ Renvoie une liste de dicts
+ { "label" : "", "value" : etudid }
+ """
+ may_be_nip = scu.is_valid_code_nip(term)
+ # term = term.upper() # conserve les accents
+ term = term.upper()
+ if (
+ not scu.ALPHANUM_EXP.match(term) # n'autorise pas les caractères spéciaux
+ and not may_be_nip
+ ):
+ data = []
+ else:
+ if may_be_nip:
+ r = ndb.SimpleDictFetch(
+ """SELECT nom, prenom, code_nip
+ FROM identite
+ WHERE
+ dept_id = %(dept_id)s
+ AND code_nip LIKE %(beginning)s
+ ORDER BY nom
+ """,
+ {"beginning": term + "%", "dept_id": g.scodoc_dept_id},
+ )
+ data = [
+ {
+ "label": "%s %s %s"
+ % (x["code_nip"], x["nom"], sco_etud.format_prenom(x["prenom"])),
+ "value": x["code_nip"],
+ }
+ for x in r
+ ]
+ else:
+ r = ndb.SimpleDictFetch(
+ """SELECT id AS etudid, nom, prenom
+ FROM identite
+ WHERE
+ dept_id = %(dept_id)s
+ AND nom LIKE %(beginning)s
+ ORDER BY nom
+ """,
+ {"beginning": term + "%", "dept_id": g.scodoc_dept_id},
+ )
+
+ data = [
+ {
+ "label": "%s %s" % (x["nom"], sco_etud.format_prenom(x["prenom"])),
+ "value": x["etudid"],
+ }
+ for x in r
+ ]
+ return data
+
+
+# ---------- Recherche sur plusieurs département
+
+
+def search_etud_in_accessible_depts(expnom=None, code_nip=None):
+ """
+ result is a list of (sorted) etuds, one list per dept.
+ """
+ result = []
+ accessible_depts = []
+ depts = Departement.query.filter_by(visible=True).all()
+ for dept in depts:
+ if current_user.has_permission(Permission.ScoView, dept=dept.acronym):
+ if expnom or code_nip:
+ accessible_depts.append(dept.acronym)
+ app.set_sco_dept(dept.acronym)
+ etuds = search_etuds_infos(expnom=expnom, code_nip=code_nip)
+ else:
+ etuds = []
+ result.append(etuds)
+ return result, accessible_depts
+
+
+def table_etud_in_accessible_depts(expnom=None):
+ """
+ Page avec table étudiants trouvés, dans tous les departements.
+ Attention: nous sommes ici au niveau de ScoDoc, pas dans un département
+ """
+ result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom)
+ H = [
+ """
""",
+ """
Recherche multi-département de "%s"
""" % expnom,
+ ]
+ for etuds in result:
+ if etuds:
+ dept_id = etuds[0]["dept"]
+ # H.append('
+ """
+ )
+ return (
+ html_sco_header.scodoc_top_html_header(page_title="Choix d'un étudiant")
+ + "\n".join(H)
+ + html_sco_header.standard_html_footer()
+ )
+
+
+def search_inscr_etud_by_nip(code_nip, format="json"):
+ """Recherche multi-departement d'un étudiant par son code NIP
+ Seuls les départements accessibles par l'utilisateur sont cherchés.
+
+ Renvoie une liste des inscriptions de l'étudiants dans tout ScoDoc:
+ code_nip, nom, prenom, civilite_str, dept, formsemestre_id, date_debut_sem, date_fin_sem
+ """
+ result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
+
+ T = []
+ for etuds in result:
+ if etuds:
+ dept_id = etuds[0]["dept"]
+ for e in etuds:
+ for sem in e["sems"]:
+ T.append(
+ {
+ "dept": dept_id,
+ "etudid": e["etudid"],
+ "code_nip": e["code_nip"],
+ "civilite_str": e["civilite_str"],
+ "nom": e["nom"],
+ "prenom": e["prenom"],
+ "formsemestre_id": sem["formsemestre_id"],
+ "date_debut_iso": sem["date_debut_iso"],
+ "date_fin_iso": sem["date_fin_iso"],
+ }
+ )
+
+ columns_ids = (
+ "dept",
+ "etudid",
+ "code_nip",
+ "civilite_str",
+ "nom",
+ "prenom",
+ "formsemestre_id",
+ "date_debut_iso",
+ "date_fin_iso",
+ )
+ tab = GenTable(columns_ids=columns_ids, rows=T)
+
+ return tab.make_page(format=format, with_html_headers=False, publish=True)
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 1195c575..f3afb4d7 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -1659,7 +1659,7 @@ def formsemestre_change_publication_bul(
"
Confirmer la %s publication des bulletins ?
" % msg,
helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins,
par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
-
+
Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc et un portail étudiant.
""",
dest_url="",
diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py
index 540282e9..2c4fd681 100644
--- a/app/scodoc/sco_formsemestre_inscriptions.py
+++ b/app/scodoc/sco_formsemestre_inscriptions.py
@@ -893,7 +893,7 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
H.append("")
H.append("
Total: %d étudiants concernés.
" % len(etudlist))
H.append(
- """
Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps ! Sauf exception, cette situation est anormale:
+ """
Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps ! Sauf exception, cette situation est anormale:
vérifier que les dates des semestres se suivent sans se chevaucher
ou si besoin désinscrire le(s) étudiant(s) de l'un des semestres (via leurs fiches individuelles).
Attention: les UE suivantes de cette formation
sont utilisées dans des
- semestres de rangs différents (eg S1 et S3). Cela peut engendrer des problèmes pour
+ semestres de rangs différents (eg S1 et S3). Cela peut engendrer des problèmes pour
la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation:
soit modifier le programme de la formation (définir des UE dans chaque semestre),
soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une
diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py
index 89338e8b..d8b5c3e0 100644
--- a/app/scodoc/sco_import_etuds.py
+++ b/app/scodoc/sco_import_etuds.py
@@ -302,7 +302,7 @@ def scolars_import_excel_file(
else:
unknown.append(f)
raise ScoValueError(
- """Nombre de colonnes incorrect (devrait être %d, et non %d)
+ """Nombre de colonnes incorrect (devrait être %d, et non %d)
(colonnes manquantes: %s, colonnes invalides: %s)"""
% (len(titles), len(fs), list(missing.keys()), unknown)
)
diff --git a/app/scodoc/sco_import_users.py b/app/scodoc/sco_import_users.py
index a0b821ea..8683c008 100644
--- a/app/scodoc/sco_import_users.py
+++ b/app/scodoc/sco_import_users.py
@@ -1,312 +1,312 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""Import d'utilisateurs via fichier Excel
-"""
-import random
-import time
-
-from email.mime.multipart import MIMEMultipart
-from flask import g, url_for
-from flask_login import current_user
-
-from app import db
-from app import email
-from app.auth.models import User, UserRole
-import app.scodoc.sco_utils as scu
-from app import log
-from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
-from app.scodoc import sco_excel
-from app.scodoc import sco_preferences
-from app.scodoc import sco_users
-
-
-TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
-COMMENTS = (
- """user_name:
- Composé de lettres (minuscules ou majuscules), de chiffres ou du caractère _
- """,
- """nom:
- Maximum 64 caractères""",
- """prenom:
- Maximum 64 caractères""",
- """email:
- Maximum 120 caractères""",
- """roles:
- un plusieurs rôles séparés par ','
- chaque role est fait de 2 composantes séparées par _:
- 1. Le role (Ens, Secr ou Admin)
- 2. Le département (en majuscule)
- Exemple: "Ens_RT,Admin_INFO"
- """,
- """dept:
- Le département d'appartenance du l'utillsateur. Laisser vide si l'utilisateur intervient dans plusieurs dépatements
- """,
-)
-
-
-def generate_excel_sample():
- """generates an excel document suitable to import users"""
- style = sco_excel.excel_make_style(bold=True)
- titles = TITLES
- titles_styles = [style] * len(titles)
- return sco_excel.excel_simple_table(
- titles=titles,
- titles_styles=titles_styles,
- sheet_name="Utilisateurs ScoDoc",
- comments=COMMENTS,
- )
-
-
-def import_excel_file(datafile, force=""):
- """
- Import scodoc users from Excel file.
- This method:
- * checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any department (or all)
- * Once the check is done ans successfull, build the list of users (does not check the data)
- * call :func:`import_users` to actually do the job
- history: scodoc7 with no SuperAdmin every Admin_XXX could import users.
- :param datafile: the stream from to the to be imported
- :return: same as import users
- """
- # Check current user privilege
- auth_name = str(current_user)
- if not current_user.is_administrator():
- raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
- # Récupération des informations sur l'utilisateur courant
- log("sco_import_users.import_excel_file by %s" % auth_name)
- # Read the data from the stream
- exceldata = datafile.read()
- if not exceldata:
- raise ScoValueError("Ficher excel vide ou invalide")
- _, data = sco_excel.excel_bytes_to_list(exceldata)
- if not data:
- raise ScoValueError(
- """Le fichier xlsx attendu semble vide !
- """
- )
- # 1- --- check title line
- fs = [scu.stripquotes(s).lower() for s in data[0]]
- log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
- # check cols
- cols = {}.fromkeys(TITLES)
- unknown = []
- for tit in fs:
- if tit not in cols:
- unknown.append(tit)
- else:
- del cols[tit]
- if cols or unknown:
- raise ScoValueError(
- """colonnes incorrectes (on attend %d, et non %d)
- (colonnes manquantes: %s, colonnes invalides: %s)"""
- % (len(TITLES), len(fs), list(cols.keys()), unknown)
- )
- # ok, same titles... : build the list of dictionaries
- users = []
- for line in data[1:]:
- d = {}
- for i in range(len(fs)):
- d[fs[i]] = line[i]
- users.append(d)
-
- return import_users(users=users, force=force)
-
-
-def import_users(users, force=""):
- """
- Import users from a list of users_descriptors.
-
- descriptors are dictionaries hosting users's data.
- The operation is atomic (all the users are imported or none)
-
- :param users: list of descriptors to be imported
-
- :return: a tuple that describe the result of the import:
- * ok: import ok or aborted
- * messages: the list of messages
- * the # of users created
-
- Implémentation:
- Pour chaque utilisateur à créer:
- * vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois)
- * générer mot de passe aléatoire
- * créer utilisateur et mettre le mot de passe
- * envoyer mot de passe par mail
- Les utilisateurs à créer sont stockés dans un dictionnaire.
- L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée
- """
-
- created = {} # uid créés
- if len(users) == 0:
- import_ok = False
- msg_list = ["Feuille vide ou illisible"]
- else:
- msg_list = []
- line = 1 # start from excel line #2
- import_ok = True
-
- def append_msg(msg):
- msg_list.append("Ligne %s : %s" % (line, msg))
-
- try:
- for u in users:
- line = line + 1
- user_ok, msg = sco_users.check_modif_user(
- 0,
- enforce_optionals=not force,
- user_name=u["user_name"],
- nom=u["nom"],
- prenom=u["prenom"],
- email=u["email"],
- roles=[r for r in u["roles"].split(",") if r],
- dept=u["dept"],
- )
- if not user_ok:
- append_msg("identifiant '%s' %s" % (u["user_name"], msg))
-
- u["passwd"] = generate_password()
- #
- # check identifiant
- if u["user_name"] in created.keys():
- user_ok = False
- append_msg(
- "l'utilisateur '%s' a déjà été décrit ligne %s"
- % (u["user_name"], created[u["user_name"]]["line"])
- )
- # check roles / ignore whitespaces around roles / build roles_string
- # roles_string (expected by User) appears as column 'roles' in excel file
- roles_list = []
- for role in u["roles"].split(","):
- try:
- role = role.strip()
- if role:
- _, _ = UserRole.role_dept_from_string(role)
- roles_list.append(role)
- except ScoValueError as value_error:
- user_ok = False
- append_msg("role %s : %s" % (role, value_error))
- u["roles_string"] = ",".join(roles_list)
- if user_ok:
- u["line"] = line
- created[u["user_name"]] = u
- else:
- import_ok = False
- except ScoValueError as value_error:
- log(f"import_users: exception: abort create {str(created.keys())}")
- raise ScoValueError(msg) from value_error
- if import_ok:
- for u in created.values():
- # Création de l'utilisateur (via SQLAlchemy)
- user = User()
- user.from_dict(u, new_user=True)
- db.session.add(user)
- db.session.commit()
- mail_password(u)
- else:
- created = {} # reset # of created users to 0
- return import_ok, msg_list, len(created)
-
-
-# --------- Génération du mot de passe initial -----------
-# Adapté de http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440564
-# Alphabet tres simple pour des mots de passe simples...
-
-
-ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU"""
-PASSLEN = 8
-RNG = random.Random(time.time())
-
-
-def generate_password():
- """This function creates a pseudo random number generator object, seeded with
- the cryptographic hash of the passString. The contents of the character set
- is then shuffled and a selection of passLength words is made from this list.
- This selection is returned as the generated password."""
- l = list(ALPHABET) # make this mutable so that we can shuffle the characters
- RNG.shuffle(l) # shuffle the character set
- # pick up only a subset from the available characters:
- return "".join(RNG.sample(l, PASSLEN))
-
-
-def mail_password(user: dict, reset=False) -> None:
- "Send password by email"
- if not user["email"]:
- return
-
- user["url"] = url_for("scodoc.index", _external=True)
- txt = (
- """
-Bonjour %(prenom)s %(nom)s,
-
-"""
- % user
- )
- if reset:
- txt += (
- """
-votre mot de passe ScoDoc a été ré-initialisé.
-
-Le nouveau mot de passe est: %(passwd)s
-Votre nom d'utilisateur est %(user_name)s
-
-Vous devrez changer ce mot de passe lors de votre première connexion
-sur %(url)s
-"""
- % user
- )
- else:
- txt += (
- """
-vous avez été déclaré comme utilisateur du logiciel de gestion de scolarité ScoDoc.
-
-Votre nom d'utilisateur est %(user_name)s
-Votre mot de passe est: %(passwd)s
-
-Le logiciel est accessible sur: %(url)s
-
-Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil).
-"""
- % user
- )
-
- txt += (
- """
-_______
-ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc.
-Pour plus d'informations sur ce logiciel, voir %s
-
-"""
- % scu.SCO_WEBSITE
- )
- msg = MIMEMultipart()
- if reset:
- subject = "Mot de passe ScoDoc"
- else:
- subject = "Votre accès ScoDoc"
- sender = sco_preferences.get_preference("email_from_addr")
- email.send_email(subject, sender, [user["email"]], txt)
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""Import d'utilisateurs via fichier Excel
+"""
+import random
+import time
+
+from email.mime.multipart import MIMEMultipart
+from flask import g, url_for
+from flask_login import current_user
+
+from app import db
+from app import email
+from app.auth.models import User, UserRole
+import app.scodoc.sco_utils as scu
+from app import log
+from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
+from app.scodoc import sco_excel
+from app.scodoc import sco_preferences
+from app.scodoc import sco_users
+
+
+TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
+COMMENTS = (
+ """user_name:
+ Composé de lettres (minuscules ou majuscules), de chiffres ou du caractère _
+ """,
+ """nom:
+ Maximum 64 caractères""",
+ """prenom:
+ Maximum 64 caractères""",
+ """email:
+ Maximum 120 caractères""",
+ """roles:
+ un plusieurs rôles séparés par ','
+ chaque role est fait de 2 composantes séparées par _:
+ 1. Le role (Ens, Secr ou Admin)
+ 2. Le département (en majuscule)
+ Exemple: "Ens_RT,Admin_INFO"
+ """,
+ """dept:
+ Le département d'appartenance du l'utillsateur. Laisser vide si l'utilisateur intervient dans plusieurs dépatements
+ """,
+)
+
+
+def generate_excel_sample():
+ """generates an excel document suitable to import users"""
+ style = sco_excel.excel_make_style(bold=True)
+ titles = TITLES
+ titles_styles = [style] * len(titles)
+ return sco_excel.excel_simple_table(
+ titles=titles,
+ titles_styles=titles_styles,
+ sheet_name="Utilisateurs ScoDoc",
+ comments=COMMENTS,
+ )
+
+
+def import_excel_file(datafile, force=""):
+ """
+ Import scodoc users from Excel file.
+ This method:
+ * checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any department (or all)
+ * Once the check is done ans successfull, build the list of users (does not check the data)
+ * call :func:`import_users` to actually do the job
+ history: scodoc7 with no SuperAdmin every Admin_XXX could import users.
+ :param datafile: the stream from to the to be imported
+ :return: same as import users
+ """
+ # Check current user privilege
+ auth_name = str(current_user)
+ if not current_user.is_administrator():
+ raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
+ # Récupération des informations sur l'utilisateur courant
+ log("sco_import_users.import_excel_file by %s" % auth_name)
+ # Read the data from the stream
+ exceldata = datafile.read()
+ if not exceldata:
+ raise ScoValueError("Ficher excel vide ou invalide")
+ _, data = sco_excel.excel_bytes_to_list(exceldata)
+ if not data:
+ raise ScoValueError(
+ """Le fichier xlsx attendu semble vide !
+ """
+ )
+ # 1- --- check title line
+ fs = [scu.stripquotes(s).lower() for s in data[0]]
+ log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
+ # check cols
+ cols = {}.fromkeys(TITLES)
+ unknown = []
+ for tit in fs:
+ if tit not in cols:
+ unknown.append(tit)
+ else:
+ del cols[tit]
+ if cols or unknown:
+ raise ScoValueError(
+ """colonnes incorrectes (on attend %d, et non %d)
+ (colonnes manquantes: %s, colonnes invalides: %s)"""
+ % (len(TITLES), len(fs), list(cols.keys()), unknown)
+ )
+ # ok, same titles... : build the list of dictionaries
+ users = []
+ for line in data[1:]:
+ d = {}
+ for i in range(len(fs)):
+ d[fs[i]] = line[i]
+ users.append(d)
+
+ return import_users(users=users, force=force)
+
+
+def import_users(users, force=""):
+ """
+ Import users from a list of users_descriptors.
+
+ descriptors are dictionaries hosting users's data.
+ The operation is atomic (all the users are imported or none)
+
+ :param users: list of descriptors to be imported
+
+ :return: a tuple that describe the result of the import:
+ * ok: import ok or aborted
+ * messages: the list of messages
+ * the # of users created
+
+ Implémentation:
+ Pour chaque utilisateur à créer:
+ * vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois)
+ * générer mot de passe aléatoire
+ * créer utilisateur et mettre le mot de passe
+ * envoyer mot de passe par mail
+ Les utilisateurs à créer sont stockés dans un dictionnaire.
+ L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée
+ """
+
+ created = {} # uid créés
+ if len(users) == 0:
+ import_ok = False
+ msg_list = ["Feuille vide ou illisible"]
+ else:
+ msg_list = []
+ line = 1 # start from excel line #2
+ import_ok = True
+
+ def append_msg(msg):
+ msg_list.append("Ligne %s : %s" % (line, msg))
+
+ try:
+ for u in users:
+ line = line + 1
+ user_ok, msg = sco_users.check_modif_user(
+ 0,
+ enforce_optionals=not force,
+ user_name=u["user_name"],
+ nom=u["nom"],
+ prenom=u["prenom"],
+ email=u["email"],
+ roles=[r for r in u["roles"].split(",") if r],
+ dept=u["dept"],
+ )
+ if not user_ok:
+ append_msg("identifiant '%s' %s" % (u["user_name"], msg))
+
+ u["passwd"] = generate_password()
+ #
+ # check identifiant
+ if u["user_name"] in created.keys():
+ user_ok = False
+ append_msg(
+ "l'utilisateur '%s' a déjà été décrit ligne %s"
+ % (u["user_name"], created[u["user_name"]]["line"])
+ )
+ # check roles / ignore whitespaces around roles / build roles_string
+ # roles_string (expected by User) appears as column 'roles' in excel file
+ roles_list = []
+ for role in u["roles"].split(","):
+ try:
+ role = role.strip()
+ if role:
+ _, _ = UserRole.role_dept_from_string(role)
+ roles_list.append(role)
+ except ScoValueError as value_error:
+ user_ok = False
+ append_msg("role %s : %s" % (role, value_error))
+ u["roles_string"] = ",".join(roles_list)
+ if user_ok:
+ u["line"] = line
+ created[u["user_name"]] = u
+ else:
+ import_ok = False
+ except ScoValueError as value_error:
+ log(f"import_users: exception: abort create {str(created.keys())}")
+ raise ScoValueError(msg) from value_error
+ if import_ok:
+ for u in created.values():
+ # Création de l'utilisateur (via SQLAlchemy)
+ user = User()
+ user.from_dict(u, new_user=True)
+ db.session.add(user)
+ db.session.commit()
+ mail_password(u)
+ else:
+ created = {} # reset # of created users to 0
+ return import_ok, msg_list, len(created)
+
+
+# --------- Génération du mot de passe initial -----------
+# Adapté de http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440564
+# Alphabet tres simple pour des mots de passe simples...
+
+
+ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU"""
+PASSLEN = 8
+RNG = random.Random(time.time())
+
+
+def generate_password():
+ """This function creates a pseudo random number generator object, seeded with
+ the cryptographic hash of the passString. The contents of the character set
+ is then shuffled and a selection of passLength words is made from this list.
+ This selection is returned as the generated password."""
+ l = list(ALPHABET) # make this mutable so that we can shuffle the characters
+ RNG.shuffle(l) # shuffle the character set
+ # pick up only a subset from the available characters:
+ return "".join(RNG.sample(l, PASSLEN))
+
+
+def mail_password(user: dict, reset=False) -> None:
+ "Send password by email"
+ if not user["email"]:
+ return
+
+ user["url"] = url_for("scodoc.index", _external=True)
+ txt = (
+ """
+Bonjour %(prenom)s %(nom)s,
+
+"""
+ % user
+ )
+ if reset:
+ txt += (
+ """
+votre mot de passe ScoDoc a été ré-initialisé.
+
+Le nouveau mot de passe est: %(passwd)s
+Votre nom d'utilisateur est %(user_name)s
+
+Vous devrez changer ce mot de passe lors de votre première connexion
+sur %(url)s
+"""
+ % user
+ )
+ else:
+ txt += (
+ """
+vous avez été déclaré comme utilisateur du logiciel de gestion de scolarité ScoDoc.
+
+Votre nom d'utilisateur est %(user_name)s
+Votre mot de passe est: %(passwd)s
+
+Le logiciel est accessible sur: %(url)s
+
+Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil).
+"""
+ % user
+ )
+
+ txt += (
+ """
+_______
+ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc.
+Pour plus d'informations sur ce logiciel, voir %s
+
+"""
+ % scu.SCO_WEBSITE
+ )
+ msg = MIMEMultipart()
+ if reset:
+ subject = "Mot de passe ScoDoc"
+ else:
+ subject = "Votre accès ScoDoc"
+ sender = sco_preferences.get_preference("email_from_addr")
+ email.send_email(subject, sender, [user["email"]], txt)
diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py
index b3aeba7a..402073b5 100644
--- a/app/scodoc/sco_liste_notes.py
+++ b/app/scodoc/sco_liste_notes.py
@@ -517,18 +517,18 @@ def _make_table_notes(
hh += "s"
hh += ", %d en attente." % (nb_att)
- pdf_title = " BORDEREAU DE SIGNATURES"
- pdf_title += "
%(titre)s" % sem
- pdf_title += " (%(mois_debut)s - %(mois_fin)s)" % sem
+ pdf_title = " BORDEREAU DE SIGNATURES"
+ pdf_title += "
%(titre)s" % sem
+ pdf_title += " (%(mois_debut)s - %(mois_fin)s)" % sem
pdf_title += " semestre %s %s" % (
sem["semestre_id"],
sem.get("modalite", ""),
)
- pdf_title += f" Notes du module {module.code} - {module.titre}"
- pdf_title += " Evaluation : %(description)s " % e
+ pdf_title += f" Notes du module {module.code} - {module.titre}"
+ pdf_title += " Evaluation : %(description)s " % e
if len(e["jour"]) > 0:
pdf_title += " (%(jour)s)" % e
- pdf_title += "(noté sur %(note_max)s )
" % e
+ pdf_title += "(noté sur %(note_max)s )
" % e
else:
hh = " %s, %s (%d étudiants)" % (
E["description"],
@@ -623,11 +623,11 @@ def _make_table_notes(
commentkeys.sort(key=lambda x: int(x[1]))
for (comment, key) in commentkeys:
C.append(
- '(%s)%s ' % (key, comment)
+ '(%s)%s ' % (key, comment)
)
if commentkeys:
C.append(
- 'Gérer les opérations '
+ 'Gérer les opérations '
% E["evaluation_id"]
)
eval_info = "xxx"
diff --git a/app/scodoc/sco_lycee.py b/app/scodoc/sco_lycee.py
index 1530d0f1..1aba7b89 100644
--- a/app/scodoc/sco_lycee.py
+++ b/app/scodoc/sco_lycee.py
@@ -1,259 +1,259 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""Rapports sur lycées d'origine des étudiants d'un semestre.
- - statistiques decisions
- - suivi cohortes
-"""
-from operator import itemgetter
-
-from flask import url_for, g, request
-
-import app
-import app.scodoc.sco_utils as scu
-from app.scodoc import html_sco_header
-from app.scodoc import sco_formsemestre
-from app.scodoc import sco_preferences
-from app.scodoc import sco_report
-from app.scodoc import sco_etud
-import sco_version
-from app.scodoc.gen_tables import GenTable
-
-
-def formsemestre_table_etuds_lycees(
- formsemestre_id, group_lycees=True, only_primo=False
-):
- """Récupère liste d'etudiants avec etat et decision."""
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- etuds = sco_report.tsp_etud_list(formsemestre_id, only_primo=only_primo)[0]
- if only_primo:
- primostr = "primo-entrants du "
- else:
- primostr = "du "
- title = "Lycées des étudiants %ssemestre " % primostr + sem["titreannee"]
- return _table_etuds_lycees(
- etuds,
- group_lycees,
- title,
- sco_preferences.SemPreferences(formsemestre_id),
- )
-
-
-def scodoc_table_etuds_lycees(format="html"):
- """Table avec _tous_ les étudiants des semestres non verrouillés
- de _tous_ les départements.
- """
- cur_dept = g.scodoc_dept
- semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems()
- etuds = []
- try:
- for (sem, dept) in semdepts:
- app.set_sco_dept(dept.acronym)
- etuds += sco_report.tsp_etud_list(sem["formsemestre_id"])[0]
- finally:
- app.set_sco_dept(cur_dept)
-
- tab, etuds_by_lycee = _table_etuds_lycees(
- etuds,
- False,
- "Lycées de TOUS les étudiants",
- sco_preferences.SemPreferences(),
- no_links=True,
- )
- tab.base_url = request.base_url
- t = tab.make_page(format=format, with_html_headers=False)
- if format != "html":
- return t
- H = [
- html_sco_header.sco_header(
- page_title=tab.page_title,
- init_google_maps=True,
- init_qtip=True,
- javascripts=["js/etud_info.js", "js/map_lycees.js"],
- ),
- """
""",
- "\n".join(F),
- t,
- """
- """,
- js_coords_lycees(etuds_by_lycee),
- html_sco_header.sco_footer(),
- ]
- return "\n".join(H)
-
-
-def qjs(txt): # quote for JS
- return txt.replace("'", r"\'").replace('"', r"\"")
-
-
-def js_coords_lycees(etuds_by_lycee):
- """Formatte liste des lycees en JSON pour Google Map"""
- L = []
- for codelycee in etuds_by_lycee:
- if codelycee:
- lyc = etuds_by_lycee[codelycee][0]
- if not lyc.get("positionlycee", False):
- continue
- listeetuds = " %d étudiants: " % len(
- etuds_by_lycee[codelycee]
- ) + ", ".join(
- [
- '%s'
- % (
- url_for(
- "scolar.ficheEtud",
- scodoc_dept=g.scodoc_dept,
- etudid=e["etudid"],
- ),
- qjs(e["nomprenom"]),
- )
- for e in etuds_by_lycee[codelycee]
- ]
- )
- pos = qjs(lyc["positionlycee"])
- legend = "%s %s" % (qjs("%(nomlycee)s (%(villelycee)s)" % lyc), listeetuds)
- L.append(
- "{'position' : '%s', 'name' : '%s', 'number' : %d }"
- % (pos, legend, len(etuds_by_lycee[codelycee]))
- )
-
- return """""" % ",".join(
- L
- )
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""Rapports sur lycées d'origine des étudiants d'un semestre.
+ - statistiques decisions
+ - suivi cohortes
+"""
+from operator import itemgetter
+
+from flask import url_for, g, request
+
+import app
+import app.scodoc.sco_utils as scu
+from app.scodoc import html_sco_header
+from app.scodoc import sco_formsemestre
+from app.scodoc import sco_preferences
+from app.scodoc import sco_report
+from app.scodoc import sco_etud
+import sco_version
+from app.scodoc.gen_tables import GenTable
+
+
+def formsemestre_table_etuds_lycees(
+ formsemestre_id, group_lycees=True, only_primo=False
+):
+ """Récupère liste d'etudiants avec etat et decision."""
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ etuds = sco_report.tsp_etud_list(formsemestre_id, only_primo=only_primo)[0]
+ if only_primo:
+ primostr = "primo-entrants du "
+ else:
+ primostr = "du "
+ title = "Lycées des étudiants %ssemestre " % primostr + sem["titreannee"]
+ return _table_etuds_lycees(
+ etuds,
+ group_lycees,
+ title,
+ sco_preferences.SemPreferences(formsemestre_id),
+ )
+
+
+def scodoc_table_etuds_lycees(format="html"):
+ """Table avec _tous_ les étudiants des semestres non verrouillés
+ de _tous_ les départements.
+ """
+ cur_dept = g.scodoc_dept
+ semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems()
+ etuds = []
+ try:
+ for (sem, dept) in semdepts:
+ app.set_sco_dept(dept.acronym)
+ etuds += sco_report.tsp_etud_list(sem["formsemestre_id"])[0]
+ finally:
+ app.set_sco_dept(cur_dept)
+
+ tab, etuds_by_lycee = _table_etuds_lycees(
+ etuds,
+ False,
+ "Lycées de TOUS les étudiants",
+ sco_preferences.SemPreferences(),
+ no_links=True,
+ )
+ tab.base_url = request.base_url
+ t = tab.make_page(format=format, with_html_headers=False)
+ if format != "html":
+ return t
+ H = [
+ html_sco_header.sco_header(
+ page_title=tab.page_title,
+ init_google_maps=True,
+ init_qtip=True,
+ javascripts=["js/etud_info.js", "js/map_lycees.js"],
+ ),
+ """
""",
+ "\n".join(F),
+ t,
+ """
+ """,
+ js_coords_lycees(etuds_by_lycee),
+ html_sco_header.sco_footer(),
+ ]
+ return "\n".join(H)
+
+
+def qjs(txt): # quote for JS
+ return txt.replace("'", r"\'").replace('"', r"\"")
+
+
+def js_coords_lycees(etuds_by_lycee):
+ """Formatte liste des lycees en JSON pour Google Map"""
+ L = []
+ for codelycee in etuds_by_lycee:
+ if codelycee:
+ lyc = etuds_by_lycee[codelycee][0]
+ if not lyc.get("positionlycee", False):
+ continue
+ listeetuds = " %d étudiants: " % len(
+ etuds_by_lycee[codelycee]
+ ) + ", ".join(
+ [
+ '%s'
+ % (
+ url_for(
+ "scolar.ficheEtud",
+ scodoc_dept=g.scodoc_dept,
+ etudid=e["etudid"],
+ ),
+ qjs(e["nomprenom"]),
+ )
+ for e in etuds_by_lycee[codelycee]
+ ]
+ )
+ pos = qjs(lyc["positionlycee"])
+ legend = "%s %s" % (qjs("%(nomlycee)s (%(villelycee)s)" % lyc), listeetuds)
+ L.append(
+ "{'position' : '%s', 'name' : '%s', 'number' : %d }"
+ % (pos, legend, len(etuds_by_lycee[codelycee]))
+ )
+
+ return """""" % ",".join(
+ L
+ )
diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py
index 6c409865..d15eb229 100644
--- a/app/scodoc/sco_moduleimpl_inscriptions.py
+++ b/app/scodoc/sco_moduleimpl_inscriptions.py
@@ -1,605 +1,605 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
-"""
-from operator import itemgetter
-
-import flask
-from flask import url_for, g, request
-from flask_login import current_user
-
-from app.comp import res_sem
-from app.comp.res_compat import NotesTableCompat
-from app.models import FormSemestre
-
-import app.scodoc.notesdb as ndb
-import app.scodoc.sco_utils as scu
-from app import log
-from app.scodoc.scolog import logdb
-from app.scodoc import html_sco_header
-from app.scodoc import htmlutils
-from app.scodoc import sco_cache
-from app.scodoc import sco_edit_module
-from app.scodoc import sco_edit_ue
-from app.scodoc import sco_etud
-from app.scodoc import sco_formsemestre
-from app.scodoc import sco_formsemestre_inscriptions
-from app.scodoc import sco_groups
-from app.scodoc import sco_moduleimpl
-from app.scodoc.sco_exceptions import ScoValueError
-from app.scodoc.sco_permissions import Permission
-
-
-def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
- """Formulaire inscription des etudiants a ce module
- * Gestion des inscriptions
- Nom TD TA TP (triable)
- [x] M. XXX YYY - - -
-
-
- ajouter TD A, TD B, TP 1, TP 2 ...
- supprimer TD A, TD B, TP 1, TP 2 ...
-
- * Si pas les droits: idem en readonly
- """
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
- formsemestre_id = M["formsemestre_id"]
- mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- # -- check lock
- if not sem["etat"]:
- raise ScoValueError("opération impossible: semestre verrouille")
- header = html_sco_header.sco_header(
- page_title="Inscription au module",
- init_qtip=True,
- javascripts=["js/etud_info.js"],
- )
- footer = html_sco_header.sco_footer()
- H = [
- header,
- """
Cette page permet d'éditer les étudiants inscrits à ce module
- (ils doivent évidemment être inscrits au semestre).
- Les étudiants cochés sont (ou seront) inscrits. Vous pouvez facilement inscrire ou
- désinscrire tous les étudiants d'un groupe à l'aide des menus "Ajouter" et "Enlever".
-
-
Aucune modification n'est prise en compte tant que l'on n'appuie pas sur le bouton
- "Appliquer les modifications".
-
- """
- % (
- moduleimpl_id,
- mod["titre"] or "(module sans titre)",
- mod["code"] or "(module sans code)",
- ),
- ]
- # Liste des inscrits à ce semestre
- inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
- formsemestre_id
- )
- for ins in inscrits:
- etuds_info = sco_etud.get_etud_info(etudid=ins["etudid"], filled=1)
- if not etuds_info:
- log(
- f"""moduleimpl_inscriptions_edit: inconsistency for etudid={ins['etudid']} !"""
- )
- raise ScoValueError(
- f"""Étudiant {ins['etudid']} inscrit mais inconnu dans la base !"""
- )
- ins["etud"] = etuds_info[0]
- inscrits.sort(key=lambda inscr: sco_etud.etud_sort_key(inscr["etud"]))
- in_m = sco_moduleimpl.do_moduleimpl_inscription_list(
- moduleimpl_id=M["moduleimpl_id"]
- )
- in_module = set([x["etudid"] for x in in_m])
- #
- partitions = sco_groups.get_partitions_list(formsemestre_id)
- #
- if not submitted:
- H.append(
- """"""
- )
- H.append(
- f"""""")
- else: # SUBMISSION
- # inscrit a ce module tous les etuds selectionnes
- sco_moduleimpl.do_moduleimpl_inscrit_etuds(
- moduleimpl_id, formsemestre_id, etuds, reset=True
- )
- return flask.redirect(
- url_for(
- "notes.moduleimpl_status",
- scodoc_dept=g.scodoc_dept,
- moduleimpl_id=moduleimpl_id,
- )
- )
- #
- H.append(footer)
- return "\n".join(H)
-
-
-def _make_menu(partitions: list[dict], title="", check="true") -> str:
- """Menu with list of all groups"""
- items = [{"title": "Tous", "attr": "onclick=\"group_select('', -1, %s)\"" % check}]
- p_idx = 0
- for partition in partitions:
- if partition["partition_name"] != None:
- p_idx += 1
- for group in sco_groups.get_partition_groups(partition):
- items.append(
- {
- "title": "%s %s"
- % (partition["partition_name"], group["group_name"]),
- "attr": "onclick=\"group_select('%s', %s, %s)\""
- % (group["group_name"], p_idx, check),
- }
- )
- return (
- '
Cette page décrit les inscriptions actuelles.
- Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en
- cliquant sur la ligne du module.
-
Note: la déinscription d'un module ne perd pas les notes. Ainsi, si
- l'étudiant est ensuite réinscrit au même module, il retrouvera ses notes.
- """
- )
-
- H.append(html_sco_header.sco_footer())
- return "\n".join(H)
-
-
-def descr_inscrs_module(moduleimpl_id, set_all, partitions):
- """returns tous_inscrits, nb_inscrits, descr"""
- ins = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
- set_m = set([x["etudid"] for x in ins]) # ens. des inscrits au module
- non_inscrits = set_all - set_m
- if len(non_inscrits) == 0:
- return True, len(ins), "" # tous inscrits
- if len(non_inscrits) <= 7: # seuil arbitraire
- return False, len(ins), "tous sauf " + _fmt_etud_set(non_inscrits)
- # Cherche les groupes:
- gr = [] # [ ( partition_name , [ group_names ] ) ]
- for partition in partitions:
- grp = [] # groupe de cette partition
- for group in sco_groups.get_partition_groups(partition):
- members = sco_groups.get_group_members(group["group_id"])
- set_g = set([m["etudid"] for m in members])
- if set_g.issubset(set_m):
- grp.append(group["group_name"])
- set_m = set_m - set_g
- gr.append((partition["partition_name"], grp))
- #
- d = []
- for (partition_name, grp) in gr:
- if grp:
- d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
- r = []
- if d:
- r.append(", ".join(d))
- if set_m:
- r.append(_fmt_etud_set(set_m))
- #
- return False, len(ins), " et ".join(r)
-
-
-def _fmt_etud_set(ins, max_list_size=7):
- # max_list_size est le nombre max de noms d'etudiants listés
- # au delà, on indique juste le nombre, sans les noms.
- if len(ins) > max_list_size:
- return "%d étudiants" % len(ins)
- etuds = []
- for etudid in ins:
- etuds.append(sco_etud.get_etud_info(etudid=etudid, filled=True)[0])
- etuds.sort(key=itemgetter("nom"))
- return ", ".join(
- [
- '%s'
- % (
- url_for(
- "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
- ),
- etud["nomprenom"],
- )
- for etud in etuds
- ]
- )
-
-
-def get_etuds_with_capitalized_ue(formsemestre_id):
- """For each UE, computes list of students capitalizing the UE.
- returns { ue_id : [ { infos } ] }
- """
- UECaps = scu.DictDefault(defaultvalue=[])
- formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
-
- inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
- args={"formsemestre_id": formsemestre_id}
- )
- ues = nt.get_ues_stat_dict()
- for ue in ues:
- for etud in inscrits:
- ue_status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"])
- if ue_status and ue_status["was_capitalized"]:
- UECaps[ue["ue_id"]].append(
- {
- "etudid": etud["etudid"],
- "ue_status": ue_status,
- "is_ins": is_inscrit_ue(
- etud["etudid"], formsemestre_id, ue["ue_id"]
- ),
- }
- )
- return UECaps
-
-
-def is_inscrit_ue(etudid, formsemestre_id, ue_id):
- """Modules de cette UE dans ce semestre
- auxquels l'étudiant est inscrit.
- """
- r = ndb.SimpleDictFetch(
- """SELECT mod.id AS module_id, mod.*
- FROM notes_moduleimpl mi, notes_modules mod,
- notes_formsemestre sem, notes_moduleimpl_inscription i
- WHERE sem.id = %(formsemestre_id)s
- AND mi.formsemestre_id = sem.id
- AND mod.id = mi.module_id
- AND mod.ue_id = %(ue_id)s
- AND i.moduleimpl_id = mi.id
- AND i.etudid = %(etudid)s
- ORDER BY mod.numero
- """,
- {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
- )
- return r
-
-
-def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
- """Desincrit l'etudiant de tous les modules de cette UE dans ce semestre."""
- cnx = ndb.GetDBConnexion()
- cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
- cursor.execute(
- """DELETE FROM notes_moduleimpl_inscription
- WHERE id IN (
- SELECT i.id FROM
- notes_moduleimpl mi, notes_modules mod,
- notes_formsemestre sem, notes_moduleimpl_inscription i
- WHERE sem.id = %(formsemestre_id)s
- AND mi.formsemestre_id = sem.id
- AND mod.id = mi.module_id
- AND mod.ue_id = %(ue_id)s
- AND i.moduleimpl_id = mi.id
- AND i.etudid = %(etudid)s
- )
- """,
- {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
- )
- logdb(
- cnx,
- method="etud_desinscrit_ue",
- etudid=etudid,
- msg="desinscription UE %s" % ue_id,
- commit=False,
- )
- sco_cache.invalidate_formsemestre(
- formsemestre_id=formsemestre_id
- ) # > desinscription etudiant des modules
-
-
-def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id):
- """Incrit l'etudiant de tous les modules de cette UE dans ce semestre."""
- # Verifie qu'il est bien inscrit au semestre
- insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
- args={"formsemestre_id": formsemestre_id, "etudid": etudid}
- )
- if not insem:
- raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
-
- cnx = ndb.GetDBConnexion()
- cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
- cursor.execute(
- """SELECT mi.id
- FROM notes_moduleimpl mi, notes_modules mod, notes_formsemestre sem
- WHERE sem.id = %(formsemestre_id)s
- AND mi.formsemestre_id = sem.id
- AND mod.id = mi.module_id
- AND mod.ue_id = %(ue_id)s
- """,
- {"formsemestre_id": formsemestre_id, "ue_id": ue_id},
- )
- res = cursor.dictfetchall()
- for moduleimpl_id in [x["id"] for x in res]:
- sco_moduleimpl.do_moduleimpl_inscription_create(
- {"moduleimpl_id": moduleimpl_id, "etudid": etudid},
- formsemestre_id=formsemestre_id,
- )
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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
+#
+##############################################################################
+
+"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
+"""
+from operator import itemgetter
+
+import flask
+from flask import url_for, g, request
+from flask_login import current_user
+
+from app.comp import res_sem
+from app.comp.res_compat import NotesTableCompat
+from app.models import FormSemestre
+
+import app.scodoc.notesdb as ndb
+import app.scodoc.sco_utils as scu
+from app import log
+from app.scodoc.scolog import logdb
+from app.scodoc import html_sco_header
+from app.scodoc import htmlutils
+from app.scodoc import sco_cache
+from app.scodoc import sco_edit_module
+from app.scodoc import sco_edit_ue
+from app.scodoc import sco_etud
+from app.scodoc import sco_formsemestre
+from app.scodoc import sco_formsemestre_inscriptions
+from app.scodoc import sco_groups
+from app.scodoc import sco_moduleimpl
+from app.scodoc.sco_exceptions import ScoValueError
+from app.scodoc.sco_permissions import Permission
+
+
+def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
+ """Formulaire inscription des etudiants a ce module
+ * Gestion des inscriptions
+ Nom TD TA TP (triable)
+ [x] M. XXX YYY - - -
+
+
+ ajouter TD A, TD B, TP 1, TP 2 ...
+ supprimer TD A, TD B, TP 1, TP 2 ...
+
+ * Si pas les droits: idem en readonly
+ """
+ M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+ formsemestre_id = M["formsemestre_id"]
+ mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ # -- check lock
+ if not sem["etat"]:
+ raise ScoValueError("opération impossible: semestre verrouille")
+ header = html_sco_header.sco_header(
+ page_title="Inscription au module",
+ init_qtip=True,
+ javascripts=["js/etud_info.js"],
+ )
+ footer = html_sco_header.sco_footer()
+ H = [
+ header,
+ """
Cette page permet d'éditer les étudiants inscrits à ce module
+ (ils doivent évidemment être inscrits au semestre).
+ Les étudiants cochés sont (ou seront) inscrits. Vous pouvez facilement inscrire ou
+ désinscrire tous les étudiants d'un groupe à l'aide des menus "Ajouter" et "Enlever".
+
+
Aucune modification n'est prise en compte tant que l'on n'appuie pas sur le bouton
+ "Appliquer les modifications".
+
+ """
+ % (
+ moduleimpl_id,
+ mod["titre"] or "(module sans titre)",
+ mod["code"] or "(module sans code)",
+ ),
+ ]
+ # Liste des inscrits à ce semestre
+ inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
+ formsemestre_id
+ )
+ for ins in inscrits:
+ etuds_info = sco_etud.get_etud_info(etudid=ins["etudid"], filled=1)
+ if not etuds_info:
+ log(
+ f"""moduleimpl_inscriptions_edit: inconsistency for etudid={ins['etudid']} !"""
+ )
+ raise ScoValueError(
+ f"""Étudiant {ins['etudid']} inscrit mais inconnu dans la base !"""
+ )
+ ins["etud"] = etuds_info[0]
+ inscrits.sort(key=lambda inscr: sco_etud.etud_sort_key(inscr["etud"]))
+ in_m = sco_moduleimpl.do_moduleimpl_inscription_list(
+ moduleimpl_id=M["moduleimpl_id"]
+ )
+ in_module = set([x["etudid"] for x in in_m])
+ #
+ partitions = sco_groups.get_partitions_list(formsemestre_id)
+ #
+ if not submitted:
+ H.append(
+ """"""
+ )
+ H.append(
+ f"""""")
+ else: # SUBMISSION
+ # inscrit a ce module tous les etuds selectionnes
+ sco_moduleimpl.do_moduleimpl_inscrit_etuds(
+ moduleimpl_id, formsemestre_id, etuds, reset=True
+ )
+ return flask.redirect(
+ url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=moduleimpl_id,
+ )
+ )
+ #
+ H.append(footer)
+ return "\n".join(H)
+
+
+def _make_menu(partitions: list[dict], title="", check="true") -> str:
+ """Menu with list of all groups"""
+ items = [{"title": "Tous", "attr": "onclick=\"group_select('', -1, %s)\"" % check}]
+ p_idx = 0
+ for partition in partitions:
+ if partition["partition_name"] != None:
+ p_idx += 1
+ for group in sco_groups.get_partition_groups(partition):
+ items.append(
+ {
+ "title": "%s %s"
+ % (partition["partition_name"], group["group_name"]),
+ "attr": "onclick=\"group_select('%s', %s, %s)\""
+ % (group["group_name"], p_idx, check),
+ }
+ )
+ return (
+ '
Cette page décrit les inscriptions actuelles.
+ Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en
+ cliquant sur la ligne du module.
+
Note: la déinscription d'un module ne perd pas les notes. Ainsi, si
+ l'étudiant est ensuite réinscrit au même module, il retrouvera ses notes.
+ """
+ )
+
+ H.append(html_sco_header.sco_footer())
+ return "\n".join(H)
+
+
+def descr_inscrs_module(moduleimpl_id, set_all, partitions):
+ """returns tous_inscrits, nb_inscrits, descr"""
+ ins = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
+ set_m = set([x["etudid"] for x in ins]) # ens. des inscrits au module
+ non_inscrits = set_all - set_m
+ if len(non_inscrits) == 0:
+ return True, len(ins), "" # tous inscrits
+ if len(non_inscrits) <= 7: # seuil arbitraire
+ return False, len(ins), "tous sauf " + _fmt_etud_set(non_inscrits)
+ # Cherche les groupes:
+ gr = [] # [ ( partition_name , [ group_names ] ) ]
+ for partition in partitions:
+ grp = [] # groupe de cette partition
+ for group in sco_groups.get_partition_groups(partition):
+ members = sco_groups.get_group_members(group["group_id"])
+ set_g = set([m["etudid"] for m in members])
+ if set_g.issubset(set_m):
+ grp.append(group["group_name"])
+ set_m = set_m - set_g
+ gr.append((partition["partition_name"], grp))
+ #
+ d = []
+ for (partition_name, grp) in gr:
+ if grp:
+ d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
+ r = []
+ if d:
+ r.append(", ".join(d))
+ if set_m:
+ r.append(_fmt_etud_set(set_m))
+ #
+ return False, len(ins), " et ".join(r)
+
+
+def _fmt_etud_set(ins, max_list_size=7):
+ # max_list_size est le nombre max de noms d'etudiants listés
+ # au delà, on indique juste le nombre, sans les noms.
+ if len(ins) > max_list_size:
+ return "%d étudiants" % len(ins)
+ etuds = []
+ for etudid in ins:
+ etuds.append(sco_etud.get_etud_info(etudid=etudid, filled=True)[0])
+ etuds.sort(key=itemgetter("nom"))
+ return ", ".join(
+ [
+ '%s'
+ % (
+ url_for(
+ "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
+ ),
+ etud["nomprenom"],
+ )
+ for etud in etuds
+ ]
+ )
+
+
+def get_etuds_with_capitalized_ue(formsemestre_id):
+ """For each UE, computes list of students capitalizing the UE.
+ returns { ue_id : [ { infos } ] }
+ """
+ UECaps = scu.DictDefault(defaultvalue=[])
+ formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
+
+ inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
+ args={"formsemestre_id": formsemestre_id}
+ )
+ ues = nt.get_ues_stat_dict()
+ for ue in ues:
+ for etud in inscrits:
+ ue_status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"])
+ if ue_status and ue_status["was_capitalized"]:
+ UECaps[ue["ue_id"]].append(
+ {
+ "etudid": etud["etudid"],
+ "ue_status": ue_status,
+ "is_ins": is_inscrit_ue(
+ etud["etudid"], formsemestre_id, ue["ue_id"]
+ ),
+ }
+ )
+ return UECaps
+
+
+def is_inscrit_ue(etudid, formsemestre_id, ue_id):
+ """Modules de cette UE dans ce semestre
+ auxquels l'étudiant est inscrit.
+ """
+ r = ndb.SimpleDictFetch(
+ """SELECT mod.id AS module_id, mod.*
+ FROM notes_moduleimpl mi, notes_modules mod,
+ notes_formsemestre sem, notes_moduleimpl_inscription i
+ WHERE sem.id = %(formsemestre_id)s
+ AND mi.formsemestre_id = sem.id
+ AND mod.id = mi.module_id
+ AND mod.ue_id = %(ue_id)s
+ AND i.moduleimpl_id = mi.id
+ AND i.etudid = %(etudid)s
+ ORDER BY mod.numero
+ """,
+ {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
+ )
+ return r
+
+
+def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
+ """Desincrit l'etudiant de tous les modules de cette UE dans ce semestre."""
+ cnx = ndb.GetDBConnexion()
+ cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
+ cursor.execute(
+ """DELETE FROM notes_moduleimpl_inscription
+ WHERE id IN (
+ SELECT i.id FROM
+ notes_moduleimpl mi, notes_modules mod,
+ notes_formsemestre sem, notes_moduleimpl_inscription i
+ WHERE sem.id = %(formsemestre_id)s
+ AND mi.formsemestre_id = sem.id
+ AND mod.id = mi.module_id
+ AND mod.ue_id = %(ue_id)s
+ AND i.moduleimpl_id = mi.id
+ AND i.etudid = %(etudid)s
+ )
+ """,
+ {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
+ )
+ logdb(
+ cnx,
+ method="etud_desinscrit_ue",
+ etudid=etudid,
+ msg="desinscription UE %s" % ue_id,
+ commit=False,
+ )
+ sco_cache.invalidate_formsemestre(
+ formsemestre_id=formsemestre_id
+ ) # > desinscription etudiant des modules
+
+
+def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id):
+ """Incrit l'etudiant de tous les modules de cette UE dans ce semestre."""
+ # Verifie qu'il est bien inscrit au semestre
+ insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
+ args={"formsemestre_id": formsemestre_id, "etudid": etudid}
+ )
+ if not insem:
+ raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
+
+ cnx = ndb.GetDBConnexion()
+ cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
+ cursor.execute(
+ """SELECT mi.id
+ FROM notes_moduleimpl mi, notes_modules mod, notes_formsemestre sem
+ WHERE sem.id = %(formsemestre_id)s
+ AND mi.formsemestre_id = sem.id
+ AND mod.id = mi.module_id
+ AND mod.ue_id = %(ue_id)s
+ """,
+ {"formsemestre_id": formsemestre_id, "ue_id": ue_id},
+ )
+ res = cursor.dictfetchall()
+ for moduleimpl_id in [x["id"] for x in res]:
+ sco_moduleimpl.do_moduleimpl_inscription_create(
+ {"moduleimpl_id": moduleimpl_id, "etudid": etudid},
+ formsemestre_id=formsemestre_id,
+ )
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index 12828e87..220e9416 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -189,7 +189,7 @@ def ficheEtud(etudid=None):
else:
info["paysdomicile"] = ""
if info["telephone"] or info["telephonemobile"]:
- info["telephones"] = " %s %s" % (
+ info["telephones"] = " %s %s" % (
info["telephonestr"],
info["telephonemobilestr"],
)
@@ -506,9 +506,9 @@ def ficheEtud(etudid=None):
Ajouter une annotation sur %(nomprenom)s:
-
+ Ces annotations sont lisibles par tous les enseignants et le secrétariat.
-
+ L'annotation commençant par "PE:" est un avis de poursuite d'études.
diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py
index e67865f8..898bd31b 100644
--- a/app/scodoc/sco_placement.py
+++ b/app/scodoc/sco_placement.py
@@ -1,637 +1,637 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""ScoDoc: génération feuille émargement et placement
-
-Contribution J.-M. Place 2021
-basée sur une idée de M. Salomon, UFC / IUT DE BELFORT-MONTBÉLIARD, 2016
-
-"""
-import random
-import time
-from copy import copy
-
-import wtforms.validators
-from flask import request, render_template
-from flask_login import current_user
-from flask_wtf import FlaskForm
-from openpyxl.styles import PatternFill, Alignment, Border, Side, Font
-from wtforms import (
- StringField,
- SubmitField,
- SelectField,
- RadioField,
- HiddenField,
- SelectMultipleField,
-)
-import app.scodoc.sco_utils as scu
-import app.scodoc.notesdb as ndb
-from app import ScoValueError
-from app.scodoc import html_sco_header, sco_preferences
-from app.scodoc import sco_edit_module
-from app.scodoc import sco_evaluations
-from app.scodoc import sco_evaluation_db
-from app.scodoc import sco_excel
-from app.scodoc.sco_excel import ScoExcelBook, COLORS
-from app.scodoc import sco_formsemestre
-from app.scodoc import sco_formsemestre_inscriptions
-from app.scodoc import sco_groups
-from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_permissions_check
-from app.scodoc.gen_tables import GenTable
-from app.scodoc import sco_etud
-import sco_version
-
-_ = lambda x: x # sans babel
-_l = _
-
-COORD = "Coordonnées"
-SEQ = "Continue"
-
-TOUS = "Tous"
-
-
-def _get_group_info(evaluation_id):
- # groupes
- groups = sco_groups.do_evaluation_listegroupes(evaluation_id, include_default=True)
- has_groups = False
- groups_tree = {}
- for group in groups:
- partition = group["partition_name"] or TOUS
- group_id = group["group_id"]
- group_name = group["group_name"] or TOUS
- if partition not in groups_tree:
- groups_tree[partition] = {}
- groups_tree[partition][group_name] = group_id
- if partition != TOUS:
- has_groups = True
- else:
- has_groups = False
- nb_groups = sum([len(groups_tree[p]) for p in groups_tree])
- return groups_tree, has_groups, nb_groups
-
-
-class PlacementForm(FlaskForm):
- """Formulaire pour placement des étudiants en Salle"""
-
- evaluation_id = HiddenField("evaluation_id")
- file_format = RadioField(
- "Format de fichier",
- choices=["pdf", "xls"],
- validators=[
- wtforms.validators.DataRequired("indiquez le format du fichier attendu"),
- ],
- )
- surveillants = StringField("Surveillants", validators=[])
- batiment = StringField("Batiment")
- salle = StringField("Salle")
- nb_rangs = SelectField(
- "nb de places en largeur",
- coerce=int,
- choices=[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
- description="largeur de la salle, en nombre de places",
- )
- etiquetage = RadioField(
- "Numérotation",
- choices=[SEQ, COORD],
- validators=[
- wtforms.validators.DataRequired("indiquez le style de numérotation"),
- ],
- )
- groups = SelectMultipleField(
- "Groupe(s)",
- validators=[],
- )
- submit = SubmitField("OK")
-
- def __init__(self, formdata=None, data=None):
- super().__init__(formdata=formdata, data=data)
- self.groups_tree = {}
- self.has_groups = None
- self.nb_groups = None
- self.tous_id = None
- self.set_evaluation_infos(data["evaluation_id"])
-
- def set_evaluation_infos(self, evaluation_id):
- """Initialise les données du formulaire avec les données de l'évaluation."""
- eval_data = sco_evaluation_db.do_evaluation_list(
- {"evaluation_id": evaluation_id}
- )
- if not eval_data:
- raise ScoValueError("invalid evaluation_id")
- self.groups_tree, self.has_groups, self.nb_groups = _get_group_info(
- evaluation_id
- )
- choices = []
- for partition in self.groups_tree:
- for groupe in self.groups_tree[partition]:
- if (
- groupe == TOUS
- ): # Affichage et valeur spécifique pour le groupe TOUS
- self.tous_id = str(self.groups_tree[partition][groupe])
- choices.append((TOUS, TOUS))
- else:
- groupe_id = str(self.groups_tree[partition][groupe])
- choices.append((groupe_id, "%s (%s)" % (str(groupe), partition)))
- self.groups.choices = choices
- # self.groups.default = [TOUS] # Ne fonctionnne pas... (ni dans la déclaration de PlaceForm.groups)
- # la réponse [] est de toute façon transposée en [ self.tous_id ] lors du traitement (cas du groupe unique)
-
-
-class _DistributeurContinu:
- """Distribue les places selon un ordre numérique."""
-
- def __init__(self):
- self.position = 1
-
- def suivant(self):
- """Retounre la désignation de la place suivante"""
- retour = self.position
- self.position += 1
- return retour
-
-
-class _Distributeur2D:
- """Distribue les places selon des coordonnées sur nb_rangs."""
-
- def __init__(self, nb_rangs):
- self.nb_rangs = nb_rangs
- self.rang = 1
- self.index = 1
-
- def suivant(self):
- """Retounre la désignation de la place suivante"""
- retour = (self.index, self.rang)
- self.rang += 1
- if self.rang > self.nb_rangs:
- self.rang = 1
- self.index += 1
- return retour
-
-
-def placement_eval_selectetuds(evaluation_id):
- """Creation de l'écran de placement"""
- form = PlacementForm(
- request.form,
- data={"evaluation_id": int(evaluation_id), "groups": TOUS},
- )
- if form.validate_on_submit():
- runner = PlacementRunner(form)
- if not runner.check_placement():
- return (
- """
Génération du placement impossible pour %s
-
(vérifiez que le semestre n'est pas verrouillé et que vous
- avez l'autorisation d'effectuer cette opération)
- { html_sco_header.sco_footer() }
- """
-
-
-def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
- "suppress all notes in this eval"
- E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
-
- if sco_permissions_check.can_edit_notes(
- current_user, E["moduleimpl_id"], allow_ens=False
- ):
- # On a le droit de modifier toutes les notes
- # recupere les etuds ayant une note
- notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
- elif sco_permissions_check.can_edit_notes(
- current_user, E["moduleimpl_id"], allow_ens=True
- ):
- # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
- notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
- evaluation_id, by_uid=current_user.id
- )
- else:
- raise AccessDenied("Modification des notes impossible pour %s" % current_user)
-
- notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in notes_db.keys()]
-
- if not dialog_confirmed:
- nb_changed, nb_suppress, existing_decisions = notes_add(
- current_user, evaluation_id, notes, do_it=False, check_inscription=False
- )
- msg = (
- "
Confirmer la suppression des %d notes ? (peut affecter plusieurs groupes)
Etape 2 (cadre vert): Indiquer le fichier Excel téléchargé à l'étape 1 et dans lequel on a saisi des notes. Remarques:
-
-
le fichier Excel peut être incomplet: on peut ne saisir que quelques notes et répéter l'opération (en téléchargeant un nouveau fichier) plus tard;
-
seules les valeurs des notes modifiées sont prises en compte;
-
seules les notes sont extraites du fichier Excel;
-
on peut optionnellement ajouter un commentaire (type "copies corrigées par Dupont", ou "Modif. suite à contestation") dans la case "Commentaire".
-
-
le fichier Excel doit impérativement être celui chargé à l'étape 1 pour cette évaluation. Il n'est pas possible d'utiliser une liste d'appel ou autre document Excel téléchargé d'une autre page.
-
-
-
-"""
- )
- H.append(html_sco_header.sco_footer())
- return "\n".join(H)
-
-
-def feuille_saisie_notes(evaluation_id, group_ids=[]):
- """Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
- evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
- if not evals:
- raise ScoValueError("invalid evaluation_id")
- eval_dict = evals[0]
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=eval_dict["moduleimpl_id"])[0]
- formsemestre_id = M["formsemestre_id"]
- Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
- sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
- mod_responsable = sco_users.user_info(M["responsable_id"])
- if eval_dict["jour"]:
- indication_date = ndb.DateDMYtoISO(eval_dict["jour"])
- else:
- indication_date = scu.sanitize_filename(eval_dict["description"])[:12]
- eval_name = "%s-%s" % (Mod["code"], indication_date)
-
- if eval_dict["description"]:
- evaltitre = "%s du %s" % (eval_dict["description"], eval_dict["jour"])
- else:
- evaltitre = "évaluation du %s" % eval_dict["jour"]
- description = "%s en %s (%s) resp. %s" % (
- evaltitre,
- Mod["abbrev"] or "",
- Mod["code"] or "",
- mod_responsable["prenomnom"],
- )
-
- groups_infos = sco_groups_view.DisplayedGroupsInfos(
- group_ids=group_ids,
- formsemestre_id=formsemestre_id,
- select_all_when_unspecified=True,
- etat=None,
- )
- groups = sco_groups.listgroups(groups_infos.group_ids)
- gr_title_filename = sco_groups.listgroups_filename(groups)
- # gr_title = sco_groups.listgroups_abbrev(groups)
- if None in [g["group_name"] for g in groups]: # tous les etudiants
- getallstudents = True
- # gr_title = "tous"
- gr_title_filename = "tous"
- else:
- getallstudents = False
- etudids = [
- x[0]
- for x in sco_groups.do_evaluation_listeetuds_groups(
- evaluation_id, groups, getallstudents=getallstudents, include_demdef=True
- )
- ]
-
- # une liste de liste de chaines: lignes de la feuille de calcul
- L = []
-
- etuds = _get_sorted_etuds(eval_dict, etudids, formsemestre_id)
- for e in etuds:
- etudid = e["etudid"]
- groups = sco_groups.get_etud_groups(etudid, formsemestre_id)
- grc = sco_groups.listgroups_abbrev(groups)
-
- L.append(
- [
- "%s" % etudid,
- e["nom"].upper(),
- e["prenom"].lower().capitalize(),
- e["inscr"]["etat"],
- grc,
- e["val"],
- e["explanation"],
- ]
- )
-
- filename = "notes_%s_%s" % (eval_name, gr_title_filename)
- xls = sco_excel.excel_feuille_saisie(
- eval_dict, sem["titreannee"], description, lines=L
- )
- return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
- # return sco_excel.send_excel_file(xls, filename)
-
-
-def has_existing_decision(M, E, etudid):
- """Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE
- Si oui, return True
- """
- formsemestre_id = M["formsemestre_id"]
- formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
- if nt.get_etud_decision_sem(etudid):
- return True
- dec_ues = nt.get_etud_decision_ues(etudid)
- if dec_ues:
- mod = sco_edit_module.module_list({"module_id": M["module_id"]})[0]
- ue_id = mod["ue_id"]
- if ue_id in dec_ues:
- return True # decision pour l'UE a laquelle appartient cette evaluation
-
- return False # pas de decision de jury affectee par cette note
-
-
-# -----------------------------
-# Nouveau formulaire saisie notes (2016)
-
-
-def saisie_notes(evaluation_id, group_ids=[]):
- """Formulaire saisie notes d'une évaluation pour un groupe"""
- if not isinstance(evaluation_id, int):
- raise ScoInvalidParamError()
- group_ids = [int(group_id) for group_id in group_ids]
- evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
- if not evals:
- raise ScoValueError("évaluation inexistante")
- E = evals[0]
- M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
- formsemestre_id = M["formsemestre_id"]
- # Check access
- # (admin, respformation, and responsable_id)
- if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
- return (
- html_sco_header.sco_header()
- + "
Modification des notes impossible pour %s
"
- % current_user.user_name
- + """
(vérifiez que le semestre n'est pas verrouillé et que vous
- avez l'autorisation d'effectuer cette opération)