diff --git a/app/__init__.py b/app/__init__.py index 0a589144..76f9471b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -253,7 +253,7 @@ def create_app(config_class=DevConfig): host_name = socket.gethostname() mail_handler = ScoSMTPHandler( mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]), - fromaddr="no-reply@" + app.config["MAIL_SERVER"], + fromaddr=app.config["SCODOC_MAIL_FROM"], toaddrs=["exception@scodoc.org"], subject="ScoDoc Exception", # unused see ScoSMTPHandler credentials=auth, diff --git a/app/auth/email.py b/app/auth/email.py index 9ac8a017..61759691 100644 --- a/app/auth/email.py +++ b/app/auth/email.py @@ -8,7 +8,7 @@ def send_password_reset_email(user): token = user.get_reset_password_token() send_email( "[ScoDoc] Réinitialisation de votre mot de passe", - sender=current_app.config["ADMINS"][0], + sender=current_app.config["SCODOC_MAIL_FROM"], recipients=[user.email], text_body=render_template("email/reset_password.txt", user=user, token=token), html_body=render_template("email/reset_password.html", user=user, token=token), diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index c3596f92..1d8ff453 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -40,6 +40,7 @@ import pandas as pd from app import db from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError @dataclass @@ -280,7 +281,11 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: for ue_poids in EvaluationUEPoids.query.join( EvaluationUEPoids.evaluation ).filter_by(moduleimpl_id=moduleimpl_id): - evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids + try: + evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids + except KeyError as exc: + pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre... + # Initialise poids non enregistrés: if np.isnan(evals_poids.values.flat).any(): ue_coefs = modimpl.module.get_ue_coef_dict() 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 642e3187..25ac6c11 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -6,13 +6,13 @@ XXX version préliminaire ScoDoc8 #sco8 sans département CODE_STR_LEN = 16 # chaine pour les codes SHORT_STR_LEN = 32 # courtes chaine, eg acronymes -APO_CODE_STR_LEN = 24 # nb de car max d'un code Apogée +APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs) 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.etudiants import ( Identite, @@ -57,7 +57,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..f9345942 --- /dev/null +++ b/app/models/config.py @@ -0,0 +1,192 @@ +# -*- coding: UTF-8 -* + +"""Model : site config WORK IN PROGRESS #WIP +""" + +from app import db, log +from app.comp import bonus_spo +from app.scodoc.sco_exceptions import ScoValueError + +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_class(cls, class_name): + """Record bonus_sport config. + If class_name not defined, raise NameError + """ + if class_name not in cls.get_bonus_sport_class_names(): + raise NameError("invalid class name for bonus_sport") + c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() + if c: + log("setting to " + class_name) + c.value = class_name + else: + c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name) + db.session.add(c) + db.session.commit() + + @classmethod + def get_bonus_sport_class_name(cls): + """Get configured bonus function name, or None if None.""" + klass = cls.get_bonus_sport_class_from_name() + if klass is None: + return "" + else: + return klass.name + + @classmethod + def get_bonus_sport_class_from_name(cls, class_name=None): + """returns bonus class with specified name. + If name not specified, return the configured function. + None if no bonus function configured. + Raises ScoValueError if class_name not found in module bonus_sport. + """ + if class_name is None: + c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() + if c is None: + return None + class_name = c.value + if class_name == "": # pas de bonus défini + return None + klass = bonus_spo.get_bonus_class_dict().get(class_name) + if klass is None: + raise ScoValueError( + f"""Fonction de calcul bonus sport inexistante: {class_name}. + (contacter votre administrateur local).""" + ) + return klass + + @classmethod + def get_bonus_sport_class_names(cls): + """List available bonus class names + (starting with empty string to represent "no bonus function"). + """ + return [""] + sorted(bonus_spo.get_bonus_class_dict().keys()) + + @classmethod + def get_bonus_sport_func(cls): + """Fonction bonus_sport ScoDoc 7 XXX + Transitoire pour les tests durant la transition #sco92 + """ + """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. + """ + from app.scodoc import bonus_sport + + 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_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/evaluations.py b/app/models/evaluations.py index 4f06fb75..b4e5f4e2 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -103,7 +103,16 @@ class Evaluation(db.Model): Note: si les poids ne sont pas initialisés (poids par défaut), ils ne sont pas affichés. """ - return ", ".join([f"{p.ue.acronyme}: {p.poids}" for p in self.ue_poids]) + # restreint aux UE du semestre dans lequel est cette évaluation + # au cas où le module ait changé de semestre et qu'il reste des poids + evaluation_semestre_idx = self.moduleimpl.module.semestre_id + return ", ".join( + [ + f"{p.ue.acronyme}: {p.poids}" + for p in self.ue_poids + if evaluation_semestre_idx == p.ue.semestre_idx + ] + ) class EvaluationUEPoids(db.Model): 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/formsemestre.py b/app/models/formsemestre.py index e1febc32..4d436f8e 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -287,7 +287,7 @@ class FormSemestre(db.Model): self.date_fin.year})""" def titre_num(self) -> str: - """Le titre est le semestre, ex ""DUT Informatique semestre 2"" """ + """Le titre et le semestre, ex ""DUT Informatique semestre 2"" """ if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID: return self.titre return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" diff --git a/app/models/preferences.py b/app/models/preferences.py index f220ee17..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.comp import bonus_spo -from app.scodoc.sco_exceptions import ScoValueError + +from app import db class ScoPreference(db.Model): @@ -19,128 +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_class(cls, class_name): - """Record bonus_sport config. - If class_name not defined, raise NameError - """ - if class_name not in cls.get_bonus_sport_class_names(): - raise NameError("invalid class name for bonus_sport") - c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() - if c: - log("setting to " + class_name) - c.value = class_name - else: - c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name) - db.session.add(c) - db.session.commit() - - @classmethod - def get_bonus_sport_class_name(cls): - """Get configured bonus function name, or None if None.""" - klass = cls.get_bonus_sport_class_from_name() - if klass is None: - return "" - else: - return klass.name - - @classmethod - def get_bonus_sport_class(cls): - """Get configured bonus function, or None if None.""" - return cls.get_bonus_sport_class_from_name() - - @classmethod - def get_bonus_sport_class_from_name(cls, class_name=None): - """returns bonus class with specified name. - If name not specified, return the configured function. - None if no bonus function configured. - Raises ScoValueError if class_name not found in module bonus_sport. - """ - if class_name is None: - c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() - if c is None: - return None - class_name = c.value - if class_name == "": # pas de bonus défini - return None - klass = bonus_spo.get_bonus_class_dict().get(class_name) - if klass is None: - raise ScoValueError( - f"""Fonction de calcul bonus sport inexistante: {class_name}. - (contacter votre administrateur local).""" - ) - return klass - - @classmethod - def get_bonus_sport_class_names(cls): - """List available functions names - (starting with empty string to represent "no bonus function"). - """ - return [""] + sorted(bonus_spo.get_bonus_class_dict().keys()) - - @classmethod - def get_bonus_sport_func(cls): - """Fonction bonus_sport ScoDoc 7 XXX - Transitoire pour les tests durant la transition #sco92 - """ - """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. - """ - from app.scodoc import bonus_sport - - 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).""" - ) diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 090f18e0..07cbd133 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -788,7 +788,12 @@ class NotesTable: moy_ue_cap = ue_cap["moy"] mu["was_capitalized"] = True event_date = event_date or ue_cap["event_date"] - if (moy_ue_cap != "NA") and (moy_ue_cap > max_moy_ue): + if ( + (moy_ue_cap != "NA") + and isinstance(moy_ue_cap, float) + and isinstance(max_moy_ue, float) + and (moy_ue_cap > max_moy_ue) + ): # meilleure UE capitalisée event_date = ue_cap["event_date"] max_moy_ue = moy_ue_cap @@ -1329,7 +1334,11 @@ class NotesTable: t[0] = results.etud_moy_gen[etudid] for i, ue in enumerate(ues, start=1): if ue["type"] != UE_SPORT: - t[i] = results.etud_moy_ue[ue["id"]][etudid] + # temporaire pour 9.1.29 ! + if ue["id"] in results.etud_moy_ue: + t[i] = results.etud_moy_ue[ue["id"]][etudid] + else: + t[i] = "" # re-trie selon la nouvelle moyenne générale: self.T.sort(key=self._row_key) # Remplace aussi le rang: diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 8fad07dd..565d1168 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_edit_module.py b/app/scodoc/sco_edit_module.py index fbc28db6..b575123e 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -32,15 +32,16 @@ import flask from flask import url_for, render_template from flask import g, request from flask_login import current_user + +from app import log +from app import models from app.models import APO_CODE_STR_LEN -from app.models import Matiere, Module, UniteEns +from app.models import Formation, Matiere, Module, UniteEns +from app.models import FormSemestre, ModuleImpl import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType -from app import log -from app import models -from app.models import Formation from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( @@ -294,6 +295,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): "title": "Code Apogée", "size": 25, "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", + "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, }, ), ( @@ -472,16 +474,31 @@ def module_edit(module_id=None): formation_id = module["formation_id"] formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) - is_apc = parcours.APC_SAE - ues_matieres = ndb.SimpleDictFetch( - """SELECT ue.acronyme, mat.*, mat.id AS matiere_id - FROM notes_matieres mat, notes_ue ue - WHERE mat.ue_id = ue.id - AND ue.formation_id = %(formation_id)s - ORDER BY ue.numero, mat.numero - """, - {"formation_id": formation_id}, - ) + is_apc = parcours.APC_SAE # BUT + in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls + if in_use: + # matières du même semestre seulement + ues_matieres = ndb.SimpleDictFetch( + """SELECT ue.acronyme, mat.*, mat.id AS matiere_id + FROM notes_matieres mat, notes_ue ue + WHERE mat.ue_id = ue.id + AND ue.formation_id = %(formation_id)s + AND ue.semestre_idx = %(semestre_idx)s + ORDER BY ue.numero, mat.numero + """, + {"formation_id": formation_id, "semestre_idx": a_module.ue.semestre_idx}, + ) + else: + # matières de la formation + ues_matieres = ndb.SimpleDictFetch( + """SELECT ue.acronyme, mat.*, mat.id AS matiere_id + FROM notes_matieres mat, notes_ue ue + WHERE mat.ue_id = ue.id + AND ue.formation_id = %(formation_id)s + ORDER BY ue.numero, mat.numero + """, + {"formation_id": formation_id}, + ) mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres] ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres] module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) @@ -500,12 +517,25 @@ def module_edit(module_id=None): ), """