# -*- 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 += """
Bac | Année | Rg |
Math | Physique | Anglais | Franç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""""""
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"""
Situation : | %(situation)s %(bourse_span)s |
%(groupes_row)s
{info_naissance}
"""
+ adresse_template
+ """
"""
)
info["annotations_mkup"] = (
f"""
"""
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