diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py new file mode 100644 index 00000000..a655f450 --- /dev/null +++ b/app/forms/main/config_apo.py @@ -0,0 +1,78 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2022 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 +# +############################################################################## + +""" +Formulaires configuration Exports Apogée (codes) +""" +import re + +from flask import flash, url_for, redirect, render_template +from flask_wtf import FlaskForm +from wtforms import SubmitField, validators +from wtforms.fields.simple import StringField + +from app import models +from app.models import ScoDocSiteConfig +from app.models import SHORT_STR_LEN + +from app.scodoc import sco_codes_parcours +from app.scodoc import sco_utils as scu + + +def _build_code_field(code): + return StringField( + label=code, + description=sco_codes_parcours.CODES_EXPL[code], + validators=[ + validators.regexp( + r"^[A-Z0-9_]*$", + message="Ne doit comporter que majuscules et des chiffres", + ), + validators.Length( + max=SHORT_STR_LEN, + message=f"L'acronyme ne doit pas dépasser {SHORT_STR_LEN} caractères", + ), + validators.DataRequired("code requis"), + ], + ) + + +class CodesDecisionsForm(FlaskForm): + ADC = _build_code_field("ADC") + ADJ = _build_code_field("ADJ") + ADM = _build_code_field("ADM") + AJ = _build_code_field("AJ") + ATB = _build_code_field("ATB") + ATJ = _build_code_field("ATJ") + ATT = _build_code_field("ATT") + CMP = _build_code_field("CMP") + DEF = _build_code_field("DEF") + DEM = _build_code_field("DEF") + NAR = _build_code_field("NAR") + RAT = _build_code_field("RAT") + submit = SubmitField("Valider") + cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/__init__.py b/app/models/__init__.py index 4a3328b3..364943aa 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -12,7 +12,7 @@ GROUPNAME_STR_LEN = 64 from app.models.raw_sql_init import create_database_functions from app.models.absences import Absence, AbsenceNotification, BilletAbsence - +from app.models.config import ScoDocSiteConfig from app.models.departements import Departement from app.models.entreprises import ( @@ -63,7 +63,7 @@ from app.models.notes import ( NotesNotes, NotesNotesLog, ) -from app.models.preferences import ScoPreference, ScoDocSiteConfig +from app.models.preferences import ScoPreference from app.models.but_refcomp import ( ApcReferentielCompetences, diff --git a/app/models/config.py b/app/models/config.py new file mode 100644 index 00000000..af04ee51 --- /dev/null +++ b/app/models/config.py @@ -0,0 +1,178 @@ +# -*- coding: UTF-8 -* + +"""Model : site config WORK IN PROGRESS #WIP +""" + +from app import db, log +from app.scodoc import bonus_sport +from app.scodoc.sco_exceptions import ScoValueError +import functools + +from app.scodoc.sco_codes_parcours import ( + ADC, + ADJ, + ADM, + AJ, + ATB, + ATJ, + ATT, + CMP, + DEF, + DEM, + NAR, + RAT, +) + +CODES_SCODOC_TO_APO = { + ADC: "ADMC", + ADJ: "ADM", + ADM: "ADM", + AJ: "AJ", + ATB: "AJAC", + ATJ: "AJAC", + ATT: "AJAC", + CMP: "COMP", + DEF: "NAR", + DEM: "NAR", + NAR: "NAR", + RAT: "ATT", +} + + +def code_scodoc_to_apo_default(code): + """Conversion code jury ScoDoc en code Apogée + (codes par défaut, c'est configurable via ScoDocSiteConfig.get_code_apo) + """ + return CODES_SCODOC_TO_APO.get(code, "DEF") + + +class ScoDocSiteConfig(db.Model): + """Config. d'un site + Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions + antérieures étaient dans scodoc_config.py + """ + + __tablename__ = "scodoc_site_config" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(128), nullable=False, index=True) + value = db.Column(db.Text()) + + BONUS_SPORT = "bonus_sport_func_name" + NAMES = { + BONUS_SPORT: str, + "always_require_ine": bool, + "SCOLAR_FONT": str, + "SCOLAR_FONT_SIZE": str, + "SCOLAR_FONT_SIZE_FOOT": str, + "INSTITUTION_NAME": str, + "INSTITUTION_ADDRESS": str, + "INSTITUTION_CITY": str, + "DEFAULT_PDF_FOOTER_TEMPLATE": str, + } + + def __init__(self, name, value): + self.name = name + self.value = value + + def __repr__(self): + return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>" + + @classmethod + def get_dict(cls) -> dict: + "Returns all data as a dict name = value" + return { + c.name: cls.NAMES.get(c.name, lambda x: x)(c.value) + for c in ScoDocSiteConfig.query.all() + } + + @classmethod + def set_bonus_sport_func(cls, func_name): + """Record bonus_sport config. + If func_name not defined, raise NameError + """ + if func_name not in cls.get_bonus_sport_func_names(): + raise NameError("invalid function name for bonus_sport") + c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() + if c: + log("setting to " + func_name) + c.value = func_name + else: + c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name) + db.session.add(c) + db.session.commit() + + @classmethod + def get_bonus_sport_func_name(cls): + """Get configured bonus function name, or None if None.""" + f = cls.get_bonus_sport_func_from_name() + if f is None: + return "" + else: + return f.__name__ + + @classmethod + def get_bonus_sport_func(cls): + """Get configured bonus function, or None if None.""" + return cls.get_bonus_sport_func_from_name() + + @classmethod + def get_bonus_sport_func_from_name(cls, func_name=None): + """returns bonus func with specified name. + If name not specified, return the configured function. + None if no bonus function configured. + Raises ScoValueError if func_name not found in module bonus_sport. + """ + if func_name is None: + c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() + if c is None: + return None + func_name = c.value + if func_name == "": # pas de bonus défini + return None + try: + return getattr(bonus_sport, func_name) + except AttributeError: + raise ScoValueError( + f"""Fonction de calcul maison inexistante: {func_name}. + (contacter votre administrateur local).""" + ) + + @classmethod + def get_bonus_sport_func_names(cls): + """List available functions names + (starting with empty string to represent "no bonus function"). + """ + return [""] + sorted( + [ + getattr(bonus_sport, name).__name__ + for name in dir(bonus_sport) + if name.startswith("bonus_") + ] + ) + + @classmethod + def get_code_apo(cls, code: str) -> str: + """La représentation d'un code pour les exports Apogée. + Par exemple, à l'iUT du H., le code ADM est réprésenté par VAL + Les codes par défaut sont donnés dans sco_apogee_csv. + + """ + cfg = ScoDocSiteConfig.query.filter_by(name=code).first() + if not cfg: + code_apo = code_scodoc_to_apo_default(code) + else: + code_apo = cfg.value + return code_apo + + @classmethod + def set_code_apo(cls, code: str, code_apo: str): + """Enregistre nouvelle représentation du code""" + if code_apo != cls.get_code_apo(code): + cfg = ScoDocSiteConfig.query.filter_by(name=code).first() + if cfg is None: + cfg = ScoDocSiteConfig(code, code_apo) + else: + cfg.value = code_apo + db.session.add(cfg) + db.session.commit() diff --git a/app/models/formations.py b/app/models/formations.py index e2273c3b..b69d566a 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -1,6 +1,7 @@ """ScoDoc 9 models : Formations """ +import app from app import db from app.comp import df_cache from app.models import SHORT_STR_LEN @@ -141,8 +142,7 @@ class Formation(db.Model): db.session.add(ue) db.session.commit() - if change: - self.invalidate_module_coefs() + app.clear_scodoc_cache() class Matiere(db.Model): diff --git a/app/models/preferences.py b/app/models/preferences.py index 59c82ec8..924f6e60 100644 --- a/app/models/preferences.py +++ b/app/models/preferences.py @@ -2,9 +2,8 @@ """Model : preferences """ -from app import db, log -from app.scodoc import bonus_sport -from app.scodoc.sco_exceptions import ScoValueError + +from app import db class ScoPreference(db.Model): @@ -19,108 +18,3 @@ class ScoPreference(db.Model): name = db.Column(db.String(128), nullable=False, index=True) value = db.Column(db.Text()) formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id")) - - -class ScoDocSiteConfig(db.Model): - """Config. d'un site - Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions - antérieures étaient dans scodoc_config.py - """ - - __tablename__ = "scodoc_site_config" - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(128), nullable=False, index=True) - value = db.Column(db.Text()) - - BONUS_SPORT = "bonus_sport_func_name" - NAMES = { - BONUS_SPORT: str, - "always_require_ine": bool, - "SCOLAR_FONT": str, - "SCOLAR_FONT_SIZE": str, - "SCOLAR_FONT_SIZE_FOOT": str, - "INSTITUTION_NAME": str, - "INSTITUTION_ADDRESS": str, - "INSTITUTION_CITY": str, - "DEFAULT_PDF_FOOTER_TEMPLATE": str, - } - - def __init__(self, name, value): - self.name = name - self.value = value - - def __repr__(self): - return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>" - - def get_dict(self) -> dict: - "Returns all data as a dict name = value" - return { - c.name: self.NAMES.get(c.name, lambda x: x)(c.value) - for c in ScoDocSiteConfig.query.all() - } - - @classmethod - def set_bonus_sport_func(cls, func_name): - """Record bonus_sport config. - If func_name not defined, raise NameError - """ - if func_name not in cls.get_bonus_sport_func_names(): - raise NameError("invalid function name for bonus_sport") - c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() - if c: - log("setting to " + func_name) - c.value = func_name - else: - c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name) - db.session.add(c) - db.session.commit() - - @classmethod - def get_bonus_sport_func_name(cls): - """Get configured bonus function name, or None if None.""" - f = cls.get_bonus_sport_func_from_name() - if f is None: - return "" - else: - return f.__name__ - - @classmethod - def get_bonus_sport_func(cls): - """Get configured bonus function, or None if None.""" - return cls.get_bonus_sport_func_from_name() - - @classmethod - def get_bonus_sport_func_from_name(cls, func_name=None): - """returns bonus func with specified name. - If name not specified, return the configured function. - None if no bonus function configured. - Raises ScoValueError if func_name not found in module bonus_sport. - """ - if func_name is None: - c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() - if c is None: - return None - func_name = c.value - if func_name == "": # pas de bonus défini - return None - try: - return getattr(bonus_sport, func_name) - except AttributeError: - raise ScoValueError( - f"""Fonction de calcul maison inexistante: {func_name}. - (contacter votre administrateur local).""" - ) - - @classmethod - def get_bonus_sport_func_names(cls): - """List available functions names - (starting with empty string to represent "no bonus function"). - """ - return [""] + sorted( - [ - getattr(bonus_sport, name).__name__ - for name in dir(bonus_sport) - if name.startswith("bonus_") - ] - ) diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index f97cc895..833a7841 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -95,30 +95,21 @@ from flask import send_file # Pour la détection auto de l'encodage des fichiers Apogée: from chardet import detect as chardet_detect +from app.models.config import ScoDocSiteConfig import app.scodoc.sco_utils as scu -import app.scodoc.notesdb as ndb from app import log from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.gen_tables import GenTable from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_codes_parcours import code_semestre_validant from app.scodoc.sco_codes_parcours import ( - ADC, - ADJ, - ADM, - AJ, - ATB, - ATJ, - ATT, - CMP, DEF, + DEM, NAR, RAT, ) from app.scodoc import sco_cache -from app.scodoc import sco_codes_parcours from app.scodoc import sco_formsemestre -from app.scodoc import sco_formsemestre_status from app.scodoc import sco_parcours_dut from app.scodoc import sco_etud @@ -132,24 +123,6 @@ APO_SEP = "\t" APO_NEWLINE = "\r\n" -def code_scodoc_to_apo(code): - """Conversion code jury ScoDoc en code Apogée""" - return { - ATT: "AJAC", - ATB: "AJAC", - ATJ: "AJAC", - ADM: "ADM", - ADJ: "ADM", - ADC: "ADMC", - AJ: "AJ", - CMP: "COMP", - "DEM": "NAR", - DEF: "NAR", - NAR: "NAR", - RAT: "ATT", - }.get(code, "DEF") - - def _apo_fmt_note(note): "Formatte une note pour Apogée (séparateur décimal: ',')" if not note and isinstance(note, float): @@ -449,7 +422,7 @@ class ApoEtud(dict): N=_apo_fmt_note(ue_status["moy"]), B=20, J="", - R=code_scodoc_to_apo(code_decision_ue), + R=ScoDocSiteConfig.get_code_apo(code_decision_ue), M="", ) else: @@ -475,13 +448,9 @@ class ApoEtud(dict): def comp_elt_semestre(self, nt, decision, etudid): """Calcul résultat apo semestre""" # resultat du semestre - decision_apo = code_scodoc_to_apo(decision["code"]) + decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"]) note = nt.get_etud_moy_gen(etudid) - if ( - decision_apo == "DEF" - or decision["code"] == "DEM" - or decision["code"] == DEF - ): + if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF: note_str = "0,01" # note non nulle pour les démissionnaires else: note_str = _apo_fmt_note(note) @@ -520,21 +489,21 @@ class ApoEtud(dict): # ou jury intermediaire et etudiant non redoublant... return self.comp_elt_semestre(cur_nt, cur_decision, etudid) - decision_apo = code_scodoc_to_apo(cur_decision["code"]) + decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"]) autre_nt = sco_cache.NotesTableCache.get(autre_sem["formsemestre_id"]) autre_decision = autre_nt.get_etud_decision_sem(etudid) if not autre_decision: # pas de decision dans l'autre => pas de résultat annuel return VOID_APO_RES - autre_decision_apo = code_scodoc_to_apo(autre_decision["code"]) + autre_decision_apo = ScoDocSiteConfig.get_code_apo(autre_decision["code"]) if ( autre_decision_apo == "DEF" - or autre_decision["code"] == "DEM" + or autre_decision["code"] == DEM or autre_decision["code"] == DEF ) or ( decision_apo == "DEF" - or cur_decision["code"] == "DEM" + or cur_decision["code"] == DEM or cur_decision["code"] == DEF ): note_str = "0,01" # note non nulle pour les démissionnaires diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 4ff29bb7..6bcb8cc3 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -125,6 +125,7 @@ CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis) NAR = "NAR" RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission) +DEM = "DEM" # codes actions REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1) @@ -140,22 +141,26 @@ BUG = "BUG" ALL = "ALL" +# Explication des codes (de demestre ou d'UE) CODES_EXPL = { - ADM: "Validé", ADC: "Validé par compensation", ADJ: "Validé par le Jury", - ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)", + ADM: "Validé", + AJ: "Ajourné", ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)", ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)", - AJ: "Ajourné", - NAR: "Echec, non autorisé à redoubler", - RAT: "En attente d'un rattrapage", + ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)", + CMP: "Code UE acquise car semestre acquis", DEF: "Défaillant", + NAR: "Échec, non autorisé à redoubler", + RAT: "En attente d'un rattrapage", } # Nota: ces explications sont personnalisables via le fichier # de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py # variable: CONFIG.CODES_EXP +# Les codes de semestres: +CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 86525c5c..3f4eb79f 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -738,7 +738,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None ) # Choix code semestre: - codes = list(sco_codes_parcours.CODES_EXPL.keys()) + codes = list(sco_codes_parcours.CODES_JURY_SEM) codes.sort() # fortuitement, cet ordre convient bien ! H.append( diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 8248491a..e14ae526 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -87,7 +87,7 @@ groupEditor = ndb.EditableTable( group_list = groupEditor.list -def get_group(group_id): +def get_group(group_id: int): """Returns group object, with partition""" r = ndb.SimpleDictFetch( """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* @@ -687,6 +687,11 @@ def setGroups( group_id = fs[0].strip() if not group_id: continue + try: + group_id = int(group_id) + except ValueError as exc: + log("setGroups: ignoring invalid group_id={group_id}") + continue group = get_group(group_id) # Anciens membres du groupe: old_members = get_group_members(group_id) diff --git a/app/scodoc/sco_portal_apogee.py b/app/scodoc/sco_portal_apogee.py index f78e9003..836be2ed 100644 --- a/app/scodoc/sco_portal_apogee.py +++ b/app/scodoc/sco_portal_apogee.py @@ -169,7 +169,9 @@ def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2): if doc: break if not doc: - raise ScoValueError("pas de réponse du portail ! (timeout=%s)" % portal_timeout) + raise ScoValueError( + f"pas de réponse du portail !
(timeout={portal_timeout}, requête: {req})" + ) etuds = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req)) # Filtre sur annee inscription Apogee: diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index d193b373..e2f28c69 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -567,7 +567,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): if "prev_decision" in row and row["prev_decision"]: counts[row["prev_decision"]] += 0 # Légende des codes - codes = list(counts.keys()) # sco_codes_parcours.CODES_EXPL.keys() + codes = list(counts.keys()) codes.sort() H.append("

