forked from ScoDoc/ScoDoc
Paramétrage dates annees scolaires (pivots) + tous test unitaires OK
This commit is contained in:
parent
3bc60c268a
commit
0148b4b2ce
@ -54,6 +54,22 @@ class BonusConfigurationForm(FlaskForm):
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration avancée"
|
||||
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")
|
||||
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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -685,7 +685,7 @@ def EtatAbsences():
|
||||
|
||||
</td></tr></table>
|
||||
</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(),
|
||||
]
|
||||
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,
|
||||
"""<form method="GET" action="CalAbs" name="f">""",
|
||||
"""<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),
|
||||
""" 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)
|
||||
if y == anneescolaire:
|
||||
if y == annee_scolaire:
|
||||
H.append("selected")
|
||||
H.append(""">%s</option>""" % y)
|
||||
H.append("""</select></form>""")
|
||||
@ -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:
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -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"]),
|
||||
)
|
||||
)
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
# 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="<h2>Semestres</h2>",
|
||||
@ -549,8 +493,10 @@ def table_formsemestres(
|
||||
"""Une table presentant des semestres"""
|
||||
for sem in sems:
|
||||
sem_set_responsable_name(sem)
|
||||
sem["_titre_num_target"] = (
|
||||
"formsemestre_status?formsemestre_id=%s" % sem["formsemestre_id"]
|
||||
sem["_titre_num_target"] = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem["formsemestre_id"],
|
||||
)
|
||||
|
||||
if not columns_ids:
|
||||
@ -592,8 +538,10 @@ def table_formsemestres(
|
||||
return tab
|
||||
|
||||
|
||||
def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False):
|
||||
"""Liste des semestres de cette etape, pour l'annee scolaire indiquée (sinon, pour toutes)"""
|
||||
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).
|
||||
"""
|
||||
ds = {} # formsemestre_id : sem
|
||||
if 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"):
|
||||
"""Affiche table des semestres correspondants à l'étape"""
|
||||
if etape_apo:
|
||||
html_title = (
|
||||
"""<h2>Semestres courants de l'étape <tt>%s</tt></h2>""" % etape_apo
|
||||
)
|
||||
html_title = f"""<h2>Semestres courants de l'étape <tt>{etape_apo}</tt></h2>"""
|
||||
else:
|
||||
html_title = """<h2>Semestres courants</h2>"""
|
||||
tab = table_formsemestres(
|
||||
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_next_section="""<form action="view_formsemestre_by_etape">
|
||||
|
@ -40,13 +40,11 @@ sem_set_list()
|
||||
"""
|
||||
|
||||
import flask
|
||||
from flask import g
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
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_formsemestre
|
||||
from app.scodoc import sco_formsemestre_status
|
||||
@ -77,6 +75,7 @@ semset_delete = _semset_editor.delete
|
||||
class SemSet(dict):
|
||||
def __init__(self, semset_id=None, title="", annee_scolaire="", sem_id=""):
|
||||
"""Load and init, or, if semset_id is not specified, create"""
|
||||
super().__init__()
|
||||
if not annee_scolaire and not semset_id:
|
||||
# on autorise annee_scolaire null si sem_id pour pouvoir lire les anciens semsets
|
||||
# mal construits...
|
||||
@ -491,7 +490,7 @@ def semset_page(format="html"):
|
||||
]
|
||||
H.append(tab.html())
|
||||
|
||||
annee_courante = int(scu.AnneeScolaire())
|
||||
annee_courante = int(scu.annee_scolaire())
|
||||
menu_annee = "\n".join(
|
||||
[
|
||||
'<option value="%s">%s</option>' % (i, i)
|
||||
|
@ -58,9 +58,7 @@ from werkzeug.http import HTTP_STATUS_CODES
|
||||
from config import Config
|
||||
from app import log
|
||||
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 import sco_exceptions
|
||||
from app.scodoc import sco_xml
|
||||
import sco_version
|
||||
|
||||
@ -161,8 +159,14 @@ EVALUATION_RATTRAPAGE = 1
|
||||
EVALUATION_SESSION2 = 2
|
||||
|
||||
# Dates et années scolaires
|
||||
MONTH_FIN_ANNEE_SCOLAIRE = 7 # juillet (TODO: passer en paramètre config.)
|
||||
DAY_FIN_ANNEE_SCOLAIRE = 31 # TODO calculer en fct du mois
|
||||
# Ces dates "pivot" sont paramétrables dans les préférences générales
|
||||
# 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 = (
|
||||
"Jan ",
|
||||
@ -910,36 +914,47 @@ def annee_scolaire_repr(year, month):
|
||||
"""representation de l'annee scolaire : '2009 - 2010'
|
||||
à partir d'une date.
|
||||
"""
|
||||
if month > MONTH_FIN_ANNEE_SCOLAIRE: # apres le 1er aout
|
||||
return "%s - %s" % (year, year + 1)
|
||||
if month >= MONTH_DEBUT_ANNEE_SCOLAIRE: # apres le 1er aout
|
||||
return f"{year} - {year + 1}"
|
||||
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:
|
||||
"""Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord..."""
|
||||
if int(month) > MONTH_FIN_ANNEE_SCOLAIRE:
|
||||
"""Annee scolaire de début.
|
||||
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)
|
||||
else:
|
||||
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
|
||||
= 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
|
||||
= 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(
|
||||
year=annee_scolaire + 1,
|
||||
month=MONTH_FIN_ANNEE_SCOLAIRE,
|
||||
day=DAY_FIN_ANNEE_SCOLAIRE,
|
||||
)
|
||||
year=annee_sco + 1, month=MONTH_DEBUT_ANNEE_SCOLAIRE, day=1
|
||||
) - datetime.timedelta(days=1)
|
||||
|
||||
|
||||
def sem_decale_str(sem):
|
||||
@ -1065,23 +1080,6 @@ def query_portal(req, msg="Portail Apogee", timeout=3):
|
||||
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(
|
||||
message="<p>Confirmer ?</p>",
|
||||
OK="OK",
|
||||
|
@ -198,7 +198,7 @@ def choix_semaine(group_id):
|
||||
def cal_select_week(year=None):
|
||||
"display calendar allowing week selection"
|
||||
if not year:
|
||||
year = scu.AnneeScolaire()
|
||||
year = scu.annee_scolaire()
|
||||
sems = sco_formsemestre.do_formsemestre_list()
|
||||
if not sems:
|
||||
js = ""
|
||||
@ -1215,9 +1215,9 @@ def add_billets_absence_form(etudid):
|
||||
@bp.route("/billets_etud/<int:etudid>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def billets_etud(etudid=False):
|
||||
"""Liste billets pour un etudiant"""
|
||||
fmt = request.args.get("format", "html")
|
||||
def billets_etud(etudid=False, format=False):
|
||||
"""Liste billets pour un étudiant"""
|
||||
fmt = format or request.args.get("format", "html")
|
||||
if not fmt in {"html", "json", "xml", "xls", "xlsx"}:
|
||||
return ScoValueError("Format invalide")
|
||||
table = sco_abs_billets.table_billets_etud(etudid)
|
||||
|
@ -636,16 +636,16 @@ def index_html():
|
||||
</li>
|
||||
<li><a class="stdlink" href="{
|
||||
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
|
||||
{scu.AnneeScolaire()-1} - {scu.AnneeScolaire()}
|
||||
{scu.annee_scolaire()-1} - {scu.annee_scolaire()}
|
||||
</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{
|
||||
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
|
||||
{scu.AnneeScolaire()} - {scu.AnneeScolaire()+1}
|
||||
{scu.annee_scolaire()} - {scu.annee_scolaire()+1}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -90,9 +90,7 @@ def test_nouvel_an_special_pp_before_pa():
|
||||
|
||||
|
||||
def test_nouvel_ete_pp_before_pa():
|
||||
assert (2023, 2) == FormSemestre.comp_periode(
|
||||
datetime.datetime(2024, 6, 1), 1, 8, 1, 2
|
||||
)
|
||||
assert (2023, 2) == FormSemestre.comp_periode(datetime.datetime(2024, 6, 1), 8, 2)
|
||||
|
||||
|
||||
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():
|
||||
assert True == sem_in_semestre_scolaire(sem_automne, 2022, 1)
|
||||
assert False == sem_in_semestre_scolaire(sem_nouvel_an, 2022, 1)
|
||||
assert False == sem_in_semestre_scolaire(sem_printemps, 2022, 1)
|
||||
assert False == sem_in_semestre_scolaire(sem_ete, 2022, 1)
|
||||
assert False == sem_in_semestre_scolaire(sem_next_year, 2022, 1)
|
||||
assert False == sem_in_semestre_scolaire(sem_prev_year, 2022, 1)
|
||||
assert True is sem_in_semestre_scolaire(sem_automne, 2022, 1)
|
||||
assert False is sem_in_semestre_scolaire(sem_nouvel_an, 2022, 1)
|
||||
assert False is sem_in_semestre_scolaire(sem_printemps, 2022, 1)
|
||||
assert False is sem_in_semestre_scolaire(sem_ete, 2022, 1)
|
||||
assert False is sem_in_semestre_scolaire(sem_next_year, 2022, 1)
|
||||
assert False is sem_in_semestre_scolaire(sem_prev_year, 2022, 1)
|
||||
|
||||
|
||||
def test_sem_in_periode2_default():
|
||||
assert False == sem_in_semestre_scolaire(sem_automne, 2022, 2)
|
||||
assert True == sem_in_semestre_scolaire(sem_nouvel_an, 2022, 2)
|
||||
assert True == sem_in_semestre_scolaire(sem_printemps, 2022, 2)
|
||||
assert True == sem_in_semestre_scolaire(sem_ete, 2022, 2)
|
||||
assert False == 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_automne, 2022, 2)
|
||||
assert True is sem_in_semestre_scolaire(sem_nouvel_an, 2022, 2)
|
||||
assert True is sem_in_semestre_scolaire(sem_printemps, 2022, 2)
|
||||
assert True is sem_in_semestre_scolaire(sem_ete, 2022, 2)
|
||||
assert False is sem_in_semestre_scolaire(sem_next_year, 2022, 1)
|
||||
assert False is sem_in_semestre_scolaire(sem_prev_year, 2022, 1)
|
||||
|
||||
|
||||
def test_sem_in_annee_default():
|
||||
assert True == sem_in_semestre_scolaire(sem_automne, 2022, 0)
|
||||
assert True == sem_in_semestre_scolaire(sem_nouvel_an, 2022)
|
||||
assert True == sem_in_semestre_scolaire(sem_printemps, 2022, 0)
|
||||
assert True == sem_in_semestre_scolaire(sem_ete, 2022, 0)
|
||||
assert False == sem_in_semestre_scolaire(sem_next_year, 2022)
|
||||
assert False == sem_in_semestre_scolaire(sem_prev_year, 2022, 0)
|
||||
assert True is sem_in_semestre_scolaire(sem_automne, 2022, 0)
|
||||
assert True is sem_in_semestre_scolaire(sem_nouvel_an, 2022)
|
||||
assert True is sem_in_semestre_scolaire(sem_printemps, 2022, 0)
|
||||
assert True is sem_in_semestre_scolaire(sem_ete, 2022, 0)
|
||||
assert False is sem_in_semestre_scolaire(sem_next_year, 2022)
|
||||
assert False is sem_in_semestre_scolaire(sem_prev_year, 2022, 0)
|
||||
|
Loading…
Reference in New Issue
Block a user