This commit is contained in:
Emmanuel Viennet 2023-09-05 21:27:14 +02:00
commit 6b43456a71
11 changed files with 170 additions and 110 deletions

View File

@ -601,9 +601,12 @@ def _create_singular(
moduleimpl_id = data.get("moduleimpl_id", False)
moduleimpl: ModuleImpl = None
if moduleimpl_id not in [False, None]:
if moduleimpl_id not in [False, None, "", "-1"]:
if moduleimpl_id != "autre":
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
try:
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
except ValueError:
moduleimpl = None
if moduleimpl is None:
errors.append("param 'moduleimpl_id': invalide")
else:
@ -810,7 +813,7 @@ def _edit_singular(assiduite_unique, data):
moduleimpl: ModuleImpl = None
if moduleimpl_id is not False:
if moduleimpl_id is not None:
if moduleimpl_id not in [None, "", "-1"]:
if moduleimpl_id == "autre":
assiduite_unique.moduleimpl_id = None
external_data = (
@ -823,7 +826,13 @@ def _edit_singular(assiduite_unique, data):
assiduite_unique.external_data = external_data
else:
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
try:
moduleimpl = ModuleImpl.query.filter_by(
id=int(moduleimpl_id)
).first()
except ValueError:
moduleimpl = None
if moduleimpl is None:
errors.append("param 'moduleimpl_id': invalide")
else:
@ -834,7 +843,7 @@ def _edit_singular(assiduite_unique, data):
else:
assiduite_unique.moduleimpl_id = moduleimpl_id
else:
assiduite_unique.moduleimpl_id = moduleimpl_id
assiduite_unique.moduleimpl_id = None
# Cas 3 : desc
desc = data.get("desc", False)

View File

@ -350,11 +350,15 @@ def compute_assiduites_justified(
if justificatifs is None:
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all()
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
assiduites_justifiees: list[int] = []
for assi in assiduites:
if assi.etat == EtatAssiduite.PRESENT:
continue
if any(
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
for j in justificatifs

View File

@ -74,9 +74,11 @@ class Identite(db.Model):
)
# Relations avec les assiduites et les justificatifs
assiduites = db.relationship("Assiduite", back_populates="etudiant", lazy="dynamic")
assiduites = db.relationship(
"Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
)
justificatifs = db.relationship(
"Justificatif", back_populates="etudiant", lazy="dynamic"
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
)
def __repr__(self):

View File

@ -212,7 +212,7 @@ def get_assiduites_stats(
output: dict = {}
calculator: CountCalculator = CountCalculator()
if "split" not in filtered:
if filtered is None or "split" not in filtered:
calculator.compute_assiduites(assiduites)
count: dict = calculator.to_dict()
@ -382,7 +382,10 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query:
def get_all_justified(
etudid: int, date_deb: datetime = None, date_fin: datetime = None
etudid: int,
date_deb: datetime = None,
date_fin: datetime = None,
moduleimpl_id: int = None,
) -> Query:
"""Retourne toutes les assiduités justifiées sur une période"""
@ -393,7 +396,9 @@ def get_all_justified(
date_deb = scu.localize_datetime(date_deb)
date_fin = scu.localize_datetime(date_fin)
justified = Assiduite.query.filter_by(est_just=True, etudid=etudid)
justified: Query = Assiduite.query.filter_by(est_just=True, etudid=etudid)
if moduleimpl_id is not None:
justified = justified.filter_by(moduleimpl_id=moduleimpl_id)
after = filter_by_date(
justified,
Assiduite,
@ -419,7 +424,7 @@ def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
def formsemestre_get_assiduites_count(
etudid: int, formsemestre: FormSemestre
etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
@ -428,9 +433,14 @@ def formsemestre_get_assiduites_count(
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
return get_assiduites_count_in_interval(
etudid,
date_debut=formsemestre.date_debut,
date_fin=formsemestre.date_fin,
date_debut=scu.localize_datetime(
datetime.combine(formsemestre.date_debut, time(8, 0))
),
date_fin=scu.localize_datetime(
datetime.combine(formsemestre.date_fin, time(18, 0))
),
metrique=scu.translate_assiduites_metric(metrique),
moduleimpl_id=moduleimpl_id,
)
@ -441,6 +451,7 @@ def get_assiduites_count_in_interval(
metrique="demi",
date_debut: datetime = None,
date_fin: datetime = None,
moduleimpl_id: int = None,
):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs, nb abs justifiées)
@ -452,33 +463,39 @@ def get_assiduites_count_in_interval(
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
r = sco_cache.AbsSemEtudCache.get(key)
if not r:
if not r or moduleimpl_id is not None:
date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso)
date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso)
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
assiduites: Query = Assiduite.query.filter_by(etudid=etudid)
assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT)
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
assiduites = filter_by_date(assiduites, Assiduite, date_debut, date_fin)
if moduleimpl_id is not None:
assiduites = assiduites.filter_by(moduleimpl_id=moduleimpl_id)
justificatifs = filter_by_date(
justificatifs, Justificatif, date_debut, date_fin
)
calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites(assiduites)
nb_abs: dict = calculator.to_dict()[metrique]
abs_just: list[Assiduite] = get_all_justified(etudid, date_debut, date_fin)
abs_just: list[Assiduite] = get_all_justified(
etudid, date_debut, date_fin, moduleimpl_id
)
calculator.reset()
calculator.compute_assiduites(abs_just)
nb_abs_just: dict = calculator.to_dict()[metrique]
r = (nb_abs, nb_abs_just)
ans = sco_cache.AbsSemEtudCache.set(key, r)
if not ans:
log("warning: get_assiduites_count failed to cache")
if moduleimpl_id is None:
ans = sco_cache.AbsSemEtudCache.set(key, r)
if not ans:
log("warning: get_assiduites_count failed to cache")
return r

View File

@ -237,7 +237,7 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
new_date: datetime.datetime = date
if new_date.tzinfo is None:
try:
new_date = timezone("Europe/Paris").localize(date)
new_date = TIME_ZONE.localize(date)
except OverflowError:
new_date = timezone("UTC").localize(date)
return new_date

View File

@ -811,11 +811,8 @@ function numberTimeToDate(nb) {
* @param {boolean} clear vidage de l'objet "assiduites" ou non
* @returns {object} l'objets Assiduités {<etudid:str> : [<assiduite>,]}
*/
function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) {
function getAssiduitesFromEtuds(clear, deb, fin) {
const etudIds = Object.keys(etuds).join(",");
const formsemestre_id = has_formsemestre
? `formsemestre_id=${getFormSemestreId()}&`
: "";
const date_debut = deb ? deb : toIsoString(getPrevDate());
const date_fin = fin ? fin : toIsoString(getNextDate());
@ -826,7 +823,7 @@ function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) {
const url_api =
getUrl() +
`/api/assiduites/group/query?date_debut=${date_debut}&${formsemestre_id}&date_fin=${date_fin}&etudids=${etudIds}`;
`/api/assiduites/group/query?date_debut=${date_debut}&date_fin=${date_fin}&etudids=${etudIds}`;
sync_get(url_api, (data, status) => {
if (status === "success") {
const dataKeys = Object.keys(data);
@ -924,14 +921,11 @@ function deleteAssiduite(assiduite_id) {
function hasModuleImpl(assiduite) {
if (assiduite.moduleimpl_id != null) return true;
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object &&
"module" in assiduite.external_data
)
return true;
return false;
return (
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null &&
assiduite.external_data.hasOwnProperty("module")
);
}
/**
@ -1057,16 +1051,13 @@ function getAssiduiteValue(field) {
* Mise à jour des assiduités d'un étudiant
* @param {String | Number} etudid identifiant de l'étudiant
*/
function actualizeEtudAssiduite(etudid, has_formsemestre = true) {
const formsemestre_id = has_formsemestre
? `formsemestre_id=${getFormSemestreId()}&`
: "";
function actualizeEtudAssiduite(etudid) {
const date_debut = toIsoString(getPrevDate());
const date_fin = toIsoString(getNextDate());
const url_api =
getUrl() +
`/api/assiduites/${etudid}/query?${formsemestre_id}date_debut=${date_debut}&date_fin=${date_fin}`;
`/api/assiduites/${etudid}/query?date_debut=${date_debut}&date_fin=${date_fin}`;
sync_get(url_api, (data, status) => {
if (status === "success") {
assiduites[etudid] = data;
@ -1331,7 +1322,7 @@ function insertEtudRow(etud, index, output = false) {
* @param {String | Number} etudid l'identifiant de l'étudiant
*/
function actualizeEtud(etudid) {
actualizeEtudAssiduite(etudid, !isSingleEtud());
actualizeEtudAssiduite(etudid);
//Actualize row
const etudHolder = document.querySelector(".etud_holder");
const ancient_row = document.getElementById(`etud_row_${etudid}`);
@ -1412,10 +1403,10 @@ function setModuleImplId(assiduite, module = null) {
const moduleimpl = module == null ? getModuleImplId() : module;
if (moduleimpl === "autre") {
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null
) {
if ("module" in assiduite.external_data) {
if (assiduite.external_data.hasOwnProperty("module")) {
assiduite.external_data.module = "Autre";
} else {
assiduite["external_data"] = { module: "Autre" };
@ -1427,10 +1418,10 @@ function setModuleImplId(assiduite, module = null) {
} else {
assiduite["moduleimpl_id"] = moduleimpl;
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null
) {
if ("module" in assiduite.external_data) {
if (assiduite.external_data.hasOwnProperty("module")) {
delete assiduite.external_data.module;
}
}
@ -1482,9 +1473,9 @@ function getCurrentAssiduiteModuleImplId() {
let mod = currentAssiduites[0].moduleimpl_id;
if (
mod == null &&
"external_data" in currentAssiduites[0] &&
currentAssiduites[0].external_data instanceof Object &&
"module" in currentAssiduites[0].external_data
currentAssiduites[0].hasOwnProperty("external_data") &&
currentAssiduites[0].external_data != null &&
currentAssiduites[0].external_data.hasOwnProperty("module")
) {
mod = currentAssiduites[0].external_data.module;
}
@ -1696,9 +1687,9 @@ function getModuleImpl(assiduite) {
if (id == null || id == undefined) {
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object &&
"module" in assiduite.external_data
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null &&
assiduite.external_data.hasOwnProperty("module")
) {
return assiduite.external_data.module;
} else {
@ -1724,10 +1715,12 @@ function getModuleImpl(assiduite) {
}
function getUser(obj) {
if ("external_data" in obj && obj.external_data != null) {
if ("enseignant" in obj.external_data) {
return obj.external_data.enseignant;
}
if (
obj.hasOwnProperty("external_data") &&
obj.external_data != null &&
obj.external_data.hasOwnProperty("enseignant")
) {
return obj.external_data.enseignant;
}
return obj.user_id;

View File

@ -533,7 +533,7 @@
}
if (get) {
getAssiduitesFromEtuds(false, false, d_debut.format(), d_fin.format())
getAssiduitesFromEtuds(false, d_debut.format(), d_fin.format())
return 0x0;
}

View File

@ -184,8 +184,11 @@
path,
(data) => {
let module = data.moduleimpl_id;
if (module == null && "external_data" in data && "module" in data.external_data) {
if (
module == null && data.hasOwnProperty("external_data") &&
data.external_data != null &&
data.external_data.hasOwnProperty('module')
) {
module = data.external_data.module.toLowerCase();
}

View File

@ -1174,22 +1174,9 @@ def view_module_abs(moduleimpl_id, fmt="html"):
rows = []
for etud in inscrits:
# TODO-ASSIDUITE ne va pas car ne filtre pas sur le moduleimpl
# nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count(etud.id, modimpl.formsemestre)
nb_abs, nb_abs_just = 0, 0 # XXX TODO-ASSIDUITE
# nb_abs = sco_abs.count_abs(
# etudid=etud.id,
# debut=debut_sem,
# fin=fin_sem,
# moduleimpl_id=moduleimpl_id,
# )
# if nb_abs:
# nb_abs_just = sco_abs.count_abs_just(
# etudid=etud.id,
# debut=debut_sem,
# fin=fin_sem,
# moduleimpl_id=moduleimpl_id,
# )
nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count(
etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id
)
rows.append(
{
"nomprenom": etud.nomprenom,

View File

@ -13,8 +13,7 @@ import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu
from app import db
from app.models import Assiduite, FormSemestre, Identite, Justificatif, ModuleImpl
# from app.scodoc import sco_abs_views, sco_formsemestre TODO-ASSIDUITE
from app.models.assiduites import compute_assiduites_justified
from app.scodoc.sco_exceptions import ScoValueError
from tests.unit import sco_fake_gen
@ -39,7 +38,6 @@ def test_bi_directional_enum(test_client):
assert BiInt.inverse()[1] == BiInt.A and BiInt.inverse()[2] == BiInt.B
@pytest.mark.skip # XXX TODO-ASSIDUITE (issue #690)
def test_general(test_client):
"""tests général du modèle assiduite"""
@ -80,11 +78,9 @@ def test_general(test_client):
date_fin="31/07/2024",
)
formsemestre_1 = sco_formsemestre.get_formsemestre(
formsemestre_id_1
) # Utiliser plutot FormSemestre de nos jours TODO-ASSIDUITE
formsemestre_2 = sco_formsemestre.get_formsemestre(formsemestre_id_2)
formsemestre_3 = sco_formsemestre.get_formsemestre(formsemestre_id_3)
formsemestre_1 = FormSemestre.get_formsemestre(formsemestre_id_1)
formsemestre_2 = FormSemestre.get_formsemestre(formsemestre_id_2)
formsemestre_3 = FormSemestre.get_formsemestre(formsemestre_id_3)
# Création des modulesimpls (4, 2 par semestre)
@ -137,7 +133,7 @@ def test_general(test_client):
etud_faux_dict = g_fake.create_etud(code_nip=None, prenom="etudfaux")
etud_faux = Identite.query.filter_by(id=etud_faux_dict["id"]).first()
# verif_migration_abs_assiduites() // Test à revoir TODO-ASSIDUITE
# verif_migration_abs_assiduites() // Test à revoir TODO-ASSIDUITE (issue #696)
ajouter_assiduites(etuds, moduleimpls, etud_faux)
justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0])
@ -145,11 +141,14 @@ def test_general(test_client):
etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3)
)
verifier_filtrage_justificatifs(etuds[0], justificatifs)
essais_cache(etuds[0].etudid, (formsemestre_1, formsemestre_2), moduleimpls)
editer_supprimer_assiduites(etuds, moduleimpls)
editer_supprimer_justificatif(etuds[0])
@pytest.mark.skip # XXX TODO-ASSIDUITE (issue #696)
# XXX TODO-ASSIDUITE (issue #696)
def verif_migration_abs_assiduites():
"""Vérification que le script de migration fonctionne correctement"""
downgrade_module(assiduites=True, justificatifs=True)
@ -476,11 +475,11 @@ def _get_justi(
).first()
def essais_cache(etudid):
def essais_cache(etudid, sems: tuple[FormSemestre], moduleimpls: list[ModuleImpl]):
"""Vérification des fonctionnalités du cache"""
date_deb: str = "2023-01-01T07:00"
date_fin: str = "2023-03-31T19:00"
date_deb: str = "2022-09-01T07:00"
date_fin: str = "2023-01-31T19:00"
assiduites_count_no_cache = scass.get_assiduites_count_in_interval(
etudid, date_deb, date_fin
@ -490,8 +489,36 @@ def essais_cache(etudid):
)
assert (
assiduites_count_cache == assiduites_count_no_cache == (34, 15)
), "Erreur cache"
assiduites_count_cache == assiduites_count_no_cache == (2, 1)
), "Erreur cache classique"
assert scass.formsemestre_get_assiduites_count(etudid, sems[0]) == (
2,
1,
), "Erreur formsemestre_get_assiduites_count (sans module) A"
assert scass.formsemestre_get_assiduites_count(etudid, sems[1]) == (
0,
0,
), "Erreur formsemestre_get_assiduites_count (sans module) B"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[0].id
) == (
1,
1,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[1].id
) == (
1,
0,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[2].id
) == (
0,
0,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
def ajouter_justificatifs(etud):
@ -543,6 +570,8 @@ def ajouter_justificatifs(etud):
db.session.commit()
justificatifs.append(just_obj)
compute_assiduites_justified(etud.etudid, justificatifs)
# Vérification de la création des justificatifs
assert [
justi for justi in justificatifs if not isinstance(justi, Justificatif)
@ -858,7 +887,7 @@ def ajouter_assiduites(
def verifier_comptage_et_filtrage_assiduites(
etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[int]
etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[FormSemestre]
):
"""
Deuxième partie:
@ -931,9 +960,6 @@ def verifier_comptage_et_filtrage_assiduites(
), "Filtrage par 'Moduleimpl' mauvais"
# Formsemestre
formsemestres = [
FormSemestre.query.filter_by(id=fms["id"]).first() for fms in formsemestres
]
assert (
scass.filter_by_formsemestre(
etu1.assiduites, Assiduite, formsemestres[0]

View File

@ -23,15 +23,18 @@ import app
from app import db
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.models import FormSemestre, Assiduite, Justificatif
from app.scodoc import sco_formsemestre
from app.scodoc import sco_bulletins
from app.scodoc import codes_cursus
from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_cursus_dut
from app.scodoc import sco_saisie_notes
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime
from app.models.assiduites import compute_assiduites_justified
DEPT = TestConfig.DEPT_TEST
@ -186,21 +189,10 @@ def run_sco_basic(verbose=False) -> FormSemestre:
# -----------------------
etudid = etuds[0]["etudid"]
# XXX TODO-ASSIDUITE
# _ = sco_abs_views.doSignaleAbsence(
# "15/01/2020", "18/01/2020", demijournee=2, etudid=etudid
# )
# _ = sco_abs_views.doJustifAbsence(
# "17/01/2020",
# "18/01/2020",
# demijournee=2,
# etudid=etudid,
# )
# nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
# assert nbabs == 6, f"incorrect nbabs ({nbabs})"
# assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})"
_signal_absences_justificatifs(etudid)
nbabs, nbabsjust = scass.get_assiduites_count(etudid, sem)
assert nbabs == 6, f"incorrect nbabs ({nbabs})"
assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})"
# --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance
# on n'a pas encore saisi de décisions
@ -251,3 +243,30 @@ def run_sco_basic(verbose=False) -> FormSemestre:
)
assert q.count() == 0
return formsemestre
def _signal_absences_justificatifs(etudid: int):
etud: Identite = Identite.query.get(etudid)
db.session.commit()
for i in range(15, 18):
db.session.add(
Assiduite.create_assiduite(
etud=etud,
date_debut=localize_datetime(datetime.datetime(2020, 1, i, 8, 0)),
date_fin=localize_datetime(datetime.datetime(2020, 1, i, 18, 0)),
etat=EtatAssiduite.ABSENT,
)
)
db.session.commit()
justif: Justificatif = Justificatif.create_justificatif(
etud=etud,
date_debut=localize_datetime(datetime.datetime(2020, 1, 17, 8, 0)),
date_fin=localize_datetime(datetime.datetime(2020, 1, 17, 18, 0)),
etat=EtatJustificatif.VALIDE,
)
db.session.add(justif)
compute_assiduites_justified(
etud.etudid,
[justif],
)
db.session.commit()