WIP: paramétrage dates antipodiques

This commit is contained in:
Emmanuel Viennet 2022-11-09 12:50:10 +01:00
parent 836c57ec98
commit ce541d1870
8 changed files with 122 additions and 79 deletions

View File

@ -358,29 +358,39 @@ class FormSemestre(db.Model):
"""Test si sem est entièrement sur la même année scolaire. """Test si sem est entièrement sur la même année scolaire.
(ce n'est pas obligatoire mais si ce n'est pas le (ce n'est pas obligatoire mais si ce n'est pas le
cas les exports Apogée risquent de mal fonctionner) cas les exports Apogée risquent de mal fonctionner)
Pivot au 1er août. Pivot au 1er août par défaut.
""" """
if self.date_debut > self.date_fin: if self.date_debut > self.date_fin:
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 < 8: # août if self.date_debut.month <= scu.MONTH_FIN_ANNEE_SCOLAIRE: # juillet
# considere que debut sur l'anne scolaire precedente # considere que debut sur l'anne scolaire precedente
annee_debut -= 1 annee_debut -= 1
annee_fin = self.date_fin.year annee_fin = self.date_fin.year
if self.date_fin.month < 9: 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 # 9 (sept) pour autoriser un début en sept et une fin en aout
annee_fin -= 1 annee_fin -= 1
return annee_debut == annee_fin return annee_debut == annee_fin
def est_decale(self): def est_decale(self):
"""Vrai si semestre "décalé" """Vrai si semestre "décalé"
c'est à dire semestres impairs commençant entre janvier et juin c'est à dire semestres impairs commençant (par défaut)
et les pairs entre juillet et decembre entre janvier et juin et les pairs entre juillet et décembre.
""" """
if self.semestre_id <= 0: if self.semestre_id <= 0:
return False # formations sans semestres return False # formations sans semestres
return (self.semestre_id % 2 and self.date_debut.month <= 6) or ( return (
not self.semestre_id % 2 and self.date_debut.month > 6 # impair
(
self.semestre_id % 2
and self.date_debut.month < scu.MONTH_FIN_ANNEE_SCOLAIRE
)
or
# pair
(
(not self.semestre_id % 2)
and self.date_debut.month >= scu.MONTH_FIN_ANNEE_SCOLAIRE
)
) )
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]: def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
@ -936,8 +946,8 @@ class NotesSemSet(db.Model):
title = db.Column(db.Text) title = db.Column(db.Text)
annee_scolaire = db.Column(db.Integer, nullable=True, default=None) annee_scolaire = db.Column(db.Integer, nullable=True, default=None)
# periode: 0 (année), 1 (Simpair), 2 (Spair) sem_id = db.Column(db.Integer, nullable=False, default=0)
sem_id = db.Column(db.Integer, nullable=True, default=None) "période: 0 (année), 1 (Simpair), 2 (Spair)"
# Association: many to many # Association: many to many

View File

