diff --git a/ImportScolars.py b/ImportScolars.py
new file mode 100644
index 000000000..c819551e7
--- /dev/null
+++ b/ImportScolars.py
@@ -0,0 +1,781 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+""" Importation des etudiants à partir de fichiers CSV
+"""
+
+import os, sys, time, pdb
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+import scolars
+import sco_formsemestre
+import sco_groups
+import sco_excel
+import sco_groups_view
+import sco_news
+from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
+from sco_formsemestre_inscriptions import do_formsemestre_inscription_with_modules
+from gen_tables import GenTable
+
+# format description (relative to Product directory))
+FORMAT_FILE = "misc/format_import_etudiants.txt"
+
+# Champs modifiables via "Import données admission"
+ADMISSION_MODIFIABLE_FIELDS = (
+ "code_nip",
+ "code_ine",
+ "date_naissance",
+ "lieu_naissance",
+ "bac",
+ "specialite",
+ "annee_bac",
+ "math",
+ "physique",
+ "anglais",
+ "francais",
+ "type_admission",
+ "boursier_prec",
+ "qualite",
+ "rapporteur",
+ "score",
+ "commentaire",
+ "classement",
+ "apb_groupe",
+ "apb_classement_gr",
+ "nomlycee",
+ "villelycee",
+ "codepostallycee",
+ "codelycee",
+ # Adresse:
+ "email",
+ "emailperso",
+ "domicile",
+ "codepostaldomicile",
+ "villedomicile",
+ "paysdomicile",
+ "telephone",
+ "telephonemobile",
+ # Debouche
+ "debouche",
+ # Groupes
+ "groupes",
+)
+
+# ----
+
+
+def sco_import_format(with_codesemestre=True):
+ "returns tuples (Attribut, Type, Table, AllowNulls, Description)"
+ r = []
+ for l in open(SCO_SRCDIR + "/" + FORMAT_FILE):
+ l = l.strip()
+ if l and l[0] != "#":
+ fs = l.split(";")
+ if len(fs) < 5:
+ # Bug: invalid format file (fatal)
+ raise ScoException(
+ "file %s has invalid format (expected %d fields, got %d) (%s)"
+ % (FORMAT_FILE, 5, len(fs), l)
+ )
+ fieldname = (
+ fs[0].strip().lower().split()[0]
+ ) # titre attribut: normalize, 1er mot seulement (nom du champ en BD)
+ typ, table, allow_nulls, description = [x.strip() for x in fs[1:5]]
+ aliases = [x.strip() for x in fs[5:] if x.strip()]
+ if fieldname not in aliases:
+ aliases.insert(0, fieldname) # prepend
+ if with_codesemestre or fs[0] != "codesemestre":
+ r.append((fieldname, typ, table, allow_nulls, description, aliases))
+ return r
+
+
+def sco_import_format_dict(with_codesemestre=True):
+ """ Attribut: { 'type': , 'table', 'allow_nulls' , 'description' }
+ """
+ fmt = sco_import_format(with_codesemestre=with_codesemestre)
+ R = collections.OrderedDict()
+ for l in fmt:
+ R[l[0]] = {
+ "type": l[1],
+ "table": l[2],
+ "allow_nulls": l[3],
+ "description": l[4],
+ "aliases": l[5],
+ }
+ return R
+
+
+def sco_import_generate_excel_sample(
+ fmt,
+ with_codesemestre=True,
+ only_tables=None,
+ with_groups=True,
+ exclude_cols=[],
+ extra_cols=[],
+ group_ids=[],
+ context=None,
+ REQUEST=None,
+):
+ """Generates an excel document based on format fmt
+ (format is the result of sco_import_format())
+ If not None, only_tables can specify a list of sql table names
+ (only columns from these tables will be generated)
+ If group_ids, liste les etudiants de ces groupes
+ """
+ style = sco_excel.Excel_MakeStyle(bold=True)
+ style_required = sco_excel.Excel_MakeStyle(bold=True, color="red")
+ titles = []
+ titlesStyles = []
+ for l in fmt:
+ name = strlower(l[0])
+ if (not with_codesemestre) and name == "codesemestre":
+ continue # pas de colonne codesemestre
+ if only_tables is not None and strlower(l[2]) not in only_tables:
+ continue # table non demandée
+ if name in exclude_cols:
+ continue # colonne exclue
+ if int(l[3]):
+ titlesStyles.append(style)
+ else:
+ titlesStyles.append(style_required)
+ titles.append(name)
+ if with_groups and "groupes" not in titles:
+ titles.append("groupes")
+ titlesStyles.append(style)
+ titles += extra_cols
+ titlesStyles += [style] * len(extra_cols)
+ if group_ids and context:
+ groups_infos = sco_groups_view.DisplayedGroupsInfos(
+ context, group_ids, REQUEST=REQUEST
+ )
+ members = groups_infos.members
+ log(
+ "sco_import_generate_excel_sample: group_ids=%s %d members"
+ % (group_ids, len(members))
+ )
+ titles = ["etudid"] + titles
+ titlesStyles = [style] + titlesStyles
+ # rempli table avec données actuelles
+ lines = []
+ for i in members:
+ etud = context.getEtudInfo(etudid=i["etudid"], filled=True)[0]
+ l = []
+ for field in titles:
+ if field == "groupes":
+ sco_groups.etud_add_group_infos(
+ context, etud, groups_infos.formsemestre, sep=";"
+ )
+ l.append(etud["partitionsgroupes"])
+ else:
+ key = strlower(field).split()[0]
+ l.append(etud.get(key, ""))
+ lines.append(l)
+ else:
+ lines = [[]] # empty content, titles only
+ return sco_excel.Excel_SimpleTable(
+ titles=titles, titlesStyles=titlesStyles, SheetName="Etudiants", lines=lines
+ )
+
+
+def students_import_excel(
+ context,
+ csvfile,
+ REQUEST=None,
+ formsemestre_id=None,
+ check_homonyms=True,
+ require_ine=False,
+):
+ "import students from Excel file"
+ diag = scolars_import_excel_file(
+ csvfile,
+ context.Notes,
+ REQUEST,
+ formsemestre_id=formsemestre_id,
+ check_homonyms=check_homonyms,
+ require_ine=require_ine,
+ exclude_cols=["photo_filename"],
+ )
+ if REQUEST:
+ if formsemestre_id:
+ dest = "formsemestre_status?formsemestre_id=%s" % formsemestre_id
+ else:
+ dest = REQUEST.URL1
+ H = [context.sco_header(REQUEST, page_title="Import etudiants")]
+ H.append("
' % dest)
+ return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def scolars_import_excel_file(
+ datafile,
+ context,
+ REQUEST,
+ formsemestre_id=None,
+ check_homonyms=True,
+ require_ine=False,
+ exclude_cols=[],
+):
+ """Importe etudiants depuis fichier Excel
+ et les inscrit dans le semestre indiqué (et à TOUS ses modules)
+ """
+ log("scolars_import_excel_file: formsemestre_id=%s" % formsemestre_id)
+ cnx = context.GetDBConnexion(autocommit=False)
+ cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+ annee_courante = time.localtime()[0]
+ always_require_ine = context.get_preference("always_require_ine")
+ exceldata = datafile.read()
+ if not exceldata:
+ raise ScoValueError("Ficher excel vide ou invalide")
+ diag, data = sco_excel.Excel_to_list(exceldata)
+ if not data: # probably a bug
+ raise ScoException("scolars_import_excel_file: empty file !")
+
+ formsemestre_to_invalidate = Set()
+
+ # 1- --- check title line
+ titles = {}
+ fmt = sco_import_format()
+ for l in fmt:
+ tit = strlower(l[0]).split()[0] # titles in lowercase, and take 1st word
+ if (
+ (not formsemestre_id) or (tit != "codesemestre")
+ ) and tit not in exclude_cols:
+ titles[tit] = l[1:] # title : (Type, Table, AllowNulls, Description)
+
+ # log("titles=%s" % titles)
+ # remove quotes, downcase and keep only 1st word
+ try:
+ fs = [strlower(stripquotes(s)).split()[0] for s in data[0]]
+ except:
+ raise ScoValueError("Titres de colonnes invalides (ou vides ?)")
+ # log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
+
+ # check columns titles
+ if len(fs) != len(titles):
+ missing = {}.fromkeys(titles.keys())
+ unknown = []
+ for f in fs:
+ if missing.has_key(f):
+ del missing[f]
+ else:
+ unknown.append(f)
+ raise ScoValueError(
+ "Nombre de colonnes incorrect (devrait être %d, et non %d) (colonnes manquantes: %s, colonnes invalides: %s)"
+ % (len(titles), len(fs), missing.keys(), unknown)
+ )
+ titleslist = []
+ for t in fs:
+ if not titles.has_key(t):
+ raise ScoValueError('Colonne invalide: "%s"' % t)
+ titleslist.append(t) #
+ # ok, same titles
+ # Start inserting data, abort whole transaction in case of error
+ created_etudids = []
+ NbImportedHomonyms = 0
+ GroupIdInferers = {}
+ try: # --- begin DB transaction
+ linenum = 0
+ for line in data[1:]:
+ linenum += 1
+ # Read fields, check and convert type
+ values = {}
+ fs = line
+ # remove quotes
+ for i in range(len(fs)):
+ if fs[i] and (
+ (fs[i][0] == '"' and fs[i][-1] == '"')
+ or (fs[i][0] == "'" and fs[i][-1] == "'")
+ ):
+ fs[i] = fs[i][1:-1]
+ for i in range(len(fs)):
+ val = fs[i].strip()
+ typ, table, an, descr, aliases = tuple(titles[titleslist[i]])
+ # log('field %s: %s %s %s %s'%(titleslist[i], table, typ, an, descr))
+ if not val and not an:
+ raise ScoValueError(
+ "line %d: null value not allowed in column %s"
+ % (linenum, titleslist[i])
+ )
+ if val == "":
+ val = None
+ else:
+ if typ == "real":
+ val = val.replace(",", ".") # si virgule a la française
+ try:
+ val = float(val)
+ except:
+ raise ScoValueError(
+ "valeur nombre reel invalide (%s) sur line %d, colonne %s"
+ % (val, linenum, titleslist[i])
+ )
+ elif typ == "integer":
+ try:
+ # on doit accepter des valeurs comme "2006.0"
+ val = val.replace(",", ".") # si virgule a la française
+ val = float(val)
+ if val % 1.0 > 1e-4:
+ raise ValueError()
+ val = int(val)
+ except:
+ raise ScoValueError(
+ "valeur nombre entier invalide (%s) sur ligne %d, colonne %s"
+ % (val, linenum, titleslist[i])
+ )
+ # xxx Ad-hoc checks (should be in format description)
+ if strlower(titleslist[i]) == "sexe":
+ try:
+ val = scolars.normalize_sexe(val)
+ except:
+ raise ScoValueError(
+ "valeur invalide pour 'SEXE' (doit etre 'M' ou 'MME' ou 'H' ou 'F', pas '%s') ligne %d, colonne %s"
+ % (val, linenum, titleslist[i])
+ )
+ # Excel date conversion:
+ if strlower(titleslist[i]) == "date_naissance":
+ if val:
+ if re.match("^[0-9]*\.?[0-9]*$", str(val)):
+ val = sco_excel.xldate_as_datetime(float(val))
+ # INE
+ if (
+ strlower(titleslist[i]) == "code_ine"
+ and always_require_ine
+ and not val
+ ):
+ raise ScoValueError(
+ "Code INE manquant sur ligne %d, colonne %s"
+ % (linenum, titleslist[i])
+ )
+
+ # --
+ values[titleslist[i]] = val
+ skip = False
+ is_new_ine = values["code_ine"] and _is_new_ine(cnx, values["code_ine"])
+ if require_ine and not is_new_ine:
+ log("skipping %s (code_ine=%s)" % (values["nom"], values["code_ine"]))
+ skip = True
+
+ if not skip:
+ if values["code_ine"] and not is_new_ine:
+ raise ScoValueError("Code INE dupliqué (%s)" % values["code_ine"])
+ # Check nom/prenom
+ ok, NbHomonyms = scolars.check_nom_prenom(
+ cnx, nom=values["nom"], prenom=values["prenom"]
+ )
+ if not ok:
+ raise ScoValueError(
+ "nom ou prénom invalide sur la ligne %d" % (linenum)
+ )
+ if NbHomonyms:
+ NbImportedHomonyms += 1
+ # Insert in DB tables
+ formsemestre_to_invalidate.add(
+ _import_one_student(
+ context,
+ cnx,
+ REQUEST,
+ formsemestre_id,
+ values,
+ GroupIdInferers,
+ annee_courante,
+ created_etudids,
+ linenum,
+ )
+ )
+
+ # Verification proportion d'homonymes: si > 10%, abandonne
+ log("scolars_import_excel_file: detected %d homonyms" % NbImportedHomonyms)
+ if check_homonyms and NbImportedHomonyms > len(created_etudids) / 10:
+ log("scolars_import_excel_file: too many homonyms")
+ raise ScoValueError(
+ "Il y a trop d'homonymes (%d étudiants)" % NbImportedHomonyms
+ )
+ except:
+ cnx.rollback()
+ log("scolars_import_excel_file: aborting transaction !")
+ # Nota: db transaction is sometimes partly commited...
+ # here we try to remove all created students
+ cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+ for etudid in created_etudids:
+ log("scolars_import_excel_file: deleting etudid=%s" % 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 admissions where etudid=%(etudid)s", {"etudid": etudid}
+ )
+ cursor.execute(
+ "delete from group_membership where etudid=%(etudid)s",
+ {"etudid": etudid},
+ )
+ cursor.execute(
+ "delete from identite where etudid=%(etudid)s", {"etudid": etudid}
+ )
+ cnx.commit()
+ log("scolars_import_excel_file: re-raising exception")
+ raise
+
+ diag.append("Import et inscription de %s étudiants" % len(created_etudids))
+
+ sco_news.add(
+ context,
+ REQUEST,
+ typ=NEWS_INSCR,
+ text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
+ % len(created_etudids),
+ object=formsemestre_id,
+ )
+
+ log("scolars_import_excel_file: completing transaction")
+ cnx.commit()
+
+ # Invalide les caches des semestres dans lesquels on a inscrit des etudiants:
+ context.Notes._inval_cache(formsemestre_id_list=formsemestre_to_invalidate)
+
+ return diag
+
+
+def _import_one_student(
+ context,
+ cnx,
+ REQUEST,
+ formsemestre_id,
+ values,
+ GroupIdInferers,
+ annee_courante,
+ created_etudids,
+ linenum,
+):
+ """
+ Import d'un étudiant et inscription dans le semestre.
+ Return: id du semestre dans lequel il a été inscrit.
+ """
+ log(
+ "scolars_import_excel_file: formsemestre_id=%s values=%s"
+ % (formsemestre_id, str(values))
+ )
+ # Identite
+ args = values.copy()
+ etudid = scolars.identite_create(cnx, args, context=context, REQUEST=REQUEST)
+ created_etudids.append(etudid)
+ # Admissions
+ args["etudid"] = etudid
+ args["annee"] = annee_courante
+ adm_id = scolars.admission_create(cnx, args)
+ # Adresse
+ args["typeadresse"] = "domicile"
+ args["description"] = "(infos admission)"
+ adresse_id = scolars.adresse_create(cnx, args)
+ # Inscription au semestre
+ args["etat"] = "I" # etat insc. semestre
+ if formsemestre_id:
+ args["formsemestre_id"] = formsemestre_id
+ else:
+ args["formsemestre_id"] = values["codesemestre"]
+ formsemestre_id = values["codesemestre"]
+ # recupere liste des groupes:
+ if formsemestre_id not in GroupIdInferers:
+ GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(
+ context, formsemestre_id
+ )
+ gi = GroupIdInferers[formsemestre_id]
+ if args["groupes"]:
+ groupes = args["groupes"].split(";")
+ else:
+ groupes = []
+ group_ids = [gi[group_name] for group_name in groupes]
+ group_ids = {}.fromkeys(group_ids).keys() # uniq
+ if None in group_ids:
+ raise ScoValueError(
+ "groupe invalide sur la ligne %d (groupe %s)" % (linenum, groupes)
+ )
+
+ do_formsemestre_inscription_with_modules(
+ context,
+ args["formsemestre_id"],
+ etudid,
+ group_ids,
+ etat="I",
+ REQUEST=REQUEST,
+ method="import_csv_file",
+ )
+ return args["formsemestre_id"]
+
+
+def _is_new_ine(cnx, code_ine):
+ "True if this code is not in DB"
+ etuds = scolars.identite_list(cnx, {"code_ine": code_ine})
+ return not etuds
+
+
+# ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB)
+def scolars_import_admission(
+ datafile, context, REQUEST, formsemestre_id=None, type_admission=None
+):
+ """Importe données admission depuis un fichier Excel quelconque
+ par exemple ceux utilisés avec APB
+
+ Cherche dans ce fichier les étudiants qui correspondent à des inscrits du
+ semestre formsemestre_id.
+ Le fichier n'a pas l'INE ni le NIP ni l'etudid, la correspondance se fait
+ via les noms/prénoms qui doivent être égaux (la casse, les accents et caractères spéciaux
+ étant ignorés).
+
+ On tolère plusieurs variantes pour chaque nom de colonne (ici aussi, la casse, les espaces
+ et les caractères spéciaux sont ignorés. Ainsi, la colonne "Prénom:" sera considéré comme "prenom".
+
+ Le parametre type_admission remplace les valeurs vides (dans la base ET dans le fichier importé) du champ type_admission.
+ Si une valeur existe ou est présente dans le fichier importé, ce paramètre est ignoré.
+
+ TODO:
+ - choix onglet du classeur
+ """
+
+ log("scolars_import_admission: formsemestre_id=%s" % formsemestre_id)
+ members = sco_groups.get_group_members(
+ context, sco_groups.get_default_group(context, formsemestre_id)
+ )
+ etuds_by_nomprenom = {} # { nomprenom : etud }
+ diag = []
+ for m in members:
+ np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"]))
+ if np in etuds_by_nomprenom:
+ msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"])
+ log(msg)
+ diag.append(msg)
+ etuds_by_nomprenom[np] = m
+
+ exceldata = datafile.read()
+ diag2, data = sco_excel.Excel_to_list(exceldata, convert_to_string=False)
+ if not data:
+ raise ScoException("scolars_import_admission: empty file !")
+ diag += diag2
+ cnx = context.GetDBConnexion()
+
+ titles = data[0]
+ # idx -> ('field', convertor)
+ fields = adm_get_fields(titles, formsemestre_id)
+ idx_nom = None
+ idx_prenom = None
+ for idx in fields:
+ if fields[idx][0] == "nom":
+ idx_nom = idx
+ if fields[idx][0] == "prenom":
+ idx_prenom = idx
+ if (idx_nom is None) or (idx_prenom is None):
+ log("fields indices=" + ", ".join([str(x) for x in fields]))
+ log("fields titles =" + ", ".join([fields[x][0] for x in fields]))
+ raise FormatError(
+ "scolars_import_admission: colonnes nom et prenom requises",
+ dest_url="form_students_import_infos_admissions?formsemestre_id=%s"
+ % formsemestre_id,
+ )
+
+ modifiable_fields = Set(ADMISSION_MODIFIABLE_FIELDS)
+
+ nline = 2 # la premiere ligne de donnees du fichier excel est 2
+ n_import = 0
+ for line in data[1:]:
+ # Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
+ nom = adm_normalize_string(line[idx_nom])
+ prenom = adm_normalize_string(line[idx_prenom])
+ if not (nom, prenom) in etuds_by_nomprenom:
+ log(
+ "unable to find %s %s among members" % (line[idx_nom], line[idx_prenom])
+ )
+ else:
+ etud = etuds_by_nomprenom[(nom, prenom)]
+ cur_adm = scolars.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
+ # peuple les champs presents dans le tableau
+ args = {}
+ for idx in fields:
+ field_name, convertor = fields[idx]
+ if field_name in modifiable_fields:
+ try:
+ val = convertor(line[idx])
+ except ValueError:
+ raise FormatError(
+ 'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
+ % (nline, field_name, line[idx]),
+ dest_url="form_students_import_infos_admissions?formsemestre_id=%s"
+ % formsemestre_id,
+ )
+ if val is not None: # note: ne peut jamais supprimer une valeur
+ args[field_name] = val
+ if args:
+ args["etudid"] = etud["etudid"]
+ args["adm_id"] = cur_adm["adm_id"]
+ # Type admission: traitement particulier
+ if not cur_adm["type_admission"] and not args.get("type_admission"):
+ args["type_admission"] = type_admission
+ scolars.etudident_edit(cnx, args)
+ adr = scolars.adresse_list(cnx, args={"etudid": etud["etudid"]})
+ if adr:
+ args["adresse_id"] = adr[0]["adresse_id"]
+ scolars.adresse_edit(
+ cnx, args
+ ) # ne passe pas le contexte: pas de notification ici
+ else:
+ args["typeadresse"] = "domicile"
+ args["description"] = "(infos admission)"
+ adresse_id = scolars.adresse_create(cnx, args)
+ # log('import_adm: %s' % args )
+ # Change les groupes si nécessaire:
+ if args["groupes"]:
+ gi = sco_groups.GroupIdInferer(context, formsemestre_id)
+ groupes = args["groupes"].split(";")
+ group_ids = [gi[group_name] for group_name in groupes]
+ group_ids = {}.fromkeys(group_ids).keys() # uniq
+ if None in group_ids:
+ raise ScoValueError(
+ "groupe invalide sur la ligne %d (groupe %s)"
+ % (nline, groupes)
+ )
+
+ for group_id in group_ids:
+ sco_groups.change_etud_group_in_partition(
+ context, args["etudid"], group_id, REQUEST=REQUEST
+ )
+ #
+ diag.append("import de %s" % (etud["nomprenom"]))
+ n_import += 1
+ nline += 1
+ diag.append("%d lignes importées" % n_import)
+ if n_import > 0:
+ context._inval_cache(formsemestre_id=formsemestre_id)
+ return diag
+
+
+_ADM_PATTERN = re.compile(r"[\W]+", re.UNICODE) # supprime tout sauf alphanum
+
+
+def adm_normalize_string(s): # normalize unicode title
+ return suppression_diacritics(_ADM_PATTERN.sub("", s.strip().lower())).replace(
+ "_", ""
+ )
+
+
+def adm_get_fields(titles, formsemestre_id):
+ """Cherche les colonnes importables dans les titres (ligne 1) du fichier excel
+ return: { idx : (field_name, convertor) }
+ """
+ # log('adm_get_fields: titles=%s' % titles)
+ Fmt = sco_import_format_dict()
+ fields = {}
+ idx = 0
+ for title in titles:
+ title_n = adm_normalize_string(title)
+ for k in Fmt:
+ for v in Fmt[k]["aliases"]:
+ if adm_normalize_string(v) == title_n:
+ typ = Fmt[k]["type"]
+ if typ == "real":
+ convertor = adm_convert_real
+ elif typ == "integer" or typ == "int":
+ convertor = adm_convert_int
+ else:
+ convertor = adm_convert_text
+ # doublons ?
+ if k in [x[0] for x in fields.values()]:
+ raise FormatError(
+ 'scolars_import_admission: titre "%s" en double (ligne 1)'
+ % (title),
+ dest_url="form_students_import_infos_admissions_apb?formsemestre_id=%s"
+ % formsemestre_id,
+ )
+ fields[idx] = (k, convertor)
+ idx += 1
+
+ return fields
+
+
+def adm_convert_text(v):
+ if type(v) == FloatType:
+ return "{:g}".format(v) # evite "1.0"
+ return v
+
+
+def adm_convert_int(v):
+ if type(v) != IntType and not v:
+ return None
+ return int(float(v)) # accept "10.0"
+
+
+def adm_convert_real(v):
+ if type(v) != FloatType and not v:
+ return None
+ return float(v)
+
+
+def adm_table_description_format(context):
+ """Table HTML (ou autre format) decrivant les donnees d'admissions importables
+ """
+ Fmt = sco_import_format_dict(with_codesemestre=False)
+ for k in Fmt:
+ Fmt[k]["attribute"] = k
+ Fmt[k]["aliases_str"] = ", ".join(Fmt[k]["aliases"])
+ if not Fmt[k]["allow_nulls"]:
+ Fmt[k]["required"] = "*"
+ if k in ADMISSION_MODIFIABLE_FIELDS:
+ Fmt[k]["writable"] = "oui"
+ else:
+ Fmt[k]["writable"] = "non"
+ titles = {
+ "attribute": "Attribut",
+ "type": "Type",
+ "required": "Requis",
+ "writable": "Modifiable",
+ "description": "Description",
+ "aliases_str": "Titres (variantes)",
+ }
+ columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
+
+ tab = GenTable(
+ titles=titles,
+ columns_ids=columns_ids,
+ rows=Fmt.values(),
+ html_sortable=True,
+ html_class="table_leftalign",
+ preferences=context.get_preferences(),
+ )
+ return tab
diff --git a/SuppressAccents.py b/SuppressAccents.py
new file mode 100644
index 000000000..f0b4d34fe
--- /dev/null
+++ b/SuppressAccents.py
@@ -0,0 +1,206 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Suppression des accents d'une chaine
+
+Source: http://wikipython.flibuste.net/moin.py/JouerAvecUnicode#head-1213938516c633958921591439c33d202244e2f4
+"""
+
+_reptable = {}
+
+
+def _fill_reptable():
+ _corresp = [
+ (
+ u"A",
+ [0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x0100, 0x0102, 0x0104],
+ ),
+ (u"AE", [0x00C6]),
+ (
+ u"a",
+ [0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x0101, 0x0103, 0x0105],
+ ),
+ (u"ae", [0x00E6]),
+ (u"C", [0x00C7, 0x0106, 0x0108, 0x010A, 0x010C]),
+ (u"c", [0x00E7, 0x0107, 0x0109, 0x010B, 0x010D]),
+ (u"D", [0x00D0, 0x010E, 0x0110]),
+ (u"d", [0x00F0, 0x010F, 0x0111]),
+ (
+ u"E",
+ [0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0112, 0x0114, 0x0116, 0x0118, 0x011A],
+ ),
+ (
+ u"e",
+ [
+ 0x00E8,
+ 0xE9,
+ 0x00E9,
+ 0x00EA,
+ 0xEB,
+ 0x00EB,
+ 0x0113,
+ 0x0115,
+ 0x0117,
+ 0x0119,
+ 0x011B,
+ ],
+ ),
+ (u"G", [0x011C, 0x011E, 0x0120, 0x0122]),
+ (u"g", [0x011D, 0x011F, 0x0121, 0x0123]),
+ (u"H", [0x0124, 0x0126]),
+ (u"h", [0x0125, 0x0127]),
+ (
+ u"I",
+ [0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x0128, 0x012A, 0x012C, 0x012E, 0x0130],
+ ),
+ (
+ u"i",
+ [0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x0129, 0x012B, 0x012D, 0x012F, 0x0131],
+ ),
+ (u"IJ", [0x0132]),
+ (u"ij", [0x0133]),
+ (u"J", [0x0134]),
+ (u"j", [0x0135]),
+ (u"K", [0x0136]),
+ (u"k", [0x0137, 0x0138]),
+ (u"L", [0x0139, 0x013B, 0x013D, 0x013F, 0x0141]),
+ (u"l", [0x013A, 0x013C, 0x013E, 0x0140, 0x0142]),
+ (u"N", [0x00D1, 0x0143, 0x0145, 0x0147, 0x014A]),
+ (u"n", [0x00F1, 0x0144, 0x0146, 0x0148, 0x0149, 0x014B]),
+ (
+ u"O",
+ [0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D8, 0x014C, 0x014E, 0x0150],
+ ),
+ (
+ u"o",
+ [0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F8, 0x014D, 0x014F, 0x0151],
+ ),
+ (u"OE", [0x0152]),
+ (u"oe", [0x0153]),
+ (u"R", [0x0154, 0x0156, 0x0158]),
+ (u"r", [0x0155, 0x0157, 0x0159]),
+ (u"S", [0x015A, 0x015C, 0x015E, 0x0160]),
+ (u"s", [0x015B, 0x015D, 0x015F, 0x01610, 0x017F, 0x0218]),
+ (u"T", [0x0162, 0x0164, 0x0166]),
+ (u"t", [0x0163, 0x0165, 0x0167]),
+ (
+ u"U",
+ [
+ 0x00D9,
+ 0x00DA,
+ 0x00DB,
+ 0x00DC,
+ 0x0168,
+ 0x016A,
+ 0x016C,
+ 0x016E,
+ 0x0170,
+ 0x172,
+ ],
+ ),
+ (
+ u"u",
+ [
+ 0x00F9,
+ 0x00FA,
+ 0x00FB,
+ 0x00FC,
+ 0x0169,
+ 0x016B,
+ 0x016D,
+ 0x016F,
+ 0x0171,
+ 0xB5,
+ ],
+ ),
+ (u"W", [0x0174]),
+ (u"w", [0x0175]),
+ (u"Y", [0x00DD, 0x0176, 0x0178]),
+ (u"y", [0x00FD, 0x00FF, 0x0177]),
+ (u"Z", [0x0179, 0x017B, 0x017D]),
+ (u"z", [0x017A, 0x017C, 0x017E]),
+ (
+ u"",
+ [
+ 0x80,
+ 0x81,
+ 0x82,
+ 0x83,
+ 0x84,
+ 0x85,
+ 0x86,
+ 0x87,
+ 0x88,
+ 0x89,
+ 0x8A,
+ 0x8B,
+ 0x8C,
+ 0x8D,
+ 0x8E,
+ 0x8F,
+ 0x90,
+ 0x91,
+ 0x92,
+ 0x93,
+ 0x94,
+ 0x95,
+ 0x96,
+ 0x97,
+ 0x98,
+ 0x99,
+ 0x9A,
+ 0x9B,
+ 0x9C,
+ 0x9D,
+ 0x9E,
+ 0x9F,
+ ],
+ ), # misc controls
+ (u" ", [0x00A0]), #  
+ (u"!", [0xA1]), # ¡
+ (u"c", [0xA2]), # cent
+ (u"L", [0xA3]), # pound
+ (u"o", [0xA4]), # currency symbol
+ (u"Y", [0xA5]), # yen
+ (u"|", [0xA6]), # Broken Bar ¦
+ (u"S", [0xA7]), # section
+ (u"", [0xA8]), # diaeresis ¨
+ (u"", [0xA9]), # copyright
+ (u'"', [0xAB, 0xBA]), # «, » <<, >>
+ (u" ", [0xAC]), # Math Not Sign
+ (u"", [0xAD]), # DashPunctuation
+ (u"(r)", [0xAE]), # registred
+ (u"-", [0xAF]), # macron
+ (u"", [0xB0]), # degre
+ (u"+-", [0xB1]), # +-
+ (u"2", [0x00B2, 0xB2]), # deux exposant
+ (u"3", [0xB3]), # 3 exposant
+ (u".", [0xB7]), # ·,
+ (u"1/4", [0xBC]), # 1/4
+ (u"1/2", [0xBD]), # 1/2
+ (u"3/4", [0xBE]), # 3/4
+ (u"e", [0x20AC]), # euro
+ (u"--", [0x2013]), # EN DASH
+ (u"'", [0x2018, 0x2019, 0x201A]), # LEFT, RIGHT SINGLE QUOTATION MARK
+ (u" ", [0x2020]), # dagger
+ ]
+ global _reptable
+ for repchar, codes in _corresp:
+ for code in codes:
+ _reptable[code] = repchar
+
+
+_fill_reptable()
+
+
+def suppression_diacritics(s):
+ """Suppression des accents et autres marques.
+
+ @param s: le texte à nettoyer.
+ @type s: str ou unicode
+ @return: le texte nettoyé de ses marques diacritiques.
+ @rtype: unicode
+ """
+ if isinstance(s, str):
+ s = unicode(s, "utf8", "replace")
+ return s.translate(_reptable)
diff --git a/TODO b/TODO
new file mode 100644
index 000000000..4041532f0
--- /dev/null
+++ b/TODO
@@ -0,0 +1,238 @@
+
+ NOTES EN VRAC / Brouillon / Trucs obsoletes
+
+
+#do_moduleimpl_list\(\{"([a-z_]*)"\s*:\s*(.*)\}\)
+#do_moduleimpl_list( $1 = $2 )
+
+#do_moduleimpl_list\([\s\n]*args[\s\n]*=[\s\n]*\{"([a-z_]*)"[\s\n]*:[\s\n]*(.*)[\s\n]*\}[\s\n]*\)
+
+Upgrade JavaScript
+ - jquery-ui-1.12.1 introduit un problème d'affichage de la barre de menu.
+ Il faudrait la revoir entièrement pour upgrader.
+ On reste donc à jquery-ui-1.10.4.custom
+ Or cette version est incompatible avec jQuery 3 (messages d'erreur dans la console)
+ On reste donc avec jQuery 1.12.14
+
+
+Suivi des requêtes utilisateurs:
+ table sql: id, ip, authuser, request
+
+
+* Optim:
+porcodeb4, avant memorisation des moy_ue:
+S1 SEM14133 cold start: min 9s, max 12s, avg > 11s
+ inval (add note): 1.33s (pas de recalcul des autres)
+ inval (add abs) : min8s, max 12s (recalcule tout :-()
+LP SEM14946 cold start: 0.7s - 0.86s
+
+
+
+----------------- LISTE OBSOLETE (très ancienne, à trier) -----------------------
+BUGS
+----
+
+ - formsemestre_inscription_with_modules
+ si inscription 'un etud deja inscrit, IntegrityError
+
+FEATURES REQUESTS
+-----------------
+
+* Bulletins:
+ . logos IUT et Univ sur bull PDF
+ . nom departement: nom abbrégé (CJ) ou complet (Carrière Juridiques)
+ . bulletin: deplacer la barre indicateur (cf OLDGEA S2: gêne)
+ . bulletin: click nom titre -> ficheEtud
+
+ . formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5"
+ et valider correctement le form !
+
+* Jury
+ . recapcomplet: revenir avec qq lignes au dessus de l'étudiant en cours
+
+
+* Divers
+ . formsemestre_editwithmodules: confirmer suppression modules
+ (et pour l'instant impossible si evaluations dans le module)
+
+* Modules et UE optionnelles:
+ . UE capitalisées: donc dispense possible dans semestre redoublé.
+ traitable en n'inscrivant pas l'etudiant au modules
+ de cette UE: faire interface utilisateur
+
+ . page pour inscription d'un etudiant a un module
+ . page pour visualiser les modules auquel un etudiant est inscrit,
+ et le desinscrire si besoin.
+
+ . ficheEtud indiquer si inscrit au module sport
+
+* Absences
+ . EtatAbsences : verifier dates (en JS)
+ . Listes absences pdf et listes groupes pdf + emargements (cf mail Nathalie)
+ . absences par demi-journées sur EtatAbsencesDate (? à vérifier)
+ . formChoixSemestreGroupe: utilisé par Absences/index_html
+ a améliorer
+
+
+* Notes et évaluations:
+ . Exception "Not an OLE file": generer page erreur plus explicite
+ . Dates evaluation: utiliser JS pour calendrier
+ . Saisie des notes: si une note invalide, l'indiquer dans le listing (JS ?)
+ . et/ou: notes invalides: afficher les noms des etudiants concernes
+ dans le message d'erreur.
+ . upload excel: message erreur peu explicite:
+ * Feuille "Saisie notes", 17 lignes
+ * Erreur: la feuille contient 1 notes invalides
+ * Notes invalides pour les id: ['10500494']
+ (pas de notes modifiées)
+ Notes chargées. <<< CONTRADICTOIRE !!
+
+ . recap complet semestre:
+ Options:
+ - choix groupes
+ - critère de tri (moy ou alphab)
+ - nb de chiffres a afficher
+
+ + definir des "catégories" d'évaluations (eg "théorie","pratique")
+ afin de n'afficher que des moyennes "de catégorie" dans
+ le bulletin.
+
+ . liste des absents à une eval et croisement avec BD absences
+
+ . notes_evaluation_listenotes
+ - afficher groupes, moyenne, #inscrits, #absents, #manquantes dans l'en-tete.
+ - lien vers modif notes (selon role)
+
+ . Export excel des notes d'evaluation: indiquer date, et autres infos en haut.
+ . Génération PDF listes notes
+ . Page recap notes moyennes par groupes (choisir type de groupe?)
+
+ . (GEA) edition tableau notes avec tous les evals d'un module
+ (comme notes_evaluation_listenotes mais avec tt les evals)
+
+
+* Non prioritaire:
+ . optimiser scolar_news_summary
+ . recapitulatif des "nouvelles"
+ - dernieres notes
+ - changement de statuts (demissions,inscriptions)
+ - annotations
+ - entreprises
+
+ . notes_table: pouvoir changer decision sans invalider tout le cache
+ . navigation: utiliser Session pour montrer historique pages vues ?
+
+
+
+------------------------------------------------------------------------
+
+
+A faire:
+ - fiche etud: code dec jury sur ligne 1
+ si ancien, indiquer autorisation inscription sous le parcours
+
+ - saisie notes: undo
+ - saisie notes: validation
+- ticket #18:
+UE capitalisées: donc dispense possible dans semestre redoublé. Traitable en n'inscrivant pas l'etudiant aux modules de cette UE: faire interface utilisateur.
+
+Prévoir d'entrer une UE capitalisée avec sa note, date d'obtention et un commentaire. Coupler avec la désincription aux modules (si l'étudiant a été inscrit avec ses condisciples).
+
+
+ - Ticket #4: Afin d'éviter les doublons, vérifier qu'il n'existe pas d'homonyme proche lors de la création manuelle d'un étudiant. (confirmé en ScoDoc 6, vérifier aussi les imports Excel)
+
+ - Ticket #74: Il est possible d'inscrire un étudiant sans prénom par un import excel !!!
+
+ - Ticket #64: saisir les absences pour la promo entiere (et pas par groupe). Des fois, je fais signer une feuille de presence en amphi a partir de la liste de tous les etudiants. Ensuite pour reporter les absents par groupe, c'est galere.
+
+ - Ticket #62: Lors des exports Excel, le format des cellules n'est pas reconnu comme numérique sous Windows (pas de problèmes avec Macintosh et Linux).
+
+A confirmer et corriger.
+
+ - Ticket #75: On peut modifier une décision de jury (et les autorisations de passage associées), mais pas la supprimer purement et simplement.
+Ajoute ce choix dans les "décisions manuelles".
+
+ - Ticket #37: Page recap notes moyennes par groupes
+Construire une page avec les moyennes dans chaque UE ou module par groupe d'étudiants.
+Et aussi pourquoi pas ventiler par type de bac, sexe, parcours (nombre de semestre de parcours) ?
+redemandé par CJ: à faire avant mai 2008 !
+
+ - Ticket #75: Synchro Apogée: choisir les etudiants
+Sur la page de syncho Apogée (formsemestre_synchro_etuds), on peut choisir (cocher) les étudiants Apogée à importer. mais on ne peut pas le faire s'ils sont déjà dans ScoDoc: il faudrait ajouter des checkboxes dans toutes les listes.
+
+ - Ticket #9: Format des valeurs de marges des bulletins.
+formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5" et valider correctement le form !
+
+ - Ticket #17: Suppression modules dans semestres
+formsemestre_editwithmodules: confirmer suppression modules
+
+ - Ticket #29: changer le stoquage des photos, garder une version HD.
+
+ - bencher NotesTable sans calcul de moyennes. Etudier un cache des moyennes de modules.
+ - listes d'utilisateurs (modules): remplacer menus par champs texte + completions javascript
+ - documenter archives sur Wiki
+ - verifier paquet Debian pour font pdf (reportab: helvetica ... plante si font indisponible)
+ - chercher comment obtenir une page d'erreur correcte pour les pages POST
+ (eg: si le font n'existe pas, archive semestre echoue sans page d'erreur)
+ ? je ne crois pas que le POST soit en cause. HTTP status=500
+ ne se produit pas avec Safari
+ - essayer avec IE / Win98
+ - faire apparaitre les diplômés sur le graphe des parcours
+ - démission: formulaire: vérifier que la date est bien dans le semestre
+
+ + graphe parcours: aligner en colonnes selon les dates (de fin), placer les diplomes
+ dans la même colone que le semestre terminal.
+
+ - modif gestion utilisateurs (donner droits en fct du dept. d'appartenance, bug #57)
+ - modif form def. utilisateur (dept appartenance)
+ - utilisateurs: source externe
+ - archivage des semestres
+
+
+ o-------------------------------------o
+
+* Nouvelle gestion utilisateurs:
+ objectif: dissocier l'authentification de la notion "d'enseignant"
+ On a une source externe "d'utilisateurs" (annuaire LDAP ou base SQL)
+ qui permet seulement de:
+ - authentifier un utilisateur (login, passwd)
+ - lister un utilisateur: login => firstname, lastname, email
+ - lister les utilisateurs
+
+ et une base interne ScoDoc "d'acteurs" (enseignants, administratifs).
+ Chaque acteur est défini par:
+ - actor_id, firstname, lastname
+ date_creation, date_expiration,
+ roles, departement,
+ email (+flag indiquant s'il faut utiliser ce mail ou celui de
+ l'utilisateur ?)
+ state (on, off) (pour desactiver avant expiration ?)
+ user_id (login) => lien avec base utilisateur
+
+ On offrira une source d'utilisateurs SQL (base partagée par tous les dept.
+ d'une instance ScoDoc), mais dans la plupart des cas les gens utiliseront
+ un annuaire LDAP.
+
+ La base d'acteurs remplace ScoUsers. Les objets ScoDoc (semestres,
+ modules etc) font référence à des acteurs (eg responsable_id est un actor_id).
+
+ Le lien entre les deux ?
+ Loger un utilisateur => authentification utilisateur + association d'un acteur
+ Cela doit se faire au niveau d'un UserFolder Zope, pour avoir les
+ bons rôles et le contrôle d'accès adéquat.
+ (Il faut donc coder notre propre UserFolder).
+ On ne peut associer qu'un acteur à l'état 'on' et non expiré.
+
+ Opérations ScoDoc:
+ - paramétrage: choisir et paramétrer source utilisateurs
+ - ajouter utilisateur: choisir un utilisateur dans la liste
+ et lui associer un nouvel acteur (choix des rôles, des dates)
+ + éventuellement: synchro d'un ensemble d'utilisateurs, basé sur
+ une requête (eg LDAP) précise (quelle interface utilisateur proposer ?)
+
+ - régulièrement (cron) aviser quelqu'un (le chef) de l'expiration des acteurs.
+ - changer etat d'un acteur (on/off)
+
+
+ o-------------------------------------o
+
diff --git a/TrivialFormulator.py b/TrivialFormulator.py
new file mode 100644
index 000000000..788d95da1
--- /dev/null
+++ b/TrivialFormulator.py
@@ -0,0 +1,769 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+
+"""Simple form generator/validator
+
+ E. Viennet 2005 - 2008
+
+ v 1.2
+"""
+
+from types import *
+
+
+def TrivialFormulator(
+ form_url,
+ values,
+ formdescription=(),
+ initvalues={},
+ method="post",
+ enctype=None,
+ submitlabel="OK",
+ name=None,
+ formid="tf",
+ cssclass="",
+ cancelbutton=None,
+ submitbutton=True,
+ submitbuttonattributes=[],
+ top_buttons=False, # place buttons at top of form
+ bottom_buttons=True, # buttons after form
+ html_foot_markup="",
+ readonly=False,
+ is_submitted=False,
+):
+ """
+ form_url : URL for this form
+ initvalues : dict giving default values
+ values : dict with all HTML form variables (may start empty)
+ is_submitted: handle form as if already submitted
+
+ Returns (status, HTML form, values)
+ status = 0 (html to display),
+ 1 (ok, validated values in "values")
+ -1 cancel (if cancelbutton specified)
+ HTML form: html string (form to insert in your web page)
+ values: None or, when the form is submitted and correctly filled,
+ a dictionnary with the requeted values.
+ formdescription: sequence [ (field, description), ... ]
+ where description is a dict with following (optional) keys:
+ default : default value for this field ('')
+ title : text titre (default to field name)
+ allow_null : if true, field can be left empty (default true)
+ type : 'string', 'int', 'float' (default to string), 'list' (only for hidden)
+ readonly : default False. if True, no form element, display current value.
+ convert_numbers: covert int and float values (from string)
+ allowed_values : list of possible values (default: any value)
+ validator : function validating the field (called with (value,field)).
+ min_value : minimum value (for floats and ints)
+ max_value : maximum value (for floats and ints)
+ explanation: text string to display next the input widget
+ title_buble: help bubble on field title (needs bubble.js or equivalent)
+ comment : comment, showed under input widget
+ withcheckbox: if true, place a checkbox at the left of the input
+ elem. Checked items will be returned in 'tf-checked'
+ attributes: a liste of strings to put in the HTML form element
+ template: HTML template for element
+ HTML elements:
+ input_type : 'text', 'textarea', 'password',
+ 'radio', 'menu', 'checkbox',
+ 'hidden', 'separator', 'file', 'date', 'boolcheckbox',
+ 'text_suggest'
+ (default text)
+ size : text field width
+ rows, cols: textarea geometry
+ labels : labels for radio or menu lists (associated to allowed_values)
+ vertical: for checkbox; if true, vertical layout
+ disabled_items: for checkbox, dict such that disabled_items[i] true if disabled checkbox
+ To use text_suggest elements, one must:
+ - specify options in text_suggest_options (a dict)
+ - HTML page must load JS AutoSuggest.js and CSS autosuggest_inquisitor.css
+ - bodyOnLoad must call JS function init_tf_form(formid)
+ """
+ method = method.lower()
+ if method == "get":
+ enctype = None
+ t = TF(
+ form_url,
+ values,
+ formdescription,
+ initvalues,
+ method,
+ enctype,
+ submitlabel,
+ name,
+ formid,
+ cssclass,
+ cancelbutton=cancelbutton,
+ submitbutton=submitbutton,
+ submitbuttonattributes=submitbuttonattributes,
+ top_buttons=top_buttons,
+ bottom_buttons=bottom_buttons,
+ html_foot_markup=html_foot_markup,
+ readonly=readonly,
+ is_submitted=is_submitted,
+ )
+ form = t.getform()
+ if t.canceled():
+ res = -1
+ elif t.submitted() and t.result:
+ res = 1
+ else:
+ res = 0
+ return res, form, t.result
+
+
+class TF:
+ def __init__(
+ self,
+ form_url,
+ values,
+ formdescription=[],
+ initvalues={},
+ method="POST",
+ enctype=None,
+ submitlabel="OK",
+ name=None,
+ formid="tf",
+ cssclass="",
+ cancelbutton=None,
+ submitbutton=True,
+ submitbuttonattributes=[],
+ top_buttons=False, # place buttons at top of form
+ bottom_buttons=True, # buttons after form
+ html_foot_markup="", # html snippet put at the end, just after the table
+ readonly=False,
+ is_submitted=False,
+ ):
+ self.form_url = form_url
+ self.values = values
+ self.formdescription = list(formdescription)
+ self.initvalues = initvalues
+ self.method = method
+ self.enctype = enctype
+ self.submitlabel = submitlabel
+ if name:
+ self.name = name
+ else:
+ self.name = formid # 'tf'
+ self.formid = formid
+ self.cssclass = cssclass
+ self.cancelbutton = cancelbutton
+ self.submitbutton = submitbutton
+ self.submitbuttonattributes = submitbuttonattributes
+ self.top_buttons = top_buttons
+ self.bottom_buttons = bottom_buttons
+ self.html_foot_markup = html_foot_markup
+ self.readonly = readonly
+ self.result = None
+ self.is_submitted = is_submitted
+ if readonly:
+ self.top_buttons = self.bottom_buttons = False
+ self.cssclass += " readonly"
+
+ def submitted(self):
+ "true if form has been submitted"
+ if self.is_submitted:
+ return True
+ return self.values.get("%s-submitted" % self.formid, False)
+
+ def canceled(self):
+ "true if form has been canceled"
+ return self.values.get("%s_cancel" % self.formid, False)
+
+ def getform(self):
+ "return HTML form"
+ R = []
+ msg = None
+ self.setdefaultvalues()
+ if self.submitted() and not self.readonly:
+ msg = self.checkvalues()
+ # display error message
+ R.append(tf_error_message(msg))
+ # form or view
+ if self.readonly:
+ R = R + self._ReadOnlyVersion(self.formdescription)
+ else:
+ R = R + self._GenForm()
+ #
+ return "\n".join(R)
+
+ __str__ = getform
+ __repr__ = getform
+
+ def setdefaultvalues(self):
+ "set default values and convert numbers to strings"
+ for (field, descr) in self.formdescription:
+ # special case for boolcheckbox
+ if descr.get("input_type", None) == "boolcheckbox" and self.submitted():
+ if not self.values.has_key(field):
+ self.values[field] = 0
+ else:
+ self.values[field] = 1
+ if not self.values.has_key(field):
+ if descr.has_key("default"): # first: default in form description
+ self.values[field] = descr["default"]
+ else: # then: use initvalues dict
+ self.values[field] = self.initvalues.get(field, "")
+ if self.values[field] == None:
+ self.values[field] = ""
+
+ # convert numbers
+ if type(self.values[field]) == type(1) or type(self.values[field]) == type(
+ 1.0
+ ):
+ self.values[field] = str(self.values[field])
+ #
+ if not self.values.has_key("tf-checked"):
+ if self.submitted():
+ # si rien n'est coché, tf-checked n'existe plus dans la reponse
+ self.values["tf-checked"] = []
+ else:
+ self.values["tf-checked"] = self.initvalues.get("tf-checked", [])
+ self.values["tf-checked"] = [str(x) for x in self.values["tf-checked"]]
+
+ def checkvalues(self):
+ "check values. Store .result and returns msg"
+ ok = 1
+ msg = []
+ for (field, descr) in self.formdescription:
+ val = self.values[field]
+ # do not check "unckecked" items
+ if descr.get("withcheckbox", False):
+ if not field in self.values["tf-checked"]:
+ continue
+ # null values
+ allow_null = descr.get("allow_null", True)
+ if not allow_null:
+ if val == "" or val == None:
+ msg.append(
+ "Le champ '%s' doit être renseigné" % descr.get("title", field)
+ )
+ ok = 0
+ # type
+ typ = descr.get("type", "string")
+ if val != "" and val != None:
+ # check only non-null values
+ if typ[:3] == "int":
+ try:
+ val = int(val)
+ self.values[field] = val
+ except:
+ msg.append(
+ "La valeur du champ '%s' doit être un nombre entier" % field
+ )
+ ok = 0
+ elif typ == "float" or typ == "real":
+ self.values[field] = self.values[field].replace(",", ".")
+ try:
+ val = float(val.replace(",", ".")) # allow ,
+ self.values[field] = val
+ except:
+ msg.append(
+ "La valeur du champ '%s' doit être un nombre" % field
+ )
+ ok = 0
+ if typ[:3] == "int" or typ == "float" or typ == "real":
+ if descr.has_key("min_value") and val < descr["min_value"]:
+ msg.append(
+ "La valeur (%d) du champ '%s' est trop petite (min=%s)"
+ % (val, field, descr["min_value"])
+ )
+ ok = 0
+
+ if descr.has_key("max_value") and val > descr["max_value"]:
+ msg.append(
+ "La valeur (%s) du champ '%s' est trop grande (max=%s)"
+ % (val, field, descr["max_value"])
+ )
+ ok = 0
+
+ # allowed values
+ if descr.has_key("allowed_values"):
+ if descr.get("input_type", None) == "checkbox":
+ # for checkboxes, val is a list
+ for v in val:
+ if not v in descr["allowed_values"]:
+ msg.append(
+ "valeur invalide (%s) pour le champ '%s'" % (val, field)
+ )
+ ok = 0
+ elif descr.get("input_type", None) == "boolcheckbox":
+ pass
+ elif not val in descr["allowed_values"]:
+ msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
+ ok = 0
+ if descr.has_key("validator"):
+ if not descr["validator"](val, field):
+ msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
+ ok = 0
+ # boolean checkbox
+ if descr.get("input_type", None) == "boolcheckbox":
+ if int(val):
+ self.values[field] = 1
+ else:
+ self.values[field] = 0
+ # open('/tmp/toto','a').write('checkvalues: val=%s (%s) values[%s] = %s\n' % (val, type(val), field, self.values[field]))
+ if descr.get("convert_numbers", False):
+ if typ[:3] == "int":
+ self.values[field] = int(self.values[field])
+ elif typ == "float" or typ == "real":
+ self.values[field] = float(self.values[field].replace(",", "."))
+ if ok:
+ self.result = self.values
+ else:
+ self.result = None
+ return msg
+
+ def _GenForm(self, method="", enctype=None, form_url=""):
+ values = self.values
+ add_no_enter_js = False # add JS function to prevent 'enter' -> submit
+ # form template
+
+ # default template for each input element
+ itemtemplate = """
Suppression de notes (permet donc de supprimer une évaluation)
+
+
Bulletins en versions courtes (seulement moyennes de chaque module), longues
+(toutes les notes) et intermédiaire (moyenne de chaque module plus notes dans les évaluations sélectionnées).
+
+
Notes moyennes sous les barres en rouge dans le tableau récapitulatif (seuil=10 sur la moyenne générale, et 8 sur chaque UE).
+
+
Colonne "groupe de TD" dans le tableau récapitulatif des notes.
+
+"""
diff --git a/ZAbsences.py b/ZAbsences.py
new file mode 100644
index 000000000..1bdd11b7a
--- /dev/null
+++ b/ZAbsences.py
@@ -0,0 +1,2516 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+""" Gestion des absences (v4)
+
+C'est la partie la plus ancienne de ScoDoc, et elle est à revoir.
+
+L'API de plus bas niveau est en gros:
+
+ AnnuleAbsencesDatesNoJust(etudid, dates)
+ CountAbs(etudid, debut, fin, matin=None, moduleimpl_id=None)
+ CountAbsJust(etudid, debut, fin, matin=None, moduleimpl_id=None)
+ ListeAbsJust(etudid, datedebut) [pas de fin ?]
+ ListeAbsNonJust(etudid, datedebut) [pas de fin ?]
+ ListeJustifs(etudid, datedebut, datefin=None, only_no_abs=True)
+
+ ListeAbsJour(date, am=True, pm=True, is_abs=None, is_just=None)
+ ListeAbsNonJustJour(date, am=True, pm=True)
+
+
+"""
+
+import urllib
+
+from sco_zope import *
+
+# ---------------
+
+from notesdb import *
+from notes_log import log
+from scolog import logdb
+from sco_utils import *
+
+# import notes_users
+from TrivialFormulator import TrivialFormulator, TF
+from gen_tables import GenTable
+import scolars
+import sco_formsemestre
+import sco_groups
+import sco_groups_view
+import sco_excel
+import sco_abs_notification, sco_abs_views
+import sco_compute_moy
+import string, re
+import time, calendar
+from mx.DateTime import DateTime as mxDateTime
+from mx.DateTime.ISO import ParseDateTimeUTC
+
+
+def _toboolean(x):
+ "convert a value to boolean (ensure backward compat with OLD intranet code)"
+ if type(x) == type(""):
+ x = x.lower()
+ if x and x != "false": # backward compat...
+ return True
+ else:
+ return False
+
+
+def MonthNbDays(month, year):
+ "returns nb of days in month"
+ if month > 7:
+ month = month + 1
+ if month % 2:
+ return 31
+ elif month == 2:
+ if calendar.isleap(year):
+ return 29
+ else:
+ return 28
+ else:
+ return 30
+
+
+class ddmmyyyy:
+ """immutable dates"""
+
+ def __init__(self, date=None, fmt="ddmmyyyy", work_saturday=False):
+ self.work_saturday = work_saturday
+ if date is None:
+ return
+ try:
+ if fmt == "ddmmyyyy":
+ self.day, self.month, self.year = string.split(date, "/")
+ elif fmt == "iso":
+ self.year, self.month, self.day = string.split(date, "-")
+ else:
+ raise ValueError("invalid format spec. (%s)" % fmt)
+ self.year = string.atoi(self.year)
+ self.month = string.atoi(self.month)
+ self.day = string.atoi(self.day)
+ except:
+ raise ScoValueError("date invalide: %s" % date)
+ # accept years YYYY or YY, uses 1970 as pivot
+ if self.year < 1970:
+ if self.year > 100:
+ raise ScoInvalidDateError("Année invalide: %s" % self.year)
+ if self.year < 70:
+ self.year = self.year + 2000
+ else:
+ self.year = self.year + 1900
+ if self.month < 1 or self.month > 12:
+ raise ScoInvalidDateError("Mois invalide: %s" % self.month)
+
+ if self.day < 1 or self.day > MonthNbDays(self.month, self.year):
+ raise ScoInvalidDateError("Jour invalide: %s" % self.day)
+
+ # weekday in 0-6, where 0 is monday
+ self.weekday = calendar.weekday(self.year, self.month, self.day)
+
+ self.time = time.mktime((self.year, self.month, self.day, 0, 0, 0, 0, 0, 0))
+
+ def iswork(self):
+ "returns true if workable day"
+ if self.work_saturday:
+ nbdays = 6
+ else:
+ nbdays = 5
+ if (
+ self.weekday >= 0 and self.weekday < nbdays
+ ): # monday-friday or monday-saturday
+ return 1
+ else:
+ return 0
+
+ def __repr__(self):
+ return "'%02d/%02d/%04d'" % (self.day, self.month, self.year)
+
+ def __str__(self):
+ return "%02d/%02d/%04d" % (self.day, self.month, self.year)
+
+ def ISO(self):
+ "iso8601 representation of the date"
+ return "%04d-%02d-%02d" % (self.year, self.month, self.day)
+
+ def next(self, days=1):
+ "date for the next day (nota: may be a non workable day)"
+ day = self.day + days
+ month = self.month
+ year = self.year
+
+ while day > MonthNbDays(month, year):
+ day = day - MonthNbDays(month, year)
+ month = month + 1
+ if month > 12:
+ month = 1
+ year = year + 1
+ return self.__class__(
+ "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday
+ )
+
+ def prev(self, days=1):
+ "date for previous day"
+ day = self.day - days
+ month = self.month
+ year = self.year
+ while day <= 0:
+ month = month - 1
+ if month == 0:
+ month = 12
+ year = year - 1
+ day = day + MonthNbDays(month, year)
+
+ return self.__class__(
+ "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday
+ )
+
+ def next_monday(self):
+ "date of next monday"
+ return self.next((7 - self.weekday) % 7)
+
+ def prev_monday(self):
+ "date of last monday, but on sunday, pick next monday"
+ if self.weekday == 6:
+ return self.next_monday()
+ else:
+ return self.prev(self.weekday)
+
+ def __cmp__(self, other):
+ """return a negative integer if self < other,
+ zero if self == other, a positive integer if self > other"""
+ return int(self.time - other.time)
+
+ def __hash__(self):
+ "we are immutable !"
+ return hash(self.time) ^ hash(str(self))
+
+
+# d = ddmmyyyy( '21/12/99' )
+
+
+def YearTable(
+ context,
+ year,
+ events=[],
+ firstmonth=9,
+ lastmonth=7,
+ halfday=0,
+ dayattributes="",
+ pad_width=8,
+):
+ """Generate a calendar table
+ events = list of tuples (date, text, color, href [,halfday])
+ where date is a string in ISO format (yyyy-mm-dd)
+ halfday is boolean (true: morning, false: afternoon)
+ text = text to put in calendar (must be short, 1-5 cars) (optional)
+ if halfday, generate 2 cells per day (morning, afternoon)
+ """
+ T = [
+ '
')
+ if n > 0:
+ H.append("%d absences (1/2 journées) %s ajoutées" % (n, j))
+ elif n == 0:
+ H.append("Aucun jour d'absence dans les dates indiquées !")
+ elif n < 0:
+ H.append("Ce billet avait déjà été traité !")
+ H.append(
+ '
")
+ return string.join(T, "\n")
+
+
+# --------------------------------------------------------------------
+#
+# Zope Product Administration
+#
+# --------------------------------------------------------------------
+def manage_addZAbsences(
+ self, id="id_ZAbsences", title="The Title for ZAbsences Object", REQUEST=None
+):
+ "Add a ZAbsences instance to a folder."
+ self._setObject(id, ZAbsences(id, title))
+ if REQUEST is not None:
+ return self.manage_main(self, REQUEST)
+ # return self.manage_editForm(self, REQUEST)
+
+
+# The form used to get the instance id from the user.
+# manage_addZAbsencesForm = DTMLFile('dtml/manage_addZAbsencesForm', globals())
+
+
+# --------------------------------------------------------------------
+#
+# Cache absences
+#
+# On cache simplement (à la demande) le nombre d'absences de chaque etudiant
+# dans un semestre donné.
+# Toute modification du semestre (invalidation) invalide le cache
+# (simple mécanisme de "listener" sur le cache de semestres)
+# Toute modification des absences d'un étudiant invalide les caches des semestres
+# concernés à cette date (en général un seul semestre)
+#
+# On ne cache pas la liste des absences car elle est rarement utilisée (calendrier,
+# absences à une date donnée).
+#
+# --------------------------------------------------------------------
+class CAbsSemEtud:
+ """Comptes d'absences d'un etudiant dans un semestre"""
+
+ def __init__(self, context, sem, etudid):
+ self.context = context
+ self.sem = sem
+ self.etudid = etudid
+ self._loaded = False
+ formsemestre_id = sem["formsemestre_id"]
+ context.Notes._getNotesCache().add_listener(
+ self.invalidate, formsemestre_id, (etudid, formsemestre_id)
+ )
+
+ def CountAbs(self):
+ if not self._loaded:
+ self.load()
+ return self._CountAbs
+
+ def CountAbsJust(self):
+ if not self._loaded:
+ self.load()
+ return self._CountAbsJust
+
+ def load(self):
+ "Load state from DB"
+ # log('loading CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id']))
+ # Reload sem, it may have changed
+ self.sem = sco_formsemestre.get_formsemestre(
+ self.context, self.sem["formsemestre_id"]
+ )
+ debut_sem = DateDMYtoISO(self.sem["date_debut"])
+ fin_sem = DateDMYtoISO(self.sem["date_fin"])
+
+ self._CountAbs = self.context.Absences.CountAbs(
+ etudid=self.etudid, debut=debut_sem, fin=fin_sem
+ )
+ self._CountAbsJust = self.context.Absences.CountAbsJust(
+ etudid=self.etudid, debut=debut_sem, fin=fin_sem
+ )
+ self._loaded = True
+
+ def invalidate(self, args=None):
+ "Notify me that DB has been modified"
+ # log('invalidate CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id']))
+ self._loaded = False
+
+
+# Accès au cache des absences
+ABS_CACHE_INST = {} # { DeptId : { formsemestre_id : { etudid : CAbsEtudSem } } }
+
+
+def getAbsSemEtud(context, sem, etudid):
+ AbsSemEtuds = getAbsSemEtuds(context, sem)
+ if not etudid in AbsSemEtuds:
+ AbsSemEtuds[etudid] = CAbsSemEtud(context, sem, etudid)
+ return AbsSemEtuds[etudid]
+
+
+def getAbsSemEtuds(context, sem):
+ u = context.GetDBConnexionString() # identifie le dept de facon fiable
+ if not u in ABS_CACHE_INST:
+ ABS_CACHE_INST[u] = {}
+ C = ABS_CACHE_INST[u]
+ if sem["formsemestre_id"] not in C:
+ C[sem["formsemestre_id"]] = {}
+ return C[sem["formsemestre_id"]]
+
+
+def invalidateAbsEtudDate(context, etudid, date):
+ """Doit etre appelé à chaque modification des absences pour cet étudiant et cette date.
+ Invalide cache absence et PDF bulletins si nécessaire.
+ date: date au format ISO
+ """
+ # Semestres a cette date:
+ etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+ sems = [
+ sem
+ for sem in etud["sems"]
+ if sem["date_debut_iso"] <= date and sem["date_fin_iso"] >= date
+ ]
+
+ # Invalide les PDF et les abscences:
+ for sem in sems:
+ # Inval cache bulletin et/ou note_table
+ if sco_compute_moy.formsemestre_expressions_use_abscounts(
+ context, sem["formsemestre_id"]
+ ):
+ pdfonly = False # seules certaines formules utilisent les absences
+ else:
+ pdfonly = (
+ True # efface toujours le PDF car il affiche en général les absences
+ )
+
+ context.Notes._inval_cache(
+ pdfonly=pdfonly, formsemestre_id=sem["formsemestre_id"]
+ )
+
+ # Inval cache compteurs absences:
+ AbsSemEtuds = getAbsSemEtuds(context, sem)
+ if etudid in AbsSemEtuds:
+ AbsSemEtuds[etudid].invalidate()
diff --git a/ZEntreprises.py b/ZEntreprises.py
new file mode 100644
index 000000000..3ee42a84e
--- /dev/null
+++ b/ZEntreprises.py
@@ -0,0 +1,898 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+""" Gestion des relations avec les entreprises
+"""
+import urllib
+
+from sco_zope import *
+
+# ---------------
+
+from notesdb import *
+from notes_log import log
+from scolog import logdb
+from sco_utils import *
+import html_sidebar
+
+from TrivialFormulator import TrivialFormulator, TF
+import scolars
+import string, re
+import time, calendar
+
+
+def _format_nom(nom):
+ "formatte nom (filtre en entree db) d'une entreprise"
+ if not nom:
+ return nom
+ nom = nom.decode(SCO_ENCODING)
+ return (nom[0].upper() + nom[1:]).encode(SCO_ENCODING)
+
+
+class EntreprisesEditor(EditableTable):
+ def delete(self, cnx, oid):
+ "delete correspondants and contacts, then self"
+ # first, delete all correspondants and contacts
+ cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+ cursor.execute(
+ "delete from entreprise_contact where entreprise_id=%(entreprise_id)s",
+ {"entreprise_id": oid},
+ )
+ cursor.execute(
+ "delete from entreprise_correspondant where entreprise_id=%(entreprise_id)s",
+ {"entreprise_id": oid},
+ )
+ cnx.commit()
+ EditableTable.delete(self, cnx, oid)
+
+ def list(
+ self,
+ cnx,
+ args={},
+ operator="and",
+ test="=",
+ sortkey=None,
+ sort_on_contact=False,
+ ZEntrepriseInstance=None,
+ ):
+ # list, then sort on date of last contact
+ R = EditableTable.list(
+ self, cnx, args=args, operator=operator, test=test, sortkey=sortkey
+ )
+ if sort_on_contact:
+ for r in R:
+ c = ZEntrepriseInstance.do_entreprise_contact_list(
+ args={"entreprise_id": r["entreprise_id"]}, disable_formatting=True
+ )
+ if c:
+ r["date"] = max([x["date"] or datetime.date.min for x in c])
+ else:
+ r["date"] = datetime.date.min
+ # sort
+ R.sort(lambda r1, r2: cmp(r2["date"], r1["date"]))
+ for r in R:
+ r["date"] = DateISOtoDMY(r["date"])
+ return R
+
+ def list_by_etud(
+ self, cnx, args={}, sort_on_contact=False, disable_formatting=False
+ ):
+ "cherche rentreprise ayant eu contact avec etudiant"
+ cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+ cursor.execute(
+ "select E.*, I.nom as etud_nom, I.prenom as etud_prenom, C.date from entreprises E, entreprise_contact C, identite I where C.entreprise_id = E.entreprise_id and C.etudid = I.etudid and I.nom ~* %(etud_nom)s ORDER BY E.nom",
+ args,
+ )
+ titles, res = [x[0] for x in cursor.description], cursor.dictfetchall()
+ R = []
+ for r in res:
+ r["etud_prenom"] = r["etud_prenom"] or ""
+ d = {}
+ for key in r:
+ v = r[key]
+ # format value
+ if not disable_formatting and self.output_formators.has_key(key):
+ v = self.output_formators[key](v)
+ d[key] = v
+ R.append(d)
+ # sort
+ if sort_on_contact:
+ R.sort(
+ lambda r1, r2: cmp(
+ r2["date"] or datetime.date.min, r1["date"] or datetime.date.min
+ )
+ )
+ for r in R:
+ r["date"] = DateISOtoDMY(r["date"] or datetime.date.min)
+ return R
+
+
+_entreprisesEditor = EntreprisesEditor(
+ "entreprises",
+ "entreprise_id",
+ (
+ "entreprise_id",
+ "nom",
+ "adresse",
+ "ville",
+ "codepostal",
+ "pays",
+ "contact_origine",
+ "secteur",
+ "privee",
+ "localisation",
+ "qualite_relation",
+ "plus10salaries",
+ "note",
+ "date_creation",
+ ),
+ sortkey="nom",
+ input_formators={"nom": _format_nom},
+)
+
+# ----------- Correspondants
+_entreprise_correspEditor = EditableTable(
+ "entreprise_correspondant",
+ "entreprise_corresp_id",
+ (
+ "entreprise_corresp_id",
+ "entreprise_id",
+ "civilite",
+ "nom",
+ "prenom",
+ "fonction",
+ "phone1",
+ "phone2",
+ "mobile",
+ "fax",
+ "mail1",
+ "mail2",
+ "note",
+ ),
+ sortkey="nom",
+)
+
+
+# ----------- Contacts
+_entreprise_contactEditor = EditableTable(
+ "entreprise_contact",
+ "entreprise_contact_id",
+ (
+ "entreprise_contact_id",
+ "date",
+ "type_contact",
+ "entreprise_id",
+ "entreprise_corresp_id",
+ "etudid",
+ "description",
+ "enseignant",
+ ),
+ sortkey="date",
+ output_formators={"date": DateISOtoDMY},
+ input_formators={"date": DateDMYtoISO},
+)
+
+# ---------------
+
+
+class ZEntreprises(
+ ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit
+):
+
+ "ZEntreprises object"
+
+ meta_type = "ZEntreprises"
+ security = ClassSecurityInfo()
+
+ # This is the list of the methods associated to 'tabs' in the ZMI
+ # Be aware that The first in the list is the one shown by default, so if
+ # the 'View' tab is the first, you will never see your tabs by cliquing
+ # on the object.
+ manage_options = (
+ ({"label": "Contents", "action": "manage_main"},)
+ + PropertyManager.manage_options # add the 'Properties' tab
+ + (
+ # this line is kept as an example with the files :
+ # dtml/manage_editZScolarForm.dtml
+ # html/ZScolar-edit.stx
+ # {'label': 'Properties', 'action': 'manage_editForm',},
+ {"label": "View", "action": "index_html"},
+ )
+ + Item.manage_options # add the 'Undo' & 'Owner' tab
+ + RoleManager.manage_options # add the 'Security' tab
+ )
+
+ # no permissions, only called from python
+ def __init__(self, id, title):
+ "initialise a new instance"
+ self.id = id
+ self.title = title
+
+ # The form used to edit this object
+ def manage_editZEntreprises(self, title, RESPONSE=None):
+ "Changes the instance values"
+ self.title = title
+ self._p_changed = 1
+ RESPONSE.redirect("manage_editForm")
+
+ # Ajout (dans l'instance) d'un dtml modifiable par Zope
+ def defaultDocFile(self, id, title, file):
+ f = open(file_path + "/dtml-editable/" + file + ".dtml")
+ file = f.read()
+ f.close()
+ self.manage_addDTMLMethod(id, title, file)
+
+ security.declareProtected(ScoEntrepriseView, "entreprise_header")
+
+ def entreprise_header(self, REQUEST=None, page_title=""):
+ "common header for all Entreprises pages"
+ authuser = REQUEST.AUTHENTICATED_USER
+ # _read_only is used to modify pages properties (links, buttons)
+ # Python methods (do_xxx in this class) are also protected individualy)
+ if authuser.has_permission(ScoEntrepriseChange, self):
+ REQUEST.set("_read_only", False)
+ else:
+ REQUEST.set("_read_only", True)
+ return self.sco_header(REQUEST, container=self, page_title=page_title)
+
+ security.declareProtected(ScoEntrepriseView, "entreprise_footer")
+
+ def entreprise_footer(self, REQUEST):
+ "common entreprise footer"
+ return self.sco_footer(REQUEST)
+
+ security.declareProtected(ScoEntrepriseView, "sidebar")
+
+ def sidebar(self, REQUEST):
+ "barre gauche (overide std sco sidebar)"
+ # rewritten from legacy DTML code
+ context = self
+ params = {"ScoURL": context.ScoURL()}
+
+ H = [
+ """
Une "formation" est un programme pédagogique structuré en UE, matières et modules. Chaque semestre se réfère à une formation. La modification d'une formation affecte tous les semestres qui s'y réfèrent.
+ """
+ )
+
+ H.append(self.sco_footer(REQUEST))
+ return "\n".join(H)
+
+ # --------------------------------------------------------------------
+ #
+ # Notes Methods
+ #
+ # --------------------------------------------------------------------
+
+ # --- Formations
+ _formationEditor = EditableTable(
+ "notes_formations",
+ "formation_id",
+ (
+ "formation_id",
+ "acronyme",
+ "titre",
+ "titre_officiel",
+ "version",
+ "formation_code",
+ "type_parcours",
+ "code_specialite",
+ ),
+ sortkey="acronyme",
+ )
+
+ security.declareProtected(ScoChangeFormation, "do_formation_create")
+
+ def do_formation_create(self, args, REQUEST):
+ "create a formation"
+ cnx = self.GetDBConnexion()
+ # check unique acronyme/titre/version
+ a = args.copy()
+ if a.has_key("formation_id"):
+ del a["formation_id"]
+ F = self.formation_list(args=a)
+ if len(F) > 0:
+ log(
+ "do_formation_create: error: %d formations matching args=%s"
+ % (len(F), a)
+ )
+ raise ScoValueError("Formation non unique (%s) !" % str(a))
+ # Si pas de formation_code, l'enleve (default SQL)
+ if args.has_key("formation_code") and not args["formation_code"]:
+ del args["formation_code"]
+ #
+ r = self._formationEditor.create(cnx, args)
+
+ sco_news.add(
+ self,
+ REQUEST,
+ typ=NEWS_FORM,
+ text="Création de la formation %(titre)s (%(acronyme)s)" % args,
+ )
+ return r
+
+ security.declareProtected(ScoChangeFormation, "do_formation_delete")
+
+ def do_formation_delete(self, oid, REQUEST):
+ """delete a formation (and all its UE, matieres, modules)
+ XXX delete all ues, will break if there are validations ! USE WITH CARE !
+ """
+ F = self.formation_list(args={"formation_id": oid})[0]
+ if self.formation_has_locked_sems(oid):
+ raise ScoLockedFormError()
+ cnx = self.GetDBConnexion()
+ # delete all UE in this formation
+ ues = self.do_ue_list({"formation_id": oid})
+ for ue in ues:
+ self._do_ue_delete(ue["ue_id"], REQUEST=REQUEST, force=True)
+
+ self._formationEditor.delete(cnx, oid)
+
+ # news
+ sco_news.add(
+ self,
+ REQUEST,
+ typ=NEWS_FORM,
+ object=oid,
+ text="Suppression de la formation %(acronyme)s" % F,
+ )
+
+ security.declareProtected(ScoView, "formation_list")
+
+ def formation_list(self, format=None, REQUEST=None, formation_id=None, args={}):
+ """List formation(s) with given id, or matching args
+ (when args is given, formation_id is ignored).
+ """
+ # logCallStack()
+ if not args:
+ if formation_id is None:
+ args = {}
+ else:
+ args = {"formation_id": formation_id}
+ cnx = self.GetDBConnexion()
+ r = self._formationEditor.list(cnx, args=args)
+ # log('%d formations found' % len(r))
+ return sendResult(REQUEST, r, name="formation", format=format)
+
+ security.declareProtected(ScoView, "formation_export")
+
+ def formation_export(
+ self, formation_id, export_ids=False, format=None, REQUEST=None
+ ):
+ "Export de la formation au format indiqué (xml ou json)"
+ return sco_formations.formation_export(
+ self, formation_id, export_ids=export_ids, format=format, REQUEST=REQUEST
+ )
+
+ security.declareProtected(ScoChangeFormation, "formation_import_xml")
+
+ def formation_import_xml(self, file, REQUEST):
+ "import d'une formation en XML"
+ log("formation_import_xml")
+ doc = file.read()
+ return sco_formations.formation_import_xml(self, REQUEST, doc)
+
+ security.declareProtected(ScoChangeFormation, "formation_import_xml_form")
+
+ def formation_import_xml_form(self, REQUEST):
+ "form import d'une formation en XML"
+ H = [
+ self.sco_header(page_title="Import d'une formation", REQUEST=REQUEST),
+ """
Import d'une formation
+
Création d'une formation (avec UE, matières, modules)
+ à partir un fichier XML (réservé aux utilisateurs avertis)
Les enseignants d'un module ont le droit de
+ saisir et modifier toutes les notes des évaluations de ce module.
+
+
Pour changer le responsable du module, passez par la
+ page "Modification du semestre", accessible uniquement au responsable de la formation (chef de département)
+
Expérimental: formule de calcul de la moyenne %(target)s
+
Dans la formule, les variables suivantes sont définies:
+
+
moy la moyenne, calculée selon la règle standard (moyenne pondérée)
+
moy_is_valid vrai si la moyenne est valide (numérique)
+
moy_val la valeur de la moyenne (nombre, valant 0 si invalide)
+
notes vecteur des notes (/20) aux %(objs)s
+
coefs vecteur des coefficients des %(objs)s, les coefs des %(objs)s sans notes (ATT, EXC) étant mis à zéro
+
cmask vecteur de 0/1, 0 si le coef correspondant a été annulé
+
Nombre d'absences: nb_abs, nb_abs_just, nb_abs_nojust (en demi-journées)
+
+
Les éléments des vecteurs sont ordonnés dans l'ordre des %(objs)s%(ordre)s.
+
Les fonctions suivantes sont utilisables: abs, cmp, dot, len, map, max, min, pow, reduce, round, sum, ifelse
+
La notation V(1,2,3) représente un vecteur (1,2,3)
+
Vous pouvez désactiver la formule (et revenir au mode de calcul "classique")
+ en supprimant le texte ou en faisant précéder la première ligne par #
+ """
+
+ security.declareProtected(ScoView, "edit_moduleimpl_expr")
+
+ def edit_moduleimpl_expr(self, REQUEST, moduleimpl_id):
+ """Edition formule calcul moyenne module
+ Accessible par Admin, dir des etud et responsable module
+ """
+ M, sem = self.can_change_ens(REQUEST, moduleimpl_id)
+ H = [
+ self.html_sem_header(
+ REQUEST,
+ 'Modification règle de calcul du module %s'
+ % (moduleimpl_id, M["module"]["titre"]),
+ sem,
+ ),
+ self._expr_help
+ % {
+ "target": "du module",
+ "objs": "évaluations",
+ "ordre": " (le premier élément est la plus ancienne évaluation)",
+ },
+ ]
+ initvalues = M
+ form = [
+ ("moduleimpl_id", {"input_type": "hidden"}),
+ (
+ "computation_expr",
+ {
+ "title": "Formule de calcul",
+ "input_type": "textarea",
+ "rows": 4,
+ "cols": 60,
+ "explanation": "formule de calcul (expérimental)",
+ },
+ ),
+ ]
+ tf = TrivialFormulator(
+ REQUEST.URL0,
+ REQUEST.form,
+ form,
+ submitlabel="Modifier formule de calcul",
+ cancelbutton="Annuler",
+ initvalues=initvalues,
+ )
+ if tf[0] == 0:
+ return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
+ elif tf[0] == -1:
+ return REQUEST.RESPONSE.redirect(
+ "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
+ )
+ else:
+ self.do_moduleimpl_edit(
+ {
+ "moduleimpl_id": moduleimpl_id,
+ "computation_expr": tf[2]["computation_expr"],
+ },
+ formsemestre_id=sem["formsemestre_id"],
+ )
+ self._inval_cache(
+ formsemestre_id=sem["formsemestre_id"]
+ ) # > modif regle calcul
+ return REQUEST.RESPONSE.redirect(
+ "moduleimpl_status?moduleimpl_id="
+ + moduleimpl_id
+ + "&head_message=règle%20de%20calcul%20modifiée"
+ )
+
+ security.declareProtected(ScoView, "view_module_abs")
+
+ def view_module_abs(self, REQUEST, moduleimpl_id, format="html"):
+ """Visulalisation des absences a un module
+ """
+ M = self.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
+ sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+ debut_sem = DateDMYtoISO(sem["date_debut"])
+ fin_sem = DateDMYtoISO(sem["date_fin"])
+ list_insc = self.do_moduleimpl_listeetuds(moduleimpl_id)
+
+ T = []
+ for etudid in list_insc:
+ nb_abs = self.Absences.CountAbs(
+ etudid=etudid, debut=debut_sem, fin=fin_sem, moduleimpl_id=moduleimpl_id
+ )
+ if nb_abs:
+ nb_abs_just = self.Absences.CountAbsJust(
+ etudid=etudid,
+ debut=debut_sem,
+ fin=fin_sem,
+ moduleimpl_id=moduleimpl_id,
+ )
+ etud = self.getEtudInfo(etudid=etudid, filled=True)[0]
+ T.append(
+ {
+ "nomprenom": etud["nomprenom"],
+ "just": nb_abs_just,
+ "nojust": nb_abs - nb_abs_just,
+ "total": nb_abs,
+ "_nomprenom_target": "ficheEtud?etudid=%s" % etudid,
+ }
+ )
+
+ H = [
+ self.html_sem_header(
+ REQUEST,
+ 'Absences du module %s'
+ % (moduleimpl_id, M["module"]["titre"]),
+ page_title="Absences du module %s" % (M["module"]["titre"]),
+ sem=sem,
+ )
+ ]
+ if not T and format == "html":
+ return (
+ "\n".join(H)
+ + "
Aucune absence signalée
"
+ + self.sco_footer(REQUEST)
+ )
+
+ tab = GenTable(
+ titles={
+ "nomprenom": "Nom",
+ "just": "Just.",
+ "nojust": "Non Just.",
+ "total": "Total",
+ },
+ columns_ids=("nomprenom", "just", "nojust", "total"),
+ rows=T,
+ html_class="table_leftalign",
+ base_url="%s?moduleimpl_id=%s" % (REQUEST.URL0, moduleimpl_id),
+ filename="absmodule_" + make_filename(M["module"]["titre"]),
+ caption="Absences dans le module %s" % M["module"]["titre"],
+ preferences=self.get_preferences(),
+ )
+
+ if format != "html":
+ return tab.make_page(self, format=format, REQUEST=REQUEST)
+
+ return "\n".join(H) + tab.html() + self.sco_footer(REQUEST)
+
+ security.declareProtected(ScoView, "edit_ue_expr")
+
+ def edit_ue_expr(self, REQUEST, formsemestre_id, ue_id):
+ """Edition formule calcul moyenne UE"""
+ # Check access
+ sem = sco_formsemestre_edit.can_edit_sem(self, REQUEST, formsemestre_id)
+ if not sem:
+ raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+ cnx = self.GetDBConnexion()
+ #
+ ue = self.do_ue_list({"ue_id": ue_id})[0]
+ H = [
+ self.html_sem_header(
+ REQUEST,
+ "Modification règle de calcul de l'UE %s (%s)"
+ % (ue["acronyme"], ue["titre"]),
+ sem,
+ ),
+ self._expr_help % {"target": "de l'UE", "objs": "modules", "ordre": ""},
+ ]
+ el = sco_compute_moy.formsemestre_ue_computation_expr_list(
+ cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
+ )
+ if el:
+ initvalues = el[0]
+ else:
+ initvalues = {}
+ form = [
+ ("ue_id", {"input_type": "hidden"}),
+ ("formsemestre_id", {"input_type": "hidden"}),
+ (
+ "computation_expr",
+ {
+ "title": "Formule de calcul",
+ "input_type": "textarea",
+ "rows": 4,
+ "cols": 60,
+ "explanation": "formule de calcul (expérimental)",
+ },
+ ),
+ ]
+ tf = TrivialFormulator(
+ REQUEST.URL0,
+ REQUEST.form,
+ form,
+ submitlabel="Modifier formule de calcul",
+ cancelbutton="Annuler",
+ initvalues=initvalues,
+ )
+ if tf[0] == 0:
+ return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
+ elif tf[0] == -1:
+ return REQUEST.RESPONSE.redirect(
+ "formsemestre_status?formsemestre_id=" + formsemestre_id
+ )
+ else:
+ if el:
+ el[0]["computation_expr"] = tf[2]["computation_expr"]
+ sco_compute_moy.formsemestre_ue_computation_expr_edit(cnx, el[0])
+ else:
+ sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, tf[2])
+
+ self._inval_cache(formsemestre_id=formsemestre_id) # > modif regle calcul
+ return REQUEST.RESPONSE.redirect(
+ "formsemestre_status?formsemestre_id="
+ + formsemestre_id
+ + "&head_message=règle%20de%20calcul%20modifiée"
+ )
+
+ security.declareProtected(ScoView, "formsemestre_enseignants_list")
+
+ def formsemestre_enseignants_list(self, REQUEST, formsemestre_id, format="html"):
+ """Liste les enseignants intervenants dans le semestre (resp. modules et chargés de TD)
+ et indique les absences saisies par chacun.
+ """
+ sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+ # resp. de modules:
+ mods = self.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+ sem_ens = {}
+ for mod in mods:
+ if not mod["responsable_id"] in sem_ens:
+ sem_ens[mod["responsable_id"]] = {"mods": [mod]}
+ else:
+ sem_ens[mod["responsable_id"]]["mods"].append(mod)
+ # charges de TD:
+ for mod in mods:
+ for ensd in mod["ens"]:
+ if not ensd["ens_id"] in sem_ens:
+ sem_ens[ensd["ens_id"]] = {"mods": [mod]}
+ else:
+ sem_ens[ensd["ens_id"]]["mods"].append(mod)
+ # compte les absences ajoutées par chacun dans tout le semestre
+ cnx = self.GetDBConnexion()
+ cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+ for ens in sem_ens:
+ cursor.execute(
+ "select * from scolog L, notes_formsemestre_inscription I where method='AddAbsence' and authenticated_user=%(authenticated_user)s and L.etudid = I.etudid and I.formsemestre_id=%(formsemestre_id)s and date > %(date_debut)s and date < %(date_fin)s",
+ {
+ "authenticated_user": ens,
+ "formsemestre_id": formsemestre_id,
+ "date_debut": DateDMYtoISO(sem["date_debut"]),
+ "date_fin": DateDMYtoISO(sem["date_fin"]),
+ },
+ )
+
+ events = cursor.dictfetchall()
+ sem_ens[ens]["nbabsadded"] = len(events)
+
+ # description textuelle des modules
+ for ens in sem_ens:
+ sem_ens[ens]["descr_mods"] = ", ".join(
+ [x["module"]["code"] for x in sem_ens[ens]["mods"]]
+ )
+
+ # ajoute infos sur enseignant:
+ for ens in sem_ens:
+ sem_ens[ens].update(self.Users.user_info(ens))
+ if sem_ens[ens]["email"]:
+ sem_ens[ens]["_email_target"] = "mailto:%s" % sem_ens[ens]["email"]
+
+ sem_ens_list = sem_ens.values()
+ sem_ens_list.sort(lambda x, y: cmp(x["nomprenom"], y["nomprenom"]))
+
+ # --- Generate page with table
+ title = "Enseignants de " + sem["titremois"]
+ T = GenTable(
+ columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"],
+ titles={
+ "nom_fmt": "Nom",
+ "prenom_fmt": "Prénom",
+ "email": "Mail",
+ "descr_mods": "Modules",
+ "nbabsadded": "Saisies Abs.",
+ },
+ rows=sem_ens_list,
+ html_sortable=True,
+ html_class="table_leftalign",
+ filename=make_filename("Enseignants-" + sem["titreannee"]),
+ html_title=self.html_sem_header(
+ REQUEST, "Enseignants du semestre", sem, with_page_header=False
+ ),
+ base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id),
+ caption="Tous les enseignants (responsables ou associés aux modules de ce semestre) apparaissent. Le nombre de saisies d'absences est le nombre d'opérations d'ajout effectuées sur ce semestre, sans tenir compte des annulations ou double saisies.",
+ preferences=self.get_preferences(formsemestre_id),
+ )
+ return T.make_page(
+ self, page_title=title, title=title, REQUEST=REQUEST, format=format
+ )
+
+ security.declareProtected(ScoView, "edit_enseignants_form_delete")
+
+ def edit_enseignants_form_delete(self, REQUEST, moduleimpl_id, ens_id):
+ "remove ens"
+ M, sem = self.can_change_ens(REQUEST, moduleimpl_id)
+ # search ens_id
+ ok = False
+ for ens in M["ens"]:
+ if ens["ens_id"] == ens_id:
+ ok = True
+ break
+ if not ok:
+ raise ScoValueError("invalid ens_id (%s)" % ens_id)
+ self.do_ens_delete(ens["modules_enseignants_id"])
+ return REQUEST.RESPONSE.redirect(
+ "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id
+ )
+
+ security.declareProtected(ScoView, "can_change_ens")
+
+ def can_change_ens(self, REQUEST, moduleimpl_id, raise_exc=True):
+ "check if current user can modify ens list (raise exception if not)"
+ M = self.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
+ # -- check lock
+ sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+ if sem["etat"] != "1":
+ if raise_exc:
+ raise ScoValueError("Modification impossible: semestre verrouille")
+ else:
+ return False
+ # -- check access
+ authuser = REQUEST.AUTHENTICATED_USER
+ uid = str(authuser)
+ # admin, resp. module ou resp. semestre
+ if (
+ uid != M["responsable_id"]
+ and not authuser.has_permission(ScoImplement, self)
+ and (uid not in sem["responsables"])
+ ):
+ if raise_exc:
+ raise AccessDenied("Modification impossible pour %s" % uid)
+ else:
+ return False
+ return M, sem
+
+ security.declareProtected(ScoView, "can_change_module_resp")
+
+ def can_change_module_resp(self, REQUEST, moduleimpl_id):
+ """Check if current user can modify module resp. (raise exception if not).
+ = Admin, et dir des etud. (si option l'y autorise)
+ """
+ M = self.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
+ # -- check lock
+ sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+ if sem["etat"] != "1":
+ raise ScoValueError("Modification impossible: semestre verrouille")
+ # -- check access
+ authuser = REQUEST.AUTHENTICATED_USER
+ uid = str(authuser)
+ # admin ou resp. semestre avec flag resp_can_change_resp
+ if not authuser.has_permission(ScoImplement, self) and (
+ (uid not in sem["responsables"]) or (not sem["resp_can_change_ens"])
+ ):
+ raise AccessDenied("Modification impossible pour %s" % uid)
+ return M, sem
+
+ # --- Gestion des inscriptions aux modules
+ _formsemestre_inscriptionEditor = EditableTable(
+ "notes_formsemestre_inscription",
+ "formsemestre_inscription_id",
+ ("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat"),
+ sortkey="formsemestre_id",
+ )
+
+ security.declareProtected(ScoEtudInscrit, "do_formsemestre_inscription_create")
+
+ def do_formsemestre_inscription_create(self, args, REQUEST, method=None):
+ "create a formsemestre_inscription (and sco event)"
+ cnx = self.GetDBConnexion()
+ log("do_formsemestre_inscription_create: args=%s" % str(args))
+ sems = sco_formsemestre.do_formsemestre_list(
+ self, {"formsemestre_id": args["formsemestre_id"]}
+ )
+ if len(sems) != 1:
+ raise ScoValueError(
+ "code de semestre invalide: %s" % args["formsemestre_id"]
+ )
+ sem = sems[0]
+ # check lock
+ if sem["etat"] != "1":
+ raise ScoValueError("inscription: semestre verrouille")
+ #
+ r = self._formsemestre_inscriptionEditor.create(cnx, args)
+ # Evenement
+ scolars.scolar_events_create(
+ cnx,
+ args={
+ "etudid": args["etudid"],
+ "event_date": time.strftime("%d/%m/%Y"),
+ "formsemestre_id": args["formsemestre_id"],
+ "event_type": "INSCRIPTION",
+ },
+ )
+ # Log etudiant
+ logdb(
+ REQUEST,
+ cnx,
+ method=method,
+ etudid=args["etudid"],
+ msg="inscription en semestre %s" % args["formsemestre_id"],
+ commit=False,
+ )
+ #
+ self._inval_cache(
+ formsemestre_id=args["formsemestre_id"]
+ ) # > inscription au semestre
+ return r
+
+ security.declareProtected(ScoImplement, "do_formsemestre_inscription_delete")
+
+ def do_formsemestre_inscription_delete(self, oid, formsemestre_id=None):
+ "delete formsemestre_inscription"
+ cnx = self.GetDBConnexion()
+ self._formsemestre_inscriptionEditor.delete(cnx, oid)
+
+ self._inval_cache(
+ formsemestre_id=formsemestre_id
+ ) # > desinscription du semestre
+
+ security.declareProtected(ScoView, "do_formsemestre_inscription_list")
+
+ def do_formsemestre_inscription_list(self, *args, **kw):
+ "list formsemestre_inscriptions"
+ cnx = self.GetDBConnexion()
+ return self._formsemestre_inscriptionEditor.list(cnx, *args, **kw)
+
+ security.declareProtected(ScoView, "do_formsemestre_inscription_listinscrits")
+
+ def do_formsemestre_inscription_listinscrits(self, formsemestre_id):
+ """Liste les inscrits (état I) à ce semestre et cache le résultat"""
+ cache = self.get_formsemestre_inscription_cache()
+ r = cache.get(formsemestre_id)
+ if r != None:
+ return r
+ # retreive list
+ r = self.do_formsemestre_inscription_list(
+ args={"formsemestre_id": formsemestre_id, "etat": "I"}
+ )
+ cache.set(formsemestre_id, r)
+ return r
+
+ security.declareProtected(ScoImplement, "do_formsemestre_inscription_edit")
+
+ def do_formsemestre_inscription_edit(self, args=None, formsemestre_id=None):
+ "edit a formsemestre_inscription"
+ cnx = self.GetDBConnexion()
+ self._formsemestre_inscriptionEditor.edit(cnx, args)
+ self._inval_cache(
+ formsemestre_id=formsemestre_id
+ ) # > modif inscription semestre (demission ?)
+
+ # Cache inscriptions semestres
+ def get_formsemestre_inscription_cache(self):
+ u = self.GetDBConnexionString()
+ if CACHE_formsemestre_inscription.has_key(u):
+ return CACHE_formsemestre_inscription[u]
+ else:
+ log("get_formsemestre_inscription_cache: new simpleCache")
+ CACHE_formsemestre_inscription[u] = sco_cache.simpleCache()
+ return CACHE_formsemestre_inscription[u]
+
+ security.declareProtected(ScoImplement, "formsemestre_desinscription")
+
+ def formsemestre_desinscription(
+ self, etudid, formsemestre_id, REQUEST=None, dialog_confirmed=False
+ ):
+ """desinscrit l'etudiant de ce semestre (et donc de tous les modules).
+ A n'utiliser qu'en cas d'erreur de saisie.
+ S'il s'agit d'un semestre extérieur et qu'il n'y a plus d'inscrit,
+ le semestre sera supprimé.
+ """
+ sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+ # -- check lock
+ if sem["etat"] != "1":
+ raise ScoValueError("desinscription impossible: semestre verrouille")
+
+ # -- Si décisions de jury, désinscription interdite
+ nt = self._getNotesCache().get_NotesTable(self, formsemestre_id)
+ if nt.etud_has_decision(etudid):
+ raise ScoValueError(
+ """Désinscription impossible: l'étudiant a une décision de jury
+ (la supprimer avant si nécessaire:
+
+ supprimer décision jury
+ )
+ """
+ % (etudid, formsemestre_id)
+ )
+ if not dialog_confirmed:
+ etud = self.getEtudInfo(etudid=etudid, filled=1)[0]
+ if sem["modalite"] != "EXT":
+ msg_ext = """
+
%s sera désinscrit de tous les modules du semestre %s (%s - %s).
+
Cette opération ne doit être utilisée que pour corriger une erreur !
+ Un étudiant réellement inscrit doit le rester, le faire éventuellement démissionner.
+
Il y a %s notes"""
+ % etat["nb_notes_total"]
+ )
+ if nb_desinscrits:
+ H.append(
+ """ (dont %s d'étudiants qui ne sont plus inscrits)"""
+ % nb_desinscrits
+ )
+ H.append(""" dans l'évaluation""")
+ if etat["nb_notes"] == 0:
+ H.append(
+ """
Vous pouvez quand même supprimer l'évaluation, les notes des étudiants désincrits seront effacées.
"
+ + " ".join([str(x) for x in bad_sem])
+ + self.sco_footer(REQUEST)
+ )
+
+ security.declareProtected(ScoView, "check_form_integrity")
+
+ def check_form_integrity(self, formation_id, fix=False, REQUEST=None):
+ "debug"
+ log("check_form_integrity: formation_id=%s fix=%s" % (formation_id, fix))
+ F = self.formation_list(args={"formation_id": formation_id})[0]
+ ues = self.do_ue_list(args={"formation_id": formation_id})
+ bad = []
+ for ue in ues:
+ mats = self.do_matiere_list(args={"ue_id": ue["ue_id"]})
+ for mat in mats:
+ mods = self.do_module_list({"matiere_id": mat["matiere_id"]})
+ for mod in mods:
+ if mod["ue_id"] != ue["ue_id"]:
+ if fix:
+ # fix mod.ue_id
+ log(
+ "fix: mod.ue_id = %s (was %s)"
+ % (ue["ue_id"], mod["ue_id"])
+ )
+ mod["ue_id"] = ue["ue_id"]
+ self.do_module_edit(mod)
+ bad.append(mod)
+ if mod["formation_id"] != formation_id:
+ bad.append(mod)
+ if bad:
+ txth = " ".join([str(x) for x in bad])
+ txt = "\n".join([str(x) for x in bad])
+ log(
+ "check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id
+ )
+ log(txt)
+ # Notify by e-mail
+ sendAlarm(self, "Notes: formation incoherente !", txt)
+ else:
+ txth = "OK"
+ log("ok")
+ return self.sco_header(REQUEST=REQUEST) + txth + self.sco_footer(REQUEST)
+
+ security.declareProtected(ScoView, "check_formsemestre_integrity")
+
+ def check_formsemestre_integrity(self, formsemestre_id, REQUEST=None):
+ "debug"
+ log("check_formsemestre_integrity: formsemestre_id=%s" % (formsemestre_id))
+ # verifie que tous les moduleimpl d'un formsemestre
+ # se réfèrent à un module dont l'UE appartient a la même formation
+ # Ancien bug: les ue_id étaient mal copiés lors des création de versions
+ # de formations
+ diag = []
+
+ Mlist = self.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+ for mod in Mlist:
+ if mod["module"]["ue_id"] != mod["matiere"]["ue_id"]:
+ diag.append(
+ "moduleimpl %s: module.ue_id=%s != matiere.ue_id=%s"
+ % (
+ mod["moduleimpl_id"],
+ mod["module"]["ue_id"],
+ mod["matiere"]["ue_id"],
+ )
+ )
+ if mod["ue"]["formation_id"] != mod["module"]["formation_id"]:
+ diag.append(
+ "moduleimpl %s: ue.formation_id=%s != mod.formation_id=%s"
+ % (
+ mod["moduleimpl_id"],
+ mod["ue"]["formation_id"],
+ mod["module"]["formation_id"],
+ )
+ )
+ if diag:
+ sendAlarm(
+ self,
+ "Notes: formation incoherente dans semestre %s !" % formsemestre_id,
+ "\n".join(diag),
+ )
+ log("check_formsemestre_integrity: formsemestre_id=%s" % formsemestre_id)
+ log("inconsistencies:\n" + "\n".join(diag))
+ else:
+ diag = ["OK"]
+ log("ok")
+ return (
+ self.sco_header(REQUEST=REQUEST)
+ + " ".join(diag)
+ + self.sco_footer(REQUEST)
+ )
+
+ security.declareProtected(ScoView, "check_integrity_all")
+
+ def check_integrity_all(self, REQUEST=None):
+ "debug: verifie tous les semestres et tt les formations"
+ # formations
+ for F in self.formation_list():
+ self.check_form_integrity(F["formation_id"], REQUEST=REQUEST)
+ # semestres
+ for sem in sco_formsemestre.do_formsemestre_list(self):
+ self.check_formsemestre_integrity(sem["formsemestre_id"], REQUEST=REQUEST)
+ return (
+ self.sco_header(REQUEST=REQUEST)
+ + "
empty page: see logs and mails
"
+ + self.sco_footer(REQUEST)
+ )
+
+ # --------------------------------------------------------------------
+
+
+# --------------------------------------------------------------------
+#
+# Zope Product Administration
+#
+# --------------------------------------------------------------------
+def manage_addZNotes(
+ self, id="id_ZNotes", title="The Title for ZNotes Object", REQUEST=None
+):
+ "Add a ZNotes instance to a folder."
+ self._setObject(id, ZNotes(id, title))
+ if REQUEST is not None:
+ return self.manage_main(self, REQUEST)
+ # return self.manage_editForm(self, REQUEST)
+
+
+# The form used to get the instance id from the user.
+manage_addZNotesForm = DTMLFile("dtml/manage_addZNotesForm", globals())
diff --git a/ZScoDoc.py b/ZScoDoc.py
new file mode 100644
index 000000000..a3f1d8902
--- /dev/null
+++ b/ZScoDoc.py
@@ -0,0 +1,954 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Site ScoDoc pour plusieurs departements:
+ gestion de l'installation et des creation de départements.
+
+ Chaque departement est géré par un ZScolar sous ZScoDoc.
+"""
+
+import time, string, glob, re, inspect
+import urllib, urllib2, cgi, xml
+
+try:
+ from cStringIO import StringIO
+except:
+ from StringIO import StringIO
+from zipfile import ZipFile
+import os.path, glob
+import traceback
+
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.MIMEBase import MIMEBase
+from email.Header import Header
+from email import Encoders
+
+from sco_zope import *
+
+#
+try:
+ import Products.ZPsycopgDA.DA as ZopeDA
+except:
+ import ZPsycopgDA.DA as ZopeDA # interp.py
+
+from sco_utils import *
+from notes_log import log
+import sco_find_etud
+from ZScoUsers import pwdFascistCheck
+
+
+class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit):
+
+ "ZScoDoc object"
+
+ meta_type = "ZScoDoc"
+ security = ClassSecurityInfo()
+ file_path = Globals.package_home(globals())
+
+ # This is the list of the methods associated to 'tabs' in the ZMI
+ # Be aware that The first in the list is the one shown by default, so if
+ # the 'View' tab is the first, you will never see your tabs by cliquing
+ # on the object.
+ manage_options = (
+ ({"label": "Contents", "action": "manage_main"},)
+ + PropertyManager.manage_options # add the 'Properties' tab
+ + (
+ # this line is kept as an example with the files :
+ # dtml/manage_editZScolarForm.dtml
+ # html/ZScolar-edit.stx
+ # {'label': 'Properties', 'action': 'manage_editForm',},
+ {"label": "View", "action": "index_html"},
+ )
+ + Item.manage_options # add the 'Undo' & 'Owner' tab
+ + RoleManager.manage_options # add the 'Security' tab
+ )
+
+ def __init__(self, id, title):
+ "Initialise a new instance of ZScoDoc"
+ self.id = id
+ self.title = title
+ self.manage_addProperty("admin_password_initialized", "0", "string")
+
+ security.declareProtected(ScoView, "ScoDocURL")
+
+ def ScoDocURL(self):
+ "base URL for this instance (top level for ScoDoc site)"
+ return self.absolute_url()
+
+ def _check_admin_perm(self, REQUEST):
+ """Check if user has permission to add/delete departements
+ """
+ authuser = REQUEST.AUTHENTICATED_USER
+ if authuser.has_role("manager") or authuser.has_permission(ScoSuperAdmin, self):
+ return ""
+ else:
+ return """
Vous n'avez pas le droit d'accéder à cette page
"""
+
+ def _check_users_folder(self, REQUEST=None):
+ """Vérifie UserFolder et le crée s'il le faut
+ """
+ try:
+ udb = self.UsersDB
+ return ""
+ except:
+ e = self._check_admin_perm(REQUEST)
+ if not e: # admin permissions:
+ self.create_users_cnx(REQUEST)
+ self.create_users_folder(REQUEST)
+ return '
Création du connecteur utilisateurs réussie
'
+ else:
+ return """
Installation non terminée: connectez vous avec les droits d'administrateur
"""
+
+ security.declareProtected("View", "create_users_folder")
+
+ def create_users_folder(self, REQUEST=None):
+ """Create Zope user folder
+ """
+ e = self._check_admin_perm(REQUEST)
+ if e:
+ return e
+
+ if REQUEST is None:
+ REQUEST = {}
+
+ REQUEST.form["pgauth_connection"] = "UsersDB"
+ REQUEST.form["pgauth_table"] = "sco_users"
+ REQUEST.form["pgauth_usernameColumn"] = "user_name"
+ REQUEST.form["pgauth_passwordColumn"] = "passwd"
+ REQUEST.form["pgauth_rolesColumn"] = "roles"
+
+ add_method = self.manage_addProduct["OFSP"].manage_addexUserFolder
+ log("create_users_folder: in %s" % self.id)
+ return add_method(
+ authId="pgAuthSource",
+ propId="nullPropSource",
+ memberId="nullMemberSource",
+ groupId="nullGroupSource",
+ cryptoId="MD51",
+ # doAuth='1', doProp='1', doMember='1', doGroup='1', allDone='1',
+ cookie_mode=2,
+ session_length=500,
+ not_session_length=0,
+ REQUEST=REQUEST,
+ )
+
+ def _fix_users_folder(self):
+ """removes docLogin and docLogout dtml methods from exUserFolder, so that we use ours.
+ (called each time be index_html, to fix old ScoDoc installations.)
+ """
+ try:
+ self.acl_users.manage_delObjects(ids=["docLogin", "docLogout"])
+ except:
+ pass
+ # add missing getAuthFailedMessage (bug in exUserFolder ?)
+ try:
+ x = self.getAuthFailedMessage
+ except:
+ log("adding getAuthFailedMessage to Zope install")
+ parent = self.aq_parent
+ from OFS.DTMLMethod import addDTMLMethod
+
+ addDTMLMethod(parent, "getAuthFailedMessage", file="Identification")
+
+ security.declareProtected("View", "create_users_cnx")
+
+ def create_users_cnx(self, REQUEST=None):
+ """Create Zope connector to UsersDB
+
+ Note: la connexion est fixée (SCOUSERS) (base crée par l'installeur) !
+ Les utilisateurs avancés pourront la changer ensuite.
+ """
+ # ce connecteur zope - db est encore pour l'instant utilisé par exUserFolder.pgAuthSource
+ # (en lecture seule en principe)
+ oid = "UsersDB"
+ log("create_users_cnx: in %s" % self.id)
+ da = ZopeDA.Connection(
+ oid,
+ "Cnx bd utilisateurs",
+ SCO_DEFAULT_SQL_USERS_CNX,
+ False,
+ check=1,
+ tilevel=2,
+ encoding="LATIN1",
+ )
+ self._setObject(oid, da)
+
+ security.declareProtected("View", "change_admin_user")
+
+ def change_admin_user(self, password, REQUEST=None):
+ """Change password of admin user"""
+ # note: controle sur le role et non pas sur une permission
+ # (non definies au top level)
+ if not REQUEST.AUTHENTICATED_USER.has_role("Manager"):
+ log("user %s is not Manager" % REQUEST.AUTHENTICATED_USER)
+ log("roles=%s" % REQUEST.AUTHENTICATED_USER.getRolesInContext(self))
+ raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+ log("trying to change admin password")
+ # 1-- check strong password
+ if pwdFascistCheck(password) != None:
+ log("refusing weak password")
+ return REQUEST.RESPONSE.redirect(
+ "change_admin_user_form?message=Mot%20de%20passe%20trop%20simple,%20recommencez"
+ )
+ # 2-- change password for admin user
+ username = "admin"
+ acl_users = self.aq_parent.acl_users
+ user = acl_users.getUser(username)
+ r = acl_users._changeUser(
+ username, password, password, user.roles, user.domains
+ )
+ if not r:
+ # OK, set property to indicate we changed the password
+ log("admin password changed successfully")
+ self.manage_changeProperties(admin_password_initialized="1")
+ return r or REQUEST.RESPONSE.redirect("index_html")
+
+ security.declareProtected("View", "change_admin_user_form")
+
+ def change_admin_user_form(self, message="", REQUEST=None):
+ """Form allowing to change the ScoDoc admin password"""
+ # note: controle sur le role et non pas sur une permission
+ # (non definies au top level)
+ if not REQUEST.AUTHENTICATED_USER.has_role("Manager"):
+ raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+ H = [
+ self.scodoc_top_html_header(
+ REQUEST, page_title="ScoDoc: changement mot de passe"
+ )
+ ]
+ if message:
+ H.append('
%s
' % message)
+ H.append(
+ """
Changement du mot de passe administrateur (utilisateur admin)