# -*- 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: %(nom)s %(prenom)s""" % 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="""

Code étudiant (%s) dupliqué !

""" % 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.

", OK=OK, dest_url=dest_url, parameters=parameters, REQUEST=REQUEST, ) else: err_page = """

Code étudiant (%s) dupliqué !

""" % 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 %(nomprenom)s' % 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() """