@ -31,20 +31,21 @@
## fonctionalités ## fonctionalités
Le menu 'synchronisation avec Apogée' ne permet pas de traiter facilement les cas Le menu 'synchronisation avec Apogée' ne permet pas de traiter facilement les cas
un même code étape est implementé dans des semestres (au sens ScoDoc) différents. un même code étape est implementé dans des formsemestres différents.
La proposition est d'ajouter à la page de description des ensembles de semestres On ajoute à la page de description des ensembles de semestres
une section permettant de faire le point sur les cas particuliers. une section permettant de faire le point sur les cas particuliers.
Cette section est composée de deux parties: Cette section est composée de deux parties:
* Une partie effectif figurent le nombre d'étudiants selon un répartition par
* Une partie effectif figurent le nombre d'étudiants selon une répartition par
semestre (en ligne) et par code étape (en colonne). On ajoute également des semestre (en ligne) et par code étape (en colonne). On ajoute également des
colonnes/lignes correspondant à des anomalies (étudiant sans code étape, sans colonnes/lignes correspondant à des anomalies (étudiant sans code étape, sans
semestre, avec deux semestres, sans NIP, etc.). semestre, avec deux semestres, sans NIP, etc.).
* La seconde partie présente la liste des étudiants. Il est possible qu'un * Une seconde partie présente la liste des étudiants. Il est possible qu'un
même nom figure deux fois dans la liste (si on a pas pu faire la correspondance même nom figure deux fois dans la liste (si on a pas pu faire la correspondance
entre une inscription apogée et un étudiant d'un semestre, par exemple). entre une inscription Apogée et un étudiant d'un semestre, par exemple).
L'activation d'un des nombres du tableau 'effectifs' restreint l'affichage de L'activation d'un des nombres du tableau 'effectifs' restreint l'affichage de
la liste aux étudiants qui contribuent à ce nombre. la liste aux étudiants qui contribuent à ce nombre.
@ -67,27 +68,25 @@ Cette classe compile la totalité des données:
Cette classe explore la suite semestres du semset. Cette classe explore la suite semestres du semset.
Pour chaque semestre, elle recense les étudiants du semestre et Pour chaque semestre, elle recense les étudiants du semestre et
les codes étapes concernés. les codes étapes concernés, puis tous les codes étapes (toujours
en important les étudiants de l'étape via le portail).
puis tous les codes étapes (toujours en important les étudiants de l'étape Enfin on dispatch chaque étudiant dans une case - soit ordinaire, soit
via le portail)
enfin on dispatch chaque étudiant dans une case - soit ordinaire, soit
correspondant à une anomalie. correspondant à une anomalie.
### Modification de sco_etape_apogee_view.py ### Modification de sco_etape_apogee_view.py
Pour insertion de l'affichage ajouté Pour insertion de l'affichage ajouté.
### Modification de sco_semset.py ### Modification de sco_semset.py
Affichage proprement dit Affichage proprement dit.
### Modification de scp_formsemestre.py ### Modification de sco_formsemestre.py
Modification/ajout de la méthode sem_in_semestre_scolaire pour permettre Modification/ajout de la méthode sem_in_semestre_scolaire pour permettre
l'inscrition de semestres décalés (S1 en septembre, ...). l'inscription de semestres décalés (S1 en septembre, ...).
Le filtrage s'effctue sur la date et non plus sur la parité du semestre (1-3/2-4). Le filtrage s'effectue sur la date et non plus sur la parité du semestre (1-3/2-4).
""" """
import json import json

View File

@ -28,6 +28,7 @@
"""Operations de base sur les formsemestres """Operations de base sur les formsemestres
""" """
from operator import itemgetter from operator import itemgetter
import datetime
import time import time
from flask import g, request from flask import g, request
@ -421,6 +422,22 @@ def sem_set_responsable_name(sem):
) )
def debut_in_semestre_scolaire(
date_debut: datetime.date, year: int = False, saison: int = 0
) -> bool:
"""Vrai si date_debut est dans l'année scolaire ou le semestre
indiquée par year et periode
(par défaut, l'année scolaire en cours).
periode:
1 = sept,
0 = janvier
None = année complète
"""
if not year:
year = scu.AnneeScolaire()
# XXX WIP à voir selon ce que fait réellement sem_in_semestre_scolaire
def sem_in_semestre_scolaire(sem, year=False, saison=0): def sem_in_semestre_scolaire(sem, year=False, saison=0):
"""n'utilise que la date de debut, pivot au 1er aout """n'utilise que la date de debut, pivot au 1er aout
si annee non specifiée, année scolaire courante si annee non specifiée, année scolaire courante
@ -436,13 +453,13 @@ def sem_in_semestre_scolaire(sem, year=False, saison=0):
if not year: if not year:
year = scu.AnneeScolaire() year = scu.AnneeScolaire()
# est-on dans la même année universitaire ? # est-on dans la même année universitaire ?
if sem["mois_debut_ord"] > 7: if sem["mois_debut_ord"] > 7: # XXX
if sem["annee_debut"] != str(year): if sem["annee_debut"] != str(year):
return False return False
else: else:
if sem["annee_debut"] != str(year + 1): if sem["annee_debut"] != str(year + 1):
return False return False
# rafinement éventuel sur le semestre # raffinement éventuel sur le semestre
# saison is None => pas de rafinement => True # saison is None => pas de rafinement => True
if saison == 0: if saison == 0:
return True return True
@ -454,36 +471,20 @@ def sem_in_semestre_scolaire(sem, year=False, saison=0):
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 debut, pivot au 1er août. N'utilise que la date de début, pivot au 1er août.
Si annee non specifiée, année scolaire courante Si année non specifiée, année scolaire courante
""" """
if not year: if not year:
year = scu.AnneeScolaire() year = scu.AnneeScolaire()
return ((sem["annee_debut"] == str(year)) and (sem["mois_debut_ord"] > 7)) or ( return (
(sem["annee_debut"] == str(year + 1)) and (sem["mois_debut_ord"] <= 7) (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_une_annee(sem): # XXX deprecated: use FormSemestre.est_sur_une_annee()
"""Test si sem est entièrement sur la même année scolaire.
(ce n'est pas obligatoire mais si ce n'est pas le cas les exports Apogée ne vont pas fonctionner)
pivot au 1er août.
"""
if sem["date_debut_iso"] > sem["date_fin_iso"]:
log("Warning: semestre %(formsemestre_id)s begins after ending !" % sem)
return False
debut = int(sem["annee_debut"])
if sem["mois_debut_ord"] < 8: # considere que debut sur l'anne scolaire precedente
debut -= 1
fin = int(sem["annee_fin"])
if (
sem["mois_fin_ord"] < 9
): # 9 (sept) pour autoriser un début en sept et une fin en aout
fin -= 1
return debut == fin
def sem_est_courant(sem): # -> FormSemestre.est_courant def sem_est_courant(sem): # -> FormSemestre.est_courant
"""Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)""" """Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)"""
now = time.strftime("%Y-%m-%d") now = time.strftime("%Y-%m-%d")

View File

@ -37,7 +37,7 @@ from app import db
from app.auth.models import User from app.auth.models import User
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 ModuleImpl from app.models import FormSemestre, ModuleImpl
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
from app.models.ues import UniteEns from app.models.ues import UniteEns
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -198,6 +198,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
M = modimpl.to_dict() M = modimpl.to_dict()
formsemestre_id = modimpl.formsemestre_id formsemestre_id = modimpl.formsemestre_id
formsemestre: FormSemestre = modimpl.formsemestre
Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0] Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
@ -205,7 +206,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
moduleimpl_id=M["moduleimpl_id"] moduleimpl_id=M["moduleimpl_id"]
) )
nt: NotesTableCompat = res_sem.load_formsemestre_results(modimpl.formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
mod_evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) mod_evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
mod_evals.sort( mod_evals.sort(
@ -333,9 +334,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
) )
# Adapté à partir d'une suggestion de DS (Le Havre) # Adapté à partir d'une suggestion de DS (Le Havre)
# Liens saisies absences seulement si permission et date courante dans le semestre # Liens saisies absences seulement si permission et date courante dans le semestre
if current_user.has_permission( if (
Permission.ScoAbsChange current_user.has_permission(Permission.ScoAbsChange)
) and sco_formsemestre.sem_est_courant(sem): and formsemestre.est_courant()
):
datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday() datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday()
group_id = sco_groups.get_default_group(formsemestre_id) group_id = sco_groups.get_default_group(formsemestre_id)
H.append( H.append(

View File

@ -104,9 +104,9 @@ class SemSet(dict):
cnx, cnx,
{"title": title, "annee_scolaire": annee_scolaire, "sem_id": sem_id}, {"title": title, "annee_scolaire": annee_scolaire, "sem_id": sem_id},
) )
log("created new semset_id=%s" % self.semset_id) log(f"created new semset_id={self.semset_id}")
self.load_sems() self.load_sems()
# analyse des semestres pour construire le bilan par semestre et par étape # Analyse des semestres pour construire le bilan par semestre et par étape
self.bilan = EtapeBilan() self.bilan = EtapeBilan()
for sem in self.sems: for sem in self.sems:
self.bilan.add_sem(sem) self.bilan.add_sem(sem)
@ -148,6 +148,7 @@ class SemSet(dict):
sem["etapes_apo_str"] = sco_formsemestre.etapes_apo_str(sorted(list(ets))) sem["etapes_apo_str"] = sco_formsemestre.etapes_apo_str(sorted(list(ets)))
def add(self, formsemestre_id): def add(self, formsemestre_id):
"Ajoute ce semestre à l'ensemble"
# check # check
if formsemestre_id in self.formsemestre_ids: if formsemestre_id in self.formsemestre_ids:
return # already there return # already there
@ -155,8 +156,7 @@ class SemSet(dict):
sem["formsemestre_id"] for sem in self.list_possible_sems() sem["formsemestre_id"] for sem in self.list_possible_sems()
]: ]:
raise ValueError( raise ValueError(
"can't add %s to set %s: incompatible sem_id" f"can't add {formsemestre_id} to set {self.semset_id}: incompatible sem_id"
% (formsemestre_id, self.semset_id)
) )
ndb.SimpleQuery( ndb.SimpleQuery(

View File

@ -160,6 +160,10 @@ EVALUATION_NORMALE = 0
EVALUATION_RATTRAPAGE = 1 EVALUATION_RATTRAPAGE = 1
EVALUATION_SESSION2 = 2 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
MONTH_NAMES_ABBREV = ( MONTH_NAMES_ABBREV = (
"Jan ", "Jan ",
"Fév ", "Fév ",
@ -461,18 +465,6 @@ def NotesURL():
return url_for("notes.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")] return url_for("notes.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
def EntreprisesURL():
"""URL of Enterprises
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Entreprises
= url de base des requêtes de ZEntreprises
et page accueil Entreprises
"""
return "NotImplemented"
# url_for("entreprises.index_html", scodoc_dept=g.scodoc_dept)[
# : -len("/index_html")
# ]
def AbsencesURL(): def AbsencesURL():
"""URL of Absences""" """URL of Absences"""
return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[ return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[
@ -918,7 +910,7 @@ 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 > 7: # apres le 1er aout if month > MONTH_FIN_ANNEE_SCOLAIRE: # apres le 1er aout
return "%s - %s" % (year, year + 1) return "%s - %s" % (year, year + 1)
else: else:
return "%s - %s" % (year - 1, year) return "%s - %s" % (year - 1, year)
@ -926,7 +918,7 @@ def annee_scolaire_repr(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 debut (septembre): heuristique pour l'hémisphère nord..."""
if int(month) > 7: if int(month) > MONTH_FIN_ANNEE_SCOLAIRE:
return int(year) return int(year)
else: else:
return int(year) - 1 return int(year) - 1
@ -943,7 +935,11 @@ def date_fin_anne_scolaire(annee_scolaire: 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 = 31 juillet de l'année suivante
""" """
return datetime.datetime(year=annee_scolaire + 1, month=7, day=31) return datetime.datetime(
year=annee_scolaire + 1,
month=MONTH_FIN_ANNEE_SCOLAIRE,
day=DAY_FIN_ANNEE_SCOLAIRE,
)
def sem_decale_str(sem): def sem_decale_str(sem):

View File

@ -0,0 +1,36 @@
"""semset_periode
Revision ID: 5542cac8c34a
Revises: 52f5f35c077f
Create Date: 2022-11-08 01:17:51.983042
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "5542cac8c34a"
down_revision = "52f5f35c077f"
branch_labels = None
depends_on = None
Session = sessionmaker()
def upgrade():
#
bind = op.get_bind()
session = Session(bind=bind)
session.execute("""UPDATE notes_semset SET sem_id=0 WHERE sem_id IS NULL;""")
op.alter_column(
"notes_semset", "sem_id", existing_type=sa.INTEGER(), nullable=False
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column("notes_semset", "sem_id", existing_type=sa.INTEGER(), nullable=True)
# ### end Alembic commands ###

View File

@ -7,7 +7,6 @@ Create Date: 2022-02-15 21:47:29.212329
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import sessionmaker # added by ev from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.