ScoDoc/scolars.py

872 lines
25 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 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
#
##############################################################################
""" Accès donnees etudiants
"""
import time
import sco_utils as scu
from sco_utils import SCO_ENCODING
from sco_exceptions import ScoGenError, ScoValueError
from notesdb import (
EditableTable,
ScoDocCursor,
DateDMYtoISO,
DateISOtoDMY,
int_null_is_null,
)
from notes_log import log
from TrivialFormulator import TrivialFormulator
import safehtml
from scolog import logdb
# from notes_table import *
import sco_news
# XXXXXXXXX HACK: zope 2.7.7 bug turaround ?
import locale
locale.setlocale(locale.LC_ALL, ("en_US", SCO_ENCODING))
from email.MIMEMultipart import ( # pylint: disable=no-name-in-module,import-error
MIMEMultipart,
)
from email.MIMEText import MIMEText # pylint: disable=no-name-in-module,import-error
from email.MIMEBase import MIMEBase # pylint: disable=no-name-in-module,import-error
from email.Header import Header # pylint: disable=no-name-in-module,import-error
from email import Encoders # pylint: disable=no-name-in-module,import-error
abbrvmonthsnames = [
"Jan ",
"Fev ",
"Mars",
"Avr ",
"Mai ",
"Juin",
"Jul ",
"Aout",
"Sept",
"Oct ",
"Nov ",
"Dec ",
]
monthsnames = [
"janvier",
"février",
"mars",
"avril",
"mai",
"juin",
"juillet",
"aout",
"septembre",
"octobre",
"novembre",
"décembre",
]
def format_etud_ident(etud):
"""Format identite de l'étudiant (modifié en place)
nom, prénom et formes associees
"""
etud["nom"] = format_nom(etud["nom"])
if "nom_usuel" in etud:
etud["nom_usuel"] = format_nom(etud["nom_usuel"])
else:
etud["nom_usuel"] = ""
etud["prenom"] = format_prenom(etud["prenom"])
etud["civilite_str"] = format_civilite(etud["civilite"])
# Nom à afficher:
if etud["nom_usuel"]:
etud["nom_disp"] = etud["nom_usuel"]
if etud["nom"]:
etud["nom_disp"] += " (" + etud["nom"] + ")"
else:
etud["nom_disp"] = etud["nom"]
etud["nomprenom"] = format_nomprenom(etud) # M. Pierre DUPONT
if etud["civilite"] == "M":
etud["ne"] = ""
elif etud["civilite"] == "F":
etud["ne"] = "e"
else: # 'X'
etud["ne"] = "(e)"
# Mail à utiliser pour les envois vers l'étudiant:
# choix qui pourrait être controé par une preference
# ici priorité au mail institutionnel:
etud["email_default"] = etud.get("email", "") or etud.get("emailperso", "")
def force_uppercase(s):
if s:
s = scu.strupper(s)
return s
def format_nomprenom(etud, reverse=False):
"""Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
"""
nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
prenom = format_prenom(etud["prenom"])
civilite = format_civilite(etud["civilite"])
if reverse:
fs = [nom, prenom]
else:
fs = [civilite, prenom, nom]
return " ".join([x for x in fs if x])
def format_prenom(s):
"Formatte prenom etudiant pour affichage"
if not s:
return ""
frags = s.split()
r = []
for frag in frags:
fs = frag.split("-")
r.append(
"-".join(
[
x.decode(SCO_ENCODING).lower().capitalize().encode(SCO_ENCODING)
for x in fs
]
)
)
return " ".join(r)
def format_nom(s, uppercase=True):
if not s:
return ""
if uppercase:
return scu.strupper(s)
else:
return format_prenom(s)
def input_civilite(s):
"""Converts external representation of civilite to internal:
'M', 'F', or 'X' (and nothing else).
Raises valueError if conversion fails.
"""
s = scu.strupper(s).strip()
if s in ("M", "M.", "MR", "H"):
return "M"
elif s in ("F", "MLLE", "MLLE.", "MELLE", "MME"):
return "F"
elif s == "X" or not s:
return "X"
raise ValueError("valeur invalide pour la civilité: %s" % s)
def format_civilite(civilite):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personne ne souhaitant pas d'affichage)
"""
try:
return {
"M": "M.",
"F": "Mme",
"X": "",
}[civilite]
except KeyError:
raise ValueError("valeur invalide pour la civilité: %s" % civilite)
def format_lycee(nomlycee):
nomlycee = nomlycee.strip()
s = scu.strlower(nomlycee)
if s[:5] == "lycee" or s[:5] == "lycée":
return nomlycee[5:]
else:
return nomlycee
def format_telephone(n):
if n is None:
return ""
if len(n) < 7:
return n
else:
n = n.replace(" ", "").replace(".", "")
i = 0
r = ""
j = len(n) - 1
while j >= 0:
r = n[j] + r
if i % 2 == 1 and j != 0:
r = " " + r
i += 1
j -= 1
if len(r) == 13 and r[0] != "0":
r = "0" + r
return r
def format_pays(s):
"laisse le pays seulement si != FRANCE"
if scu.strupper(s) != "FRANCE":
return s
else:
return ""
PIVOT_YEAR = 70
def pivot_year(y):
if y == "" or y is None:
return None
y = int(round(float(y)))
if y >= 0 and y < 100:
if y < PIVOT_YEAR:
y = y + 2000
else:
y = y + 1900
return y
_identiteEditor = EditableTable(
"identite",
"etudid",
(
"etudid",
"nom",
"nom_usuel",
"prenom",
"civilite", # 'M", "F", or "X"
"date_naissance",
"lieu_naissance",
"dept_naissance",
"nationalite",
"statut",
"boursier",
"foto",
"photo_filename",
"code_ine",
"code_nip",
),
sortkey="nom",
input_formators={
"nom": force_uppercase,
"prenom": force_uppercase,
"civilite": input_civilite,
"date_naissance": DateDMYtoISO,
},
output_formators={"date_naissance": DateISOtoDMY},
convert_null_outputs_to_empty=True,
allow_set_id=True, # car on specifie le code Apogee a la creation
)
identite_delete = _identiteEditor.delete
def identite_list(cnx, *a, **kw):
"""List, adding on the fly 'annee_naissance' and 'civilite_str' (M., Mme, "")."""
objs = _identiteEditor.list(cnx, *a, **kw)
for o in objs:
if o["date_naissance"]:
o["annee_naissance"] = int(o["date_naissance"].split("/")[2])
else:
o["annee_naissance"] = o["date_naissance"]
o["civilite_str"] = format_civilite(o["civilite"])
return objs
def identite_edit_nocheck(cnx, args):
"""Modifie les champs mentionnes dans args, sans verification ni notification."""
_identiteEditor.edit(cnx, args)
def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
"""Check if nom and prenom are valid.
Also check for duplicates (homonyms), excluding etudid :
in general, homonyms are allowed, but it may be useful to generate a warning.
Returns:
True | False, NbHomonyms
"""
if not nom or (not prenom and not scu.CONFIG.ALLOW_NULL_PRENOM):
return False, 0
nom = nom.decode(SCO_ENCODING).lower().strip().encode(SCO_ENCODING)
if prenom:
prenom = prenom.decode(SCO_ENCODING).lower().strip().encode(SCO_ENCODING)
# Don't allow some special cars (eg used in sql regexps)
if scu.FORBIDDEN_CHARS_EXP.search(nom) or scu.FORBIDDEN_CHARS_EXP.search(prenom):
return False, 0
# Now count homonyms:
cursor = cnx.cursor(cursor_factory=ScoDocCursor)
req = "select etudid from identite where lower(nom) ~ %(nom)s and lower(prenom) ~ %(prenom)s"
if etudid:
req += " and etudid <> %(etudid)s"
cursor.execute(req, {"nom": nom, "prenom": prenom, "etudid": etudid})
res = cursor.dictfetchall()
return True, len(res)
def _check_duplicate_code(cnx, args, code_name, context, edit=True, REQUEST=None):
etudid = args.get("etudid", None)
if args.get(code_name, None):
etuds = identite_list(cnx, {code_name: str(args[code_name])})
# log('etuds=%s'%etuds)
nb_max = 0
if edit:
nb_max = 1
if len(etuds) > nb_max:
listh = [] # liste des doubles
for e in etuds:
listh.append(
"""Autre étudiant: <a href="ficheEtud?etudid=%(etudid)s">%(nom)s %(prenom)s</a>"""
% e
)
if etudid:
OK = "retour à la fiche étudiant"
dest_url = "ficheEtud"
parameters = {"etudid": etudid}
else:
if args.has_key("tf-submitted"):
del args["tf-submitted"]
OK = "Continuer"
dest_url = "etudident_create_form"
parameters = args
else:
OK = "Annuler"
dest_url = ""
parameters = {}
if context:
err_page = context.confirmDialog(
message="""<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name,
helpmsg="""Le %s %s est déjà utilisé: un seul étudiant peut avoir ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.<p><ul><li>"""
% (code_name, args[code_name])
+ "</li><li>".join(listh)
+ "</li></ul><p>",
OK=OK,
dest_url=dest_url,
parameters=parameters,
REQUEST=REQUEST,
)
else:
err_page = """<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name
log("*** error: code %s duplique: %s" % (code_name, args[code_name]))
raise ScoGenError(err_page)
def identite_edit(cnx, args, context=None, REQUEST=None):
"""Modifie l'identite d'un étudiant.
Si context et notification et difference, envoie message notification.
"""
_check_duplicate_code(cnx, args, "code_nip", context, edit=True, REQUEST=REQUEST)
_check_duplicate_code(cnx, args, "code_ine", context, edit=True, REQUEST=REQUEST)
notify_to = None
if context:
try:
notify_to = context.get_preference("notify_etud_changes_to")
except:
pass
if notify_to:
# etat AVANT edition pour envoyer diffs
before = identite_list(cnx, {"etudid": args["etudid"]})[0]
identite_edit_nocheck(cnx, args)
# Notification du changement par e-mail:
if notify_to:
etud = context.getEtudInfo(etudid=args["etudid"], filled=True)[0]
after = identite_list(cnx, {"etudid": args["etudid"]})[0]
notify_etud_change(
context,
notify_to,
etud,
before,
after,
"Modification identite %(nomprenom)s" % etud,
)
def identite_create(cnx, args, context=None, REQUEST=None):
"check unique etudid, then create"
_check_duplicate_code(cnx, args, "code_nip", context, edit=False, REQUEST=REQUEST)
_check_duplicate_code(cnx, args, "code_ine", context, edit=False, REQUEST=REQUEST)
if args.has_key("etudid"):
etudid = args["etudid"]
r = identite_list(cnx, {"etudid": etudid})
if r:
raise ScoValueError(
"Code identifiant (etudid) déjà utilisé ! (%s)" % etudid
)
return _identiteEditor.create(cnx, args)
def notify_etud_change(context, email_addr, etud, before, after, subject):
"""Send email notifying changes to etud
before and after are two dicts, with values before and after the change.
"""
txt = [
"Code NIP:" + etud["code_nip"],
"Civilité: " + etud["civilite_str"],
"Nom: " + etud["nom"],
"Prénom: " + etud["prenom"],
"Etudid: " + etud["etudid"],
"\n",
"Changements effectués:",
]
n = 0
for key in after.keys():
if before[key] != after[key]:
txt.append('%s: %s (auparavant: "%s")' % (key, after[key], before[key]))
n += 1
if not n:
return # pas de changements
txt = "\n".join(txt)
# build mail
log("notify_etud_change: sending notification to %s" % email_addr)
log("notify_etud_change: subject: %s" % subject)
log(txt)
msg = MIMEMultipart()
subj = Header("[ScoDoc] " + subject, SCO_ENCODING)
msg["Subject"] = subj
msg["From"] = context.get_preference("email_from_addr")
msg["To"] = email_addr
mime_txt = MIMEText(txt, "plain", SCO_ENCODING)
msg.attach(mime_txt)
context.sendEmail(msg)
return txt
# --------
# Note: la table adresse n'est pas dans dans la table "identite"
# car on prevoit plusieurs adresses par etudiant (ie domicile, entreprise)
_adresseEditor = EditableTable(
"adresse",
"adresse_id",
(
"adresse_id",
"etudid",
"email",
"emailperso",
"domicile",
"codepostaldomicile",
"villedomicile",
"paysdomicile",
"telephone",
"telephonemobile",
"fax",
"typeadresse",
"entreprise_id",
"description",
),
convert_null_outputs_to_empty=True,
)
adresse_create = _adresseEditor.create
adresse_delete = _adresseEditor.delete
adresse_list = _adresseEditor.list
def adresse_edit(cnx, args, context=None):
"""Modifie l'adresse d'un étudiant.
Si context et notification et difference, envoie message notification.
"""
notify_to = None
if context:
try:
notify_to = context.get_preference("notify_etud_changes_to")
except:
pass
if notify_to:
# etat AVANT edition pour envoyer diffs
before = adresse_list(cnx, {"etudid": args["etudid"]})[0]
_adresseEditor.edit(cnx, args)
# Notification du changement par e-mail:
if notify_to:
etud = context.getEtudInfo(etudid=args["etudid"], filled=True)[0]
after = adresse_list(cnx, {"etudid": args["etudid"]})[0]
notify_etud_change(
context,
notify_to,
etud,
before,
after,
"Modification adresse %(nomprenom)s" % etud,
)
def getEmail(cnx, etudid):
"get email institutionnel etudiant (si plusieurs adresses, prend le premier non null"
adrs = adresse_list(cnx, {"etudid": etudid})
for adr in adrs:
if adr["email"]:
return adr["email"]
return ""
# ---------
_admissionEditor = EditableTable(
"admissions",
"adm_id",
(
"adm_id",
"etudid",
"annee",
"bac",
"specialite",
"annee_bac",
"math",
"physique",
"anglais",
"francais",
"rang",
"qualite",
"rapporteur",
"decision",
"score",
"classement",
"apb_groupe",
"apb_classement_gr",
"commentaire",
"nomlycee",
"villelycee",
"codepostallycee",
"codelycee",
"debouche",
"type_admission",
"boursier_prec",
),
input_formators={
"annee": pivot_year,
"bac": force_uppercase,
"specialite": force_uppercase,
"annee_bac": pivot_year,
"classement": int_null_is_null,
"apb_classsment_gr": int_null_is_null,
},
output_formators={"type_admission": lambda x: x or scu.TYPE_ADMISSION_DEFAULT},
convert_null_outputs_to_empty=True,
)
admission_create = _admissionEditor.create
admission_delete = _admissionEditor.delete
admission_list = _admissionEditor.list
admission_edit = _admissionEditor.edit
# Edition simultanee de identite et admission
class EtudIdentEditor:
def create(self, cnx, args, context=None, REQUEST=None):
etudid = identite_create(cnx, args, context, REQUEST)
args["etudid"] = etudid
admission_create(cnx, args)
return etudid
def list(self, *args, **kw):
R = identite_list(*args, **kw)
Ra = admission_list(*args, **kw)
# print len(R), len(Ra)
# merge: add admission fields to identite
A = {}
for r in Ra:
A[r["etudid"]] = r
res = []
for i in R:
res.append(i)
if A.has_key(i["etudid"]):
# merge
res[-1].update(A[i["etudid"]])
else: # pas d'etudiant trouve
# print "*** pas d'info admission pour %s" % str(i)
void_adm = {
k: None
for k in _admissionEditor.dbfields
if k != "etudid" and k != "adm_id"
}
res[-1].update(void_adm)
# tri par nom
res.sort(lambda x, y: cmp(x["nom"] + x["prenom"], y["nom"] + y["prenom"]))
return res
def edit(self, cnx, args, context=None, REQUEST=None):
identite_edit(cnx, args, context, REQUEST)
if "adm_id" in args: # safety net
admission_edit(cnx, args)
_etudidentEditor = EtudIdentEditor()
etudident_list = _etudidentEditor.list
etudident_edit = _etudidentEditor.edit
etudident_create = _etudidentEditor.create
def make_etud_args(etudid=None, code_nip=None, REQUEST=None, raise_exc=True):
"""forme args dict pour requete recherche etudiant
On peut specifier etudid
ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine
(dans cet ordre).
"""
args = None
if etudid:
args = {"etudid": etudid}
elif code_nip:
args = {"code_nip": code_nip}
elif REQUEST:
if REQUEST.form.has_key("etudid"):
args = {"etudid": REQUEST.form["etudid"]}
elif REQUEST.form.has_key("code_nip"):
args = {"code_nip": REQUEST.form["code_nip"]}
elif REQUEST.form.has_key("code_ine"):
args = {"code_ine": REQUEST.form["code_ine"]}
if not args and raise_exc:
raise ValueError("getEtudInfo: no parameter !")
return args
def create_etud(context, cnx, args={}, REQUEST=None):
"""Creation d'un étudiant. génère aussi évenement et "news".
Args:
args: dict avec les attributs de l'étudiant
Returns:
etud, l'étudiant créé.
"""
# creation d'un etudiant
etudid = etudident_create(cnx, args, context=context, REQUEST=REQUEST)
# crée une adresse vide (chaque etudiant doit etre dans la table "adresse" !)
_ = adresse_create(
cnx,
{
"etudid": etudid,
"typeadresse": "domicile",
"description": "(creation individuelle)",
},
)
# event
scolar_events_create(
cnx,
args={
"etudid": etudid,
"event_date": time.strftime("%d/%m/%Y"),
"formsemestre_id": None,
"event_type": "CREATION",
},
)
# log
logdb(
REQUEST,
cnx,
method="etudident_edit_form",
etudid=etudid,
msg="creation initiale",
)
etud = etudident_list(cnx, {"etudid": etudid})[0]
context.fillEtudsInfo([etud])
etud["url"] = "ficheEtud?etudid=%(etudid)s" % etud
sco_news.add(
context,
REQUEST,
typ=sco_news.NEWS_INSCR,
object=None, # pas d'object pour ne montrer qu'un etudiant
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
url=etud["url"],
)
return etud
# ---------- "EVENTS"
_scolar_eventsEditor = EditableTable(
"scolar_events",
"event_id",
(
"event_id",
"etudid",
"event_date",
"formsemestre_id",
"ue_id",
"event_type",
"comp_formsemestre_id",
),
sortkey="event_date",
convert_null_outputs_to_empty=True,
output_formators={"event_date": DateISOtoDMY},
input_formators={"event_date": DateDMYtoISO},
)
# scolar_events_create = _scolar_eventsEditor.create
scolar_events_delete = _scolar_eventsEditor.delete
scolar_events_list = _scolar_eventsEditor.list
scolar_events_edit = _scolar_eventsEditor.edit
def scolar_events_create(cnx, args):
# several "events" may share the same values
_scolar_eventsEditor.create(cnx, args, has_uniq_values=False)
# --------
_etud_annotationsEditor = EditableTable(
"etud_annotations",
"id",
(
"id",
"date",
"etudid",
"author",
"comment",
"zope_authenticated_user",
"zope_remote_addr",
),
sortkey="date desc",
convert_null_outputs_to_empty=True,
output_formators={"comment": safehtml.HTML2SafeHTML, "date": DateISOtoDMY},
)
etud_annotations_create = _etud_annotationsEditor.create
etud_annotations_delete = _etud_annotationsEditor.delete
etud_annotations_list = _etud_annotationsEditor.list
etud_annotations_edit = _etud_annotationsEditor.edit
def add_annotations_to_etud_list(context, etuds):
"""Add key 'annotations' describing annotations of etuds
(used to list all annotations of a group)
"""
cnx = context.GetDBConnexion()
for etud in etuds:
l = []
for a in etud_annotations_list(cnx, args={"etudid": etud["etudid"]}):
l.append("%(comment)s (%(date)s)" % a)
etud["annotations_str"] = ", ".join(l)
# -------- APPRECIATIONS (sur bulletins) -------------------
# Les appreciations sont dans la table postgres notes_appreciations
_appreciationsEditor = EditableTable(
"notes_appreciations",
"id",
(
"id",
"date",
"etudid",
"formsemestre_id",
"author",
"comment",
"zope_authenticated_user",
"zope_remote_addr",
),
sortkey="date desc",
convert_null_outputs_to_empty=True,
output_formators={"comment": safehtml.HTML2SafeHTML, "date": DateISOtoDMY},
)
appreciations_create = _appreciationsEditor.create
appreciations_delete = _appreciationsEditor.delete
appreciations_list = _appreciationsEditor.list
appreciations_edit = _appreciationsEditor.edit
# -------- Noms des Lycées à partir du code
def read_etablissements():
filename = scu.SCO_SRCDIR + "/" + scu.CONFIG.ETABL_FILENAME
log("reading %s" % filename)
f = open(filename)
L = [x[:-1].split(";") for x in f]
E = {}
for l in L[1:]:
E[l[0]] = {
"name": l[1],
"address": l[2],
"codepostal": l[3],
"commune": l[4],
"position": l[5] + "," + l[6],
}
return E
ETABLISSEMENTS = None
def get_etablissements():
global ETABLISSEMENTS
if ETABLISSEMENTS is None:
ETABLISSEMENTS = read_etablissements()
return ETABLISSEMENTS
def get_lycee_infos(codelycee):
E = get_etablissements()
return E.get(codelycee, None)
def format_lycee_from_code(codelycee):
"Description lycee à partir du code"
E = get_etablissements()
if codelycee in E:
e = E[codelycee]
nomlycee = e["name"]
return "%s (%s)" % (nomlycee, e["commune"])
else:
return "%s (établissement inconnu)" % codelycee
def etud_add_lycee_infos(etud):
"""Si codelycee est renseigné, ajout les champs au dict"""
if etud["codelycee"]:
il = get_lycee_infos(etud["codelycee"])
if il:
if not etud["codepostallycee"]:
etud["codepostallycee"] = il["codepostal"]
if not etud["nomlycee"]:
etud["nomlycee"] = il["name"]
if not etud["villelycee"]:
etud["villelycee"] = il["commune"]
if not etud.get("positionlycee", None):
if il["position"] != "0.0,0.0":
etud["positionlycee"] = il["position"]
return etud
""" Conversion fichier original:
f = open('etablissements.csv')
o = open('etablissements2.csv', 'w')
o.write( f.readline() )
for l in f:
fs = l.split(';')
nom = ' '.join( [ strcapitalize(x) for x in fs[1].split() ] )
adr = ' '.join( [ strcapitalize(x) for x in fs[2].split() ] )
ville=' '.join( [ strcapitalize(x) for x in fs[4].split() ] )
o.write( '%s;%s;%s;%s;%s\n' % (fs[0], nom, adr, fs[3], ville))
o.close()
"""