diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index d08719375a..27eb932964 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -54,6 +54,22 @@ class BonusConfigurationForm(FlaskForm): class ScoDocConfigurationForm(FlaskForm): "Panneau de configuration avancée" enable_entreprises = BooleanField("activer le module entreprises") + month_debut_annee_scolaire = SelectField( + label="Mois de début des années scolaires", + description="""Date pivot. En France métropolitaine, août. + S'applique à tous les départements.""", + choices=[ + (i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1) + ], + ) + month_debut_periode2 = SelectField( + label="Mois de début deuxième période de l'année", + description="""Date pivot. En France métropolitaine, décembre. + S'applique à tous les départements.""", + choices=[ + (i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1) + ], + ) submit_scodoc = SubmitField("Valider") cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True}) @@ -67,7 +83,11 @@ def configuration(): } ) form_scodoc = ScoDocConfigurationForm( - data={"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled()} + data={ + "enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(), + "month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(), + "month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(), + } ) if request.method == "POST" and ( form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data @@ -94,6 +114,22 @@ def configuration(): "Module entreprise " + ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé") ) + if ScoDocSiteConfig.set_month_debut_annee_scolaire( + int(form_scodoc.data["month_debut_annee_scolaire"]) + ): + flash( + f"""Début des années scolaires fixé au mois de { + scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_annee_scolaire()-1] + }""" + ) + if ScoDocSiteConfig.set_month_debut_periode2( + int(form_scodoc.data["month_debut_periode2"]) + ): + flash( + f"""Début des années scolaires fixé au mois de { + scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_periode2()-1] + }""" + ) return redirect(url_for("scodoc.index")) return render_template( diff --git a/app/models/config.py b/app/models/config.py index cb65d51987..1b31eb120f 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -6,7 +6,7 @@ from flask import flash from app import db, log from app.comp import bonus_spo -from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc import sco_utils as scu from app.scodoc.sco_codes_parcours import ( ABAN, @@ -83,6 +83,8 @@ class ScoDocSiteConfig(db.Model): "INSTITUTION_CITY": str, "DEFAULT_PDF_FOOTER_TEMPLATE": str, "enable_entreprises": bool, + "month_debut_annee_scolaire": int, + "month_debut_periode2": int, } def __init__(self, name, value): @@ -223,3 +225,73 @@ class ScoDocSiteConfig(db.Model): db.session.commit() return True return False + + @classmethod + def _get_int_field(cls, name: str, default=None) -> int: + """Valeur d'un champs integer""" + cfg = ScoDocSiteConfig.query.filter_by(name=name).first() + if (cfg is None) or cfg.value is None: + return default + return int(cfg.value) + + @classmethod + def _set_int_field( + cls, + name: str, + value: int, + default=None, + range_values: tuple = (), + ) -> bool: + """Set champs integer. True si changement.""" + if value != cls._get_int_field(name, default=default): + if not isinstance(value, int) or ( + range_values and (value < range_values[0]) or (value > range_values[1]) + ): + raise ValueError("invalid value") + cfg = ScoDocSiteConfig.query.filter_by(name=name).first() + if cfg is None: + cfg = ScoDocSiteConfig(name=name, value=str(value)) + else: + cfg.value = str(value) + db.session.add(cfg) + db.session.commit() + return True + return False + + @classmethod + def get_month_debut_annee_scolaire(cls) -> int: + """Mois de début de l'année scolaire.""" + return cls._get_int_field( + "month_debut_annee_scolaire", scu.MONTH_DEBUT_ANNEE_SCOLAIRE + ) + + @classmethod + def get_month_debut_periode2(cls) -> int: + """Mois de début de l'année scolaire.""" + return cls._get_int_field("month_debut_periode2", scu.MONTH_DEBUT_PERIODE2) + + @classmethod + def set_month_debut_annee_scolaire( + cls, month: int = scu.MONTH_DEBUT_ANNEE_SCOLAIRE + ) -> bool: + """Fixe le mois de début des années scolaires. + True si changement. + """ + if cls._set_int_field( + "month_debut_annee_scolaire", month, scu.MONTH_DEBUT_ANNEE_SCOLAIRE, (1, 12) + ): + log(f"set_month_debut_annee_scolaire({month})") + return True + return False + + @classmethod + def set_month_debut_periode2(cls, month: int = scu.MONTH_DEBUT_PERIODE2) -> bool: + """Fixe le mois de début des années scolaires. + True si changement. + """ + if cls._set_int_field( + "month_debut_periode2", month, scu.MONTH_DEBUT_PERIODE2, (1, 12) + ): + log(f"set_month_debut_periode2({month})") + return True + return False diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 6d60bd5dee..61ed814934 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -5,44 +5,42 @@ # See LICENSE ############################################################################## +# pylint génère trop de faux positifs avec les colonnes date: +# pylint: disable=no-member,not-an-iterable + """ScoDoc models: formsemestre """ import datetime from functools import cached_property -from flask import flash, g import flask_sqlalchemy +from flask import flash, g from sqlalchemy.sql import text -from app import db -from app import log -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN +import app.scodoc.sco_utils as scu +from app import db, log +from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN from app.models.but_refcomp import ( ApcAnneeParcours, ApcNiveau, ApcParcours, ApcParcoursNiveauCompetence, ApcReferentielCompetences, + parcours_formsemestre, ) -from app.models.groups import GroupDescr, Partition -from app.scodoc.sco_exceptions import ScoValueError - -import app.scodoc.sco_utils as scu -from app.models.but_refcomp import parcours_formsemestre +from app.models.config import ScoDocSiteConfig from app.models.etudiants import Identite from app.models.formations import Formation -from app.models.modules import Module +from app.models.groups import GroupDescr, Partition from app.models.moduleimpls import ModuleImpl, ModuleImplInscription +from app.models.modules import Module from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation - -from app.scodoc import sco_codes_parcours -from app.scodoc import sco_preferences -from app.scodoc.sco_vdi import ApoEtapeVDI +from app.scodoc import sco_codes_parcours, sco_preferences +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import MONTH_NAMES_ABBREV +from app.scodoc.sco_vdi import ApoEtapeVDI class FormSemestre(db.Model): @@ -226,7 +224,8 @@ class FormSemestre(db.Model): d["mois_debut_ord"] = self.date_debut.month d["mois_fin_ord"] = self.date_fin.month # La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre - # devrait sans doute pouvoir etre changé... + # devrait sans doute pouvoir etre changé... XXX PIVOT + d["periode"] = self.periode() if self.date_debut.month >= 8 and self.date_debut.month <= 10: d["periode"] = 1 # typiquement, début en septembre: S1, S3... else: @@ -345,7 +344,7 @@ class FormSemestre(db.Model): (les dates de début et fin sont incluses) """ today = datetime.date.today() - return (self.date_debut <= today) and (today <= self.date_fin) + return self.date_debut <= today <= self.date_fin def contient_periode(self, date_debut, date_fin) -> bool: """Vrai si l'intervalle [date_debut, date_fin] est @@ -361,14 +360,16 @@ class FormSemestre(db.Model): Pivot au 1er août par défaut. """ if self.date_debut > self.date_fin: + flash(f"Dates début/fin inversées pour le semestre {self.titre_annee()}") log(f"Warning: semestre {self.id} begins after ending !") annee_debut = self.date_debut.year - if self.date_debut.month <= scu.MONTH_FIN_ANNEE_SCOLAIRE: # juillet - # considere que debut sur l'anne scolaire precedente + month_debut_annee = ScoDocSiteConfig.get_month_debut_annee_scolaire() + if self.date_debut.month < month_debut_annee: + # début sur l'année scolaire précédente (juillet inclus par défaut) annee_debut -= 1 annee_fin = self.date_fin.year - if self.date_fin.month <= (scu.MONTH_FIN_ANNEE_SCOLAIRE + 1): - # 9 (sept) pour autoriser un début en sept et une fin en aout + if self.date_fin.month < (month_debut_annee + 1): + # 9 (sept) pour autoriser un début en sept et une fin en août annee_fin -= 1 return annee_debut == annee_fin @@ -383,16 +384,74 @@ class FormSemestre(db.Model): # impair ( self.semestre_id % 2 - and self.date_debut.month < scu.MONTH_FIN_ANNEE_SCOLAIRE + and self.date_debut.month < scu.MONTH_DEBUT_ANNEE_SCOLAIRE ) or # pair ( (not self.semestre_id % 2) - and self.date_debut.month >= scu.MONTH_FIN_ANNEE_SCOLAIRE + and self.date_debut.month >= scu.MONTH_DEBUT_ANNEE_SCOLAIRE ) ) + @classmethod + def comp_periode( + cls, + date_debut: datetime, + mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE, + mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2, + jour_pivot_annee=1, + jour_pivot_periode=1, + ): + """Calcule la session associée à un formsemestre commençant en date_debut + sous la forme (année, période) + année: première année de l'année scolaire + période = 1 (première période de l'année scolaire, souvent automne) + ou 2 (deuxième période de l'année scolaire, souvent printemps) + Les quatre derniers paramètres forment les dates pivots pour l'année + (1er août par défaut) et pour la période (1er décembre par défaut). + + Les calculs se font à partir de la date de début indiquée. + Exemples dans tests/unit/test_periode + + Implémentation: + Cas à considérer pour le calcul de la période + + pa < pp -----------------|-------------------|----------------> + (A-1, P:2) pa (A, P:1) pp (A, P:2) + pp < pa -----------------|-------------------|----------------> + (A-1, P:1) pp (A-1, P:2) pa (A, P:1) + """ + pivot_annee = 100 * mois_pivot_annee + jour_pivot_annee + pivot_periode = 100 * mois_pivot_periode + jour_pivot_periode + pivot_sem = 100 * date_debut.month + date_debut.day + if pivot_sem < pivot_annee: + annee = date_debut.year - 1 + else: + annee = date_debut.year + if pivot_annee < pivot_periode: + if pivot_sem < pivot_annee or pivot_sem >= pivot_periode: + periode = 2 + else: + periode = 1 + else: + if pivot_sem < pivot_periode or pivot_sem >= pivot_annee: + periode = 1 + else: + periode = 2 + return annee, periode + + def periode(self) -> int: + """La période: + * 1 : première période: automne à Paris + * 2 : deuxième période, printemps à Paris + """ + return FormSemestre.comp_periode( + self.date_debut, + mois_pivot_annee=ScoDocSiteConfig.get_month_debut_annee_scolaire(), + mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(), + ) + def etapes_apo_vdi(self) -> list[ApoEtapeVDI]: "Liste des vdis" # was read_formsemestre_etapes @@ -443,7 +502,7 @@ class FormSemestre(db.Model): def annee_scolaire(self) -> int: """L'année de début de l'année scolaire. - Par exemple, 2022 si le semestre va de septebre 2022 à février 2023.""" + Par exemple, 2022 si le semestre va de septembre 2022 à février 2023.""" return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month) def annee_scolaire_str(self): @@ -493,7 +552,9 @@ class FormSemestre(db.Model): ) def titre_annee(self) -> str: - """ """ + """Le titre avec l'année + 'DUT Réseaux et Télécommunications semestre 3 FAP 2020-2021' + """ titre_annee = ( f"{self.titre_num()} {self.modalite or ''} {self.date_debut.year}" ) @@ -685,7 +746,7 @@ class FormSemestre(db.Model): def etud_validations_description_html(self, etudid: int) -> str: """Description textuelle des validations de jury de cet étudiant dans ce semestre""" - from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee + from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE vals_sem = ScolarFormSemestreValidation.query.filter_by( etudid=etudid, formsemestre_id=self.id, ue_id=None diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index 2ec4439368..e24f25f428 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -685,7 +685,7 @@ def EtatAbsences(): """ - % (scu.AnneeScolaire(), datetime.datetime.now().strftime("%d/%m/%Y")), + % (scu.annee_scolaire(), datetime.datetime.now().strftime("%d/%m/%Y")), html_sco_header.sco_footer(), ] return "\n".join(H) @@ -719,15 +719,27 @@ def formChoixSemestreGroupe(all=False): return "\n".join(H) +def _convert_sco_year(year) -> int: + try: + year = int(year) + if year > 1900 and year < 2999: + return year + except: + raise ScoValueError("année scolaire invalide") + + def CalAbs(etudid, sco_year=None): """Calendrier des absences d'un etudiant""" # crude portage from 1999 DTML etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etudid = etud["etudid"] - anneescolaire = int(scu.AnneeScolaire(sco_year)) - datedebut = str(anneescolaire) + "-08-01" - datefin = str(anneescolaire + 1) + "-07-31" - annee_courante = scu.AnneeScolaire() + if sco_year: + annee_scolaire = _convert_sco_year(sco_year) + else: + annee_scolaire = scu.annee_scolaire() + datedebut = str(annee_scolaire) + "-08-01" + datefin = str(annee_scolaire + 1) + "-07-31" + annee_courante = scu.annee_scolaire() nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin) nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin) events = [] @@ -746,7 +758,7 @@ def CalAbs(etudid, sco_year=None): events.append( (str(a["jour"]), "X", "#8EA2C6", "", a["matin"], a["description"]) ) - CalHTML = sco_abs.YearTable(anneescolaire, events=events, halfday=1) + CalHTML = sco_abs.YearTable(annee_scolaire, events=events, halfday=1) # H = [ @@ -777,12 +789,12 @@ def CalAbs(etudid, sco_year=None): CalHTML, """
""") @@ -811,7 +823,11 @@ def ListeAbsEtud( """ # si absjust_only, table absjust seule (export xls ou pdf) absjust_only = scu.to_bool(absjust_only) - datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year) + if sco_year: + annee_scolaire = _convert_sco_year(sco_year) + else: + annee_scolaire = scu.annee_scolaire() + datedebut = f"{annee_scolaire}-{scu.MONTH_DEBUT_ANNEE_SCOLAIRE+1}-01" etudid = etudid or False etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True) if not etuds: diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 3f863ebe48..154846bf0b 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -511,7 +511,7 @@ class ApoEtud(dict): # print 'comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id']) if not cur_sem: # l'étudiant n'a pas de semestre courant ?! - log("comp_elt_annuel: etudid %s has no cur_sem" % etudid) + log(f"comp_elt_annuel: etudid {etudid} has no cur_sem") return VOID_APO_RES cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"]) cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre) @@ -586,15 +586,10 @@ class ApoEtud(dict): (sem["semestre_id"] == apo_data.cur_semestre_id) and (apo_data.etape in sem["etapes"]) and ( - # sco_formsemestre.sem_in_annee_scolaire(sem, apo_data.annee_scolaire) # TODO à remplacer par ? sco_formsemestre.sem_in_semestre_scolaire( sem, apo_data.annee_scolaire, - 0, - # jour_pivot_annee, - # mois_pivot_annee, - # jour_pivot_periode, - # mois_pivot_periode + 0, # annee complete ) ) ) diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 6253d95a48..5caba24a6e 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -49,6 +49,7 @@ from app.scodoc import sco_groups from app.scodoc import sco_photos from app.scodoc import sco_preferences from app.scodoc import sco_etud +from app.scodoc.sco_xml import quote_xml_attr # -------- Bulletin en JSON @@ -129,12 +130,12 @@ def formsemestre_bulletinetud_published_dict( etudid=etudid, code_nip=etudinfo["code_nip"], code_ine=etudinfo["code_ine"], - nom=scu.quote_xml_attr(etudinfo["nom"]), - prenom=scu.quote_xml_attr(etudinfo["prenom"]), - civilite=scu.quote_xml_attr(etudinfo["civilite_str"]), - photo_url=scu.quote_xml_attr(sco_photos.etud_photo_url(etudinfo, fast=True)), - email=scu.quote_xml_attr(etudinfo["email"]), - emailperso=scu.quote_xml_attr(etudinfo["emailperso"]), + nom=quote_xml_attr(etudinfo["nom"]), + prenom=quote_xml_attr(etudinfo["prenom"]), + civilite=quote_xml_attr(etudinfo["civilite_str"]), + photo_url=quote_xml_attr(sco_photos.etud_photo_url(etudinfo, fast=True)), + email=quote_xml_attr(etudinfo["email"]), + emailperso=quote_xml_attr(etudinfo["emailperso"]), ) d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients # Disponible pour publication ? @@ -209,9 +210,9 @@ def formsemestre_bulletinetud_published_dict( rang, effectif = nt.get_etud_ue_rang(ue["ue_id"], etudid) u = dict( id=ue["ue_id"], - numero=scu.quote_xml_attr(ue["numero"]), - acronyme=scu.quote_xml_attr(ue["acronyme"]), - titre=scu.quote_xml_attr(ue["titre"]), + numero=quote_xml_attr(ue["numero"]), + acronyme=quote_xml_attr(ue["acronyme"]), + titre=quote_xml_attr(ue["titre"]), note=dict( value=scu.fmt_note(ue_status["cur_moy_ue"] if ue_status else ""), min=scu.fmt_note(ue["min"]), @@ -223,7 +224,7 @@ def formsemestre_bulletinetud_published_dict( rang=rang, effectif=effectif, ects=ects_txt, - code_apogee=scu.quote_xml_attr(ue["code_apogee"]), + code_apogee=quote_xml_attr(ue["code_apogee"]), ) d["ue"].append(u) u["module"] = [] @@ -247,11 +248,11 @@ def formsemestre_bulletinetud_published_dict( code=mod["code"], coefficient=mod["coefficient"], numero=mod["numero"], - titre=scu.quote_xml_attr(mod["titre"]), - abbrev=scu.quote_xml_attr(mod["abbrev"]), + titre=quote_xml_attr(mod["titre"]), + abbrev=quote_xml_attr(mod["abbrev"]), # ects=ects, ects des modules maintenant inutilisés note=dict(value=mod_moy), - code_apogee=scu.quote_xml_attr(mod["code_apogee"]), + code_apogee=quote_xml_attr(mod["code_apogee"]), ) m["note"].update(modstat) for k in ("min", "max", "moy"): # formatte toutes les notes @@ -291,7 +292,7 @@ def formsemestre_bulletinetud_published_dict( evaluation_id=e[ "evaluation_id" ], # CM : ajout pour permettre de faire le lien sur les bulletins en ligne avec l'évaluation - description=scu.quote_xml_attr(e["description"]), + description=quote_xml_attr(e["description"]), note=val, ) ) @@ -318,7 +319,7 @@ def formsemestre_bulletinetud_published_dict( e["heure_fin"], null_is_empty=True ), coefficient=e["coefficient"], - description=scu.quote_xml_attr(e["description"]), + description=quote_xml_attr(e["description"]), incomplete="1", ) ) @@ -332,9 +333,9 @@ def formsemestre_bulletinetud_published_dict( d["ue_capitalisee"].append( dict( id=ue["ue_id"], - numero=scu.quote_xml_attr(ue["numero"]), - acronyme=scu.quote_xml_attr(ue["acronyme"]), - titre=scu.quote_xml_attr(ue["titre"]), + numero=quote_xml_attr(ue["numero"]), + acronyme=quote_xml_attr(ue["acronyme"]), + titre=quote_xml_attr(ue["titre"]), note=scu.fmt_note(ue_status["moy"]), coefficient_ue=scu.fmt_note(ue_status["coef_ue"]), date_capitalisation=ndb.DateDMYtoISO(ue_status["event_date"]), @@ -358,7 +359,7 @@ def formsemestre_bulletinetud_published_dict( for app in apprecs: d["appreciation"].append( dict( - comment=scu.quote_xml_attr(app["comment"]), + comment=quote_xml_attr(app["comment"]), date=ndb.DateDMYtoISO(app["date"]), ) ) diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index f173b56ba2..661b0bd56f 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -53,7 +53,6 @@ from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat from app.models.formsemestre import FormSemestre from app.scodoc import sco_abs from app.scodoc import sco_codes_parcours -from app.scodoc import sco_cache from app.scodoc import sco_edit_ue from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre @@ -62,6 +61,7 @@ from app.scodoc import sco_photos from app.scodoc import sco_preferences from app.scodoc import sco_etud from app.scodoc import sco_xml +from app.scodoc.sco_xml import quote_xml_attr # -------- Bulletin en XML # (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict() @@ -131,13 +131,13 @@ def make_xml_formsemestre_bulletinetud( etudid=str(etudid), code_nip=str(etudinfo["code_nip"]), code_ine=str(etudinfo["code_ine"]), - nom=scu.quote_xml_attr(etudinfo["nom"]), - prenom=scu.quote_xml_attr(etudinfo["prenom"]), - civilite=scu.quote_xml_attr(etudinfo["civilite_str"]), - sexe=scu.quote_xml_attr(etudinfo["civilite_str"]), # compat - photo_url=scu.quote_xml_attr(sco_photos.etud_photo_url(etudinfo)), - email=scu.quote_xml_attr(etudinfo["email"]), - emailperso=scu.quote_xml_attr(etudinfo["emailperso"]), + nom=quote_xml_attr(etudinfo["nom"]), + prenom=quote_xml_attr(etudinfo["prenom"]), + civilite=quote_xml_attr(etudinfo["civilite_str"]), + sexe=quote_xml_attr(etudinfo["civilite_str"]), # compat + photo_url=quote_xml_attr(sco_photos.etud_photo_url(etudinfo)), + email=quote_xml_attr(etudinfo["email"]), + emailperso=quote_xml_attr(etudinfo["emailperso"]), ) ) @@ -210,10 +210,10 @@ def make_xml_formsemestre_bulletinetud( x_ue = Element( "ue", id=str(ue["ue_id"]), - numero=scu.quote_xml_attr(ue["numero"]), - acronyme=scu.quote_xml_attr(ue["acronyme"]), - titre=scu.quote_xml_attr(ue["titre"]), - code_apogee=scu.quote_xml_attr(ue["code_apogee"]), + numero=quote_xml_attr(ue["numero"]), + acronyme=quote_xml_attr(ue["acronyme"]), + titre=quote_xml_attr(ue["titre"]), + code_apogee=quote_xml_attr(ue["code_apogee"]), ) doc.append(x_ue) if ue["type"] != sco_codes_parcours.UE_SPORT: @@ -255,9 +255,9 @@ def make_xml_formsemestre_bulletinetud( code=str(mod["code"] or ""), coefficient=str(mod["coefficient"]), numero=str(mod["numero"]), - titre=scu.quote_xml_attr(mod["titre"]), - abbrev=scu.quote_xml_attr(mod["abbrev"]), - code_apogee=scu.quote_xml_attr(mod["code_apogee"]) + titre=quote_xml_attr(mod["titre"]), + abbrev=quote_xml_attr(mod["abbrev"]), + code_apogee=quote_xml_attr(mod["code_apogee"]) # ects=ects ects des modules maintenant inutilisés ) x_ue.append(x_mod) @@ -302,7 +302,7 @@ def make_xml_formsemestre_bulletinetud( ), coefficient=str(e["coefficient"]), evaluation_type=str(e["evaluation_type"]), - description=scu.quote_xml_attr(e["description"]), + description=quote_xml_attr(e["description"]), # notes envoyées sur 20, ceci juste pour garder trace: note_max_origin=str(e["note_max"]), ) @@ -333,7 +333,7 @@ def make_xml_formsemestre_bulletinetud( e["heure_fin"], null_is_empty=True ), coefficient=str(e["coefficient"]), - description=scu.quote_xml_attr(e["description"]), + description=quote_xml_attr(e["description"]), incomplete="1", # notes envoyées sur 20, ceci juste pour garder trace: note_max_origin=str(e["note_max"] or ""), @@ -348,9 +348,9 @@ def make_xml_formsemestre_bulletinetud( x_ue = Element( "ue_capitalisee", id=str(ue["ue_id"]), - numero=scu.quote_xml_attr(ue["numero"]), - acronyme=scu.quote_xml_attr(ue["acronyme"]), - titre=scu.quote_xml_attr(ue["titre"]), + numero=quote_xml_attr(ue["numero"]), + acronyme=quote_xml_attr(ue["acronyme"]), + titre=quote_xml_attr(ue["titre"]), ) doc.append(x_ue) x_ue.append(Element("note", value=scu.fmt_note(ue_status["moy"]))) @@ -383,7 +383,7 @@ def make_xml_formsemestre_bulletinetud( ), ) x_situation = Element("situation") - x_situation.text = scu.quote_xml_attr(infos["situation"]) + x_situation.text = quote_xml_attr(infos["situation"]) doc.append(x_situation) if dpv: decision = dpv["decisions"][0] @@ -418,9 +418,9 @@ def make_xml_formsemestre_bulletinetud( Element( "decision_ue", ue_id=str(ue["ue_id"]), - numero=scu.quote_xml_attr(ue["numero"]), - acronyme=scu.quote_xml_attr(ue["acronyme"]), - titre=scu.quote_xml_attr(ue["titre"]), + numero=quote_xml_attr(ue["numero"]), + acronyme=quote_xml_attr(ue["acronyme"]), + titre=quote_xml_attr(ue["titre"]), code=decision["decisions_ue"][ue_id]["code"], ) ) @@ -443,7 +443,7 @@ def make_xml_formsemestre_bulletinetud( "appreciation", date=ndb.DateDMYtoISO(appr["date"]), ) - x_appr.text = scu.quote_xml_attr(appr["comment"]) + x_appr.text = quote_xml_attr(appr["comment"]) doc.append(x_appr) if is_appending: diff --git a/app/scodoc/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py index 294443627c..2c945a7a22 100644 --- a/app/scodoc/sco_etape_bilan.py +++ b/app/scodoc/sco_etape_bilan.py @@ -137,7 +137,7 @@ class DataEtudiant(object): self.data_apogee = None self.data_scodoc = None self.etapes = set() # l'ensemble des étapes où il est inscrit - self.semestres = set() # l'ensemble des semestres où il est inscrit + self.semestres = set() # l'ensemble des formsemestre_id où il est inscrit self.tags = set() # les anomalies relevées self.ind_row = "-" # là où il compte dans les effectifs (ligne et colonne) self.ind_col = "-" @@ -145,8 +145,8 @@ class DataEtudiant(object): def add_etape(self, etape): self.etapes.add(etape) - def add_semestre(self, semestre): - self.semestres.add(semestre) + def add_semestre(self, formsemestre_id: int): + self.semestres.add(formsemestre_id) def set_apogee(self, data_apogee): self.data_apogee = data_apogee @@ -231,25 +231,30 @@ def entete_liste_etudiant(): """ -class EtapeBilan(object): +class EtapeBilan: """ Structure de donnée représentation l'état global de la comparaison ScoDoc/Apogée """ def __init__(self): - self.semestres = ( - {} - ) # Dictionnaire des formsemestres du semset (formsemestre_id -> semestre) + self.semestres = {} + "Dictionnaire des formsemestres du semset (formsemestre_id -> semestre)" self.etapes = [] # Liste des étapes apogées du semset (clé_apogée) - # pour les descriptions qui suivents: + # pour les descriptions qui suivent: # cle_etu = nip si non vide, sinon etudid # data_etu = { nip, etudid, data_apogee, data_scodoc } - self.etudiants = {} # cle_etu -> data_etu - self.keys_etu = {} # nip -> [ etudid* ] - self.etu_semestre = {} # semestre -> { key_etu } - self.etu_etapes = {} # etape -> { key_etu } - self.repartition = {} # (ind_row, ind_col) -> nombre d étudiants - self.tag_count = {} # nombre d'animalies détectées (par type d'anomalie) + self.etudiants = {} + "cle_etu -> data_etu" + self.keys_etu = {} + "nip -> [ etudid* ]" + self.etu_semestre = {} + "semestre -> { key_etu }" + self.etu_etapes = {} + "etape -> { key_etu }" + self.repartition = {} + "(ind_row, ind_col) -> nombre d étudiants" + self.tag_count = {} + "nombre d'anomalies détectées (par type d'anomalie)" # on collectionne les indicatifs trouvés pour n'afficher que les indicatifs 'utiles' self.indicatifs = {} @@ -273,7 +278,8 @@ class EtapeBilan(object): self.tag_count[tag] = 0 self.tag_count[tag] += 1 - def set_indicatif(self, item, as_row): # item = semestre ou key_etape + def set_indicatif(self, item, as_row): + """item = semestre ou key_etape""" if as_row: indicatif = "R" + chr(self.top_row + 97) self.all_rows_ind.append(indicatif) @@ -288,7 +294,7 @@ class EtapeBilan(object): if self.top_col > 26: log("Dépassement (plus de 26 étapes dans la table diagnostic") - def add_sem(self, semestre): + def add_sem(self, sem: dict): """ Prise en compte d'un semestre dans le bilan. * ajoute le semestre et les étudiants du semestre @@ -296,16 +302,16 @@ class EtapeBilan(object): :param semestre: Le semestre à prendre en compte :return: None """ - self.semestres[semestre["formsemestre_id"]] = semestre + self.semestres[sem["formsemestre_id"]] = sem # if anneeapogee == None: # année d'inscription par défaut - anneeapogee = str( - annee_scolaire_debut(semestre["annee_debut"], semestre["mois_debut_ord"]) + annee_apogee = str( + annee_scolaire_debut(sem["annee_debut"], sem["mois_debut_ord"]) ) - self.set_indicatif(semestre["formsemestre_id"], True) - for etape in semestre["etapes"]: - self.add_etape(etape.etape_vdi, anneeapogee) + self.set_indicatif(sem["formsemestre_id"], True) + for etape in sem["etapes"]: + self.add_etape(etape.etape_vdi, annee_apogee) - def add_etape(self, etape_str, anneeapogee): + def add_etape(self, etape_str, annee_apogee): """ Prise en compte d'une étape apogée :param etape_str: La clé de l'étape à prendre en compte @@ -313,7 +319,7 @@ class EtapeBilan(object): :return: None """ if etape_str != "": - key_etape = etape_to_key(anneeapogee, etape_str) + key_etape = etape_to_key(annee_apogee, etape_str) if key_etape not in self.etapes: self.etapes.append(key_etape) self.set_indicatif( @@ -367,7 +373,7 @@ class EtapeBilan(object): self.etudiants[key_etu].add_etape(etape) return key_etu - def register_etud_scodoc(self, etud, semestre): + def register_etud_scodoc(self, etud: dict, sem: dict): """ Enregistrement de l'étudiant par rapport à son semestre :param etud: Les données de l'étudiant @@ -380,10 +386,10 @@ class EtapeBilan(object): if key_etu not in self.etudiants: data = DataEtudiant(nip, etudid) data.set_scodoc(etud) - data.add_semestre(semestre) + data.add_semestre(sem) self.etudiants[key_etu] = data else: - self.etudiants[key_etu].add_semestre(semestre) + self.etudiants[key_etu].add_semestre(sem) return key_etu def load_listes(self): @@ -393,12 +399,12 @@ class EtapeBilan(object): * Puis pour toutes les étapes :return: None """ - for semestre in self.semestres: - etuds = self.semestres[semestre]["etuds"] - self.etu_semestre[semestre] = set() + for formsemestre_id, sem in self.semestres.items(): + etuds = sem["etuds"] + self.etu_semestre[formsemestre_id] = set() for etud in etuds: - key_etu = self.register_etud_scodoc(etud, semestre) - self.etu_semestre[semestre].add(key_etu) + key_etu = self.register_etud_scodoc(etud, formsemestre_id) + self.etu_semestre[formsemestre_id].add(key_etu) for key_etape in self.etapes: anneeapogee, etapestr = key_to_values(key_etape) @@ -426,17 +432,17 @@ class EtapeBilan(object): self.repartition[ROW_CUMUL, self.indicatifs[key_etape]] = 0 # recherche des nip identiques - for nip in self.keys_etu: + for nip, keys_etu_nip in self.keys_etu.items(): if nip != "": - nbnips = len(self.keys_etu[nip]) + nbnips = len(keys_etu_nip) if nbnips > 1: - for i, etudid in enumerate(self.keys_etu[nip]): + for i, etudid in enumerate(keys_etu_nip): data_etu = self.etudiants[nip, etudid] data_etu.add_tag(NIP_NON_UNIQUE) - data_etu.nip = data_etu.nip + " (%d/%d)" % (i + 1, nbnips) + data_etu.nip = data_etu.nip + f" ({i+1}/{nbnips})" self.inc_tag_count(NIP_NON_UNIQUE) - for nip in self.keys_etu: - for etudid in self.keys_etu[nip]: + for nip, keys_etu_nip in self.keys_etu.items(): + for etudid in keys_etu_nip: key_etu = (nip, etudid) data_etu = self.etudiants[key_etu] ind_col = "-" @@ -504,7 +510,7 @@ class EtapeBilan(object): if (ind_row, ind_col) in self.repartition: count = self.repartition[ind_row, ind_col] if count > 1: - comptage = "(%d étudiants)" % count + comptage = f"({count} étudiants)" else: comptage = "(1 étudiant)" else: diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index 632f5b72c6..bcf06fceac 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -939,7 +939,18 @@ def fill_etuds_info(etuds: list[dict], add_admission=True): def etud_inscriptions_infos(etudid: int, ne="") -> dict: - """Dict avec les informations sur les semestres passés et courant""" + """Dict avec les informations sur les semestres passés et courant. + { + "sems" : , + "ins" : , + "cursem" : , + "inscription" : , + "inscriptionstr" : , + "inscription_formsemestre_id" : , + "etatincursem" : , + "situation" : , + } + """ from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 38b0e9cb98..931618951e 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -27,26 +27,23 @@ """Operations de base sur les formsemestres """ -from operator import itemgetter import datetime import time +from operator import itemgetter -from flask import g, request +from flask import g, request, url_for import app -from app import log -from app.models import Departement - -from app.scodoc import sco_codes_parcours -from app.scodoc import sco_cache -from app.scodoc import sco_formations -from app.scodoc import sco_preferences -from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID -from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidIdType -from app.scodoc.sco_vdi import ApoEtapeVDI import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu +from app import log +from app.models import Departement +from app.models import FormSemestre +from app.scodoc import sco_cache, sco_codes_parcours, sco_formations, sco_preferences +from app.scodoc.gen_tables import GenTable +from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID +from app.scodoc.sco_exceptions import ScoInvalidIdType, ScoValueError +from app.scodoc.sco_vdi import ApoEtapeVDI _formsemestreEditor = ndb.EditableTable( "notes_formsemestre", @@ -82,11 +79,9 @@ _formsemestreEditor = ndb.EditableTable( "date_debut": ndb.DateDMYtoISO, "date_fin": ndb.DateDMYtoISO, "etat": bool, - "gestion_compensation": bool, "bul_hide_xml": bool, "block_moyennes": bool, "block_moyenne_generale": bool, - "gestion_semestrielle": bool, "gestion_compensation": bool, "gestion_semestrielle": bool, "resp_can_edit": bool, @@ -99,7 +94,7 @@ _formsemestreEditor = ndb.EditableTable( def get_formsemestre(formsemestre_id, raise_soft_exc=False): "list ONE formsemestre" if formsemestre_id is None: - raise ValueError(f"get_formsemestre: id manquant") + raise ValueError("get_formsemestre: id manquant") if formsemestre_id in g.stored_get_formsemestre: return g.stored_get_formsemestre[formsemestre_id] if not isinstance(formsemestre_id, int): @@ -107,7 +102,7 @@ def get_formsemestre(formsemestre_id, raise_soft_exc=False): raise ScoInvalidIdType("get_formsemestre: formsemestre_id must be an integer !") sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id}) if not sems: - log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id) + log(f"get_formsemestre: invalid formsemestre_id ({formsemestre_id})") if raise_soft_exc: raise ScoValueError(f"semestre {formsemestre_id} inconnu !") else: @@ -240,8 +235,8 @@ def etapes_apo_str(etapes): def do_formsemestre_create(args, silent=False): "create a formsemestre" - from app.scodoc import sco_groups from app.models import ScolarNews + from app.scodoc import sco_groups cnx = ndb.GetDBConnexion() formsemestre_id = _formsemestreEditor.create(cnx, args) @@ -422,97 +417,46 @@ def sem_set_responsable_name(sem): ) -def get_periode( - debut: datetime, - jour_pivot_annee=1, - mois_pivot_annee=8, - jour_pivot_periode=1, - mois_pivot_periode=12, -): - """Calcule la session associée à un formsemestre sous la forme (année, période) - année: première année de l'année scolaire - période = 1 (première période de l'année scolaire anciennement automne) - ou 2 (deuxième période de l'année scolaire - anciennement printemps) - les quatre derniers paramètres forment les dates pivots pour l'année (1er août par défaut) - et pour la période (1er décembre par défaut). - Tous les calculs se font à partir de la date de début du formsemestre. - Exemples dans tests/unit/test_periode - """ - """Implementation - Cas à considérer pour le calcul de la période - - pa < pp -----------------|-------------------|----------------> - (A-1, P:2) pa (A, P:1) pp (A, P:2) - pp < pa -----------------|-------------------|----------------> - (A-1, P:1) pp (A-1, P:2) pa (A, P:1) - """ - pa = 100 * mois_pivot_annee + jour_pivot_annee - pp = 100 * mois_pivot_periode + jour_pivot_periode - ps = 100 * debut.month + debut.day - if ps < pa: - annee = debut.year - 1 - else: - annee = debut.year - if pa < pp: - if ps < pa or ps > pp: - periode = 2 - else: - periode = 1 - else: - if ps < pp or ps > pa: - periode = 1 - else: - periode = 2 - return annee, periode - - def sem_in_semestre_scolaire( sem, year=False, periode=None, - jour_pivot_annee=1, - mois_pivot_annee=8, - jour_pivot_periode=1, - mois_pivot_periode=12, -): - """n'utilise que la date de debut, - si annee non specifiée, année scolaire courante - la période garde les même convention que semset["sem_id"]; - * 1 : premère période + mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE, + mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2, +) -> bool: + """Vrai si la date du début du semestre est dans la période indiquée (1,2,0) + du semestre `periode` de l'année scolaire indiquée + (ou, à défaut, de celle en cours). + + La période utilise les même conventions que semset["sem_id"]; + * 1 : première période * 2 : deuxième période - * 0 ou periode non précisée: annualisé (donc inclut toutes les périodes) + * 0 ou période non précisée: annualisé (donc inclut toutes les périodes) ) """ if not year: - year = scu.AnneeScolaire() - # calcule l'année universitaire et la periode - sem_annee, sem_periode = get_periode( + year = scu.annee_scolaire() + # n'utilise pas le jour pivot + jour_pivot_annee = jour_pivot_periode = 1 + # calcule l'année universitaire et la période + sem_annee, sem_periode = FormSemestre.comp_periode( datetime.datetime.fromisoformat(sem["date_debut_iso"]), - jour_pivot_annee, mois_pivot_annee, - jour_pivot_periode, mois_pivot_periode, + jour_pivot_annee, + jour_pivot_periode, ) if periode is None or periode == 0: return sem_annee == year - else: - return sem_annee == year and sem_periode == periode + return sem_annee == year and sem_periode == periode -# def sem_in_annee_scolaire(sem, year=False): -# """Test si sem appartient à l'année scolaire year (int). -# N'utilise que la date de début, pivot au 1er août. -# Si année non specifiée, année scolaire courante -# """ -# if not year: -# year = scu.AnneeScolaire() -# return ( -# (sem["annee_debut"] == str(year)) -# and (sem["mois_debut_ord"] > scu.MONTH_FIN_ANNEE_SCOLAIRE) -# ) or ( -# (sem["annee_debut"] == str(year + 1)) -# and (sem["mois_debut_ord"] <= scu.MONTH_FIN_ANNEE_SCOLAIRE) -# ) +def sem_in_annee_scolaire(sem, year=False): + """Test si sem appartient à l'année scolaire year (int). + N'utilise que la date de début, pivot au 1er août. + Si année non specifiée, année scolaire courante + """ + return sem_in_semestre_scolaire(sem, year, periode=0) def sem_est_courant(sem): # -> FormSemestre.est_courant @@ -520,7 +464,7 @@ def sem_est_courant(sem): # -> FormSemestre.est_courant now = time.strftime("%Y-%m-%d") debut = ndb.DateDMYtoISO(sem["date_debut"]) fin = ndb.DateDMYtoISO(sem["date_fin"]) - return (debut <= now) and (now <= fin) + return debut <= now <= fin def scodoc_get_all_unlocked_sems(): @@ -540,7 +484,7 @@ def scodoc_get_all_unlocked_sems(): def table_formsemestres( - sems, + sems: list[dict], columns_ids=(), sup_columns_ids=(), html_title="