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("
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 ! +
+configuration des codes de décision