# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """ScoDoc fiche_etud Fiche description d'un étudiant et de son parcours """ from flask import url_for, g, render_template, request from flask_login import current_user import sqlalchemy as sa from app import log from app.auth.models import User from app.but import cursus_but, validations_view from app.models import ( Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig, ValidationDUT120, ) from app.scodoc import ( codes_cursus, htmlutils, sco_archives_etud, sco_bac, sco_cursus, sco_etud, sco_groups, sco_permissions_check, sco_report, ) from app.scodoc.html_sidebar import retreive_formsemestre_from_request from app.scodoc.sco_bulletins import etud_descr_situation_semestre from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table from app.scodoc.sco_permissions import Permission import app.scodoc.sco_utils as scu def _menu_scolarite( authuser, formsemestre: FormSemestre, etudid: int, etat_inscription: str ): """HTML pour menu "scolarite" pour un etudiant dans un semestre. Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant. """ locked = not formsemestre.etat if locked: lockicon = scu.icontag("lock32_img", title="verrouillé", border="0") return lockicon # no menu if not authuser.has_permission( Permission.EtudInscrit ) and not authuser.has_permission(Permission.EtudChangeGroups): return "" # no menu args = {"etudid": etudid, "formsemestre_id": formsemestre.id} if etat_inscription != scu.DEMISSION: dem_title = "Démission" dem_url = "scolar.form_dem" else: dem_title = "Annuler la démission" dem_url = "scolar.do_cancel_dem" # Note: seul un etudiant inscrit (I) peut devenir défaillant. if etat_inscription != codes_cursus.DEF: def_title = "Déclarer défaillance" def_url = "scolar.form_def" elif etat_inscription == codes_cursus.DEF: def_title = "Annuler la défaillance" def_url = "scolar.do_cancel_def" def_enabled = ( (etat_inscription != scu.DEMISSION) and authuser.has_permission(Permission.EtudInscrit) and not locked ) items = [ { "title": dem_title, "endpoint": dem_url, "args": args, "enabled": authuser.has_permission(Permission.EtudInscrit) and not locked, }, { "title": "Validation du semestre (jury)", "endpoint": "notes.formsemestre_validation_etud_form", "args": args, "enabled": authuser.has_permission(Permission.EtudInscrit) and not locked, }, { "title": def_title, "endpoint": def_url, "args": args, "enabled": def_enabled, }, { "title": "Désinscrire (en cas d'erreur)", "endpoint": "notes.formsemestre_desinscription", "args": args, "enabled": authuser.has_permission(Permission.EtudInscrit) and not locked, }, { "title": "Inscrire à un module optionnel (ou au sport)", "endpoint": "notes.formsemestre_inscription_option", "args": args, "enabled": authuser.has_permission(Permission.EtudInscrit) and not locked, }, { "title": "Gérer les validations d'UEs antérieures", "endpoint": "notes.formsemestre_validate_previous_ue", "args": args, "enabled": formsemestre.can_edit_jury(), }, { "title": "Enregistrer un semestre effectué ailleurs", "endpoint": "notes.formsemestre_ext_create_form", "args": args, "enabled": authuser.has_permission(Permission.EditFormSemestre), }, { "title": "Affecter les notes manquantes", "endpoint": "notes.formsemestre_note_etuds_sans_notes", "args": args, "enabled": authuser.has_permission(Permission.EditAllNotes), }, { "title": "Inscrire à un autre semestre", "endpoint": "notes.formsemestre_inscription_with_modules_form", "args": {"etudid": etudid}, "enabled": authuser.has_permission(Permission.EtudInscrit), }, ] return htmlutils.make_menu( "Scolarité", items, css_class="direction_etud", alone=True ) def fiche_etud(etudid=None): "fiche d'informations sur un etudiant" restrict_etud_data = not current_user.has_permission(Permission.ViewEtudData) try: etud = Identite.get_etud(etudid) except Exception as exc: log(f"fiche_etud: etudid={etudid!r} request.args={request.args!r}") raise ScoValueError("Étudiant inexistant !") from exc # la sidebar est differente s'il y a ou pas un etudid # voir html_sidebar.sidebar() g.etudid = etudid info = etud.to_dict_scodoc7(restrict=restrict_etud_data) if etud.prenom_etat_civil: info["etat_civil"] = ( "

