diff --git a/app/models/__init__.py b/app/models/__init__.py index 4a3328b32d..364943aa8e 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/preferences.py b/app/models/preferences.py index 59c82ec805..924f6e604f 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 f97cc895ee..833a7841ff 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 4ff29bb79c..38d3e4fe8c 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) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 8248491af1..87d50e3f78 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -87,8 +87,10 @@ groupEditor = ndb.EditableTable( group_list = groupEditor.list -def get_group(group_id): +def get_group(group_id: int): """Returns group object, with partition""" + if not isinstance(group_id, int): + raise ValueError("invalid group_id (%s)" % group_id) r = ndb.SimpleDictFetch( """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* FROM group_descr gd, partition p @@ -687,6 +689,10 @@ def setGroups( group_id = fs[0].strip() if not group_id: continue + try: + group_id = int(group_id) + except ValueError as exc: + raise ValueError("invalid group_id: not an integer") 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 f78e90034f..836be2ed9b 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/templates/configuration.html b/app/templates/configuration.html index b874d48dbf..f9d060ad83 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/scodoc.py b/app/views/scodoc.py index a7aedaa88d..02bc1fb113 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(): diff --git a/app/views/users.py b/app/views/users.py index cc9b9db01a..fe65348cfc 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/scodoc.py b/scodoc.py index 976443a3e3..f18f0892b3 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()