Paramétrage dates annees scolaires (pivots) + tous test unitaires OK

This commit is contained in:
Emmanuel Viennet 2022-11-13 14:55:18 +01:00
parent 3bc60c268a
commit 0148b4b2ce
15 changed files with 439 additions and 300 deletions

View File

@ -54,6 +54,22 @@ class BonusConfigurationForm(FlaskForm):
class ScoDocConfigurationForm(FlaskForm): class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration avancée" "Panneau de configuration avancée"
enable_entreprises = BooleanField("activer le module <em>entreprises</em>") enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
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") submit_scodoc = SubmitField("Valider")
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True}) cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
@ -67,7 +83,11 @@ def configuration():
} }
) )
form_scodoc = ScoDocConfigurationForm( 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 ( if request.method == "POST" and (
form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data
@ -94,6 +114,22 @@ def configuration():
"Module entreprise " "Module entreprise "
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé") + ("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 redirect(url_for("scodoc.index"))
return render_template( return render_template(

View File

@ -6,7 +6,7 @@
from flask import flash from flask import flash
from app import db, log from app import db, log
from app.comp import bonus_spo 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 ( from app.scodoc.sco_codes_parcours import (
ABAN, ABAN,
@ -83,6 +83,8 @@ class ScoDocSiteConfig(db.Model):
"INSTITUTION_CITY": str, "INSTITUTION_CITY": str,
"DEFAULT_PDF_FOOTER_TEMPLATE": str, "DEFAULT_PDF_FOOTER_TEMPLATE": str,
"enable_entreprises": bool, "enable_entreprises": bool,
"month_debut_annee_scolaire": int,
"month_debut_periode2": int,
} }
def __init__(self, name, value): def __init__(self, name, value):
@ -223,3 +225,73 @@ class ScoDocSiteConfig(db.Model):
db.session.commit() db.session.commit()
return True return True
return False 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

View File

@ -5,44 +5,42 @@
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
# pylint génère trop de faux positifs avec les colonnes date:
# pylint: disable=no-member,not-an-iterable
"""ScoDoc models: formsemestre """ScoDoc models: formsemestre
""" """
import datetime import datetime
from functools import cached_property from functools import cached_property
from flask import flash, g
import flask_sqlalchemy import flask_sqlalchemy
from flask import flash, g
from sqlalchemy.sql import text from sqlalchemy.sql import text
from app import db import app.scodoc.sco_utils as scu
from app import log from app import db, log
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcAnneeParcours, ApcAnneeParcours,
ApcNiveau, ApcNiveau,
ApcParcours, ApcParcours,
ApcParcoursNiveauCompetence, ApcParcoursNiveauCompetence,
ApcReferentielCompetences, ApcReferentielCompetences,
parcours_formsemestre,
) )
from app.models.groups import GroupDescr, Partition from app.models.config import ScoDocSiteConfig
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.etudiants import Identite from app.models.etudiants import Identite
from app.models.formations import Formation 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.moduleimpls import ModuleImpl, ModuleImplInscription
from app.models.modules import Module
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_codes_parcours, sco_preferences
from app.scodoc import sco_codes_parcours from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
from app.scodoc.sco_vdi import ApoEtapeVDI
class FormSemestre(db.Model): class FormSemestre(db.Model):
@ -226,7 +224,8 @@ class FormSemestre(db.Model):
d["mois_debut_ord"] = self.date_debut.month d["mois_debut_ord"] = self.date_debut.month
d["mois_fin_ord"] = self.date_fin.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 # 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: if self.date_debut.month >= 8 and self.date_debut.month <= 10:
d["periode"] = 1 # typiquement, début en septembre: S1, S3... d["periode"] = 1 # typiquement, début en septembre: S1, S3...
else: else:
@ -345,7 +344,7 @@ class FormSemestre(db.Model):
(les dates de début et fin sont incluses) (les dates de début et fin sont incluses)
""" """
today = datetime.date.today() 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: def contient_periode(self, date_debut, date_fin) -> bool:
"""Vrai si l'intervalle [date_debut, date_fin] est """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. Pivot au 1er août par défaut.
""" """
if self.date_debut > self.date_fin: 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 !") log(f"Warning: semestre {self.id} begins after ending !")
annee_debut = self.date_debut.year annee_debut = self.date_debut.year
if self.date_debut.month <= scu.MONTH_FIN_ANNEE_SCOLAIRE: # juillet month_debut_annee = ScoDocSiteConfig.get_month_debut_annee_scolaire()
# considere que debut sur l'anne scolaire precedente 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_debut -= 1
annee_fin = self.date_fin.year annee_fin = self.date_fin.year
if self.date_fin.month <= (scu.MONTH_FIN_ANNEE_SCOLAIRE + 1): if self.date_fin.month < (month_debut_annee + 1):
# 9 (sept) pour autoriser un début en sept et une fin en aout # 9 (sept) pour autoriser un début en sept et une fin en août
annee_fin -= 1 annee_fin -= 1
return annee_debut == annee_fin return annee_debut == annee_fin
@ -383,16 +384,74 @@ class FormSemestre(db.Model):
# impair # impair
( (
self.semestre_id % 2 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 or
# pair # pair
( (
(not self.semestre_id % 2) (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]: def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
"Liste des vdis" "Liste des vdis"
# was read_formsemestre_etapes # was read_formsemestre_etapes
@ -443,7 +502,7 @@ class FormSemestre(db.Model):
def annee_scolaire(self) -> int: def annee_scolaire(self) -> int:
"""L'année de début de l'année scolaire. """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) return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
def annee_scolaire_str(self): def annee_scolaire_str(self):
@ -493,7 +552,9 @@ class FormSemestre(db.Model):
) )
def titre_annee(self) -> str: 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 = ( titre_annee = (
f"{self.titre_num()} {self.modalite or ''} {self.date_debut.year}" 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: def etud_validations_description_html(self, etudid: int) -> str:
"""Description textuelle des validations de jury de cet étudiant dans ce semestre""" """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( vals_sem = ScolarFormSemestreValidation.query.filter_by(
etudid=etudid, formsemestre_id=self.id, ue_id=None etudid=etudid, formsemestre_id=self.id, ue_id=None

View File

@ -685,7 +685,7 @@ def EtatAbsences():
</td></tr></table> </td></tr></table>
</form>""" </form>"""
% (scu.AnneeScolaire(), datetime.datetime.now().strftime("%d/%m/%Y")), % (scu.annee_scolaire(), datetime.datetime.now().strftime("%d/%m/%Y")),
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
return "\n".join(H) return "\n".join(H)
@ -719,15 +719,27 @@ def formChoixSemestreGroupe(all=False):
return "\n".join(H) 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): def CalAbs(etudid, sco_year=None):
"""Calendrier des absences d'un etudiant""" """Calendrier des absences d'un etudiant"""
# crude portage from 1999 DTML # crude portage from 1999 DTML
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"] etudid = etud["etudid"]
anneescolaire = int(scu.AnneeScolaire(sco_year)) if sco_year:
datedebut = str(anneescolaire) + "-08-01" annee_scolaire = _convert_sco_year(sco_year)
datefin = str(anneescolaire + 1) + "-07-31" else:
annee_courante = scu.AnneeScolaire() 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) nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin)
nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin) nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin)
events = [] events = []
@ -746,7 +758,7 @@ def CalAbs(etudid, sco_year=None):
events.append( events.append(
(str(a["jour"]), "X", "#8EA2C6", "", a["matin"], a["description"]) (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 = [ H = [
@ -777,12 +789,12 @@ def CalAbs(etudid, sco_year=None):
CalHTML, CalHTML,
"""<form method="GET" action="CalAbs" name="f">""", """<form method="GET" action="CalAbs" name="f">""",
"""<input type="hidden" name="etudid" value="%s"/>""" % etudid, """<input type="hidden" name="etudid" value="%s"/>""" % etudid,
"""Année scolaire %s-%s""" % (anneescolaire, anneescolaire + 1), """Année scolaire %s-%s""" % (annee_scolaire, annee_scolaire + 1),
"""&nbsp;&nbsp;Changer année: <select name="sco_year" onchange="document.f.submit()">""", """&nbsp;&nbsp;Changer année: <select name="sco_year" onchange="document.f.submit()">""",
] ]
for y in range(annee_courante, min(annee_courante - 6, anneescolaire - 6), -1): for y in range(annee_courante, min(annee_courante - 6, annee_scolaire - 6), -1):
H.append("""<option value="%s" """ % y) H.append("""<option value="%s" """ % y)
if y == anneescolaire: if y == annee_scolaire:
H.append("selected") H.append("selected")
H.append(""">%s</option>""" % y) H.append(""">%s</option>""" % y)
H.append("""</select></form>""") H.append("""</select></form>""")
@ -811,7 +823,11 @@ def ListeAbsEtud(
""" """
# si absjust_only, table absjust seule (export xls ou pdf) # si absjust_only, table absjust seule (export xls ou pdf)
absjust_only = scu.to_bool(absjust_only) 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 etudid = etudid or False
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True) etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
if not etuds: if not etuds:

View File

@ -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']) # print 'comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id'])
if not cur_sem: if not cur_sem:
# l'étudiant n'a pas de semestre courant ?! # 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 return VOID_APO_RES
cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"]) cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"])
cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre) 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) (sem["semestre_id"] == apo_data.cur_semestre_id)
and (apo_data.etape in sem["etapes"]) and (apo_data.etape in sem["etapes"])
and ( and (
# sco_formsemestre.sem_in_annee_scolaire(sem, apo_data.annee_scolaire) # TODO à remplacer par ?
sco_formsemestre.sem_in_semestre_scolaire( sco_formsemestre.sem_in_semestre_scolaire(
sem, sem,
apo_data.annee_scolaire, apo_data.annee_scolaire,
0, 0, # annee complete
# jour_pivot_annee,
# mois_pivot_annee,
# jour_pivot_periode,
# mois_pivot_periode
) )
) )
) )

View File

@ -49,6 +49,7 @@ from app.scodoc import sco_groups
from app.scodoc import sco_photos from app.scodoc import sco_photos
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.sco_xml import quote_xml_attr
# -------- Bulletin en JSON # -------- Bulletin en JSON
@ -129,12 +130,12 @@ def formsemestre_bulletinetud_published_dict(
etudid=etudid, etudid=etudid,
code_nip=etudinfo["code_nip"], code_nip=etudinfo["code_nip"],
code_ine=etudinfo["code_ine"], code_ine=etudinfo["code_ine"],
nom=scu.quote_xml_attr(etudinfo["nom"]), nom=quote_xml_attr(etudinfo["nom"]),
prenom=scu.quote_xml_attr(etudinfo["prenom"]), prenom=quote_xml_attr(etudinfo["prenom"]),
civilite=scu.quote_xml_attr(etudinfo["civilite_str"]), civilite=quote_xml_attr(etudinfo["civilite_str"]),
photo_url=scu.quote_xml_attr(sco_photos.etud_photo_url(etudinfo, fast=True)), photo_url=quote_xml_attr(sco_photos.etud_photo_url(etudinfo, fast=True)),
email=scu.quote_xml_attr(etudinfo["email"]), email=quote_xml_attr(etudinfo["email"]),
emailperso=scu.quote_xml_attr(etudinfo["emailperso"]), emailperso=quote_xml_attr(etudinfo["emailperso"]),
) )
d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients
# Disponible pour publication ? # Disponible pour publication ?
@ -209,9 +210,9 @@ def formsemestre_bulletinetud_published_dict(
rang, effectif = nt.get_etud_ue_rang(ue["ue_id"], etudid) rang, effectif = nt.get_etud_ue_rang(ue["ue_id"], etudid)
u = dict( u = dict(
id=ue["ue_id"], id=ue["ue_id"],
numero=scu.quote_xml_attr(ue["numero"]), numero=quote_xml_attr(ue["numero"]),
acronyme=scu.quote_xml_attr(ue["acronyme"]), acronyme=quote_xml_attr(ue["acronyme"]),
titre=scu.quote_xml_attr(ue["titre"]), titre=quote_xml_attr(ue["titre"]),
note=dict( note=dict(
value=scu.fmt_note(ue_status["cur_moy_ue"] if ue_status else ""), value=scu.fmt_note(ue_status["cur_moy_ue"] if ue_status else ""),
min=scu.fmt_note(ue["min"]), min=scu.fmt_note(ue["min"]),
@ -223,7 +224,7 @@ def formsemestre_bulletinetud_published_dict(
rang=rang, rang=rang,
effectif=effectif, effectif=effectif,
ects=ects_txt, ects=ects_txt,
code_apogee=scu.quote_xml_attr(ue["code_apogee"]), code_apogee=quote_xml_attr(ue["code_apogee"]),
) )
d["ue"].append(u) d["ue"].append(u)
u["module"] = [] u["module"] = []
@ -247,11 +248,11 @@ def formsemestre_bulletinetud_published_dict(
code=mod["code"], code=mod["code"],
coefficient=mod["coefficient"], coefficient=mod["coefficient"],
numero=mod["numero"], numero=mod["numero"],
titre=scu.quote_xml_attr(mod["titre"]), titre=quote_xml_attr(mod["titre"]),
abbrev=scu.quote_xml_attr(mod["abbrev"]), abbrev=quote_xml_attr(mod["abbrev"]),
# ects=ects, ects des modules maintenant inutilisés # ects=ects, ects des modules maintenant inutilisés
note=dict(value=mod_moy), 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) m["note"].update(modstat)
for k in ("min", "max", "moy"): # formatte toutes les notes for k in ("min", "max", "moy"): # formatte toutes les notes
@ -291,7 +292,7 @@ def formsemestre_bulletinetud_published_dict(
evaluation_id=e[ evaluation_id=e[
"evaluation_id" "evaluation_id"
], # CM : ajout pour permettre de faire le lien sur les bulletins en ligne avec l'évaluation ], # 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, note=val,
) )
) )
@ -318,7 +319,7 @@ def formsemestre_bulletinetud_published_dict(
e["heure_fin"], null_is_empty=True e["heure_fin"], null_is_empty=True
), ),
coefficient=e["coefficient"], coefficient=e["coefficient"],
description=scu.quote_xml_attr(e["description"]), description=quote_xml_attr(e["description"]),
incomplete="1", incomplete="1",
) )
) )
@ -332,9 +333,9 @@ def formsemestre_bulletinetud_published_dict(
d["ue_capitalisee"].append( d["ue_capitalisee"].append(
dict( dict(
id=ue["ue_id"], id=ue["ue_id"],
numero=scu.quote_xml_attr(ue["numero"]), numero=quote_xml_attr(ue["numero"]),
acronyme=scu.quote_xml_attr(ue["acronyme"]), acronyme=quote_xml_attr(ue["acronyme"]),
titre=scu.quote_xml_attr(ue["titre"]), titre=quote_xml_attr(ue["titre"]),
note=scu.fmt_note(ue_status["moy"]), note=scu.fmt_note(ue_status["moy"]),
coefficient_ue=scu.fmt_note(ue_status["coef_ue"]), coefficient_ue=scu.fmt_note(ue_status["coef_ue"]),
date_capitalisation=ndb.DateDMYtoISO(ue_status["event_date"]), date_capitalisation=ndb.DateDMYtoISO(ue_status["event_date"]),
@ -358,7 +359,7 @@ def formsemestre_bulletinetud_published_dict(
for app in apprecs: for app in apprecs:
d["appreciation"].append( d["appreciation"].append(
dict( dict(
comment=scu.quote_xml_attr(app["comment"]), comment=quote_xml_attr(app["comment"]),
date=ndb.DateDMYtoISO(app["date"]), date=ndb.DateDMYtoISO(app["date"]),
) )
) )

View File

@ -53,7 +53,6 @@ from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_codes_parcours 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_edit_ue
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre 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_preferences
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_xml from app.scodoc import sco_xml
from app.scodoc.sco_xml import quote_xml_attr
# -------- Bulletin en XML # -------- Bulletin en XML
# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict() # (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
@ -131,13 +131,13 @@ def make_xml_formsemestre_bulletinetud(
etudid=str(etudid), etudid=str(etudid),
code_nip=str(etudinfo["code_nip"]), code_nip=str(etudinfo["code_nip"]),
code_ine=str(etudinfo["code_ine"]), code_ine=str(etudinfo["code_ine"]),
nom=scu.quote_xml_attr(etudinfo["nom"]), nom=quote_xml_attr(etudinfo["nom"]),
prenom=scu.quote_xml_attr(etudinfo["prenom"]), prenom=quote_xml_attr(etudinfo["prenom"]),
civilite=scu.quote_xml_attr(etudinfo["civilite_str"]), civilite=quote_xml_attr(etudinfo["civilite_str"]),
sexe=scu.quote_xml_attr(etudinfo["civilite_str"]), # compat sexe=quote_xml_attr(etudinfo["civilite_str"]), # compat
photo_url=scu.quote_xml_attr(sco_photos.etud_photo_url(etudinfo)), photo_url=quote_xml_attr(sco_photos.etud_photo_url(etudinfo)),
email=scu.quote_xml_attr(etudinfo["email"]), email=quote_xml_attr(etudinfo["email"]),
emailperso=scu.quote_xml_attr(etudinfo["emailperso"]), emailperso=quote_xml_attr(etudinfo["emailperso"]),
) )
) )
@ -210,10 +210,10 @@ def make_xml_formsemestre_bulletinetud(
x_ue = Element( x_ue = Element(
"ue", "ue",
id=str(ue["ue_id"]), id=str(ue["ue_id"]),
numero=scu.quote_xml_attr(ue["numero"]), numero=quote_xml_attr(ue["numero"]),
acronyme=scu.quote_xml_attr(ue["acronyme"]), acronyme=quote_xml_attr(ue["acronyme"]),
titre=scu.quote_xml_attr(ue["titre"]), titre=quote_xml_attr(ue["titre"]),
code_apogee=scu.quote_xml_attr(ue["code_apogee"]), code_apogee=quote_xml_attr(ue["code_apogee"]),
) )
doc.append(x_ue) doc.append(x_ue)
if ue["type"] != sco_codes_parcours.UE_SPORT: if ue["type"] != sco_codes_parcours.UE_SPORT:
@ -255,9 +255,9 @@ def make_xml_formsemestre_bulletinetud(
code=str(mod["code"] or ""), code=str(mod["code"] or ""),
coefficient=str(mod["coefficient"]), coefficient=str(mod["coefficient"]),
numero=str(mod["numero"]), numero=str(mod["numero"]),
titre=scu.quote_xml_attr(mod["titre"]), titre=quote_xml_attr(mod["titre"]),
abbrev=scu.quote_xml_attr(mod["abbrev"]), abbrev=quote_xml_attr(mod["abbrev"]),
code_apogee=scu.quote_xml_attr(mod["code_apogee"]) code_apogee=quote_xml_attr(mod["code_apogee"])
# ects=ects ects des modules maintenant inutilisés # ects=ects ects des modules maintenant inutilisés
) )
x_ue.append(x_mod) x_ue.append(x_mod)
@ -302,7 +302,7 @@ def make_xml_formsemestre_bulletinetud(
), ),
coefficient=str(e["coefficient"]), coefficient=str(e["coefficient"]),
evaluation_type=str(e["evaluation_type"]), 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: # notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e["note_max"]), note_max_origin=str(e["note_max"]),
) )
@ -333,7 +333,7 @@ def make_xml_formsemestre_bulletinetud(
e["heure_fin"], null_is_empty=True e["heure_fin"], null_is_empty=True
), ),
coefficient=str(e["coefficient"]), coefficient=str(e["coefficient"]),
description=scu.quote_xml_attr(e["description"]), description=quote_xml_attr(e["description"]),
incomplete="1", incomplete="1",
# notes envoyées sur 20, ceci juste pour garder trace: # notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e["note_max"] or ""), note_max_origin=str(e["note_max"] or ""),
@ -348,9 +348,9 @@ def make_xml_formsemestre_bulletinetud(
x_ue = Element( x_ue = Element(
"ue_capitalisee", "ue_capitalisee",
id=str(ue["ue_id"]), id=str(ue["ue_id"]),
numero=scu.quote_xml_attr(ue["numero"]), numero=quote_xml_attr(ue["numero"]),
acronyme=scu.quote_xml_attr(ue["acronyme"]), acronyme=quote_xml_attr(ue["acronyme"]),
titre=scu.quote_xml_attr(ue["titre"]), titre=quote_xml_attr(ue["titre"]),
) )
doc.append(x_ue) doc.append(x_ue)
x_ue.append(Element("note", value=scu.fmt_note(ue_status["moy"]))) 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 = Element("situation")
x_situation.text = scu.quote_xml_attr(infos["situation"]) x_situation.text = quote_xml_attr(infos["situation"])
doc.append(x_situation) doc.append(x_situation)
if dpv: if dpv:
decision = dpv["decisions"][0] decision = dpv["decisions"][0]
@ -418,9 +418,9 @@ def make_xml_formsemestre_bulletinetud(
Element( Element(
"decision_ue", "decision_ue",
ue_id=str(ue["ue_id"]), ue_id=str(ue["ue_id"]),
numero=scu.quote_xml_attr(ue["numero"]), numero=quote_xml_attr(ue["numero"]),
acronyme=scu.quote_xml_attr(ue["acronyme"]), acronyme=quote_xml_attr(ue["acronyme"]),
titre=scu.quote_xml_attr(ue["titre"]), titre=quote_xml_attr(ue["titre"]),
code=decision["decisions_ue"][ue_id]["code"], code=decision["decisions_ue"][ue_id]["code"],
) )
) )
@ -443,7 +443,7 @@ def make_xml_formsemestre_bulletinetud(
"appreciation", "appreciation",
date=ndb.DateDMYtoISO(appr["date"]), 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) doc.append(x_appr)
if is_appending: if is_appending:

View File

@ -137,7 +137,7 @@ class DataEtudiant(object):
self.data_apogee = None self.data_apogee = None
self.data_scodoc = None self.data_scodoc = None
self.etapes = set() # l'ensemble des étapes où il est inscrit 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.tags = set() # les anomalies relevées
self.ind_row = "-" # là où il compte dans les effectifs (ligne et colonne) self.ind_row = "-" # là où il compte dans les effectifs (ligne et colonne)
self.ind_col = "-" self.ind_col = "-"
@ -145,8 +145,8 @@ class DataEtudiant(object):
def add_etape(self, etape): def add_etape(self, etape):
self.etapes.add(etape) self.etapes.add(etape)
def add_semestre(self, semestre): def add_semestre(self, formsemestre_id: int):
self.semestres.add(semestre) self.semestres.add(formsemestre_id)
def set_apogee(self, data_apogee): def set_apogee(self, data_apogee):
self.data_apogee = 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 Structure de donnée représentation l'état global de la comparaison ScoDoc/Apogée
""" """
def __init__(self): def __init__(self):
self.semestres = ( self.semestres = {}
{} "Dictionnaire des formsemestres du semset (formsemestre_id -> semestre)"
) # Dictionnaire des formsemestres du semset (formsemestre_id -> semestre)
self.etapes = [] # Liste des étapes apogées du semset (clé_apogée) 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 # cle_etu = nip si non vide, sinon etudid
# data_etu = { nip, etudid, data_apogee, data_scodoc } # data_etu = { nip, etudid, data_apogee, data_scodoc }
self.etudiants = {} # cle_etu -> data_etu self.etudiants = {}
self.keys_etu = {} # nip -> [ etudid* ] "cle_etu -> data_etu"
self.etu_semestre = {} # semestre -> { key_etu } self.keys_etu = {}
self.etu_etapes = {} # etape -> { key_etu } "nip -> [ etudid* ]"
self.repartition = {} # (ind_row, ind_col) -> nombre d étudiants self.etu_semestre = {}
self.tag_count = {} # nombre d'animalies détectées (par type d'anomalie) "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' # on collectionne les indicatifs trouvés pour n'afficher que les indicatifs 'utiles'
self.indicatifs = {} self.indicatifs = {}
@ -273,7 +278,8 @@ class EtapeBilan(object):
self.tag_count[tag] = 0 self.tag_count[tag] = 0
self.tag_count[tag] += 1 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: if as_row:
indicatif = "R" + chr(self.top_row + 97) indicatif = "R" + chr(self.top_row + 97)
self.all_rows_ind.append(indicatif) self.all_rows_ind.append(indicatif)
@ -288,7 +294,7 @@ class EtapeBilan(object):
if self.top_col > 26: if self.top_col > 26:
log("Dépassement (plus de 26 étapes dans la table diagnostic") 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. Prise en compte d'un semestre dans le bilan.
* ajoute le semestre et les étudiants du semestre * ajoute le semestre et les étudiants du semestre
@ -296,16 +302,16 @@ class EtapeBilan(object):
:param semestre: Le semestre à prendre en compte :param semestre: Le semestre à prendre en compte
:return: None :return: None
""" """
self.semestres[semestre["formsemestre_id"]] = semestre self.semestres[sem["formsemestre_id"]] = sem
# if anneeapogee == None: # année d'inscription par défaut # if anneeapogee == None: # année d'inscription par défaut
anneeapogee = str( annee_apogee = str(
annee_scolaire_debut(semestre["annee_debut"], semestre["mois_debut_ord"]) annee_scolaire_debut(sem["annee_debut"], sem["mois_debut_ord"])
) )
self.set_indicatif(semestre["formsemestre_id"], True) self.set_indicatif(sem["formsemestre_id"], True)
for etape in semestre["etapes"]: for etape in sem["etapes"]:
self.add_etape(etape.etape_vdi, anneeapogee) 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 Prise en compte d'une étape apogée
:param etape_str: La clé de l'étape à prendre en compte :param etape_str: La clé de l'étape à prendre en compte
@ -313,7 +319,7 @@ class EtapeBilan(object):
:return: None :return: None
""" """
if etape_str != "": 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: if key_etape not in self.etapes:
self.etapes.append(key_etape) self.etapes.append(key_etape)
self.set_indicatif( self.set_indicatif(
@ -367,7 +373,7 @@ class EtapeBilan(object):
self.etudiants[key_etu].add_etape(etape) self.etudiants[key_etu].add_etape(etape)
return key_etu 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 Enregistrement de l'étudiant par rapport à son semestre
:param etud: Les données de l'étudiant :param etud: Les données de l'étudiant
@ -380,10 +386,10 @@ class EtapeBilan(object):
if key_etu not in self.etudiants: if key_etu not in self.etudiants:
data = DataEtudiant(nip, etudid) data = DataEtudiant(nip, etudid)
data.set_scodoc(etud) data.set_scodoc(etud)
data.add_semestre(semestre) data.add_semestre(sem)
self.etudiants[key_etu] = data self.etudiants[key_etu] = data
else: else:
self.etudiants[key_etu].add_semestre(semestre) self.etudiants[key_etu].add_semestre(sem)
return key_etu return key_etu
def load_listes(self): def load_listes(self):
@ -393,12 +399,12 @@ class EtapeBilan(object):
* Puis pour toutes les étapes * Puis pour toutes les étapes
:return: None :return: None
""" """
for semestre in self.semestres: for formsemestre_id, sem in self.semestres.items():
etuds = self.semestres[semestre]["etuds"] etuds = sem["etuds"]
self.etu_semestre[semestre] = set() self.etu_semestre[formsemestre_id] = set()
for etud in etuds: for etud in etuds:
key_etu = self.register_etud_scodoc(etud, semestre) key_etu = self.register_etud_scodoc(etud, formsemestre_id)
self.etu_semestre[semestre].add(key_etu) self.etu_semestre[formsemestre_id].add(key_etu)
for key_etape in self.etapes: for key_etape in self.etapes:
anneeapogee, etapestr = key_to_values(key_etape) anneeapogee, etapestr = key_to_values(key_etape)
@ -426,17 +432,17 @@ class EtapeBilan(object):
self.repartition[ROW_CUMUL, self.indicatifs[key_etape]] = 0 self.repartition[ROW_CUMUL, self.indicatifs[key_etape]] = 0
# recherche des nip identiques # recherche des nip identiques
for nip in self.keys_etu: for nip, keys_etu_nip in self.keys_etu.items():
if nip != "": if nip != "":
nbnips = len(self.keys_etu[nip]) nbnips = len(keys_etu_nip)
if nbnips > 1: 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 = self.etudiants[nip, etudid]
data_etu.add_tag(NIP_NON_UNIQUE) data_etu.add_tag(NIP_NON_UNIQUE)
data_etu.nip = data_etu.nip + "&nbsp;(%d/%d)" % (i + 1, nbnips) data_etu.nip = data_etu.nip + f"&nbsp;({i+1}/{nbnips})"
self.inc_tag_count(NIP_NON_UNIQUE) self.inc_tag_count(NIP_NON_UNIQUE)
for nip in self.keys_etu: for nip, keys_etu_nip in self.keys_etu.items():
for etudid in self.keys_etu[nip]: for etudid in keys_etu_nip:
key_etu = (nip, etudid) key_etu = (nip, etudid)
data_etu = self.etudiants[key_etu] data_etu = self.etudiants[key_etu]
ind_col = "-" ind_col = "-"
@ -504,7 +510,7 @@ class EtapeBilan(object):
if (ind_row, ind_col) in self.repartition: if (ind_row, ind_col) in self.repartition:
count = self.repartition[ind_row, ind_col] count = self.repartition[ind_row, ind_col]
if count > 1: if count > 1:
comptage = "(%d étudiants)" % count comptage = f"({count} étudiants)"
else: else:
comptage = "(1 étudiant)" comptage = "(1 étudiant)"
else: else:

View File

@ -939,7 +939,18 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
def etud_inscriptions_infos(etudid: int, ne="") -> dict: 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
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions

View File

@ -27,26 +27,23 @@
"""Operations de base sur les formsemestres """Operations de base sur les formsemestres
""" """
from operator import itemgetter
import datetime import datetime
import time import time
from operator import itemgetter
from flask import g, request from flask import g, request, url_for
import app 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.notesdb as ndb
import app.scodoc.sco_utils as scu 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( _formsemestreEditor = ndb.EditableTable(
"notes_formsemestre", "notes_formsemestre",
@ -82,11 +79,9 @@ _formsemestreEditor = ndb.EditableTable(
"date_debut": ndb.DateDMYtoISO, "date_debut": ndb.DateDMYtoISO,
"date_fin": ndb.DateDMYtoISO, "date_fin": ndb.DateDMYtoISO,
"etat": bool, "etat": bool,
"gestion_compensation": bool,
"bul_hide_xml": bool, "bul_hide_xml": bool,
"block_moyennes": bool, "block_moyennes": bool,
"block_moyenne_generale": bool, "block_moyenne_generale": bool,
"gestion_semestrielle": bool,
"gestion_compensation": bool, "gestion_compensation": bool,
"gestion_semestrielle": bool, "gestion_semestrielle": bool,
"resp_can_edit": bool, "resp_can_edit": bool,
@ -99,7 +94,7 @@ _formsemestreEditor = ndb.EditableTable(
def get_formsemestre(formsemestre_id, raise_soft_exc=False): def get_formsemestre(formsemestre_id, raise_soft_exc=False):
"list ONE formsemestre" "list ONE formsemestre"
if formsemestre_id is None: 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: if formsemestre_id in g.stored_get_formsemestre:
return g.stored_get_formsemestre[formsemestre_id] return g.stored_get_formsemestre[formsemestre_id]
if not isinstance(formsemestre_id, int): 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 !") raise ScoInvalidIdType("get_formsemestre: formsemestre_id must be an integer !")
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id}) sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
if not sems: 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: if raise_soft_exc:
raise ScoValueError(f"semestre {formsemestre_id} inconnu !") raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
else: else:
@ -240,8 +235,8 @@ def etapes_apo_str(etapes):
def do_formsemestre_create(args, silent=False): def do_formsemestre_create(args, silent=False):
"create a formsemestre" "create a formsemestre"
from app.scodoc import sco_groups
from app.models import ScolarNews from app.models import ScolarNews
from app.scodoc import sco_groups
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
formsemestre_id = _formsemestreEditor.create(cnx, args) 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( def sem_in_semestre_scolaire(
sem, sem,
year=False, year=False,
periode=None, periode=None,
jour_pivot_annee=1, mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
mois_pivot_annee=8, mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
jour_pivot_periode=1, ) -> bool:
mois_pivot_periode=12, """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
"""n'utilise que la date de debut, (ou, à défaut, de celle en cours).
si annee non specifiée, année scolaire courante
la période garde les même convention que semset["sem_id"]; La période utilise les même conventions que semset["sem_id"];
* 1 : premère période * 1 : première période
* 2 : deuxième 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: if not year:
year = scu.AnneeScolaire() year = scu.annee_scolaire()
# calcule l'année universitaire et la periode # n'utilise pas le jour pivot
sem_annee, sem_periode = get_periode( 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"]), datetime.datetime.fromisoformat(sem["date_debut_iso"]),
jour_pivot_annee,
mois_pivot_annee, mois_pivot_annee,
jour_pivot_periode,
mois_pivot_periode, mois_pivot_periode,
jour_pivot_annee,
jour_pivot_periode,
) )
if periode is None or periode == 0: if periode is None or periode == 0:
return sem_annee == year 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): def sem_in_annee_scolaire(sem, year=False):
# """Test si sem appartient à l'année scolaire year (int). """Test si sem appartient à l'année scolaire year (int).
# N'utilise que la date de début, pivot au 1er août. N'utilise que la date de début, pivot au 1er août.
# Si année non specifiée, année scolaire courante Si année non specifiée, année scolaire courante
# """ """
# if not year: return sem_in_semestre_scolaire(sem, year, periode=0)
# 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_est_courant(sem): # -> FormSemestre.est_courant 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") now = time.strftime("%Y-%m-%d")
debut = ndb.DateDMYtoISO(sem["date_debut"]) debut = ndb.DateDMYtoISO(sem["date_debut"])
fin = ndb.DateDMYtoISO(sem["date_fin"]) fin = ndb.DateDMYtoISO(sem["date_fin"])
return (debut <= now) and (now <= fin) return debut <= now <= fin
def scodoc_get_all_unlocked_sems(): def scodoc_get_all_unlocked_sems():
@ -540,7 +484,7 @@ def scodoc_get_all_unlocked_sems():
def table_formsemestres( def table_formsemestres(
sems, sems: list[dict],
columns_ids=(), columns_ids=(),
sup_columns_ids=(), sup_columns_ids=(),
html_title="<h2>Semestres</h2>", html_title="<h2>Semestres</h2>",
@ -549,8 +493,10 @@ def table_formsemestres(
"""Une table presentant des semestres""" """Une table presentant des semestres"""
for sem in sems: for sem in sems:
sem_set_responsable_name(sem) sem_set_responsable_name(sem)
sem["_titre_num_target"] = ( sem["_titre_num_target"] = url_for(
"formsemestre_status?formsemestre_id=%s" % sem["formsemestre_id"] "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=sem["formsemestre_id"],
) )
if not columns_ids: if not columns_ids:
@ -592,8 +538,10 @@ def table_formsemestres(
return tab return tab
def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False): def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False) -> list[dict]:
"""Liste des semestres de cette etape, pour l'annee scolaire indiquée (sinon, pour toutes)""" """Liste des semestres de cette etape,
pour l'annee scolaire indiquée (sinon, pour toutes).
"""
ds = {} # formsemestre_id : sem ds = {} # formsemestre_id : sem
if etape_apo: if etape_apo:
sems = do_formsemestre_list(args={"etape_apo": etape_apo}) sems = do_formsemestre_list(args={"etape_apo": etape_apo})
@ -618,14 +566,12 @@ def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False):
def view_formsemestre_by_etape(etape_apo=None, format="html"): def view_formsemestre_by_etape(etape_apo=None, format="html"):
"""Affiche table des semestres correspondants à l'étape""" """Affiche table des semestres correspondants à l'étape"""
if etape_apo: if etape_apo:
html_title = ( html_title = f"""<h2>Semestres courants de l'étape <tt>{etape_apo}</tt></h2>"""
"""<h2>Semestres courants de l'étape <tt>%s</tt></h2>""" % etape_apo
)
else: else:
html_title = """<h2>Semestres courants</h2>""" html_title = """<h2>Semestres courants</h2>"""
tab = table_formsemestres( tab = table_formsemestres(
list_formsemestre_by_etape( list_formsemestre_by_etape(
etape_apo=etape_apo, annee_scolaire=scu.AnneeScolaire() etape_apo=etape_apo, annee_scolaire=scu.annee_scolaire()
), ),
html_title=html_title, html_title=html_title,
html_next_section="""<form action="view_formsemestre_by_etape"> html_next_section="""<form action="view_formsemestre_by_etape">

View File

@ -40,13 +40,11 @@ sem_set_list()
""" """
import flask import flask
from flask import g
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_etape_apogee from app.scodoc import sco_etape_apogee
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_status
@ -77,6 +75,7 @@ semset_delete = _semset_editor.delete
class SemSet(dict): class SemSet(dict):
def __init__(self, semset_id=None, title="", annee_scolaire="", sem_id=""): def __init__(self, semset_id=None, title="", annee_scolaire="", sem_id=""):
"""Load and init, or, if semset_id is not specified, create""" """Load and init, or, if semset_id is not specified, create"""
super().__init__()
if not annee_scolaire and not semset_id: if not annee_scolaire and not semset_id:
# on autorise annee_scolaire null si sem_id pour pouvoir lire les anciens semsets # on autorise annee_scolaire null si sem_id pour pouvoir lire les anciens semsets
# mal construits... # mal construits...
@ -491,7 +490,7 @@ def semset_page(format="html"):
] ]
H.append(tab.html()) H.append(tab.html())
annee_courante = int(scu.AnneeScolaire()) annee_courante = int(scu.annee_scolaire())
menu_annee = "\n".join( menu_annee = "\n".join(
[ [
'<option value="%s">%s</option>' % (i, i) '<option value="%s">%s</option>' % (i, i)

View File

@ -58,9 +58,7 @@ from werkzeug.http import HTTP_STATUS_CODES
from config import Config from config import Config
from app import log from app import log
from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_xml import quote_xml_attr
from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL
from app.scodoc import sco_exceptions
from app.scodoc import sco_xml from app.scodoc import sco_xml
import sco_version import sco_version
@ -161,8 +159,14 @@ EVALUATION_RATTRAPAGE = 1
EVALUATION_SESSION2 = 2 EVALUATION_SESSION2 = 2
# Dates et années scolaires # Dates et années scolaires
MONTH_FIN_ANNEE_SCOLAIRE = 7 # juillet (TODO: passer en paramètre config.) # Ces dates "pivot" sont paramétrables dans les préférences générales
DAY_FIN_ANNEE_SCOLAIRE = 31 # TODO calculer en fct du mois # on donne ici les valeurs par défaut.
# Les semestres commençant à partir du 1er août 20XX sont
# dans l'année scolaire 20XX
MONTH_DEBUT_ANNEE_SCOLAIRE = 8 # août
# Les semestres commençant à partir du 1er décembre
# sont "2eme période" (S_pair):
MONTH_DEBUT_PERIODE2 = MONTH_DEBUT_ANNEE_SCOLAIRE + 4
MONTH_NAMES_ABBREV = ( MONTH_NAMES_ABBREV = (
"Jan ", "Jan ",
@ -910,36 +914,47 @@ def annee_scolaire_repr(year, month):
"""representation de l'annee scolaire : '2009 - 2010' """representation de l'annee scolaire : '2009 - 2010'
à partir d'une date. à partir d'une date.
""" """
if month > MONTH_FIN_ANNEE_SCOLAIRE: # apres le 1er aout if month >= MONTH_DEBUT_ANNEE_SCOLAIRE: # apres le 1er aout
return "%s - %s" % (year, year + 1) return f"{year} - {year + 1}"
else: else:
return "%s - %s" % (year - 1, year) return f"{year - 1} - {year}"
def annee_scolaire() -> int:
"""Année de debut de l'annee scolaire courante"""
t = time.localtime()
year, month = t[0], t[1]
return annee_scolaire_debut(year, month)
def annee_scolaire_debut(year, month) -> int: def annee_scolaire_debut(year, month) -> int:
"""Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord...""" """Annee scolaire de début.
if int(month) > MONTH_FIN_ANNEE_SCOLAIRE: Par défaut (hémisphère nord), l'année du mois de août
précédent la date indiquée.
"""
if int(month) >= MONTH_DEBUT_ANNEE_SCOLAIRE:
return int(year) return int(year)
else: else:
return int(year) - 1 return int(year) - 1
def date_debut_anne_scolaire(annee_scolaire: int) -> datetime: def date_debut_anne_scolaire(annee_sco: int) -> datetime:
"""La date de début de l'année scolaire """La date de début de l'année scolaire
= 1er aout (par défaut, le 1er aout)
""" """
return datetime.datetime(year=annee_scolaire, month=8, day=1) return datetime.datetime(year=annee_sco, month=MONTH_DEBUT_ANNEE_SCOLAIRE, day=1)
def date_fin_anne_scolaire(annee_scolaire: int) -> datetime: def date_fin_anne_scolaire(annee_sco: int) -> datetime:
"""La date de fin de l'année scolaire """La date de fin de l'année scolaire
= 31 juillet de l'année suivante (par défaut, le 31 juillet de l'année suivante)
""" """
# on prend la date de début de l'année scolaire suivante,
# et on lui retre 1 jour.
# On s'affranchit ainsi des problèmes de durées de mois.
return datetime.datetime( return datetime.datetime(
year=annee_scolaire + 1, year=annee_sco + 1, month=MONTH_DEBUT_ANNEE_SCOLAIRE, day=1
month=MONTH_FIN_ANNEE_SCOLAIRE, ) - datetime.timedelta(days=1)
day=DAY_FIN_ANNEE_SCOLAIRE,
)
def sem_decale_str(sem): def sem_decale_str(sem):
@ -1065,23 +1080,6 @@ def query_portal(req, msg="Portail Apogee", timeout=3):
return r.text return r.text
def AnneeScolaire(sco_year=None) -> int:
"annee de debut de l'annee scolaire courante"
if sco_year:
year = sco_year
try:
year = int(year)
if year > 1900 and year < 2999:
return year
except:
raise sco_exceptions.ScoValueError("invalid sco_year")
t = time.localtime()
year, month = t[0], t[1]
if month < 8: # le "pivot" est le 1er aout
year = year - 1
return year
def confirm_dialog( def confirm_dialog(
message="<p>Confirmer ?</p>", message="<p>Confirmer ?</p>",
OK="OK", OK="OK",

View File

@ -198,7 +198,7 @@ def choix_semaine(group_id):
def cal_select_week(year=None): def cal_select_week(year=None):
"display calendar allowing week selection" "display calendar allowing week selection"
if not year: if not year:
year = scu.AnneeScolaire() year = scu.annee_scolaire()
sems = sco_formsemestre.do_formsemestre_list() sems = sco_formsemestre.do_formsemestre_list()
if not sems: if not sems:
js = "" js = ""
@ -1215,9 +1215,9 @@ def add_billets_absence_form(etudid):
@bp.route("/billets_etud/<int:etudid>") @bp.route("/billets_etud/<int:etudid>")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def billets_etud(etudid=False): def billets_etud(etudid=False, format=False):
"""Liste billets pour un etudiant""" """Liste billets pour un étudiant"""
fmt = request.args.get("format", "html") fmt = format or request.args.get("format", "html")
if not fmt in {"html", "json", "xml", "xls", "xlsx"}: if not fmt in {"html", "json", "xml", "xls", "xlsx"}:
return ScoValueError("Format invalide") return ScoValueError("Format invalide")
table = sco_abs_billets.table_billets_etud(etudid) table = sco_abs_billets.table_billets_etud(etudid)

View File

@ -636,16 +636,16 @@ def index_html():
</li> </li>
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
url_for("notes.export_recap_formations_annee_scolaire", url_for("notes.export_recap_formations_annee_scolaire",
scodoc_dept=g.scodoc_dept, annee_scolaire=scu.AnneeScolaire()-1) scodoc_dept=g.scodoc_dept, annee_scolaire=scu.annee_scolaire()-1)
}">exporter les formations de l'année scolaire }">exporter les formations de l'année scolaire
{scu.AnneeScolaire()-1} - {scu.AnneeScolaire()} {scu.annee_scolaire()-1} - {scu.annee_scolaire()}
</a> </a>
</li> </li>
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
url_for("notes.export_recap_formations_annee_scolaire", url_for("notes.export_recap_formations_annee_scolaire",
scodoc_dept=g.scodoc_dept, annee_scolaire=scu.AnneeScolaire()) scodoc_dept=g.scodoc_dept, annee_scolaire=scu.annee_scolaire())
}">exporter les formations de l'année scolaire }">exporter les formations de l'année scolaire
{scu.AnneeScolaire()} - {scu.AnneeScolaire()+1} {scu.annee_scolaire()} - {scu.annee_scolaire()+1}
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -90,9 +90,7 @@ def test_nouvel_an_special_pp_before_pa():
def test_nouvel_ete_pp_before_pa(): def test_nouvel_ete_pp_before_pa():
assert (2023, 2) == FormSemestre.comp_periode( assert (2023, 2) == FormSemestre.comp_periode(datetime.datetime(2024, 6, 1), 8, 2)
datetime.datetime(2024, 6, 1), 1, 8, 1, 2
)
def test_automne_special_pp_before_pa(): def test_automne_special_pp_before_pa():
@ -108,27 +106,27 @@ sem_prev_year = {"date_debut_iso": "2022-07-31"}
def test_sem_in_periode1_default(): def test_sem_in_periode1_default():
assert True == sem_in_semestre_scolaire(sem_automne, 2022, 1) assert True is sem_in_semestre_scolaire(sem_automne, 2022, 1)
assert False == sem_in_semestre_scolaire(sem_nouvel_an, 2022, 1) assert False is sem_in_semestre_scolaire(sem_nouvel_an, 2022, 1)
assert False == sem_in_semestre_scolaire(sem_printemps, 2022, 1) assert False is sem_in_semestre_scolaire(sem_printemps, 2022, 1)
assert False == sem_in_semestre_scolaire(sem_ete, 2022, 1) assert False is sem_in_semestre_scolaire(sem_ete, 2022, 1)
assert False == sem_in_semestre_scolaire(sem_next_year, 2022, 1) assert False is sem_in_semestre_scolaire(sem_next_year, 2022, 1)
assert False == sem_in_semestre_scolaire(sem_prev_year, 2022, 1) assert False is sem_in_semestre_scolaire(sem_prev_year, 2022, 1)
def test_sem_in_periode2_default(): def test_sem_in_periode2_default():
assert False == sem_in_semestre_scolaire(sem_automne, 2022, 2) assert False is sem_in_semestre_scolaire(sem_automne, 2022, 2)
assert True == sem_in_semestre_scolaire(sem_nouvel_an, 2022, 2) assert True is sem_in_semestre_scolaire(sem_nouvel_an, 2022, 2)
assert True == sem_in_semestre_scolaire(sem_printemps, 2022, 2) assert True is sem_in_semestre_scolaire(sem_printemps, 2022, 2)
assert True == sem_in_semestre_scolaire(sem_ete, 2022, 2) assert True is sem_in_semestre_scolaire(sem_ete, 2022, 2)
assert False == sem_in_semestre_scolaire(sem_next_year, 2022, 1) assert False is sem_in_semestre_scolaire(sem_next_year, 2022, 1)
assert False == sem_in_semestre_scolaire(sem_prev_year, 2022, 1) assert False is sem_in_semestre_scolaire(sem_prev_year, 2022, 1)
def test_sem_in_annee_default(): def test_sem_in_annee_default():
assert True == sem_in_semestre_scolaire(sem_automne, 2022, 0) assert True is sem_in_semestre_scolaire(sem_automne, 2022, 0)
assert True == sem_in_semestre_scolaire(sem_nouvel_an, 2022) assert True is sem_in_semestre_scolaire(sem_nouvel_an, 2022)
assert True == sem_in_semestre_scolaire(sem_printemps, 2022, 0) assert True is sem_in_semestre_scolaire(sem_printemps, 2022, 0)
assert True == sem_in_semestre_scolaire(sem_ete, 2022, 0) assert True is sem_in_semestre_scolaire(sem_ete, 2022, 0)
assert False == sem_in_semestre_scolaire(sem_next_year, 2022) assert False is sem_in_semestre_scolaire(sem_next_year, 2022)
assert False == sem_in_semestre_scolaire(sem_prev_year, 2022, 0) assert False is sem_in_semestre_scolaire(sem_prev_year, 2022, 0)