From 415810496f36b3d4baed4bb28291c16d8bc1a5ac Mon Sep 17 00:00:00 2001 From: viennet Date: Tue, 15 Dec 2020 08:35:44 +0100 Subject: [PATCH] Work in progress: new updater and code cleaning --- ImportScolars.py | 2 +- ZScoDoc.py | 55 +++++++++++---- config/scodoc_config_tmpl.py | 128 +++++++++++++++++++++++++++++++++ config/upgrade.sh | 13 ++++ notes_log.py | 9 +-- pe_avislatex.py | 25 ++++--- pe_tools.py | 10 ++- sco_abs_notification.py | 22 +++--- sco_bulletins.py | 14 ++-- sco_codes_parcours.py | 3 +- sco_config.py | 109 ++++++++++++++++++++++++++++ sco_config_load.py | 44 ++++++++++++ sco_dump_db.py | 22 +++--- sco_news.py | 11 ++- sco_photos.py | 4 +- sco_portal_apogee.py | 4 +- sco_preferences.py | 9 +-- sco_utils.py | 133 ++++++++++++++++------------------- sco_zope.py | 2 + scolars.py | 11 ++- 20 files changed, 460 insertions(+), 170 deletions(-) create mode 100644 config/scodoc_config_tmpl.py create mode 100644 sco_config.py create mode 100644 sco_config_load.py diff --git a/ImportScolars.py b/ImportScolars.py index 47c18bdb..98e83ca8 100644 --- a/ImportScolars.py +++ b/ImportScolars.py @@ -93,7 +93,7 @@ ADMISSION_MODIFIABLE_FIELDS = ( def sco_import_format(with_codesemestre=True): "returns tuples (Attribut, Type, Table, AllowNulls, Description)" r = [] - for l in open(SCO_SRCDIR + "/" + FORMAT_FILE): + for l in open(SCO_SRC_DIR + "/" + FORMAT_FILE): l = l.strip() if l and l[0] != "#": fs = l.split(";") diff --git a/ZScoDoc.py b/ZScoDoc.py index 83732b4d..4205ed51 100644 --- a/ZScoDoc.py +++ b/ZScoDoc.py @@ -33,6 +33,7 @@ import time, string, glob, re, inspect import urllib, urllib2, cgi, xml +import datetime try: from cStringIO import StringIO @@ -42,13 +43,22 @@ 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 email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email.header import Header -from sco_zope import * +from sco_zope import ( + ObjectManager, + PropertyManager, + RoleManager, + Item, + Persistent, + Implicit, + ClassSecurityInfo, + DTMLFile, + Globals, +) # try: @@ -56,7 +66,21 @@ try: except: import ZPsycopgDA.DA as ZopeDA # interp.py -from sco_utils import * +from sco_utils import ( + SCO_DEFAULT_SQL_USERS_CNX, + SCO_ENCODING, + CUSTOM_HTML_HEADER_CNX, + SCO_USERS_LIST, + SCO_WEBSITE, + SCO_EXC_MAIL, + SCO_DEV_MAIL, + scodoc_html2txt, + get_svn_version, + VERSION, + SCODOC_CFG_DIR, +) +from sco_permissions import ScoView, ScoSuperAdmin +from sco_exceptions import AccessDenied from notes_log import log import sco_find_etud from ZScoUsers import pwdFascistCheck @@ -111,7 +135,7 @@ class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Imp def _check_users_folder(self, REQUEST=None): """Vérifie UserFolder et le crée s'il le faut""" try: - udb = self.UsersDB + self.UsersDB return "" except: e = self._check_admin_perm(REQUEST) @@ -164,11 +188,11 @@ class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Imp pass # add missing getAuthFailedMessage (bug in exUserFolder ?) try: - x = self.getAuthFailedMessage + self.getAuthFailedMessage except: log("adding getAuthFailedMessage to Zope install") parent = self.aq_parent - from OFS.DTMLMethod import addDTMLMethod + from OFS.DTMLMethod import addDTMLMethod # pylint: disable=import-error addDTMLMethod(parent, "getAuthFailedMessage", file="Identification") @@ -264,7 +288,7 @@ class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Imp r = [] for folder in folders: try: - s = folder.Scolarite + _ = folder.Scolarite r.append(folder) except: pass @@ -646,8 +670,8 @@ Problème de connexion (identifiant, mot de passe): contacter votre responsa **kv ): "Recuperation des exceptions Zope" - sco_exc_mail = SCO_EXC_MAIL - sco_dev_mail = SCO_DEV_MAIL + sco_exc_mail = SCO_EXC_MAIL # pylint: disable=unused-variable + sco_dev_mail = SCO_DEV_MAIL # pylint: disable=unused-variable # neat (or should I say dirty ?) hack to get REQUEST # in fact, our caller (probably SimpleItem.py) has the REQUEST variable # that we'd like to use for our logs, but does not pass it as an argument. @@ -714,6 +738,7 @@ Problème de connexion (identifiant, mot de passe): contacter votre responsa ) # display error traceback (? may open a security risk via xss attack ?) # log('exc B') + # pylint: disable=unused-variable txt_html = self._report_request(REQUEST, fmt="html") H.append( """

Zope Traceback (à envoyer par mail à %(sco_dev_mail)s)

@@ -734,6 +759,7 @@ Problème de connexion (identifiant, mot de passe): contacter votre responsa pass # --- Mail: + # pylint: disable=unused-variable error_traceback_txt = scodoc_html2txt(error_tb) txt = ( """ @@ -752,6 +778,7 @@ ErrorType: %(error_type)s def _report_request(self, REQUEST, fmt="txt"): """string describing current request for bug reports""" + # pylint: disable=unused-variable AUTHENTICATED_USER = REQUEST.get("AUTHENTICATED_USER", "") dt = time.asctime() URL = REQUEST.get("URL", "") @@ -887,7 +914,7 @@ subversion: %(svn_version)s """Liste de id de departements definis par create_dept.sh (fichiers depts/*.cfg) """ - filenames = glob.glob(SCODOC_VAR_DIR + "/config/depts/*.cfg") + filenames = glob.glob(SCODOC_CFG_DIR + "/config/depts/*.cfg") ids = [os.path.split(os.path.splitext(f)[0])[1] for f in filenames] return ids diff --git a/config/scodoc_config_tmpl.py b/config/scodoc_config_tmpl.py new file mode 100644 index 00000000..55cab115 --- /dev/null +++ b/config/scodoc_config_tmpl.py @@ -0,0 +1,128 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +# +# Configuration globale de ScoDoc (version juin 2009) +# Ce fichier est copié dans /opt/scodoc/var/scodoc/config +# par les scripts d'installation/mise à jour. + +# La plupart des réglages sont stoqués en base de donnée et accessibles via le web +# (pages de paramètres ou préférences). +# Les valeurs indiquées ici sont les valeurs initiales que prendront +# les paramètres lors de la création d'un nouveau département, +# elles ne sont plus utilisées ensuite. + +# Nota: il y a aussi des réglages dans sco_utils.py, mais ils nécessitent +# souvent de comprendre le code qui les utilise pour ne pas faire d'erreur: attention. + + +class CFG: + pass + + +CONFIG = CFG() + +# set to 1 if you want to require INE: +# CONFIG.always_require_ine = 0 + +# The base URL, use only if you are behind a proxy +# eg "https://scodoc.example.net/ScoDoc" +# CONFIG.ABSOLUTE_URL = "" + +# ----------------------------------------------------- +# -------------- Documents PDF +# ----------------------------------------------------- + +# Taille du l'image logo: largeur/hauteur (ne pas oublier le . !!!) +# W/H XXX provisoire: utilisera PIL pour connaitre la taille de l'image +# CONFIG.LOGO_FOOTER_ASPECT = 326 / 96.0 +# Taille dans le document en millimetres +# CONFIG.LOGO_FOOTER_HEIGHT = 10 +# Proportions logo (donné ici pour IUTV) +# CONFIG.LOGO_HEADER_ASPECT = 549 / 346.0 +# Taille verticale dans le document en millimetres +# CONFIG.LOGO_HEADER_HEIGHT = 28 + +# Pied de page PDF : un format Python, %(xxx)s est remplacé par la variable xxx. +# Les variables définies sont: +# day : Day of the month as a decimal number [01,31] +# month : Month as a decimal number [01,12]. +# year : Year without century as a decimal number [00,99]. +# Year : Year with century as a decimal number. +# hour : Hour (24-hour clock) as a decimal number [00,23]. +# minute: Minute as a decimal number [00,59]. +# +# server_url: URL du serveur ScoDoc +# scodoc_name: le nom du logiciel (ScoDoc actuellement, voir VERSION.py) + +# CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s" + + +# +# ------------- Calcul bonus modules optionnels (sport, culture...) ------------- +# + +# CONFIG.compute_bonus = bonus_sport.bonus_iutv +# Mettre "bonus_demo" pour logguer des informations utiles au developpement... + +# +# ------------- Capitalisation des UEs ------------- +# Deux écoles: +# - règle "DUT": capitalisation des UE obtenues avec moyenne UE >= 10 ET de toutes les UE +# des semestres validés (ADM, ADC, AJ). (conforme à l'arrêté d'août 2005) +# +# - règle "LMD": capitalisation uniquement des UE avec moy. > 10 + +# Si vrai, capitalise toutes les UE des semestres validés (règle "DUT"). +# CONFIG.CAPITALIZE_ALL_UES = True + + +# +# ----------------------------------------------------- +# +# -------------- Personnalisation des pages +# +# ----------------------------------------------------- +# Nom (chemin complet) d'un fichier .html à inclure juste après le +# le des pages ScoDoc +# CONFIG.CUSTOM_HTML_HEADER = "" + +# Fichier html a inclure en fin des pages (juste avant le ) +# CONFIG.CUSTOM_HTML_FOOTER = "" + +# Fichier .html à inclure dans la pages connexion/déconnexion (accueil) +# si on veut que ce soit différent (par défaut la même chose) +# CONFIG.CUSTOM_HTML_HEADER_CNX = CONFIG.CUSTOM_HTML_HEADER +# CONFIG.CUSTOM_HTML_FOOTER_CNX = CONFIG.CUSTOM_HTML_FOOTER + + +# ----------------------------------------------------- +# -------------- Noms de Lycées +# ----------------------------------------------------- +# Fichier de correspondance codelycee -> noms +# (chemin relatif au repertoire d'install des sources) +# CONFIG.ETABL_FILENAME = "config/etablissements.csv" + + +# ---------------------------------------------------- +# -------------- Divers: +# ---------------------------------------------------- +# True for UCAC (étudiants camerounais sans prénoms) +# CONFIG.ALLOW_NULL_PRENOM = False + +# Taille max des fichiers archive etudiants (en octets) +# CONFIG.ETUD_MAX_FILE_SIZE = 10 * 1024 * 1024 + +# Si pas de photo et portail, publie l'url (était vrai jusqu'en oct 2016) +# CONFIG.PUBLISH_PORTAL_PHOTO_URL = False + +# Si > 0: longueur minimale requise des nouveaux mots de passe +# (le test cracklib.FascistCheck s'appliquera dans tous les cas) +# CONFIG.MIN_PASSWORD_LENGTH = 0 + + +# Ce dictionnaire est fusionné à celui de sco_codes_parcours +# pour définir les codes jury et explications associées +# CONFIG.CODES_EXPL = { +# # AJ : 'Ajourné (échec)', +# } diff --git a/config/upgrade.sh b/config/upgrade.sh index 894fc078..3bec0947 100755 --- a/config/upgrade.sh +++ b/config/upgrade.sh @@ -118,6 +118,19 @@ if [ $? -ne 0 ] then /opt/zope213/bin/pip install requests fi +/opt/zope213/bin/python -c "import attrdict" >& /dev/null +if [ $? -ne 0 ] +then + /opt/zope213/bin/pip install attrdict +fi + +# Check that local configuration file is installed +LOCAL_CONFIG_FILENAME="/opt/scodoc/var/scodoc/config/scodoc_local.py" +if [ ! -e "$LOCAL_CONFIG_FILENAME" ] +then + cp "$SCODOC_DIR"/config/scodoc_config_tmpl.py "$LOCAL_CONFIG_FILENAME" + chmod 600 "$LOCAL_CONFIG_FILENAME" +fi # Ensure www-data can duplicate databases (for dumps) su -c $'psql -c \'alter role "www-data" with CREATEDB;\'' "$POSTGRES_SUPERUSER" diff --git a/notes_log.py b/notes_log.py index a2b29a9e..89a2a681 100644 --- a/notes_log.py +++ b/notes_log.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- import pdb, os, sys, time, re, inspect -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -from email.Header import Header import traceback +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email.header import Header # Simple & stupid file logguer, used only to debug # (logging to SQL is done in scolog) @@ -88,7 +89,7 @@ def retreive_dept(): return "" try: url = REQUEST.URL - m = re.match("^.*ScoDoc/(\w+).*$", url) + m = re.match(r"^.*ScoDoc/(\w+).*$", url) return m.group(1) except: return "" diff --git a/pe_avislatex.py b/pe_avislatex.py index 06fda736..eb6858d5 100644 --- a/pe_avislatex.py +++ b/pe_avislatex.py @@ -50,7 +50,7 @@ DONNEE_MANQUANTE = ( # ---------------------------------------------------------------------------------------- def get_code_latex_from_modele(fichier): """Lit le code latex à partir d'un modèle. Renvoie une chaine unicode. - + Le fichier doit contenir le chemin relatif vers le modele : attention pas de vérification du format d'encodage Le fichier doit donc etre enregistré avec le même codage que ScoDoc (utf-8) @@ -85,7 +85,7 @@ def get_tags_latex(code_latex): à la lecture d'un modèle d'avis pe). Ces tags sont répérés par les balises **, débutant et finissant le tag et sont renvoyés sous la forme d'une liste. - + result: liste de chaines unicode """ if code_latex: @@ -144,7 +144,7 @@ def comp_latex_parcourstimeline(etudiant, promo, taille=17): # ---------------------------------------------------------------------------------------- def interprete_tag_latex(tag): - """Découpe les tags latex de la forme S1:groupe:dut:min et renvoie si possible + """Découpe les tags latex de la forme S1:groupe:dut:min et renvoie si possible le résultat sous la forme d'un quadruplet. """ infotag = tag.split(":") @@ -164,7 +164,7 @@ def get_code_latex_avis_etudiant( donnees_etudiant, un_avis_latex, annotationPE, footer_latex, prefs ): """ - Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses + Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses donnees_etudiant contenu dans le dictionnaire de synthèse du jury PE et en suivant un fichier modele donné @@ -228,8 +228,8 @@ def get_code_latex_avis_etudiant( # ---------------------------------------------------------------------------------------- def get_annotation_PE(context, etudid, tag_annotation_pe): - """Renvoie l'annotation PE dans la liste de ces annotations ; - Cette annotation est reconnue par la présence d'un tag **PE** + """Renvoie l'annotation PE dans la liste de ces annotations ; + Cette annotation est reconnue par la présence d'un tag **PE** (cf. context.get_preferences -> pe_tag_annotation_avis_latex). Result: chaine unicode @@ -269,8 +269,8 @@ def get_annotation_PE(context, etudid, tag_annotation_pe): # ---------------------------------------------------------------------------------------- def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ): - """Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée, - une valeur indiquée par un champ ; + """Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée, + une valeur indiquée par un champ ; si champ est une liste, renvoie la liste des valeurs extraites. Result: chaine unicode ou liste de chaines unicode @@ -322,7 +322,7 @@ def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ) # ---------------------------------------------------------------------------------------- def get_bilanParTag(donnees_etudiant, groupe="groupe"): - """Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans + """Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans les données étudiants, ses résultats. result: chaine unicode """ @@ -460,11 +460,11 @@ def get_templates_from_distrib(template="avis"): if template in ["avis", "footer"]: # pas de preference pour le template: utilise fichier du serveur - p = os.path.join(SCO_SRCDIR, pe_local_tmpl) + p = os.path.join(SCO_SRC_DIR, pe_local_tmpl) if os.path.exists(p): template_latex = get_code_latex_from_modele(p) else: - p = os.path.join(SCO_SRCDIR, pe_default_tmpl) + p = os.path.join(SCO_SRC_DIR, pe_default_tmpl) if os.path.exists(p): template_latex = get_code_latex_from_modele(p) else: @@ -474,8 +474,7 @@ def get_templates_from_distrib(template="avis"): # ---------------------------------------------------------------------------------------- def table_syntheseAnnotationPE(context, syntheseJury, tag_annotation_pe): - """Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant - """ + """Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant""" sT = SeqGenTable() # le fichier excel à générer # Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom diff --git a/pe_tools.py b/pe_tools.py index d501cc48..cb854ccc 100644 --- a/pe_tools.py +++ b/pe_tools.py @@ -97,7 +97,7 @@ def print_semestres_description(sems, avec_affichage_debug=False): # ---------------------------------------------------------------------------------------- def calcul_age(born): - """Calcule l'age à partir de la date de naissance sous forme d'une chaine de caractère 'jj/mm/aaaa'. + """Calcule l'age à partir de la date de naissance sous forme d'une chaine de caractère 'jj/mm/aaaa'. Aucun test de validité sur le format de la date n'est fait. """ if not isinstance(born, str) or born == "": @@ -122,8 +122,7 @@ def remove_accents(input_unicode_str): def escape_for_latex(s): - """Protège les caractères pour inclusion dans du source LaTeX - """ + """Protège les caractères pour inclusion dans du source LaTeX""" if not s: return "" conv = { @@ -162,8 +161,7 @@ def list_directory_filenames(path): def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip): - """Read pathname server file and add content to zip under path_in_zip - """ + """Read pathname server file and add content to zip under path_in_zip""" rooted_path_in_zip = os.path.join(ziproot, path_in_zip) data = open(pathname).read() zipfile.writestr(rooted_path_in_zip, data) @@ -177,7 +175,7 @@ def add_pe_stuff_to_zip(context, zipfile, ziproot): Also copy logos """ - PE_AUX_DIR = os.path.join(SCO_SRCDIR, "config/doc_poursuites_etudes") + PE_AUX_DIR = os.path.join(SCO_SRC_DIR, "config/doc_poursuites_etudes") distrib_dir = os.path.join(PE_AUX_DIR, "distrib") distrib_pathnames = list_directory_filenames( distrib_dir diff --git a/sco_abs_notification.py b/sco_abs_notification.py index 7af6e578..b77042e9 100644 --- a/sco_abs_notification.py +++ b/sco_abs_notification.py @@ -32,10 +32,9 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence. """ -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -from email.Header import Header -from email import Encoders +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.header import Header from notesdb import * from sco_utils import * @@ -47,7 +46,7 @@ import sco_formsemestre def abs_notify(context, etudid, date): """Check if notifications are requested and send them - Considère le nombre d'absence dans le semestre courant + Considère le nombre d'absence dans le semestre courant (s'il n'y a pas de semestre courant, ne fait rien, car l'etudiant n'est pas inscrit au moment de l'absence!). """ @@ -64,8 +63,7 @@ def abs_notify(context, etudid, date): def do_abs_notify(context, sem, etudid, date, nbabs, nbabsjust): - """Given new counts of absences, check if notifications are requested and send them. - """ + """Given new counts of absences, check if notifications are requested and send them.""" # prefs fallback to global pref if sem is None: if sem: formsemestre_id = sem["formsemestre_id"] @@ -131,8 +129,7 @@ def abs_notify_send( def abs_notify_get_destinations(context, sem, prefs, etudid, date, nbabs, nbabsjust): - """Returns set of destination emails to be notified - """ + """Returns set of destination emails to be notified""" formsemestre_id = sem["formsemestre_id"] destinations = [] # list of email address to notify @@ -176,8 +173,8 @@ def abs_notify_is_above_threshold(context, etudid, nbabs, nbabsjust, formsemestr nbabs: nombre d'absence (de tous types, unité de compte = demi-journée) nbabsjust: nombre d'absences justifiées - - (nbabs > abs_notify_abs_threshold) + + (nbabs > abs_notify_abs_threshold) (nbabs - nbabs_last_notified) > abs_notify_abs_increment """ abs_notify_abs_threshold = context.get_preference( @@ -282,8 +279,7 @@ def retreive_current_formsemestre(context, etudid, cur_date): def mod_with_evals_at_date(context, date_abs, etudid): - """Liste des moduleimpls avec des evaluations a la date indiquée - """ + """Liste des moduleimpls avec des evaluations a la date indiquée""" req = """SELECT m.* FROM notes_moduleimpl m, notes_evaluation e, notes_moduleimpl_inscription i WHERE m.moduleimpl_id = e.moduleimpl_id AND e.moduleimpl_id = i.moduleimpl_id AND i.etudid = %(etudid)s AND e.jour = %(date_abs)s""" diff --git a/sco_bulletins.py b/sco_bulletins.py index e20d72de..d6b935c3 100644 --- a/sco_bulletins.py +++ b/sco_bulletins.py @@ -28,11 +28,11 @@ """Génération des bulletins de notes """ -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -from email.MIMEBase import MIMEBase -from email.Header import Header -from email import Encoders +import email +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email.header import Header import htmlutils, time from reportlab.lib.colors import Color @@ -331,7 +331,7 @@ def formsemestre_bulletinetud_dict( context, ue_status["formsemestre_id"] ) # > toutes notes - u["modules_capitalized"], junk = _ue_mod_bulletin( + u["modules_capitalized"], _ = _ue_mod_bulletin( context, etudid, formsemestre_id, @@ -990,7 +990,7 @@ def mail_bulletin(context, formsemestre_id, I, pdfdata, filename, recipient_addr att = MIMEBase("application", "pdf") att.add_header("Content-Disposition", "attachment", filename=filename) att.set_payload(pdfdata) - Encoders.encode_base64(att) + email.encoders.encode_base64(att) msg.attach(att) log("mail bulletin a %s" % msg["To"]) context.sendEmail(msg) diff --git a/sco_codes_parcours.py b/sco_codes_parcours.py index 5a57b63d..d90d8493 100644 --- a/sco_codes_parcours.py +++ b/sco_codes_parcours.py @@ -116,7 +116,8 @@ CODES_EXPL = { RAT: "En attente d'un rattrapage", DEF: "Défaillant", } -# Nota: ces explications sont personnalisables via le fichier de config scodoc_config.py +# Nota: ces explications sont personnalisables via le fichier +# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py # variable: CONFIG.CODES_EXP CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé diff --git a/sco_config.py b/sco_config.py new file mode 100644 index 00000000..83f13e35 --- /dev/null +++ b/sco_config.py @@ -0,0 +1,109 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +"""Configuration de ScoDoc (version 2020) + NE PAS MODIFIER localement ce fichier ! + mais éditer /opt/scodoc/var/scodoc/config/scodoc_local.py +""" + +from attrdict import AttrDict +import bonus_sport + +CONFIG = AttrDict() + +# set to 1 if you want to require INE: +CONFIG.always_require_ine = 0 + +# The base URL, use only if you are behind a proxy +# eg "https://scodoc.example.net/ScoDoc" +CONFIG.ABSOLUTE_URL = "" + +# ----------------------------------------------------- +# -------------- Documents PDF +# ----------------------------------------------------- + +# Taille du l'image logo: largeur/hauteur (ne pas oublier le . !!!) +# W/H XXX provisoire: utilisera PIL pour connaitre la taille de l'image +CONFIG.LOGO_FOOTER_ASPECT = 326 / 96.0 +# Taille dans le document en millimetres +CONFIG.LOGO_FOOTER_HEIGHT = 10 +# Proportions logo (donné ici pour IUTV) +CONFIG.LOGO_HEADER_ASPECT = 549 / 346.0 +# Taille verticale dans le document en millimetres +CONFIG.LOGO_HEADER_HEIGHT = 28 + + +# Pied de page PDF : un format Python, %(xxx)s est remplacé par la variable xxx. +# Les variables définies sont: +# day : Day of the month as a decimal number [01,31] +# month : Month as a decimal number [01,12]. +# year : Year without century as a decimal number [00,99]. +# Year : Year with century as a decimal number. +# hour : Hour (24-hour clock) as a decimal number [00,23]. +# minute: Minute as a decimal number [00,59]. +# +# server_url: URL du serveur ScoDoc +# scodoc_name: le nom du logiciel (ScoDoc actuellement, voir VERSION.py) +CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s" + +# +# ------------- Calcul bonus modules optionnels (sport, culture...) ------------- +# + +CONFIG.compute_bonus = bonus_sport.bonus_iutv +# Mettre "bonus_demo" pour logguer des informations utiles au developpement... + +# ------------- Capitalisation des UEs ------------- +# Deux écoles: +# - règle "DUT": capitalisation des UE obtenues avec moyenne UE >= 10 ET de toutes les UE +# des semestres validés (ADM, ADC, AJ). (conforme à l'arrêté d'août 2005) +# +# - règle "LMD": capitalisation uniquement des UE avec moy. > 10 + +# Si vrai, capitalise toutes les UE des semestres validés (règle "DUT"). +# CONFIG.CAPITALIZE_ALL_UES = True + +# ----------------------------------------------------- +# -------------- Personnalisation des pages +# ----------------------------------------------------- +# Nom (chemin complet) d'un fichier .html à inclure juste après le +# le des pages ScoDoc +CONFIG.CUSTOM_HTML_HEADER = "" + +# Fichier html a inclure en fin des pages (juste avant le ) +CONFIG.CUSTOM_HTML_FOOTER = "" + +# Fichier .html à inclure dans la pages connexion/déconnexion (accueil) +# si on veut que ce soit différent (par défaut la même chose) +CONFIG.CUSTOM_HTML_HEADER_CNX = CONFIG.CUSTOM_HTML_HEADER +CONFIG.CUSTOM_HTML_FOOTER_CNX = CONFIG.CUSTOM_HTML_FOOTER + +# ----------------------------------------------------- +# -------------- Noms de Lycées +# ----------------------------------------------------- + +# Fichier de correspondance codelycee -> noms +# (chemin relatif au repertoire d'install des sources) +CONFIG.ETABL_FILENAME = "config/etablissements.csv" + +# ---------------------------------------------------- +# -------------- Divers: +# ---------------------------------------------------- +# True for UCAC (étudiants camerounais sans prénoms) +CONFIG.ALLOW_NULL_PRENOM = False + +# Taille max des fichiers archive etudiants (en octets) +# CONFIG.ETUD_MAX_FILE_SIZE = 10 * 1024 * 1024 + +# Si pas de photo et portail, publie l'url (était vrai jusqu'en oct 2016) +CONFIG.PUBLISH_PORTAL_PHOTO_URL = False + +# Si > 0: longueur minimale requise des nouveaux mots de passe +# (le test cracklib.FascistCheck s'appliquera dans tous les cas) +CONFIG.MIN_PASSWORD_LENGTH = 0 + +# Ce dictionnaire est fusionné à celui de sco_codes_parcours +# pour définir les codes jury et explications associées +CONFIG.CODES_EXPL = { + # AJ : 'Ajourné (échec)', +} diff --git a/sco_config_load.py b/sco_config_load.py new file mode 100644 index 00000000..b980c9d4 --- /dev/null +++ b/sco_config_load.py @@ -0,0 +1,44 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +"""Chargement de la configuration locale +""" +import os +import sys + +import sco_utils +from sco_utils import log, SCODOC_CFG_DIR +import sco_config + +# scodoc_local defines a CONFIG object +# here we check if there is a local config file + + +def load_local_configuration(): + """Load local configuration file (if exists) + and merge it with CONFIG. + """ + # this path should be synced with upgrade.sh + LOCAL_CONFIG_FILENAME = os.path.join(SCODOC_CFG_DIR, "scodoc_local.py") + LOCAL_CONFIG = None + if os.path.exists(LOCAL_CONFIG_FILENAME): + if not SCODOC_CFG_DIR in sys.path: + sys.path.insert(1, SCODOC_CFG_DIR) + try: + from scodoc_local import CONFIG as LOCAL_CONFIG + + log("imported %s" % LOCAL_CONFIG_FILENAME) + except ImportError: + log("Error: can't import %s" % LOCAL_CONFIG_FILENAME) + del sys.path[1] + if LOCAL_CONFIG is None: + return + # Now merges local config in our CONFIG + for x in [x for x in dir(LOCAL_CONFIG) if x[0] != "_"]: + v = getattr(LOCAL_CONFIG, x) + if not v in sco_config.CONFIG: + log("Warning: local config setting unused parameter %s (skipped)" % x) + else: + if v != sco_config.CONFIG[x]: + log("Setting parameter %s from %s" % (x, LOCAL_CONFIG_FILENAME)) + sco_config.CONFIG[x] = v diff --git a/sco_dump_db.py b/sco_dump_db.py index 53d007b0..54f2a230 100644 --- a/sco_dump_db.py +++ b/sco_dump_db.py @@ -50,11 +50,10 @@ pg_dump SCORT | psql ANORT import fcntl import subprocess import requests -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 email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email.header import Header from notesdb import * from sco_utils import * @@ -64,8 +63,7 @@ SCO_DUMP_LOCK = "/tmp/scodump.lock" def sco_dump_and_send_db(context, REQUEST=None): - """Dump base de données du département courant et l'envoie anonymisée pour debug - """ + """Dump base de données du département courant et l'envoie anonymisée pour debug""" H = [context.sco_header(REQUEST, page_title="Assistance technique")] # get currect (dept) DB name: cursor = SimpleQuery(context, "SELECT current_database()", {}) @@ -150,9 +148,8 @@ def _duplicate_db(db_name, ano_db_name): def _anonymize_db(ano_db_name): - """Anonymize a departement database - """ - cmd = os.path.join(SCO_CONFIG_DIR, "anonymize_db.py") + """Anonymize a departement database""" + cmd = os.path.join(SCO_TOOLS_DIR, "anonymize_db.py") log("_anonymize_db: {}".format(cmd)) try: out = subprocess.check_output([cmd, ano_db_name]) @@ -171,8 +168,7 @@ def _get_scodoc_serial(context): def _send_db(context, REQUEST, ano_db_name): - """Dump this (anonymized) database and send it to tech support - """ + """Dump this (anonymized) database and send it to tech support""" log("dumping anonymized database {}".format(ano_db_name)) try: data = subprocess.check_output("pg_dump {} | gzip".format(ano_db_name), shell=1) @@ -195,7 +191,7 @@ def _send_db(context, REQUEST, ano_db_name): "nomcomplet" ], "sco_version": SCOVERSION, - "sco_subversion": get_svn_version(SCO_CONFIG_DIR), + "sco_subversion": get_svn_version(SCO_TOOLS_DIR), }, ) return r diff --git a/sco_news.py b/sco_news.py index 5b5d2368..27a46109 100644 --- a/sco_news.py +++ b/sco_news.py @@ -33,11 +33,9 @@ from cStringIO import StringIO import datetime, re import time from stripogram import html2text, html2safehtml -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -from email.Header import Header -from email import Encoders - +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.header import Header from notesdb import * from notes_log import log @@ -252,8 +250,7 @@ def scolar_news_summary_rss(context, title, sco_url, n=5): def _send_news_by_mail(context, n): - """Notify by email - """ + """Notify by email""" infos = _get_formsemestre_infos_from_news(context, n) formsemestre_id = infos.get("formsemestre_id", None) prefs = context.get_preferences(formsemestre_id=formsemestre_id) diff --git a/sco_photos.py b/sco_photos.py index 12d126df..597fdfd7 100644 --- a/sco_photos.py +++ b/sco_photos.py @@ -53,7 +53,7 @@ from PIL import Image as PILImage from cStringIO import StringIO import glob -from sco_utils import CONFIG, SCO_SRCDIR +from sco_utils import CONFIG, SCO_SRC_DIR from notes_log import log import scolars @@ -62,7 +62,7 @@ from scolog import logdb # Full paths on server's filesystem. Something like "/opt/scodoc/var/scodoc/photos" PHOTO_DIR = os.path.join(os.environ["INSTANCE_HOME"], "var", "scodoc", "photos") -ICONS_DIR = os.path.join(SCO_SRCDIR, "static", "icons") +ICONS_DIR = os.path.join(SCO_SRC_DIR, "static", "icons") UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg") UNKNOWN_IMAGE_URL = "get_photo_image?etudid=" # with empty etudid => unknown face image IMAGE_EXT = ".jpg" diff --git a/sco_portal_apogee.py b/sco_portal_apogee.py index f1e30162..108db046 100644 --- a/sco_portal_apogee.py +++ b/sco_portal_apogee.py @@ -37,9 +37,9 @@ import datetime import sco_utils from sco_utils import ScoEtudInscrit, log, ScoValueError, DictDefault -from sco_utils import SCO_TMPDIR, SCO_ENCODING +from sco_utils import SCO_TMP_DIR, SCO_ENCODING -SCO_CACHE_ETAPE_FILENAME = os.path.join(SCO_TMPDIR, "last_etapes.xml") +SCO_CACHE_ETAPE_FILENAME = os.path.join(SCO_TMP_DIR, "last_etapes.xml") def has_portal(context): diff --git a/sco_preferences.py b/sco_preferences.py index 75b5989d..c5912b2b 100644 --- a/sco_preferences.py +++ b/sco_preferences.py @@ -49,7 +49,7 @@ Chaque parametre est défini dans la base de données SQL par: Au niveau du code interface, on défini pour chaque préférence: - name (clé) - title : titre en français - - initvalue : valeur initiale, chargée depuis config/scodoc_config.py + - initvalue : valeur initiale - explanation: explication en français - size: longueur du chap texte - input_type: textarea,separator,... type de widget TrivialFormulator a utiliser @@ -90,13 +90,6 @@ sinon, elle ne concerne que le semestre indiqué. - editer les preferences d'un semestre: sem_preferences(context,formsemestre_id).edit() -* Valeurs par défaut: -On a deux valeurs par défaut possibles: - - via le fichier scodoc_config.py, qui peut être modifié localement. - - si rien dans scodoc_config.py, la valeur définie par - sco_preferences.py est utilisée (ne pas modifier ce fichier). - - * Implémentation: sco_preferences.py PREF_CATEGORIES : définition des catégories de préférences (pour diff --git a/sco_utils.py b/sco_utils.py index 059d3cc7..90f78564 100644 --- a/sco_utils.py +++ b/sco_utils.py @@ -186,53 +186,84 @@ def get_mention(moy): return NOTES_MENTIONS_LABS[bisect.bisect_right(NOTES_MENTIONS_TH, moy)] +class DictDefault(dict): # obsolete, use collections.defaultdict + """A dictionnary with default value for all keys + Each time a non existent key is requested, it is added to the dict. + (used in python 2.4, can't use new __missing__ method) + """ + + defaultvalue = 0 + + def __init__(self, defaultvalue=0, kv_dict={}): + dict.__init__(self) + self.defaultvalue = defaultvalue + self.update(kv_dict) + + def __getitem__(self, k): + if self.has_key(k): + return self.get(k) + value = copy.copy(self.defaultvalue) + self[k] = value + return value + + +class WrapDict: + """Wrap a dict so that getitem returns '' when values are None""" + + def __init__(self, adict, NoneValue=""): + self.dict = adict + self.NoneValue = NoneValue + + def __getitem__(self, key): + value = self.dict[key] + if value is None: + return self.NoneValue + else: + return value + + +def group_by_key(d, key): + g = DictDefault(defaultvalue=[]) + for e in d: + g[e[key]].append(e) + return g + + # ----- Global lock for critical sections (except notes_tables caches) GSL = thread.allocate_lock() # Global ScoDoc Lock if "INSTANCE_HOME" in os.environ: # ----- Repertoire "var" (local) SCODOC_VAR_DIR = os.path.join(os.environ["INSTANCE_HOME"], "var", "scodoc") + # ----- Repertoire "config" modifiable + # /opt/scodoc/var/scodoc/config + SCODOC_CFG_DIR = os.path.join(SCODOC_VAR_DIR, "config") # ----- Version information - SCODOC_VERSION_DIR = os.path.join(SCODOC_VAR_DIR, "config", "version") + SCODOC_VERSION_DIR = os.path.join(SCODOC_CFG_DIR, "version") # ----- Repertoire tmp - SCO_TMPDIR = os.path.join(SCODOC_VAR_DIR, "tmp") - if not os.path.exists(SCO_TMPDIR): - os.mkdir(SCO_TMPDIR, 0o755) + SCO_TMP_DIR = os.path.join(SCODOC_VAR_DIR, "tmp") + if not os.path.exists(SCO_TMP_DIR): + os.mkdir(SCO_TMP_DIR, 0o755) # ----- Les logos: /opt/scodoc/var/scodoc/config/logos - SCODOC_LOGOS_DIR = os.path.join(SCODOC_VAR_DIR, "config", "logos") + SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos") - # ----- Repertoire "config" (devrait s'appeler "tools"...) - SCO_CONFIG_DIR = os.path.join( - os.environ["INSTANCE_HOME"], "Products", "ScoDoc", "config" - ) + # Dans les sources: + SCO_SRC_DIR = os.path.join(os.environ["INSTANCE_HOME"], "Products", "ScoDoc") + # - Les outils distribués + SCO_TOOLS_DIR = os.path.join(SCO_SRC_DIR, "config") # ----- Lecture du fichier de configuration -SCO_SRCDIR = os.path.split(VERSION.__file__)[0] -if SCO_SRCDIR: - SCO_SRCDIR += "/" -else: - SCO_SRCDIR = "/opt/scodoc/Products/ScoDoc/" # debug mode -CONFIG = None -try: - _config_filename = SCO_SRCDIR + "config/scodoc_config.py" - _config_text = open(_config_filename).read() -except: - sys.stderr.write("sco_utils: cannot open configuration file %s" % _config_filename) - raise - -try: - exec(_config_text) -except: - sys.stderr.write("sco_utils: error in configuration file %s" % _config_filename) - raise +import sco_config +import sco_config_load +sco_config_load.load_local_configuration() +CONFIG = sco_config.CONFIG if hasattr(CONFIG, "CODES_EXPL"): CODES_EXPL.update( CONFIG.CODES_EXPL ) # permet de customiser les explications de codes - if CONFIG.CUSTOM_HTML_HEADER: CUSTOM_HTML_HEADER = open(CONFIG.CUSTOM_HTML_HEADER).read() else: @@ -297,50 +328,6 @@ JSON_MIMETYPE = "application/json" LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "png") # remind that PIL does not read pdf - -class DictDefault(dict): # obsolete, use collections.defaultdict - """A dictionnary with default value for all keys - Each time a non existent key is requested, it is added to the dict. - (used in python 2.4, can't use new __missing__ method) - """ - - defaultvalue = 0 - - def __init__(self, defaultvalue=0, kv_dict={}): - dict.__init__(self) - self.defaultvalue = defaultvalue - self.update(kv_dict) - - def __getitem__(self, k): - if self.has_key(k): - return self.get(k) - value = copy.copy(self.defaultvalue) - self[k] = value - return value - - -class WrapDict: - """Wrap a dict so that getitem returns '' when values are None""" - - def __init__(self, adict, NoneValue=""): - self.dict = adict - self.NoneValue = NoneValue - - def __getitem__(self, key): - value = self.dict[key] - if value is None: - return self.NoneValue - else: - return value - - -def group_by_key(d, key): - g = DictDefault(defaultvalue=[]) - for e in d: - g[e[key]].append(e) - return g - - # Admissions des étudiants # Différents types de voies d'admission: # (stocké en texte libre dans la base, mais saisie par menus pour harmoniser) @@ -791,7 +778,7 @@ def icontag(name, file_format="png", **attrs): """ if ("width" not in attrs) or ("height" not in attrs): if name not in ICONSIZES: - img_file = SCO_SRCDIR + "/static/icons/%s.%s" % (name, file_format) + img_file = SCO_SRC_DIR + "/static/icons/%s.%s" % (name, file_format) im = PILImage.open(img_file) width, height = im.size[0], im.size[1] ICONSIZES[name] = (width, height) # cache diff --git a/sco_zope.py b/sco_zope.py index 6c8ccbfa..ef227069 100644 --- a/sco_zope.py +++ b/sco_zope.py @@ -28,6 +28,8 @@ """Imports et configuration des composants Zope """ +# Allows code linting on platforms without Zope: +# pylint: disable=import-error from OFS.SimpleItem import Item # Basic zope object from OFS.PropertyManager import PropertyManager # provide the 'Properties' tab with the diff --git a/scolars.py b/scolars.py index f39f9d92..6981729e 100644 --- a/scolars.py +++ b/scolars.py @@ -42,11 +42,10 @@ import locale locale.setlocale(locale.LC_ALL, ("en_US", SCO_ENCODING)) -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 email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.header import Header +from email.mime.base import MIMEBase abbrvmonthsnames = [ "Jan ", @@ -713,7 +712,7 @@ appreciations_edit = _appreciationsEditor.edit # -------- Noms des Lycées à partir du code def read_etablissements(): - filename = SCO_SRCDIR + "/" + CONFIG.ETABL_FILENAME + filename = SCO_SRC_DIR + "/" + CONFIG.ETABL_FILENAME log("reading %s" % filename) f = open(filename) L = [x[:-1].split(";") for x in f]