Etat-civil: " + etud.civilite_etat_civil_str + " " + etud.prenom_etat_civil + " " + etud.nom + "

" ) else: info["etat_civil"] = "" info["ScoURL"] = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept) info["authuser"] = current_user if restrict_etud_data: info["info_naissance"] = "" adresse = None else: info["info_naissance"] = info["date_naissance"] if info["lieu_naissance"]: info["info_naissance"] += " à " + info["lieu_naissance"] if info["dept_naissance"]: info["info_naissance"] += f" ({info['dept_naissance']})" adresse = etud.adresses.first() info.update(_format_adresse(adresse)) info.update(etud.inscription_descr()) info["etudfoto"] = etud.photo_html() # Champ dépendant des permissions: if current_user.has_permission( Permission.EtudChangeAdr ) and current_user.has_permission(Permission.ViewEtudData): info[ "modifadresse" ] = f"""modifier adresse""" else: info["modifadresse"] = "" # Groupes: inscription_courante = etud.inscription_courante() sco_groups.etud_add_group_infos( info, inscription_courante.formsemestre.id if inscription_courante else None, only_to_show=True, ) # Parcours de l'étudiant last_formsemestre = None inscriptions = etud.inscriptions() info["last_formsemestre_id"] = ( inscriptions[0].formsemestre.id if inscriptions else "" ) sem_info = {} for inscription in inscriptions: formsemestre = inscription.formsemestre if inscription.etat != scu.INSCRIT: descr, _ = etud_descr_situation_semestre( etudid, formsemestre, etud.e, show_date_inscr=False, ) grlink = f"""{descr["situation"]}""" else: e = {"etudid": etudid} sco_groups.etud_add_group_infos(e, formsemestre.id, only_to_show=True) grlinks = [] for partition in e["partitions"].values(): if partition["partition_name"]: gr_name = partition["group_name"] else: gr_name = "tous" grlinks.append( f"""{gr_name} """ ) grlink = ", ".join(grlinks) # infos ajoutées au semestre dans le parcours (groupe, menu) menu = _menu_scolarite(current_user, formsemestre, etudid, inscription.etat) if menu: sem_info[formsemestre.id] = ( "
" + grlink + "" + menu + "
" ) else: sem_info[formsemestre.id] = grlink if inscriptions: Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"]) info["liste_inscriptions"] = formsemestre_recap_parcours_table( Se, etudid, with_links=False, sem_info=sem_info, with_all_columns=False, ) info["link_bul_pdf"] = ( """PDF interdits par l'admin.""" if ScoDocSiteConfig.is_bul_pdf_disabled() else f""" Tous les bulletins """ ) last_formsemestre: FormSemestre = inscriptions[0].formsemestre if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2: info[ "link_bul_pdf" ] += f""" Visualiser les compétences BUT """ info["link_inscrire_ailleurs"] = ( f"""Inscrire à un autre semestre """ if current_user.has_permission(Permission.EtudInscrit) else "" ) can_edit_jury = current_user.has_permission(Permission.EtudInscrit) info[ "link_inscrire_ailleurs" ] += f""" {'Éditer' if can_edit_jury else 'Détail de'} toutes décisions de jury """ info[ "link_bilan_ects" ] = f"""ECTS""" else: # non inscrit l = [f"""

