diff --git a/.gitignore b/.gitignore index 6d49cf2c8..9a2e411be 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ Thumbs.db *.code-workspace +copy diff --git a/README.md b/README.md index b1117729b..753fc3dc9 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,12 @@ (c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt) -VERSION EXPERIMENTALE - NE PAS DEPLOYER - TESTS EN COURS - Installation: voir instructions à jour sur Documentation utilisateur: ## Version ScoDoc 9 -N'utiliser que pour les développements et tests. - La version ScoDoc 9 est basée sur Flask (au lieu de Zope) et sur **python 3.9+**. @@ -22,13 +18,13 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes). -### État actuel (27 août 21) +### État actuel (26 sept 21) - - Tests en cours, notamment système d'installation et de migration. + - 9.0 reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf: **Fonctionnalités non intégrées:** - - feuille "placement" (en cours) + - génération LaTeX des avis de poursuite d'études - ancien module "Entreprises" (obsolete) diff --git a/app/__init__.py b/app/__init__.py index ae4c0be1f..a3f048391 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -17,14 +17,14 @@ from flask import render_template from flask.logging import default_handler from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate -from flask_login import LoginManager +from flask_login import LoginManager, current_user from flask_mail import Mail from flask_bootstrap import Bootstrap from flask_moment import Moment from flask_caching import Cache import sqlalchemy -from app.scodoc.sco_exceptions import ScoValueError, APIInvalidParams +from app.scodoc.sco_exceptions import ScoGenError, ScoValueError, APIInvalidParams from config import DevConfig import sco_version @@ -82,7 +82,7 @@ def postgresql_server_error(e): return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503 -class RequestFormatter(logging.Formatter): +class LogRequestFormatter(logging.Formatter): """Ajoute URL et remote_addr for logging""" def format(self, record): @@ -92,6 +92,34 @@ class RequestFormatter(logging.Formatter): else: record.url = None record.remote_addr = None + record.sco_user = current_user + + return super().format(record) + + +class LogExceptionFormatter(logging.Formatter): + """Formatteur pour les exceptions: ajoute détails""" + + def format(self, record): + if has_request_context(): + record.url = request.url + record.remote_addr = request.environ.get( + "HTTP_X_FORWARDED_FOR", request.remote_addr + ) + record.http_referrer = request.referrer + record.http_method = request.method + if request.method == "GET": + record.http_params = str(request.args) + else: + # rep = reprlib.Repr() # abbrège + record.http_params = str(request.form)[:2048] + else: + record.url = None + record.remote_addr = None + record.http_referrer = None + record.http_method = None + record.http_params = None + record.sco_user = current_user return super().format(record) @@ -105,8 +133,24 @@ class ScoSMTPHandler(SMTPHandler): return subject +class ReverseProxied(object): + """Adaptateur wsgi qui nous permet d'avoir toutes les URL calculées en https + sauf quand on est en dev. + La variable HTTP_X_FORWARDED_PROTO est positionnée par notre config nginx""" + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + scheme = environ.get("HTTP_X_FORWARDED_PROTO") + if scheme: + environ["wsgi.url_scheme"] = scheme # ou forcer à https ici ? + return self.app(environ, start_response) + + def create_app(config_class=DevConfig): app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static") + app.wsgi_app = ReverseProxied(app.wsgi_app) app.logger.setLevel(logging.DEBUG) app.config.from_object(config_class) @@ -119,6 +163,7 @@ def create_app(config_class=DevConfig): cache.init_app(app) sco_cache.CACHE = cache + app.register_error_handler(ScoGenError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error) app.register_error_handler(500, internal_server_error) app.register_error_handler(503, postgresql_server_error) @@ -148,9 +193,16 @@ def create_app(config_class=DevConfig): absences_bp, url_prefix="/ScoDoc//Scolarite/Absences" ) app.register_blueprint(api_bp, url_prefix="/ScoDoc/api") - scodoc_exc_formatter = RequestFormatter( - "[%(asctime)s] %(remote_addr)s requested %(url)s\n" - "%(levelname)s in %(module)s: %(message)s" + scodoc_log_formatter = LogRequestFormatter( + "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n" + "%(levelname)s: %(message)s" + ) + scodoc_exc_formatter = LogExceptionFormatter( + "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n" + "%(levelname)s: %(message)s\n" + "Referrer: %(http_referrer)s\n" + "Method: %(http_method)s\n" + "Params: %(http_params)s\n" ) if not app.testing: if not app.debug: @@ -179,7 +231,7 @@ def create_app(config_class=DevConfig): app.logger.addHandler(mail_handler) else: # Pour logs en DEV uniquement: - default_handler.setFormatter(scodoc_exc_formatter) + default_handler.setFormatter(scodoc_log_formatter) # Config logs pour DEV et PRODUCTION # Configuration des logs (actifs aussi en mode development) @@ -188,9 +240,17 @@ def create_app(config_class=DevConfig): file_handler = WatchedFileHandler( app.config["SCODOC_LOG_FILE"], encoding="utf-8" ) - file_handler.setFormatter(scodoc_exc_formatter) + file_handler.setFormatter(scodoc_log_formatter) file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) + # Log pour les erreurs (exceptions) uniquement: + # usually /opt/scodoc-data/log/scodoc_exc.log + file_handler = WatchedFileHandler( + app.config["SCODOC_ERR_FILE"], encoding="utf-8" + ) + file_handler.setFormatter(scodoc_exc_formatter) + file_handler.setLevel(logging.ERROR) + app.logger.addHandler(file_handler) # app.logger.setLevel(logging.INFO) app.logger.info(f"{sco_version.SCONAME} {sco_version.SCOVERSION} startup") diff --git a/app/api/errors.py b/app/api/errors.py index ed8d0f3f6..b86d2b236 100644 --- a/app/api/errors.py +++ b/app/api/errors.py @@ -20,7 +20,9 @@ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.from flask import jsonify +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from flask import jsonify from werkzeug.http import HTTP_STATUS_CODES diff --git a/app/auth/models.py b/app/auth/models.py index 8b3597565..d71ecf3f7 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -25,7 +25,7 @@ from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS import app.scodoc.sco_utils as scu from app.scodoc import sco_etud # a deplacer dans scu -VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\\\.]+$") +VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$") class User(UserMixin, db.Model): diff --git a/app/decorators.py b/app/decorators.py index 3696d56ca..65b89905b 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -20,6 +20,7 @@ import flask_login import app from app.auth.models import User +import app.scodoc.sco_utils as scu class ZUser(object): @@ -39,69 +40,6 @@ class ZUser(object): raise NotImplementedError() -class ZRequest(object): - "Emulating Zope 2 REQUEST" - - def __init__(self): - if current_app.config["DEBUG"]: - self.URL = request.base_url - self.BASE0 = request.url_root - else: - self.URL = request.base_url.replace("http://", "https://") - self.BASE0 = request.url_root.replace("http://", "https://") - self.URL0 = self.URL - # query_string is bytes: - self.QUERY_STRING = request.query_string.decode("utf-8") - self.REQUEST_METHOD = request.method - self.AUTHENTICATED_USER = current_user - self.REMOTE_ADDR = request.remote_addr - if request.method == "POST": - # request.form is a werkzeug.datastructures.ImmutableMultiDict - # must copy to get a mutable version (needed by TrivialFormulator) - self.form = request.form.copy() - if request.files: - # Add files in form: - self.form.update(request.files) - for k in request.form: - if k.endswith(":list"): - self.form[k[:-5]] = request.form.getlist(k) - elif request.method == "GET": - self.form = {} - for k in request.args: - # current_app.logger.debug("%s\t%s" % (k, request.args.getlist(k))) - if k.endswith(":list"): - self.form[k[:-5]] = request.args.getlist(k) - else: - self.form[k] = request.args[k] - # current_app.logger.info("ZRequest.form=%s" % str(self.form)) - self.RESPONSE = ZResponse() - - def __str__(self): - return """REQUEST - URL={r.URL} - QUERY_STRING={r.QUERY_STRING} - REQUEST_METHOD={r.REQUEST_METHOD} - AUTHENTICATED_USER={r.AUTHENTICATED_USER} - form={r.form} - """.format( - r=self - ) - - -class ZResponse(object): - "Emulating Zope 2 RESPONSE" - - def __init__(self): - self.headers = {} - - def redirect(self, url): - # current_app.logger.debug("ZResponse redirect to:" + str(url)) - return flask.redirect(url) # http 302 - - def setHeader(self, header, value): - self.headers[header.lower()] = value - - def scodoc(func): """Décorateur pour toutes les fonctions ScoDoc Affecte le département à g @@ -132,7 +70,6 @@ def permission_required(permission): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): - # current_app.logger.info("PERMISSION; kwargs=%s" % str(kwargs)) scodoc_dept = getattr(g, "scodoc_dept", None) if not current_user.has_permission(permission, scodoc_dept): abort(403) @@ -193,7 +130,6 @@ def admin_required(f): def scodoc7func(func): """Décorateur pour intégrer les fonctions Zope 2 de ScoDoc 7. - Ajoute l'argument REQUEST s'il est dans la signature de la fonction. Les paramètres de la query string deviennent des (keywords) paramètres de la fonction. """ @@ -206,19 +142,20 @@ def scodoc7func(func): 1. via a Flask route ("top level call") 2. or be called directly from Python. - If called via a route, this decorator setups a REQUEST object (emulating Zope2 REQUEST) """ # Détermine si on est appelé via une route ("toplevel") # ou par un appel de fonction python normal. - top_level = not hasattr(g, "zrequest") + top_level = not hasattr(g, "scodoc7_decorated") if not top_level: # ne "redécore" pas return func(*args, **kwargs) + g.scodoc7_decorated = True # --- Emulate Zope's REQUEST - REQUEST = ZRequest() - g.zrequest = REQUEST - req_args = REQUEST.form # args from query string (get) or form (post) - # --- Add positional arguments + # REQUEST = ZRequest() + # g.zrequest = REQUEST + # args from query string (get) or form (post) + req_args = scu.get_request_args() + ## --- Add positional arguments pos_arg_values = [] argspec = inspect.getfullargspec(func) # current_app.logger.info("argspec=%s" % str(argspec)) @@ -227,10 +164,12 @@ def scodoc7func(func): arg_names = argspec.args[:-nb_default_args] else: arg_names = argspec.args - for arg_name in arg_names: - if arg_name == "REQUEST": # special case - pos_arg_values.append(REQUEST) + for arg_name in arg_names: # pour chaque arg de la fonction vue + if arg_name == "REQUEST": # ne devrait plus arriver ! + # debug check, TODO remove after tests + raise ValueError("invalid REQUEST parameter !") else: + # peut produire une KeyError s'il manque un argument attendu: v = req_args[arg_name] # try to convert all arguments to INTEGERS # necessary for db ids and boolean values @@ -244,9 +183,9 @@ def scodoc7func(func): # Add keyword arguments if nb_default_args: for arg_name in argspec.args[-nb_default_args:]: - if arg_name == "REQUEST": # special case - kwargs[arg_name] = REQUEST - elif arg_name in req_args: + # if arg_name == "REQUEST": # special case + # kwargs[arg_name] = REQUEST + if arg_name in req_args: # set argument kw optionnel v = req_args[arg_name] # try to convert all arguments to INTEGERS @@ -270,13 +209,13 @@ def scodoc7func(func): # Build response, adding collected http headers: headers = [] kw = {"response": value, "status": 200} - if g.zrequest: - headers = g.zrequest.RESPONSE.headers - if not headers: - # no customized header, speedup: - return value - if "content-type" in headers: - kw["mimetype"] = headers["content-type"] + # if g.zrequest: + # headers = g.zrequest.RESPONSE.headers + # if not headers: + # # no customized header, speedup: + # return value + # if "content-type" in headers: + # kw["mimetype"] = headers["content-type"] r = flask.Response(**kw) for h in headers: r.headers[h] = headers[h] diff --git a/app/models/absences.py b/app/models/absences.py index 8653e5265..e6e243b7c 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -73,3 +73,17 @@ class BilletAbsence(db.Model): entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) # true si l'absence _pourrait_ etre justifiée justified = db.Column(db.Boolean(), default=False, server_default="false") + + def to_dict(self): + data = { + "id": self.id, + "billet_id": self.id, + "etudid": self.etudid, + "abs_begin": self.abs_begin, + "abs_end": self.abs_begin, + "description": self.description, + "etat": self.etat, + "entry_date": self.entry_date, + "justified": self.justified, + } + return data diff --git a/app/models/departements.py b/app/models/departements.py index c0a928e36..36aa8d4c6 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -33,7 +33,7 @@ class Departement(db.Model): semsets = db.relationship("NotesSemSet", lazy="dynamic", backref="departement") def __repr__(self): - return f"" + return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>" def to_dict(self): data = { diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 57eadbc78..2511f0ca5 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -41,7 +41,10 @@ class Identite(db.Model): code_nip = db.Column(db.Text()) code_ine = db.Column(db.Text()) # Ancien id ScoDoc7 pour les migrations de bases anciennes + # ne pas utiliser après migrate_scodoc7_dept_archive scodoc7_id = db.Column(db.Text(), nullable=True) + # + billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") class Adresse(db.Model): diff --git a/app/models/formations.py b/app/models/formations.py index 756879d59..5002ab08b 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -11,7 +11,7 @@ class NotesFormation(db.Model): """Programme pédagogique d'une formation""" __tablename__ = "notes_formations" - __table_args__ = (db.UniqueConstraint("acronyme", "titre", "version"),) + __table_args__ = (db.UniqueConstraint("dept_id", "acronyme", "titre", "version"),) id = db.Column(db.Integer, primary_key=True) formation_id = db.synonym("id") @@ -30,8 +30,12 @@ class NotesFormation(db.Model): type_parcours = db.Column(db.Integer, default=0, server_default="0") code_specialite = db.Column(db.String(SHORT_STR_LEN)) + ues = db.relationship("NotesUE", backref="formation", lazy="dynamic") formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation") + def __repr__(self): + return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>" + class NotesUE(db.Model): """Unité d'Enseignement""" @@ -61,6 +65,9 @@ class NotesUE(db.Model): # coef UE, utilise seulement si l'option use_ue_coefs est activée: coefficient = db.Column(db.Float) + def __repr__(self): + return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>" + class NotesMatiere(db.Model): """Matières: regroupe les modules d'une UE diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 06adb3bea..89468ab2f 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -41,6 +41,10 @@ class FormSemestre(db.Model): bul_hide_xml = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) + # Bloque le calcul des moyennes (générale et d'UE) + block_moyennes = db.Column( + db.Boolean(), nullable=False, default=False, server_default="false" + ) # semestres decales (pour gestion jurys): gestion_semestrielle = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" @@ -70,6 +74,7 @@ class FormSemestre(db.Model): "NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre" ) # Ancien id ScoDoc7 pour les migrations de bases anciennes + # ne pas utiliser après migrate_scodoc7_dept_archive scodoc7_id = db.Column(db.Text(), nullable=True) def __init__(self, **kwargs): diff --git a/app/pe/README.md b/app/pe/README.md new file mode 100644 index 000000000..bace3081b --- /dev/null +++ b/app/pe/README.md @@ -0,0 +1,8 @@ +# Module "Avis de poursuite d'étude" + +Conçu et développé sur ScoDoc 7 par Cléo Baras (IUT de Grenoble) pour le DUT. + +Actuellement non opérationnel dans ScoDoc 9. + + + diff --git a/app/scodoc/pe_avislatex.py b/app/pe/pe_avislatex.py similarity index 90% rename from app/scodoc/pe_avislatex.py rename to app/pe/pe_avislatex.py index 70fe390df..b98218eea 100644 --- a/app/scodoc/pe_avislatex.py +++ b/app/pe/pe_avislatex.py @@ -33,9 +33,9 @@ import os import codecs import re -from app.scodoc import pe_jurype -from app.scodoc import pe_tagtable -from app.scodoc import pe_tools +from app.pe import pe_tagtable +from app.pe import pe_jurype +from app.pe import pe_tools import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -48,7 +48,7 @@ from app.scodoc import sco_etud DEBUG = False # Pour debug et repérage des prints à changer en Log DONNEE_MANQUANTE = ( - u"" # Caractère de remplacement des données manquantes dans un avis PE + "" # Caractère de remplacement des données manquantes dans un avis PE ) # ---------------------------------------------------------------------------------------- @@ -102,17 +102,17 @@ def comp_latex_parcourstimeline(etudiant, promo, taille=17): result: chaine unicode (EV:) """ codelatexDebut = ( - u""" + """" \\begin{parcourstimeline}{**debut**}{**fin**}{**nbreSemestres**}{%d} """ % taille ) - modeleEvent = u""" + modeleEvent = """ \\parcoursevent{**nosem**}{**nomsem**}{**descr**} """ - codelatexFin = u""" + codelatexFin = """ \\end{parcourstimeline} """ reslatex = codelatexDebut @@ -125,13 +125,13 @@ def comp_latex_parcourstimeline(etudiant, promo, taille=17): for no_sem in range(etudiant["nbSemestres"]): descr = modeleEvent nom_semestre_dans_parcours = parcours[no_sem]["nom_semestre_dans_parcours"] - descr = descr.replace(u"**nosem**", str(no_sem + 1)) + descr = descr.replace("**nosem**", str(no_sem + 1)) if no_sem % 2 == 0: - descr = descr.replace(u"**nomsem**", nom_semestre_dans_parcours) - descr = descr.replace(u"**descr**", u"") + descr = descr.replace("**nomsem**", nom_semestre_dans_parcours) + descr = descr.replace("**descr**", "") else: - descr = descr.replace(u"**nomsem**", u"") - descr = descr.replace(u"**descr**", nom_semestre_dans_parcours) + descr = descr.replace("**nomsem**", "") + descr = descr.replace("**descr**", nom_semestre_dans_parcours) reslatex += descr reslatex += codelatexFin return reslatex @@ -166,7 +166,7 @@ def get_code_latex_avis_etudiant( result: chaine unicode """ if not donnees_etudiant or not un_avis_latex: # Cas d'un template vide - return annotationPE if annotationPE else u"" + return annotationPE if annotationPE else "" # Le template latex (corps + footer) code = un_avis_latex + "\n\n" + footer_latex @@ -189,17 +189,17 @@ def get_code_latex_avis_etudiant( ) # La macro parcourstimeline - elif tag_latex == u"parcourstimeline": + elif tag_latex == "parcourstimeline": valeur = comp_latex_parcourstimeline( donnees_etudiant, donnees_etudiant["promo"] ) # Le tag annotationPE - elif tag_latex == u"annotation": + elif tag_latex == "annotation": valeur = annotationPE # Le tag bilanParTag - elif tag_latex == u"bilanParTag": + elif tag_latex == "bilanParTag": valeur = get_bilanParTag(donnees_etudiant) # Les tags "simples": par ex. nom, prenom, civilite, ... @@ -249,14 +249,14 @@ def get_annotation_PE(etudid, tag_annotation_pe): ]["comment_u"] annotationPE = exp.sub( - u"", annotationPE + "", annotationPE ) # Suppression du tag d'annotation PE - annotationPE = annotationPE.replace(u"\r", u"") # Suppression des \r + annotationPE = annotationPE.replace("\r", "") # Suppression des \r annotationPE = annotationPE.replace( - u"
", u"\n\n" + "
", "\n\n" ) # Interprète les retours chariots html return annotationPE - return u"" # pas d'annotations + return "" # pas d'annotations # ---------------------------------------------------------------------------------------- @@ -282,7 +282,7 @@ def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ) ): donnees_numeriques = donnees_etudiant[aggregat][groupe][tag_scodoc] if champ == "rang": - valeur = u"%s/%d" % ( + valeur = "%s/%d" % ( donnees_numeriques[ pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index("rang") ], @@ -303,9 +303,9 @@ def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ) if isinstance( donnees_numeriques[indice_champ], float ): # valeur numérique avec formattage unicode - valeur = u"%2.2f" % donnees_numeriques[indice_champ] + valeur = "%2.2f" % donnees_numeriques[indice_champ] else: - valeur = u"%s" % donnees_numeriques[indice_champ] + valeur = "%s" % donnees_numeriques[indice_champ] return valeur @@ -356,29 +356,27 @@ def get_bilanParTag(donnees_etudiant, groupe="groupe"): ("\\textit{" + rang + "}") if note else "" ) # rang masqué si pas de notes - code_latex = u"\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n" - code_latex += u"\\hline \n" + code_latex = "\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n" + code_latex += "\\hline \n" code_latex += ( - u" & " + " & " + " & ".join(["\\textbf{" + intitule + "}" for (agg, intitule, _) in entete]) + " \\\\ \n" ) - code_latex += u"\\hline" - code_latex += u"\\hline \n" + code_latex += "\\hline" + code_latex += "\\hline \n" for (i, ligne_val) in enumerate(valeurs["note"]): titre = lignes[i] # règle le pb d'encodage + code_latex += "\\textbf{" + titre + "} & " + " & ".join(ligne_val) + "\\\\ \n" code_latex += ( - u"\\textbf{" + titre + u"} & " + " & ".join(ligne_val) + u"\\\\ \n" - ) - code_latex += ( - u" & " - + u" & ".join( - [u"{\\scriptsize " + clsmt + u"}" for clsmt in valeurs["rang"][i]] + " & " + + " & ".join( + ["{\\scriptsize " + clsmt + "}" for clsmt in valeurs["rang"][i]] ) - + u"\\\\ \n" + + "\\\\ \n" ) - code_latex += u"\\hline \n" - code_latex += u"\\end{tabular}" + code_latex += "\\hline \n" + code_latex += "\\end{tabular}" return code_latex @@ -397,21 +395,15 @@ def get_avis_poursuite_par_etudiant( nom = jury.syntheseJury[etudid]["nom"].replace(" ", "-") prenom = jury.syntheseJury[etudid]["prenom"].replace(" ", "-") - nom_fichier = ( - u"avis_poursuite_" - + pe_tools.remove_accents(nom) - + "_" - + pe_tools.remove_accents(prenom) - + "_" - + str(etudid) + nom_fichier = scu.sanitize_filename( + "avis_poursuite_%s_%s_%s" % (nom, prenom, etudid) ) if pe_tools.PE_DEBUG: pe_tools.pe_print("fichier latex =" + nom_fichier, type(nom_fichier)) # Entete (commentaire) - contenu_latex = ( - u"%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + u"\n" + "%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + "\n" ) # les annnotations diff --git a/app/scodoc/pe_jurype.py b/app/pe/pe_jurype.py similarity index 99% rename from app/scodoc/pe_jurype.py rename to app/pe/pe_jurype.py index 5fce90705..a7d302ad8 100644 --- a/app/scodoc/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -52,10 +52,10 @@ from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours # sco_codes_parcours.NEXT -> sem suivant from app.scodoc import sco_etud from app.scodoc import sco_formsemestre -from app.scodoc import pe_tagtable -from app.scodoc import pe_tools -from app.scodoc import pe_semestretag -from app.scodoc import pe_settag +from app.pe import pe_tagtable +from app.pe import pe_tools +from app.pe import pe_semestretag +from app.pe import pe_settag # ---------------------------------------------------------------------------------------- def comp_nom_semestre_dans_parcours(sem): @@ -946,7 +946,7 @@ class JuryPE(object): return list(taglist) def get_allTagInSyntheseJury(self): - """Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag """ + """Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag""" allTags = set() for nom in JuryPE.PARCOURS.keys(): allTags = allTags.union(set(self.get_allTagForAggregat(nom))) diff --git a/app/scodoc/pe_semestretag.py b/app/pe/pe_semestretag.py similarity index 99% rename from app/scodoc/pe_semestretag.py rename to app/pe/pe_semestretag.py index 80e256201..a6729def9 100644 --- a/app/scodoc/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -40,7 +40,7 @@ from app import log from app.scodoc import sco_codes_parcours from app.scodoc import sco_cache from app.scodoc import sco_tag_module -from app.scodoc import pe_tagtable +from app.pe import pe_tagtable class SemestreTag(pe_tagtable.TableTag): diff --git a/app/scodoc/pe_settag.py b/app/pe/pe_settag.py similarity index 99% rename from app/scodoc/pe_settag.py rename to app/pe/pe_settag.py index 05eba20f2..8fde8d20a 100644 --- a/app/scodoc/pe_settag.py +++ b/app/pe/pe_settag.py @@ -36,8 +36,8 @@ Created on Fri Sep 9 09:15:05 2016 @author: barasc """ -from app.scodoc.pe_tools import pe_print, PE_DEBUG -from app.scodoc import pe_tagtable +from app.pe.pe_tools import pe_print, PE_DEBUG +from app.pe import pe_tagtable class SetTag(pe_tagtable.TableTag): diff --git a/app/scodoc/pe_tagtable.py b/app/pe/pe_tagtable.py similarity index 100% rename from app/scodoc/pe_tagtable.py rename to app/pe/pe_tagtable.py diff --git a/app/scodoc/pe_tools.py b/app/pe/pe_tools.py similarity index 96% rename from app/scodoc/pe_tools.py rename to app/pe/pe_tools.py index 5be8e2d98..aef083982 100644 --- a/app/scodoc/pe_tools.py +++ b/app/pe/pe_tools.py @@ -167,8 +167,19 @@ 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""" rooted_path_in_zip = os.path.join(ziproot, path_in_zip) - data = open(pathname).read() - zipfile.writestr(rooted_path_in_zip, data) + zipfile.write(filename=pathname, arcname=rooted_path_in_zip) + # data = open(pathname).read() + # zipfile.writestr(rooted_path_in_zip, data) + + +def add_refs_to_register(register, directory): + """Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme + filename => pathname + """ + length = len(directory) + for pathname in list_directory_filenames(directory): + filename = pathname[length + 1 :] + register[filename] = pathname def add_pe_stuff_to_zip(zipfile, ziproot): @@ -179,30 +190,16 @@ def add_pe_stuff_to_zip(zipfile, ziproot): Also copy logos """ + register = {} + # first add standard (distrib references) distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib") - distrib_pathnames = list_directory_filenames( - distrib_dir - ) # eg /opt/scodoc/tools/doc_poursuites_etudes/distrib/modeles/toto.tex - l = len(distrib_dir) - distrib_filenames = {x[l + 1 :] for x in distrib_pathnames} # eg modeles/toto.tex - + add_refs_to_register(register=register, directory=distrib_dir) + # then add local references (some oh them may overwrite distrib refs) local_dir = os.path.join(REP_LOCAL_AVIS, "local") - local_pathnames = list_directory_filenames(local_dir) - l = len(local_dir) - local_filenames = {x[l + 1 :] for x in local_pathnames} - - for filename in distrib_filenames | local_filenames: - if filename in local_filenames: - add_local_file_to_zip( - zipfile, ziproot, os.path.join(local_dir, filename), "avis/" + filename - ) - else: - add_local_file_to_zip( - zipfile, - ziproot, - os.path.join(distrib_dir, filename), - "avis/" + filename, - ) + add_refs_to_register(register=register, directory=local_dir) + # at this point register contains all refs (filename, pathname) to be saved + for filename, pathname in register.items(): + add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename) # Logos: (add to logos/ directory in zip) logos_names = ["logo_header.jpg", "logo_footer.jpg"] diff --git a/app/scodoc/pe_view.py b/app/pe/pe_view.py similarity index 93% rename from app/scodoc/pe_view.py rename to app/pe/pe_view.py index 45c97acd6..14efdada3 100644 --- a/app/scodoc/pe_view.py +++ b/app/pe/pe_view.py @@ -42,10 +42,9 @@ from app.scodoc import sco_formsemestre from app.scodoc import html_sco_header from app.scodoc import sco_preferences -from app.scodoc import pe_tools -from app.scodoc.pe_tools import PE_LATEX_ENCODING -from app.scodoc import pe_jurype -from app.scodoc import pe_avislatex +from app.pe import pe_tools +from app.pe import pe_jurype +from app.pe import pe_avislatex def _pe_view_sem_recap_form(formsemestre_id): @@ -90,7 +89,6 @@ def pe_view_sem_recap( semBase = sco_formsemestre.get_formsemestre(formsemestre_id) jury = pe_jurype.JuryPE(semBase) - # Ajout avis LaTeX au même zip: etudids = list(jury.syntheseJury.keys()) @@ -150,18 +148,14 @@ def pe_view_sem_recap( footer_latex, prefs, ) - - jury.add_file_to_zip( - ("avis/" + nom_fichier + ".tex").encode(PE_LATEX_ENCODING), - contenu_latex.encode(PE_LATEX_ENCODING), - ) + jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex) latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico # Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous doc_latex = "\n% -----\n".join( ["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())] ) - jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex.encode(PE_LATEX_ENCODING)) + jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex) # Ajoute image, LaTeX class file(s) and modeles pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP) diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py index ebe5c52a8..e324742c5 100644 --- a/app/scodoc/TrivialFormulator.py +++ b/app/scodoc/TrivialFormulator.py @@ -8,6 +8,7 @@ v 1.3 (python3) """ +import html def TrivialFormulator( @@ -134,7 +135,7 @@ class TF(object): is_submitted=False, ): self.form_url = form_url - self.values = values + self.values = values.copy() self.formdescription = list(formdescription) self.initvalues = initvalues self.method = method @@ -722,7 +723,9 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts); if str(descr["allowed_values"][i]) == str(self.values[field]): R.append('%s' % labels[i]) elif input_type == "textarea": - R.append('
%s
' % self.values[field]) + R.append( + '
%s
' % html.escape(self.values[field]) + ) elif input_type == "separator" or input_type == "hidden": pass elif input_type == "file": diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py index 4a3a83e61..1dcfaa45c 100644 --- a/app/scodoc/bonus_sport.py +++ b/app/scodoc/bonus_sport.py @@ -379,6 +379,25 @@ def bonus_iutbethune(notes_sport, coefs, infos=None): return bonus +def bonus_iutbeziers(notes_sport, coefs, infos=None): + """Calcul bonus modules optionels (sport, culture), regle IUT BEZIERS + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + sport , etc) non rattaches à une unité d'enseignement. Les points + au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés et 3% de ces points cumulés s'ajoutent à + la moyenne générale du semestre déjà obtenue par l'étudiant. + """ + sumc = sum(coefs) # assumes sum. coefs > 0 + # note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée + bonus = sum([(x - 10) * 0.03 for x in notes_sport if x > 10]) + # le total du bonus ne doit pas dépasser 0.3 - Fred, 28/01/2020 + + if bonus > 0.3: + bonus = 0.3 + return bonus + + def bonus_demo(notes_sport, coefs, infos=None): """Fausse fonction "bonus" pour afficher les informations disponibles et aider les développeurs. @@ -386,8 +405,8 @@ def bonus_demo(notes_sport, coefs, infos=None): qui est ECRASE à chaque appel. *** Ne pas utiliser en production !!! *** """ - f = open("/tmp/scodoc_bonus.log", "w") # mettre 'a' pour ajouter en fin - f.write("\n---------------\n" + pprint.pformat(infos) + "\n") + with open("/tmp/scodoc_bonus.log", "w") as f: # mettre 'a' pour ajouter en fin + f.write("\n---------------\n" + pprint.pformat(infos) + "\n") # Statut de chaque UE # for ue_id in infos['moy_ues']: # ue_status = infos['moy_ues'][ue_id] diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index a318f93ee..f6ac6c343 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -185,6 +185,9 @@ class GenTable(object): else: self.preferences = DEFAULT_TABLE_PREFERENCES() + def __repr__(self): + return f"" + def get_nb_cols(self): return len(self.columns_ids) @@ -468,7 +471,10 @@ class GenTable(object): def excel(self, wb=None): """Simple Excel representation of the table""" - ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) + if wb is None: + ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) + else: + ses = wb.create_sheet(sheet_name=self.xls_sheet_name) ses.rows += self.xls_before_table style_bold = sco_excel.excel_make_style(bold=True) style_base = sco_excel.excel_make_style() @@ -482,9 +488,7 @@ class GenTable(object): ses.append_blank_row() # empty line ses.append_single_cell_row(self.origin, style_base) if wb is None: - return ses.generate_standalone() - else: - ses.generate_embeded() + return ses.generate() def text(self): "raw text representation of the table" @@ -573,7 +577,7 @@ class GenTable(object): """ doc = ElementTree.Element( self.xml_outer_tag, - id=self.table_id, + id=str(self.table_id), origin=self.origin or "", caption=self.caption or "", ) @@ -587,7 +591,7 @@ class GenTable(object): v = row.get(cid, "") if v is None: v = "" - x_cell = ElementTree.Element(cid, value=str(v)) + x_cell = ElementTree.Element(str(cid), value=str(v)) x_row.append(x_cell) return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) @@ -610,7 +614,6 @@ class GenTable(object): format="html", page_title="", filename=None, - REQUEST=None, javascripts=[], with_html_headers=True, publish=True, @@ -643,35 +646,53 @@ class GenTable(object): H.append(html_sco_header.sco_footer()) return "\n".join(H) elif format == "pdf": - objects = self.pdf() - doc = sco_pdf.pdf_basic_page( - objects, title=title, preferences=self.preferences + pdf_objs = self.pdf() + pdf_doc = sco_pdf.pdf_basic_page( + pdf_objs, title=title, preferences=self.preferences ) if publish: - return scu.sendPDFFile(REQUEST, doc, filename + ".pdf") + return scu.send_file( + pdf_doc, + filename, + suffix=".pdf", + mime=scu.PDF_MIMETYPE, + ) else: - return doc - elif format == "xls" or format == "xlsx": + return pdf_doc + elif format == "xls" or format == "xlsx": # dans les 2 cas retourne du xlsx xls = self.excel() if publish: - return sco_excel.send_excel_file( - REQUEST, xls, filename + scu.XLSX_SUFFIX + return scu.send_file( + xls, + filename, + suffix=scu.XLSX_SUFFIX, + mime=scu.XLSX_MIMETYPE, ) else: return xls elif format == "text": return self.text() elif format == "csv": - return scu.sendCSVFile(REQUEST, self.text(), filename + ".csv") + return scu.send_file( + self.text(), + filename, + suffix=".csv", + mime=scu.CSV_MIMETYPE, + attached=True, + ) elif format == "xml": xml = self.xml() - if REQUEST and publish: - REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) + if publish: + return scu.send_file( + xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE + ) return xml elif format == "json": js = self.json() - if REQUEST and publish: - REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE) + if publish: + return scu.send_file( + js, filename, suffix=".json", mime=scu.JSON_MIMETYPE + ) return js else: log("make_page: format=%s" % format) @@ -732,5 +753,5 @@ if __name__ == "__main__": document.build(objects) data = doc.getvalue() open("/tmp/gen_table.pdf", "wb").write(data) - p = T.make_page(format="pdf", REQUEST=None) + p = T.make_page(format="pdf") open("toto.pdf", "wb").write(p) diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 4944f6e40..7e35c3b33 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -87,10 +87,6 @@ Problème de connexion (identifiant, mot de passe): contacter votre responsa ) -_TOP_LEVEL_CSS = """ - """ - _HTML_BEGIN = """ @@ -105,31 +101,30 @@ _HTML_BEGIN = """ - - - - + + + - - - + + + - + - + - - + + """ def scodoc_top_html_header(page_title="ScoDoc: bienvenue"): H = [ _HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING}, - _TOP_LEVEL_CSS, """""", scu.CUSTOM_HTML_HEADER_CNX, ] @@ -185,13 +180,10 @@ def sco_header( init_jquery = True H = [ - """ - - + """ + %(page_title)s - - @@ -206,9 +198,7 @@ def sco_header( ) if init_google_maps: # It may be necessary to add an API key: - H.append( - '' - ) + H.append('') # Feuilles de style additionnelles: for cssstyle in cssstyles: @@ -223,9 +213,9 @@ def sco_header( - - - + + + """ """ ) - H.append( - '' - ) + H.append('') # qTip if init_qtip: H.append( - '' + '' ) H.append( '' @@ -253,32 +241,25 @@ def sco_header( if init_jquery_ui: H.append( - '' - ) - # H.append('') - H.append( - '' + '' ) + # H.append('') + H.append('') if init_google_maps: H.append( - '' + '' ) if init_datatables: H.append( '' ) - H.append( - '' - ) + H.append('') # JS additionels for js in javascripts: - H.append( - """\n""" - % js - ) + H.append("""\n""" % js) H.append( - """