forked from ScoDoc/ScoDoc
895 lines
35 KiB
Python
895 lines
35 KiB
Python
# -*- 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
|
|
#
|
|
##############################################################################
|
|
|
|
"""Synchronisation des listes d'étudiants avec liste portail (Apogée)
|
|
"""
|
|
|
|
import time
|
|
from operator import itemgetter
|
|
|
|
from flask import g, render_template, url_for
|
|
from flask_login import current_user
|
|
|
|
from app import db, log
|
|
from app.models import Admission, Adresse, FormSemestre, Identite, ScolarNews
|
|
|
|
import app.scodoc.sco_utils as scu
|
|
import app.scodoc.notesdb as ndb
|
|
from app.scodoc import sco_cache
|
|
from app.scodoc import sco_formsemestre
|
|
from app.scodoc import sco_formsemestre_inscriptions
|
|
from app.scodoc import sco_groups
|
|
from app.scodoc import sco_inscr_passage
|
|
from app.scodoc import sco_portal_apogee
|
|
from app.scodoc import sco_etud
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
from app.scodoc.sco_permissions import Permission
|
|
|
|
# Clés utilisées pour la synchro
|
|
EKEY_APO = "nip"
|
|
EKEY_SCO = "code_nip"
|
|
EKEY_NAME = "code NIP"
|
|
|
|
|
|
# view:
|
|
def formsemestre_synchro_etuds(
|
|
formsemestre_id,
|
|
etuds: list = None,
|
|
inscrits_without_key: list = None,
|
|
annee_apogee=None,
|
|
submitted=False,
|
|
dialog_confirmed=False,
|
|
export_cat_xls=None,
|
|
read_only=False,
|
|
):
|
|
"""Synchronise les étudiants de ce semestre avec ceux d'Apogée.
|
|
|
|
etuds : liste des codes NIP des etudiants a inscrire (ou deja inscrits)
|
|
inscrits_without_key : etudids des etudiants sans code NIP a laisser inscrits
|
|
read_only : Affiche sans permettre modifications
|
|
|
|
On a plusieurs cas de figure: L'étudiant peut être
|
|
1- présent dans Apogée et inscrit dans le semestre ScoDoc (etuds_ok)
|
|
2- dans Apogée, dans ScoDoc, mais pas inscrit dans le semestre (etuds_noninscrits)
|
|
3- dans Apogée et pas dans ScoDoc (a_importer)
|
|
4- inscrit dans le semestre ScoDoc, mais pas trouvé dans Apogée (sur la base du code NIP)
|
|
|
|
Que faire ?
|
|
Cas 1: rien à faire
|
|
Cas 2: inscrire dans le semestre
|
|
Cas 3: importer l'étudiant (le créer)
|
|
puis l'inscrire à ce semestre.
|
|
Cas 4: lister les etudiants absents d'Apogée (indiquer leur code NIP...)
|
|
|
|
- présenter les différents cas
|
|
- l'utilisateur valide (cocher les étudiants à importer/inscrire)
|
|
- go
|
|
|
|
etuds: apres sélection par l'utilisateur, la liste des étudiants selectionnés
|
|
que l'on va importer/inscrire
|
|
"""
|
|
etuds = etuds or []
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
inscrits_without_key = inscrits_without_key or []
|
|
log(f"formsemestre_synchro_etuds: formsemestre_id={formsemestre_id}")
|
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
sem["etape_apo_str"] = sco_formsemestre.formsemestre_etape_apo_str(sem)
|
|
# Write access ?
|
|
if not current_user.has_permission(Permission.EtudInscrit):
|
|
read_only = True
|
|
if read_only:
|
|
submitted = False
|
|
dialog_confirmed = False
|
|
# -- check lock
|
|
if not sem["etat"]:
|
|
raise ScoValueError("opération impossible: semestre verrouille")
|
|
if not sem["etapes"]:
|
|
raise ScoValueError(
|
|
f"""opération impossible: ce semestre n'a pas de code étape
|
|
(voir <a class="stdlink" href="{
|
|
url_for('notes.formsemestre_editwithmodules',
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
|
}">Modifier ce semestre</a>)
|
|
""",
|
|
safe=True,
|
|
)
|
|
base_url = url_for(
|
|
"notes.formsemestre_synchro_etuds",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id,
|
|
annee_apogee=annee_apogee or None, # si None, le param n'est pas dans l'URL
|
|
)
|
|
|
|
if annee_apogee is None: # année d'inscription par défaut
|
|
annee_apogee = scu.annee_scolaire_debut(
|
|
sem["annee_debut"], sem["mois_debut_ord"]
|
|
)
|
|
annee_apogee = str(annee_apogee)
|
|
|
|
if isinstance(etuds, str):
|
|
etuds = etuds.split(",") # vient du form de confirmation
|
|
elif isinstance(etuds, int):
|
|
etuds = [str(etuds)]
|
|
if isinstance(inscrits_without_key, int):
|
|
inscrits_without_key = [inscrits_without_key]
|
|
elif isinstance(inscrits_without_key, str):
|
|
inscrits_without_key = inscrits_without_key.split(",")
|
|
elif not isinstance(inscrits_without_key, list):
|
|
raise ValueError("invalid type for inscrits_without_key")
|
|
inscrits_without_key = [int(x) for x in inscrits_without_key if x]
|
|
(
|
|
etuds_by_cat,
|
|
a_importer,
|
|
a_inscrire,
|
|
inscrits_set,
|
|
inscrits_without_key_all,
|
|
etudsapo_ident,
|
|
) = list_synch(sem, annee_apogee=annee_apogee)
|
|
if export_cat_xls:
|
|
filename = export_cat_xls
|
|
xls = _build_page(
|
|
sem,
|
|
etuds_by_cat,
|
|
annee_apogee,
|
|
export_cat_xls=export_cat_xls,
|
|
base_url=base_url,
|
|
read_only=read_only,
|
|
)
|
|
return scu.send_file(
|
|
xls,
|
|
mime=scu.XLS_MIMETYPE,
|
|
filename=filename,
|
|
suffix=scu.XLSX_SUFFIX,
|
|
)
|
|
|
|
H = []
|
|
if not submitted:
|
|
H += _build_page(
|
|
sem,
|
|
etuds_by_cat,
|
|
annee_apogee,
|
|
base_url=base_url,
|
|
read_only=read_only,
|
|
)
|
|
else:
|
|
etuds_set = set(etuds)
|
|
a_importer = a_importer.intersection(etuds_set)
|
|
a_desinscrire = inscrits_set - etuds_set
|
|
log("inscrits_without_key_all=%s" % set(inscrits_without_key_all))
|
|
log("inscrits_without_key=%s" % inscrits_without_key)
|
|
a_desinscrire_without_key = set(inscrits_without_key_all) - set(
|
|
inscrits_without_key
|
|
)
|
|
log("a_desinscrire_without_key=%s" % a_desinscrire_without_key)
|
|
inscrits_ailleurs = set(sco_inscr_passage.list_inscrits_date(formsemestre))
|
|
a_inscrire = a_inscrire.intersection(etuds_set)
|
|
|
|
if not dialog_confirmed:
|
|
# Confirmation
|
|
if a_importer:
|
|
H.append("<h3>Étudiants à importer et inscrire :</h3><ol>")
|
|
for key in a_importer:
|
|
nom = f"""{etudsapo_ident[key]['nom']} {etudsapo_ident[key].get("prenom", "")}"""
|
|
H.append(f"<li>{nom}</li>")
|
|
H.append("</ol>")
|
|
|
|
if a_inscrire:
|
|
H.append("<h3>Étudiants à inscrire :</h3><ol>")
|
|
for key in a_inscrire:
|
|
nom = f"""{etudsapo_ident[key]['nom']} {etudsapo_ident[key].get("prenom", "")}"""
|
|
H.append(f"<li>{nom}</li>")
|
|
H.append("</ol>")
|
|
|
|
a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
|
|
if a_inscrire_en_double:
|
|
H.append(
|
|
"<h3>dont étudiants déjà inscrits dans un autre semestre:</h3><ol>"
|
|
)
|
|
for key in a_inscrire_en_double:
|
|
nom = f"""{etudsapo_ident[key]['nom']} {etudsapo_ident[key].get("prenom", "")}"""
|
|
H.append(f'<li class="inscrit-ailleurs">{nom}</li>')
|
|
H.append("</ol>")
|
|
|
|
if a_desinscrire:
|
|
H.append("<h3>Étudiants à désinscrire :</h3><ol>")
|
|
for key in a_desinscrire:
|
|
etud = sco_etud.get_etud_info(filled=True, code_nip=key)[0]
|
|
H.append('<li class="desinscription">%(nomprenom)s</li>' % etud)
|
|
H.append("</ol>")
|
|
if a_desinscrire_without_key:
|
|
H.append("<h3>Étudiants à désinscrire (sans code):</h3><ol>")
|
|
for etudid in a_desinscrire_without_key:
|
|
etud = inscrits_without_key_all[etudid]
|
|
sco_etud.format_etud_ident(etud)
|
|
H.append('<li class="desinscription">%(nomprenom)s</li>' % etud)
|
|
H.append("</ol>")
|
|
|
|
todo = (
|
|
a_importer or a_inscrire or a_desinscrire or a_desinscrire_without_key
|
|
)
|
|
if not todo:
|
|
H.append("""<h3>Il n'y a rien à modifier !</h3>""")
|
|
H.append(
|
|
scu.confirm_dialog(
|
|
dest_url="formsemestre_synchro_etuds",
|
|
add_headers=False,
|
|
cancel_url="formsemestre_synchro_etuds?formsemestre_id="
|
|
+ str(formsemestre_id),
|
|
OK="Effectuer l'opération" if todo else "OK",
|
|
parameters={
|
|
"formsemestre_id": formsemestre_id,
|
|
"etuds": ",".join(etuds),
|
|
"inscrits_without_key": ",".join(
|
|
[str(x) for x in inscrits_without_key]
|
|
),
|
|
"submitted": 1,
|
|
"annee_apogee": annee_apogee,
|
|
},
|
|
)
|
|
)
|
|
else:
|
|
# OK, do it
|
|
|
|
# Conversions des listes de codes NIP en listes de codes etudid
|
|
def nip2etudid(code_nip):
|
|
etud = sco_etud.get_etud_info(code_nip=code_nip)[0]
|
|
return etud["etudid"]
|
|
|
|
etudids_a_inscrire = [nip2etudid(x) for x in a_inscrire]
|
|
etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire]
|
|
etudids_a_desinscrire += a_desinscrire_without_key
|
|
#
|
|
# check decisions jury ici pour éviter de recontruire le cache
|
|
# après chaque desinscription
|
|
sco_formsemestre_inscriptions.check_if_has_decision_jury(
|
|
formsemestre, a_desinscrire
|
|
)
|
|
with sco_cache.DeferredSemCacheManager():
|
|
do_import_etuds_from_portal(formsemestre, a_importer, etudsapo_ident)
|
|
sco_inscr_passage.do_inscrit(formsemestre, etudids_a_inscrire)
|
|
sco_inscr_passage.do_desinscrit(
|
|
formsemestre, etudids_a_desinscrire, check_has_dec_jury=False
|
|
)
|
|
|
|
H.append(
|
|
f"""<h3>Opération effectuée</h3>
|
|
<ul>
|
|
<li><a class="stdlink" href="{
|
|
url_for('notes.formsemestre_synchro_etuds',
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
|
|
)}">Continuer la synchronisation</a>
|
|
</li>"""
|
|
)
|
|
#
|
|
partitions = sco_groups.get_partitions_list(
|
|
formsemestre_id, with_default=False
|
|
)
|
|
if partitions: # il y a au moins une vraie partition
|
|
H.append(
|
|
f"""<li><a class="stdlink" href="{
|
|
url_for("scolar.partition_editor",
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
|
|
)}">Répartir les groupes de {partitions[0]["partition_name"]}</a>
|
|
</li>
|
|
"""
|
|
)
|
|
|
|
return render_template(
|
|
"sco_page.j2", title="Synchronisation des étudiants", content="\n".join(H)
|
|
)
|
|
|
|
|
|
def _build_page(
|
|
sem,
|
|
etuds_by_cat,
|
|
annee_apogee,
|
|
export_cat_xls=None,
|
|
base_url="",
|
|
read_only=False,
|
|
):
|
|
if export_cat_xls:
|
|
return sco_inscr_passage.etuds_select_boxes(
|
|
etuds_by_cat, export_cat_xls=export_cat_xls, base_url=base_url
|
|
)
|
|
year = time.localtime()[0]
|
|
if annee_apogee and abs(year - int(annee_apogee)) < 50:
|
|
years = list(
|
|
range(
|
|
min(year - 1, int(annee_apogee) - 1), max(year, int(annee_apogee)) + 1
|
|
)
|
|
)
|
|
else:
|
|
years = list(range(year - 1, year + 1))
|
|
annee_apogee = ""
|
|
options = []
|
|
for y in years:
|
|
if str(y) == annee_apogee:
|
|
sel = "selected"
|
|
else:
|
|
sel = ""
|
|
options.append('<option value="%s" %s>%s</option>' % (str(y), sel, str(y)))
|
|
if annee_apogee:
|
|
sel = ""
|
|
else:
|
|
sel = "selected"
|
|
options.append('<option value="" %s>toutes</option>' % sel)
|
|
# sem['etape_apo_str'] = sem['etape_apo'] or '-'
|
|
|
|
H = [
|
|
"""<h2 class="formsemestre">Synchronisation des étudiants du semestre avec Apogée</h2>""",
|
|
"""<p>Actuellement <b>%d</b> inscrits dans ce semestre.</p>"""
|
|
% (
|
|
len(etuds_by_cat["etuds_ok"]["etuds"])
|
|
+ len(etuds_by_cat["etuds_nonapogee"]["etuds"])
|
|
+ len(etuds_by_cat["inscrits_without_key"]["etuds"])
|
|
),
|
|
f"""<p>Code étape Apogée: {sem['etape_apo_str']}</p>
|
|
<form method="post" action="formsemestre_synchro_etuds">
|
|
Année Apogée: <select id="annee_apogee" name="annee_apogee"
|
|
onchange="document.location='formsemestre_synchro_etuds?formsemestre_id={
|
|
sem['formsemestre_id']
|
|
}&annee_apogee='+document.getElementById('annee_apogee').value">
|
|
""",
|
|
"\n".join(options),
|
|
"""</select>
|
|
""",
|
|
(
|
|
""
|
|
if read_only
|
|
else f"""
|
|
<input type="hidden" name="formsemestre_id" value="{sem['formsemestre_id']}"/>
|
|
<input type="submit" name="submitted" value="Appliquer les modifications"/>
|
|
<a href="#help">aide</a>
|
|
"""
|
|
),
|
|
sco_inscr_passage.etuds_select_boxes(
|
|
etuds_by_cat,
|
|
sel_inscrits=False,
|
|
show_empty_boxes=True,
|
|
base_url=base_url,
|
|
read_only=read_only,
|
|
),
|
|
(
|
|
""
|
|
if read_only
|
|
else """<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>"""
|
|
),
|
|
formsemestre_synchro_etuds_help(sem),
|
|
"""</form>""",
|
|
]
|
|
return H
|
|
|
|
|
|
def list_synch(sem, annee_apogee=None):
|
|
""""""
|
|
inscrits = sco_inscr_passage.list_inscrits(sem["formsemestre_id"], with_dems=True)
|
|
# Tous les ensembles d'etudiants sont ici des ensembles de codes NIP (voir EKEY_SCO)
|
|
# (sauf inscrits_without_key)
|
|
inscrits_set = set()
|
|
inscrits_without_key = {} # etudid : etud sans code NIP
|
|
for e in inscrits.values():
|
|
if not e[EKEY_SCO]:
|
|
inscrits_without_key[e["etudid"]] = e
|
|
e["inscrit"] = True # checkbox state
|
|
else:
|
|
inscrits_set.add(e[EKEY_SCO])
|
|
|
|
date_finalisation_inscr_by_nip = {} # nip : datefinalisationinscription_str
|
|
|
|
etapes = sem["etapes"]
|
|
etudsapo_set = set()
|
|
etudsapo_ident = {}
|
|
etuds_payes = set() # étudiants ayant payé (avec balise <paiementinscription> true)
|
|
for etape in etapes:
|
|
if etape:
|
|
etudsapo = sco_portal_apogee.get_inscrits_etape(
|
|
etape, annee_apogee=annee_apogee
|
|
)
|
|
etudsapo_set |= {x[EKEY_APO] for x in etudsapo}
|
|
for e in etudsapo:
|
|
if e[EKEY_APO] not in etudsapo_ident:
|
|
etudsapo_ident[e[EKEY_APO]] = e
|
|
date_finalisation_inscr_by_nip[e[EKEY_APO]] = e[
|
|
"datefinalisationinscription"
|
|
]
|
|
# note: si le portail ne renseigne pas cette balise, suppose que paiement ok
|
|
etuds_payes |= {
|
|
x[EKEY_APO] for x in etudsapo if x.get("paiementinscription", True)
|
|
}
|
|
|
|
# categories:
|
|
etuds_ok = etudsapo_set.intersection(inscrits_set)
|
|
etuds_aposco, a_importer, key2etudid = list_all(etudsapo_set)
|
|
etuds_noninscrits = etuds_aposco - inscrits_set
|
|
etuds_nonapogee = inscrits_set - etudsapo_set
|
|
#
|
|
cnx = ndb.GetDBConnexion()
|
|
|
|
# Tri listes
|
|
def set_to_sorted_list(etudset, etud_apo=False, is_inscrit=False):
|
|
def key2etud(key, etud_apo=False):
|
|
if not etud_apo:
|
|
etudid = key2etudid[key]
|
|
etuds = sco_etud.identite_list(cnx, {"etudid": etudid})
|
|
if not etuds: # ? cela ne devrait pas arriver XXX
|
|
log(f"XXX key2etud etudid={etudid}, type {type(etudid)}")
|
|
etud = etuds[0]
|
|
etud["inscrit"] = is_inscrit # checkbox state
|
|
etud["datefinalisationinscription"] = (
|
|
date_finalisation_inscr_by_nip.get(key, None)
|
|
)
|
|
if key in etudsapo_ident:
|
|
etud["etape"] = etudsapo_ident[key].get("etape", "")
|
|
else:
|
|
# etudiant Apogee
|
|
etud = etudsapo_ident[key]
|
|
|
|
etud["etudid"] = ""
|
|
etud["civilite"] = etud.get(
|
|
"sexe", etud.get("gender", "")
|
|
) # la cle 'sexe' est prioritaire sur 'gender'
|
|
etud["inscrit"] = is_inscrit # checkbox state
|
|
if key in etuds_payes:
|
|
etud["paiementinscription"] = True
|
|
else:
|
|
etud["paiementinscription"] = False
|
|
return etud
|
|
|
|
etuds = [key2etud(x, etud_apo) for x in etudset]
|
|
etuds.sort(key=itemgetter("nom"))
|
|
return etuds
|
|
|
|
#
|
|
boites = {
|
|
"etuds_a_importer": {
|
|
"etuds": set_to_sorted_list(a_importer, is_inscrit=True, etud_apo=True),
|
|
"infos": {
|
|
"id": "etuds_a_importer",
|
|
"title": "Étudiants dans Apogée à importer",
|
|
"help": """Ces étudiants sont inscrits dans cette étape Apogée mais ne sont pas connus par ScoDoc:
|
|
cocher les noms à importer et inscrire puis appuyer sur le bouton "Appliquer".""",
|
|
"title_target": "",
|
|
"with_checkbox": True,
|
|
"etud_key": EKEY_APO, # clé à stocker dans le formulaire html
|
|
"filename": "etuds_a_importer",
|
|
},
|
|
"nomprenoms": etudsapo_ident,
|
|
},
|
|
"etuds_noninscrits": {
|
|
"etuds": set_to_sorted_list(etuds_noninscrits, is_inscrit=True),
|
|
"infos": {
|
|
"id": "etuds_noninscrits",
|
|
"title": "Étudiants non inscrits dans ce semestre",
|
|
"help": """Ces étudiants sont déjà connus par ScoDoc, sont inscrits dans cette étape Apogée mais ne sont pas inscrits à ce semestre ScoDoc. Cochez les étudiants à inscrire.""",
|
|
"comment": """ dans ScoDoc et Apogée, <br>mais pas inscrits
|
|
dans ce semestre""",
|
|
"title_target": "",
|
|
"with_checkbox": True,
|
|
"etud_key": EKEY_SCO,
|
|
"filename": "etuds_non_inscrits",
|
|
},
|
|
},
|
|
"etuds_nonapogee": {
|
|
"etuds": set_to_sorted_list(etuds_nonapogee, is_inscrit=True),
|
|
"infos": {
|
|
"id": "etuds_nonapogee",
|
|
"title": "Étudiants ScoDoc inconnus dans cette étape Apogée",
|
|
"help": """Ces étudiants sont inscrits dans ce semestre ScoDoc, ont un code NIP, mais ne sont pas inscrits dans cette étape Apogée. Soit ils sont en retard pour leur inscription, soit il s'agit d'une erreur: vérifiez avec le service Scolarité de votre établissement. Autre possibilité: votre code étape semestre (%s) est incorrect ou vous n'avez pas choisi la bonne année d'inscription."""
|
|
% sem["etape_apo_str"],
|
|
"comment": " à vérifier avec la Scolarité",
|
|
"title_target": "",
|
|
"with_checkbox": True,
|
|
"etud_key": EKEY_SCO,
|
|
"filename": "etuds_non_apogee",
|
|
},
|
|
},
|
|
"inscrits_without_key": {
|
|
"etuds": list(inscrits_without_key.values()),
|
|
"infos": {
|
|
"id": "inscrits_without_key",
|
|
"title": "Étudiants ScoDoc sans clé Apogée (NIP)",
|
|
"help": """Ces étudiants sont inscrits dans ce semestre ScoDoc, mais n'ont pas de code NIP: on ne peut pas les mettre en correspondance avec Apogée. Utiliser le lien 'Changer les données identité' dans le menu 'Etudiant' sur leur fiche pour ajouter cette information.""",
|
|
"title_target": "",
|
|
"with_checkbox": True,
|
|
"checkbox_name": "inscrits_without_key",
|
|
"filename": "inscrits_without_key",
|
|
},
|
|
},
|
|
"etuds_ok": {
|
|
"etuds": set_to_sorted_list(etuds_ok, is_inscrit=True),
|
|
"infos": {
|
|
"id": "etuds_ok",
|
|
"title": "Étudiants dans Apogée et déjà inscrits",
|
|
"help": """Ces etudiants sont inscrits dans le semestre ScoDoc et sont présents dans Apogée:
|
|
tout est donc correct. Décocher les étudiants que vous souhaitez désinscrire.""",
|
|
"title_target": "",
|
|
"with_checkbox": True,
|
|
"etud_key": EKEY_SCO,
|
|
"filename": "etuds_inscrits_ok_apo",
|
|
},
|
|
},
|
|
}
|
|
return (
|
|
boites,
|
|
a_importer,
|
|
etuds_noninscrits,
|
|
inscrits_set,
|
|
inscrits_without_key,
|
|
etudsapo_ident,
|
|
)
|
|
|
|
|
|
def list_all(etudsapo_set):
|
|
"""Cherche le sous-ensemble des etudiants Apogee de ce semestre
|
|
qui existent dans ScoDoc.
|
|
"""
|
|
# on charge TOUS les etudiants (au pire qq 100000 ?)
|
|
# si tres grosse base, il serait mieux de faire une requete
|
|
# d'interrogation par etudiant.
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
cursor.execute(
|
|
"SELECT "
|
|
+ EKEY_SCO
|
|
+ """, id AS etudid
|
|
FROM identite WHERE dept_id=%(dept_id)s
|
|
""",
|
|
{"dept_id": g.scodoc_dept_id},
|
|
)
|
|
key2etudid = dict([(x[0], x[1]) for x in cursor.fetchall()])
|
|
all_set = set(key2etudid.keys())
|
|
|
|
# ne retient que ceux dans Apo
|
|
etuds_aposco = etudsapo_set.intersection(
|
|
all_set
|
|
) # a la fois dans Apogee et dans ScoDoc
|
|
a_importer = etudsapo_set - all_set # dans Apogee, mais inconnus dans ScoDoc
|
|
return etuds_aposco, a_importer, key2etudid
|
|
|
|
|
|
def formsemestre_synchro_etuds_help(sem):
|
|
formsemestre_id = sem["formsemestre_id"]
|
|
default_group_id = sco_groups.get_default_group(sem["formsemestre_id"])
|
|
return f"""<div class="pas_help pas_help_left"><h3><a name="help">Explications</a></h3>
|
|
<p>Cette page permet d'importer dans le semestre destination
|
|
<a class="stdlink"
|
|
href="{url_for('notes.formsemestre_status', scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id)
|
|
}">{sem['titreannee']}</a>
|
|
les étudiants inscrits dans l'étape Apogée correspondante
|
|
(<b><tt>{sem['etape_apo_str']}</tt></b>)
|
|
</p>
|
|
<p>Au départ, tous les étudiants d'Apogée sont sélectionnés; vous pouvez
|
|
en déselectionner certains. Tous les étudiants cochés seront inscrits au semestre ScoDoc,
|
|
les autres seront si besoin désinscrits. Aucune modification n'est effectuée avant
|
|
d'appuyer sur le bouton "Appliquer les modifications".</p>
|
|
|
|
<h4>Autres fonctions utiles</h4>
|
|
<ul>
|
|
<li><a class="stdlink" href="{
|
|
url_for("scolar.check_group_apogee", scodoc_dept=g.scodoc_dept,
|
|
group_id=default_group_id)
|
|
}">vérification des codes Apogée</a> (des étudiants déjà inscrits)
|
|
</li>
|
|
<li>le <a class="stdlink" href="{url_for('notes.formsemestre_inscr_passage',
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id)}">
|
|
formulaire de passage</a> qui permet aussi de désinscrire des étudiants
|
|
en cas d'erreur, etc.</li>
|
|
</ul>
|
|
</div>
|
|
"""
|
|
|
|
|
|
def gender2civilite(gender):
|
|
"""Le portail code en 'M', 'F', et ScoDoc en 'M', 'F', 'X'"""
|
|
if gender == "M" or gender == "F" or gender == "X":
|
|
return gender
|
|
elif not gender:
|
|
return "X"
|
|
log('gender2civilite: invalid value "%s", defaulting to "X"' % gender)
|
|
return "X" # "X" en général n'est pas affiché, donc bon choix si invalide
|
|
|
|
|
|
def get_opt_str(etud: dict, k) -> str | None:
|
|
"etud[k].strip() ou None"
|
|
v = etud.get(k, None)
|
|
if not v:
|
|
return v
|
|
return v.strip()
|
|
|
|
|
|
def get_annee_naissance(ddmmyyyyy: str) -> int:
|
|
"""Extrait l'année de la date stockée en dd/mm/yyyy dans le XML portail"""
|
|
if not ddmmyyyyy:
|
|
return None
|
|
try:
|
|
return int(ddmmyyyyy.split("/")[2])
|
|
except (ValueError, IndexError):
|
|
return None
|
|
|
|
|
|
def do_import_etuds_from_portal(formsemestre: FormSemestre, a_importer, etudsapo_ident):
|
|
"""Inscrit les etudiants Apogee dans ce semestre."""
|
|
log(f"do_import_etuds_from_portal: a_importer={a_importer}")
|
|
if not a_importer:
|
|
return
|
|
cnx = ndb.GetDBConnexion()
|
|
created_etudids = []
|
|
|
|
try: # --- begin DB transaction
|
|
for key in a_importer:
|
|
etud_portal: dict = etudsapo_ident[key]
|
|
# -> toutes les infos renvoyées par le portail
|
|
|
|
# Traduit les infos portail en infos pour ScoDoc:
|
|
address = etud_portal.get("address", "").strip()
|
|
if address[-2:] == "\\n": # certains champs se terminent par \n
|
|
address = address[:-2]
|
|
|
|
args = {
|
|
"code_nip": etud_portal["nip"],
|
|
"nom": etud_portal["nom"].strip(),
|
|
"prenom": etud_portal["prenom"].strip(),
|
|
# Les champs suivants sont facultatifs (pas toujours renvoyés par le portail)
|
|
"code_ine": etud_portal.get("ine", "").strip(),
|
|
"civilite": gender2civilite(etud_portal["gender"].strip()),
|
|
"etape": etud_portal.get("etape", None),
|
|
"email": etud_portal.get("mail", "").strip(),
|
|
"emailperso": etud_portal.get("mailperso", "").strip(),
|
|
"date_naissance": etud_portal.get("naissance", "").strip(),
|
|
"lieu_naissance": etud_portal.get("ville_naissance", "").strip(),
|
|
"dept_naissance": etud_portal.get("code_dep_naissance", "").strip(),
|
|
"domicile": address,
|
|
"codepostaldomicile": etud_portal.get("postalcode", "").strip(),
|
|
"villedomicile": etud_portal.get("city", "").strip(),
|
|
"paysdomicile": etud_portal.get("country", "").strip(),
|
|
"telephone": etud_portal.get("phone", "").strip(),
|
|
"typeadresse": "domicile",
|
|
"boursier": etud_portal.get("bourse", None),
|
|
"description": "infos portail",
|
|
}
|
|
|
|
# Identite
|
|
etud: Identite = Identite.create_from_dict(args)
|
|
db.session.flush()
|
|
created_etudids.append(etud.id)
|
|
# Adresse
|
|
adresse = etud.adresses.first()
|
|
adresse.from_dict(args)
|
|
|
|
# Admissions
|
|
do_import_etud_admission(etud, etud_portal)
|
|
|
|
# Inscription au semestre
|
|
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
|
|
formsemestre.id,
|
|
etud.id,
|
|
etat=scu.INSCRIT,
|
|
etape=args["etape"],
|
|
method="synchro_apogee",
|
|
)
|
|
except:
|
|
cnx.rollback()
|
|
log("do_import_etuds_from_portal: aborting transaction !")
|
|
# Nota: db transaction is sometimes partly commited...
|
|
# here we try to remove all created students
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
for etudid in created_etudids:
|
|
log(f"do_import_etuds_from_portal: deleting etudid={etudid}")
|
|
cursor.execute(
|
|
"delete from notes_moduleimpl_inscription where etudid=%(etudid)s",
|
|
{"etudid": etudid},
|
|
)
|
|
cursor.execute(
|
|
"delete from notes_formsemestre_inscription where etudid=%(etudid)s",
|
|
{"etudid": etudid},
|
|
)
|
|
cursor.execute(
|
|
"delete from scolar_events where etudid=%(etudid)s", {"etudid": etudid}
|
|
)
|
|
cursor.execute(
|
|
"delete from adresse where etudid=%(etudid)s", {"etudid": etudid}
|
|
)
|
|
cursor.execute(
|
|
"delete from group_membership where etudid=%(etudid)s",
|
|
{"etudid": etudid},
|
|
)
|
|
cursor.execute(
|
|
"delete from identite where id=%(etudid)s", {"etudid": etudid}
|
|
)
|
|
cnx.commit()
|
|
log("do_import_etuds_from_portal: re-raising exception")
|
|
# > import: modif identite, adresses, inscriptions
|
|
sco_cache.invalidate_formsemestre()
|
|
raise
|
|
|
|
ScolarNews.add(
|
|
typ=ScolarNews.NEWS_INSCR,
|
|
text=f"Import Apogée de {len(created_etudids)} étudiants en ",
|
|
obj=formsemestre.id,
|
|
)
|
|
|
|
|
|
def do_import_etud_admission(etud: Identite, etud_data: dict, import_identite=False):
|
|
"""Importe les donnees admission pour cet etud.
|
|
etud_data est un dictionnaire traduit du XML portail
|
|
"""
|
|
annee_courante = time.localtime()[0]
|
|
serie_bac, spe_bac = _get_bac(etud_data)
|
|
# Les champs n'ont pas les mêmes noms dans Apogee et dans ScoDoc:
|
|
args = {
|
|
"annee": get_opt_str(etud_data, "inscription") or annee_courante,
|
|
"bac": serie_bac,
|
|
"specialite": spe_bac,
|
|
"annee_bac": get_opt_str(etud_data, "anneebac"),
|
|
"codelycee": get_opt_str(etud_data, "lycee"),
|
|
"nomlycee": get_opt_str(etud_data, "nom_lycee"),
|
|
"villelycee": get_opt_str(etud_data, "ville_lycee"),
|
|
"codepostallycee": get_opt_str(etud_data, "codepostal_lycee"),
|
|
"boursier": get_opt_str(etud_data, "bourse"),
|
|
}
|
|
if etud.admission is None:
|
|
etud.admission = Admission()
|
|
args = {k: v for k, v in args.items() if v not in ("", None)}
|
|
etud.admission.from_dict(args)
|
|
|
|
# Reimport des identités
|
|
if import_identite:
|
|
args = {}
|
|
# Les champs n'ont pas les mêmes noms dans Apogee et dans ScoDoc:
|
|
fields_apo_sco = [
|
|
("naissance", "date_naissance"),
|
|
("ville_naissance", "lieu_naissance"),
|
|
("code_dep_naissance", "dept_naissance"),
|
|
("nom", "nom"),
|
|
("prenom", "prenom"),
|
|
("ine", "code_ine"),
|
|
("bourse", "boursier"),
|
|
]
|
|
for apo_field, sco_field in fields_apo_sco:
|
|
x = etud_data.get(apo_field, "").strip()
|
|
if x:
|
|
args[sco_field] = x
|
|
# Champs spécifiques:
|
|
civilite = gender2civilite(etud_data["gender"].strip())
|
|
if civilite:
|
|
args["civilite"] = civilite
|
|
|
|
etud.from_dict(args)
|
|
db.session.add(etud)
|
|
db.session.commit()
|
|
db.session.refresh(etud)
|
|
|
|
|
|
def _get_bac(etud) -> tuple[str | None, str | None]:
|
|
bac = get_opt_str(etud, "bac")
|
|
if not bac:
|
|
return None, None
|
|
serie_bac = bac.split("-")[0]
|
|
if len(serie_bac) < 8:
|
|
spe_bac = bac[len(serie_bac) + 1 :]
|
|
else:
|
|
serie_bac = bac
|
|
spe_bac = None
|
|
return serie_bac, spe_bac
|
|
|
|
|
|
def update_etape_formsemestre_inscription(ins, etud):
|
|
"""Met à jour l'étape de l'inscription.
|
|
|
|
Args:
|
|
ins (dict): formsemestre_inscription
|
|
etud (dict): etudiant portail Apo
|
|
"""
|
|
if etud["etape"] != ins["etape"]:
|
|
ins["etape"] = etud["etape"]
|
|
sco_formsemestre_inscriptions.do_formsemestre_inscription_edit(args=ins)
|
|
|
|
|
|
def formsemestre_import_etud_admission(
|
|
formsemestre_id: int, import_identite=True, import_email=False
|
|
) -> tuple[list[Identite], list[Identite], list[tuple[Identite, str]]]:
|
|
"""Tente d'importer les données admission depuis le portail
|
|
pour tous les étudiants du semestre.
|
|
Si import_identite==True, recopie l'identité (nom/prenom/sexe/date_naissance)
|
|
de chaque étudiant depuis le portail.
|
|
N'affecte pas les etudiants inconnus sur le portail.
|
|
Renvoie:
|
|
- etuds_no_nip: liste d'étudiants sans code NIP
|
|
- etuds_unknown: etudiants avec NIP mais inconnus du portail
|
|
- changed_mails: (etudiant, old_mail) pour ceux dont le mail a changé
|
|
"""
|
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
|
{"formsemestre_id": formsemestre_id}
|
|
)
|
|
log(f"formsemestre_import_etud_admission: {formsemestre_id} ({len(ins)} etuds)")
|
|
etuds_no_nip: list[Identite] = []
|
|
etuds_unknown: list[Identite] = []
|
|
changed_mails: list[tuple[Identite, str]] = [] # modification d'adresse mails
|
|
|
|
# Essaie de recuperer les etudiants des étapes, car
|
|
# la requete get_inscrits_etape est en général beaucoup plus
|
|
# rapide que les requetes individuelles get_etud_apogee
|
|
annee_apogee = str(
|
|
scu.annee_scolaire_debut(sem["annee_debut"], sem["mois_debut_ord"])
|
|
)
|
|
apo_etuds = {} # nip : etud apo
|
|
for etape in sem["etapes"]:
|
|
etudsapo = sco_portal_apogee.get_inscrits_etape(
|
|
etape, annee_apogee=annee_apogee
|
|
)
|
|
apo_etuds.update({e["nip"]: e for e in etudsapo})
|
|
|
|
for i in ins:
|
|
etudid = i["etudid"]
|
|
etud: Identite = Identite.query.get_or_404(etudid)
|
|
code_nip = etud.code_nip
|
|
if not code_nip:
|
|
etuds_no_nip.append(etud)
|
|
else:
|
|
data_apo = apo_etuds.get(code_nip)
|
|
if not data_apo:
|
|
# pas vu dans les etudiants de l'étape, tente en individuel
|
|
data_apo = sco_portal_apogee.get_etud_apogee(code_nip)
|
|
if data_apo:
|
|
update_etape_formsemestre_inscription(i, data_apo)
|
|
do_import_etud_admission(
|
|
etud,
|
|
data_apo,
|
|
import_identite=import_identite,
|
|
)
|
|
adresse = etud.adresses.first()
|
|
if adresse is None:
|
|
adresse = Adresse()
|
|
etud.adresses.append(adresse)
|
|
apo_emailperso = data_apo.get("mailperso", "")
|
|
if adresse.emailperso and not apo_emailperso:
|
|
apo_emailperso = adresse.emailperso
|
|
if import_email:
|
|
if not "mail" in data_apo:
|
|
raise ScoValueError(
|
|
"la réponse portail n'a pas le champ requis 'mail'"
|
|
)
|
|
if (
|
|
adresse.email != data_apo["mail"]
|
|
or adresse.emailperso != apo_emailperso
|
|
):
|
|
old_mail = adresse.email
|
|
adresse.email = data_apo["mail"]
|
|
adresse.emailperso = apo_emailperso
|
|
db.session.add(adresse)
|
|
# notifie seulement les changements d'adresse mail institutionnelle
|
|
if adresse.email != data_apo["mail"]:
|
|
changed_mails.append((etud, old_mail))
|
|
else:
|
|
etuds_unknown.append(etud)
|
|
db.session.commit()
|
|
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])
|
|
return etuds_no_nip, etuds_unknown, changed_mails
|