Étudiant{etud.e} non inscrit{etud.e}"""] if current_user.has_permission(Permission.EtudInscrit): l.append( f"""inscrire""" ) l.append("") info["liste_inscriptions"] = "\n".join(l) info["link_bul_pdf"] = "" info["link_inscrire_ailleurs"] = "" info["link_bilan_ects"] = "" # Liste des annotations html_annotations_list = "\n".join( [] if restrict_etud_data else get_html_annotations_list(etud) ) # fiche admission if etud.admission: infos_admission = _infos_admission(etud, restrict_etud_data) has_adm_notes = any( infos_admission[k] for k in ("math", "physique", "anglais", "francais") ) has_bac_info = any( infos_admission[k] for k in ( "bac_specialite", "annee_bac", "rapporteur", "commentaire", "classement", "type_admission", "rap", ) ) if has_bac_info or has_adm_notes: adm_tmpl = """

Informations admission
""" if has_adm_notes: adm_tmpl += """
BacAnnéeRg MathPhysiqueAnglaisFrançais
%(bac_specialite)s %(annee_bac)s %(classement)s %(math)s%(physique)s%(anglais)s%(francais)s
""" adm_tmpl += """
Bac %(bac_specialite)s obtenu en %(annee_bac)s
%(info_lycee)s
""" if infos_admission["type_admission"] or infos_admission["classement"]: adm_tmpl += """
""" if infos_admission["type_admission"]: adm_tmpl += """Voie d'admission: %(type_admission)s """ if infos_admission["classement"]: adm_tmpl += """Rang admission: %(classement)s""" if infos_admission["type_admission"] or infos_admission["classement"]: adm_tmpl += "
" if infos_admission["rap"]: adm_tmpl += """
%(rap)s
""" adm_tmpl += """""" else: adm_tmpl = "" # pas de boite "info admission" info["adm_data"] = adm_tmpl % infos_admission else: info["adm_data"] = "" # Fichiers archivés: info["fichiers_archive_htm"] = ( "" if restrict_etud_data else ( '
Fichiers associés
' + sco_archives_etud.etud_list_archives_html(etud) ) ) # Devenir de l'étudiant: has_debouche = True if sco_permissions_check.can_edit_suivi(): suivi_readonly = "0" link_add_suivi = """
  • ajouter une ligne
  • """ else: suivi_readonly = "1" link_add_suivi = "" if has_debouche: info[ "debouche_html" ] = f"""
    Devenir:
      {link_add_suivi}
    """ else: info["debouche_html"] = "" # pas de boite "devenir" # Inscriptions info[ "inscriptions_mkup" ] = f"""
    Cursus
    {info["liste_inscriptions"]} {info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]} {info["link_bilan_ects"]}
    """ # if info["groupes"].strip(): info[ "groupes_row" ] = f""" Groupes :{info['groupes']} """ else: info["groupes_row"] = "" info["menus_etud"] = menus_etud(etudid) if info["boursier"] and not restrict_etud_data: info["bourse_span"] = """boursier""" else: info["bourse_span"] = "" # Liens vers compétences BUT if last_formsemestre and last_formsemestre.formation.is_apc(): try: but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation) except ScoValueError: but_cursus = None refcomp = last_formsemestre.formation.referentiel_competence if refcomp: ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau( refcomp, etud ) ects_total = sum((v.ects() for v in ue_validation_by_niveau.values())) else: ects_total = "" validation_dut120 = ValidationDUT120.query.filter_by(etudid=etudid).first() validation_dut120_html = ( f"""Diplôme DUT décerné en  S{validation_dut120.formsemestre.semestre_id} """ if validation_dut120 else "" ) info[ "but_cursus_mkup" ] = f"""
    {render_template( "but/cursus_etud.j2", cursus=but_cursus, scu=scu, validation_dut120_html=validation_dut120_html, ) if but_cursus else 'problème configuration formation BUT'}
    Total ECTS BUT: {ects_total:g}
    """ else: info["but_cursus_mkup"] = "" adresse_template = ( "" if restrict_etud_data else """
    Adresse : %(domicile)s %(codepostaldomicile)s %(villedomicile)s %(paysdomicile)s %(modifadresse)s %(telephones)s
    """ ) info_naissance = ( f"""Né{etud.e} le : {info["info_naissance"]} """ if info["info_naissance"] else "" ) situation_template = ( f"""
    %(groupes_row)s {info_naissance}
    Situation :%(situation)s %(bourse_span)s
    """ + adresse_template + """
    """ ) info["annotations_mkup"] = ( f"""
    Annotations
    {html_annotations_list}
    Ajouter une annotation sur {etud.nomprenom}:
    Ces annotations sont lisibles par tous les utilisateurs ayant la permission ViewEtudData dans ce département (souvent les enseignants et le secrétariat).
    L'annotation commençant par "PE:" est un avis de poursuite d'études.
    """ if not restrict_etud_data else "" ) tmpl = ( """

    %(nomprenom)s (%(inscription)s)

    %(etat_civil)s %(email_link)s
    %(etudfoto)s
    """ + situation_template + """ %(inscriptions_mkup)s %(but_cursus_mkup)s
    %(adm_data)s %(fichiers_archive_htm)s
    %(debouche_html)s %(annotations_mkup)s
    code NIP: %(code_nip)s
    """ ) return render_template( "sco_page.j2", content=tmpl % info, title=f"Fiche étudiant {etud.nomprenom}", cssstyles=[ "libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/jury_but.css", "css/cursus_but.css", ], javascripts=[ "libjs/jinplace-1.2.1.min.js", "js/ue_list.js", "libjs/jQuery-tagEditor/jquery.tag-editor.min.js", "libjs/jQuery-tagEditor/jquery.caret.min.js", "js/recap_parcours.js", "js/etud_debouche.js", ], ) def _format_adresse(adresse: Adresse | None) -> dict: """{ "telephonestr" : ..., "telephonemobilestr" : ... } (formats html)""" d = { "telephonestr": ( ("Tél.: " + scu.format_telephone(adresse.telephone)) if (adresse and adresse.telephone) else "" ), "telephonemobilestr": ( ("Mobile: " + scu.format_telephone(adresse.telephonemobile)) if (adresse and adresse.telephonemobile) else "" ), # e-mail: "email_link": ( ", ".join( [ f"""{m}""" for m in [adresse.email, adresse.emailperso] if m ] ) if adresse and (adresse.email or adresse.emailperso) else "" ), "domicile": ( (adresse.domicile or "") if adresse and ( adresse.domicile or adresse.codepostaldomicile or adresse.villedomicile ) else "inconnue" ), "paysdomicile": ( f"{sco_etud.format_pays(adresse.paysdomicile)}" if adresse and adresse.paysdomicile else "" ), } d["telephones"] = ( f"
    {d['telephonestr']}    {d['telephonemobilestr']}" if adresse and (adresse.telephone or adresse.telephonemobile) else "" ) return d def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict: """dict with admission data, restricted or not""" # info sur rapporteur et son commentaire rap = "" if not restrict_etud_data: if etud.admission.rapporteur or etud.admission.commentaire: rap = "Note du rapporteur" if etud.admission.rapporteur: rap += f" ({etud.admission.rapporteur})" rap += ": " if etud.admission.commentaire: rap += f"{etud.admission.commentaire}" # nom du lycée if restrict_etud_data: info_lycee = "" elif etud.admission.nomlycee: info_lycee = "Lycée " + sco_etud.format_lycee(etud.admission.nomlycee) if etud.admission.villelycee: info_lycee += f" ({etud.admission.villelycee})" info_lycee += "
    " elif etud.admission.codelycee: info_lycee = sco_etud.format_lycee_from_code(etud.admission.codelycee) else: info_lycee = "" return { # infos accessibles à tous: "bac_specialite": f"{etud.admission.bac or ''}{(' '+(etud.admission.specialite or '')) if etud.admission.specialite else ''}", "annee_bac": etud.admission.annee_bac or "", # infos protégées par ViewEtudData: "info_lycee": info_lycee, "rapporteur": etud.admission.rapporteur if not restrict_etud_data else "", "rap": rap, "commentaire": ( (etud.admission.commentaire or "") if not restrict_etud_data else "" ), "classement": ( (etud.admission.classement or "") if not restrict_etud_data else "" ), "type_admission": ( (etud.admission.type_admission or "") if not restrict_etud_data else "" ), "math": (etud.admission.math or "") if not restrict_etud_data else "", "physique": (etud.admission.physique or "") if not restrict_etud_data else "", "anglais": (etud.admission.anglais or "") if not restrict_etud_data else "", "francais": (etud.admission.francais or "") if not restrict_etud_data else "", } def get_html_annotations_list(etud: Identite) -> list[str]: """Liste de chaînes html décrivant les annotations.""" html_annotations_list = [] annotations = EtudAnnotation.query.filter_by(etudid=etud.id).order_by( sa.desc(EtudAnnotation.date) ) for annot in annotations: del_link = ( f"""{ scu.icontag( "delete_img", border="0", alt="suppress", title="Supprimer cette annotation", ) }""" if sco_permissions_check.can_suppress_annotation(annot.id) else "" ) author = User.query.filter_by(user_name=annot.author).first() html_annotations_list.append( f"""Le { annot.date.strftime(scu.DATE_FMT) if annot.date else "?"} par {author.get_prenomnom() if author else "?"} : {annot.comment or ""}{del_link} """ ) return html_annotations_list def menus_etud(etudid): """Menu etudiant (operations sur l'etudiant)""" authuser = current_user etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] menuEtud = [ { "title": etud["nomprenom"], "endpoint": "scolar.fiche_etud", "args": {"etudid": etud["etudid"]}, "enabled": True, "helpmsg": "Fiche étudiant", }, { "title": "Changer la photo", "endpoint": "scolar.form_change_photo", "args": {"etudid": etud["etudid"]}, "enabled": authuser.has_permission(Permission.EtudChangeAdr), }, { "title": "Changer les données identité/admission", "endpoint": "scolar.etudident_edit_form", "args": {"etudid": etud["etudid"]}, "enabled": authuser.has_permission(Permission.EtudInscrit) and authuser.has_permission(Permission.ViewEtudData), }, { "title": "Copier dans un autre département...", "endpoint": "scolar.etud_copy_in_other_dept", "args": {"etudid": etud["etudid"]}, "enabled": authuser.has_permission(Permission.EtudInscrit), }, { "title": "Supprimer cet étudiant...", "endpoint": "scolar.etudident_delete", "args": {"etudid": etud["etudid"]}, "enabled": authuser.has_permission(Permission.EtudInscrit), }, { "title": "Voir le journal...", "endpoint": "scolar.show_etud_log", "args": {"etudid": etud["etudid"]}, "enabled": True, }, ] return htmlutils.make_menu( "Étudiant", menuEtud, alone=True, css_class="menu-etudiant" ) def etud_info_html(etudid, with_photo="1", debug=False): """An HTML div with basic information and links about this etud. Used for popups information windows. """ formsemestre_id = retreive_formsemestre_from_request() with_photo = int(with_photo) etud = Identite.get_etud(etudid) photo_html = etud.photo_html(title="fiche de " + etud.nomprenom) code_cursus, _ = sco_report.get_code_cursus_etud( etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", " ) if etud.admission: bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite) bac_abbrev = bac.abbrev() else: bac_abbrev = "-" H = f"""
    Bac: {bac_abbrev}
    {code_cursus}
    """ # Informations sur l'etudiant dans le semestre courant: if formsemestre_id: # un semestre est spécifié par la page formsemestre = FormSemestre.get_formsemestre(formsemestre_id) else: # le semestre "en cours" pour l'étudiant inscription_courante = etud.inscription_courante() formsemestre = ( inscription_courante.formsemestre if inscription_courante else None ) if formsemestre: groups = sco_groups.get_etud_groups(etudid, formsemestre.id) grc = sco_groups.listgroups_abbrev(groups) H += f"""
    En S{formsemestre.semestre_id}: {grc}
    """ H += "
    " # fin partie gauche (eid_left) if with_photo: H += '' + photo_html + "" H += "
    " if debug: return render_template("sco_page.j2", title="debug", content=H) return H