Explication des codes

") lines = [] diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index d0a5407d..9ccbd9c8 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -153,7 +153,10 @@ def _check_notes(notes, evaluation, mod): for (etudid, note) in notes: note = str(note).strip().upper() - etudid = int(etudid) # + try: + etudid = int(etudid) # + except ValueError as exc: + raise ScoValueError(f"Code étudiant ({etudid}) invalide") if note[:3] == "DEM": continue # skip ! if note: diff --git a/app/templates/config_codes_decisions.html b/app/templates/config_codes_decisions.html new file mode 100644 index 00000000..0c2f32b2 --- /dev/null +++ b/app/templates/config_codes_decisions.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

Configuration des codes de décision exportés vers Apogée

+ + +
+

Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury +et les validations de semestres ou d'UE. les valeurs indiquées ici sont utilisées +dans les exports Apogée. +

+

Ne les modifier que si vous savez ce que vous faites ! +

+
+
+
+ {{ wtf.quick_form(form) }} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/configuration.html b/app/templates/configuration.html index b874d48d..f9d060ad 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -92,6 +92,8 @@
Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):
{{ render_field(form.bonus_sport_func_name, onChange="submit_form()")}} +

Exports Apogée

+

configuration des codes de décision

Bibliothèque de logos

{% for dept_entry in form.depts.entries %} {% set dept_form = dept_entry.form %} diff --git a/app/views/notes.py b/app/views/notes.py index 35c9ca83..4be64afb 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -290,13 +290,17 @@ def formsemestre_bulletinetud( if etudid: etud = models.Identite.query.get_or_404(etudid) elif code_nip: - etud = models.Identite.query.filter_by( - code_nip=str(code_nip) - ).first_or_404() + etud = ( + models.Identite.query.filter_by(code_nip=str(code_nip)) + .filter_by(dept_id=formsemestre.dept_id) + .first_or_404() + ) elif code_ine: - etud = models.Identite.query.filter_by( - code_ine=str(code_ine) - ).first_or_404() + etud = ( + models.Identite.query.filter_by(code_ine=str(code_ine)) + .filter_by(dept_id=formsemestre.dept_id) + .first_or_404() + ) else: raise ScoValueError( "Paramètre manquant: spécifier code_nip ou etudid ou code_ine" diff --git a/app/views/scodoc.py b/app/views/scodoc.py index a7aedaa8..394dfe83 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -33,49 +33,38 @@ Emmanuel Viennet, 2021 import datetime import io -import wtforms.validators - -from app.auth.models import User -import os - import flask from flask import abort, flash, url_for, redirect, render_template, send_file from flask import request -from flask.app import Flask import flask_login from flask_login.utils import login_required, current_user -from flask_wtf import FlaskForm -from flask_wtf.file import FileField, FileAllowed -from werkzeug.exceptions import BadRequest, NotFound -from wtforms import SelectField, SubmitField, FormField, validators, Form, FieldList -from wtforms.fields import IntegerField -from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField -from wtforms.validators import ValidationError, DataRequired, Email, EqualTo +from PIL import Image as PILImage + +from werkzeug.exceptions import BadRequest, NotFound + -import app from app import db +from app.auth.models import User from app.forms.main import config_forms from app.forms.main.create_dept import CreateDeptForm +from app.forms.main.config_apo import CodesDecisionsForm +from app import models from app.models import Departement, Identite from app.models import departements from app.models import FormSemestre, FormSemestreInscription -import sco_version -from app.scodoc import sco_logos +from app.models import ScoDocSiteConfig +from app.scodoc import sco_codes_parcours, sco_logos from app.scodoc import sco_find_etud from app.scodoc import sco_utils as scu from app.decorators import ( admin_required, scodoc7func, scodoc, - permission_required_compat_scodoc7, - permission_required, ) from app.scodoc.sco_exceptions import AccessDenied -from app.scodoc.sco_logos import find_logo from app.scodoc.sco_permissions import Permission from app.views import scodoc_bp as bp - -from PIL import Image as PILImage +import sco_version @bp.route("/") @@ -132,6 +121,28 @@ def toggle_dept_vis(dept_id): return redirect(url_for("scodoc.index")) +@bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"]) +@admin_required +def config_codes_decisions(): + """Form config codes decisions""" + form = CodesDecisionsForm() + if request.method == "POST" and form.cancel.data: # cancel button + return redirect(url_for("scodoc.index")) + if form.validate_on_submit(): + for code in models.config.CODES_SCODOC_TO_APO: + ScoDocSiteConfig.set_code_apo(code, getattr(form, code).data) + flash(f"Codes décisions enregistrés.") + return redirect(url_for("scodoc.index")) + elif request.method == "GET": + for code in models.config.CODES_SCODOC_TO_APO: + getattr(form, code).data = ScoDocSiteConfig.get_code_apo(code) + return render_template( + "config_codes_decisions.html", + form=form, + title="Configuration des codes de décisions", + ) + + @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"]) @login_required def table_etud_in_accessible_depts(): @@ -255,14 +266,16 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True): suffix = logo.suffix if small: with PILImage.open(logo.filepath) as im: - im.thumbnail(SMALL_SIZE) - stream = io.BytesIO() # on garde le même format (on pourrait plus simplement générer systématiquement du JPEG) fmt = { # adapt suffix to be compliant with PIL save format "PNG": "PNG", "JPG": "JPEG", "JPEG": "JPEG", }[suffix.upper()] + if fmt == "JPEG": + im = im.convert("RGB") + im.thumbnail(SMALL_SIZE) + stream = io.BytesIO() im.save(stream, fmt) stream.seek(0) return send_file(stream, mimetype=f"image/{fmt}") diff --git a/app/views/users.py b/app/views/users.py index cc9b9db0..fe65348c 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -81,7 +81,7 @@ _l = _ class ChangePasswordForm(FlaskForm): user_name = HiddenField() old_password = PasswordField(_l("Identifiez-vous")) - new_password = PasswordField(_l("Nouveau mot de passe")) + new_password = PasswordField(_l("Nouveau mot de passe de l'utilisateur")) bis_password = PasswordField( _l("Répéter"), validators=[ diff --git a/sco_version.py b/sco_version.py index 36fffe4e..55a2c785 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.29" +SCOVERSION = "9.1.32" SCONAME = "ScoDoc" diff --git a/scodoc.py b/scodoc.py index 976443a3..f18f0892 100755 --- a/scodoc.py +++ b/scodoc.py @@ -300,14 +300,6 @@ def delete_dept(dept): # delete-dept from app.scodoc import notesdb as ndb from app.scodoc import sco_dept - if False: - click.confirm( - f"""Attention: Cela va effacer toutes les données du département {dept} - (étudiants, notes, formations, etc) - Voulez-vous vraiment continuer ? - """, - abort=True, - ) db.reflect() ndb.open_db_connection() d = models.Departement.query.filter_by(acronym=dept).first()