forked from ScoDoc/ScoDoc
optimisation migration abs to assiduites (WIP)
This commit is contained in:
parent
e18990d804
commit
53c9658ce1
@ -81,13 +81,11 @@ class Assiduite(db.Model):
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
assiduites: list[Assiduite] = etud.assiduites.all()
|
||||
|
||||
if is_period_conflicting(date_debut, date_fin, assiduites):
|
||||
assiduites: list[Assiduite] = etud.assiduites
|
||||
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||
raise ScoValueError(
|
||||
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||
)
|
||||
|
||||
if moduleimpl is not None:
|
||||
# Vérification de l'existence du module pour l'étudiant
|
||||
if moduleimpl.est_inscrit(etud):
|
||||
@ -114,6 +112,32 @@ class Assiduite(db.Model):
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
@classmethod
|
||||
def fast_create_assiduite(
|
||||
cls,
|
||||
etudid: int,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatAssiduite,
|
||||
moduleimpl_id: int = None,
|
||||
description: str = None,
|
||||
entry_date: datetime = None,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudid=etudid,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
|
||||
class Justificatif(db.Model):
|
||||
"""
|
||||
@ -185,8 +209,8 @@ class Justificatif(db.Model):
|
||||
) -> object or int:
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
justificatifs: list[Justificatif] = etud.justificatifs.all()
|
||||
if is_period_conflicting(date_debut, date_fin, justificatifs):
|
||||
justificatifs: list[Justificatif] = etud.justificatifs
|
||||
if is_period_conflicting(date_debut, date_fin, justificatifs, Justificatif):
|
||||
raise ScoValueError(
|
||||
"Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
|
||||
)
|
||||
@ -202,11 +226,35 @@ class Justificatif(db.Model):
|
||||
|
||||
return nouv_justificatif
|
||||
|
||||
@classmethod
|
||||
def fast_create_justificatif(
|
||||
cls,
|
||||
etudid: int,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
raison: str = None,
|
||||
entry_date: datetime = None,
|
||||
) -> object or int:
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
|
||||
nouv_justificatif = Justificatif(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudid=etudid,
|
||||
raison=raison,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
return nouv_justificatif
|
||||
|
||||
|
||||
def is_period_conflicting(
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
collection: list[Assiduite or Justificatif],
|
||||
collection_cls: Assiduite or Justificatif,
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si une date n'entre pas en collision
|
||||
@ -215,12 +263,15 @@ def is_period_conflicting(
|
||||
|
||||
date_debut = localize_datetime(date_debut)
|
||||
date_fin = localize_datetime(date_fin)
|
||||
unified = [
|
||||
uni
|
||||
for uni in collection
|
||||
if is_period_overlapping(
|
||||
(date_debut, date_fin), (uni.date_debut, uni.date_fin), bornes=False
|
||||
)
|
||||
]
|
||||
|
||||
return len(unified) != 0
|
||||
if (
|
||||
collection.filter_by(date_debut=date_debut, date_fin=date_fin).first()
|
||||
is not None
|
||||
):
|
||||
return True
|
||||
|
||||
count: int = collection.filter(
|
||||
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||
).count()
|
||||
|
||||
return count > 0
|
||||
|
43
app/profiler.py
Normal file
43
app/profiler.py
Normal file
@ -0,0 +1,43 @@
|
||||
from time import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Profiler:
|
||||
OUTPUT: str = "/tmp/scodoc.profiler.csv"
|
||||
|
||||
def __init__(self, tag: str) -> None:
|
||||
self.tag: str = tag
|
||||
self.start_time: time = None
|
||||
self.stop_time: time = None
|
||||
|
||||
def start(self):
|
||||
self.start_time = time()
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
self.stop_time = time()
|
||||
return self
|
||||
|
||||
def elapsed(self) -> float:
|
||||
return self.stop_time - self.start_time
|
||||
|
||||
def dates(self) -> tuple[datetime, datetime]:
|
||||
return datetime.fromtimestamp(self.start_time), datetime.fromtimestamp(
|
||||
self.stop_time
|
||||
)
|
||||
|
||||
def write(self):
|
||||
with open(Profiler.OUTPUT, "a") as file:
|
||||
dates: tuple = self.dates()
|
||||
date_str = (dates[0].isoformat(), dates[1].isoformat())
|
||||
file.write(f"\n{self.tag},{self.elapsed() : .2}")
|
||||
|
||||
@classmethod
|
||||
def write_in(cls, msg: str):
|
||||
with open(cls.OUTPUT, "a") as file:
|
||||
file.write(f"\n# {msg}")
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
with open(cls.OUTPUT, "w") as file:
|
||||
file.write("")
|
@ -39,6 +39,7 @@ from hashlib import md5
|
||||
import numbers
|
||||
import os
|
||||
import re
|
||||
from shutil import get_terminal_size
|
||||
import _thread
|
||||
import time
|
||||
import unicodedata
|
||||
@ -88,6 +89,60 @@ ETATS_INSCRIPTION = {
|
||||
}
|
||||
|
||||
|
||||
def printProgressBar(
|
||||
iteration,
|
||||
total,
|
||||
prefix="",
|
||||
suffix="",
|
||||
finish_msg="",
|
||||
decimals=1,
|
||||
length=100,
|
||||
fill="█",
|
||||
autosize=False,
|
||||
):
|
||||
"""
|
||||
Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique)
|
||||
@params:
|
||||
iteration - Required : index du point donné (Int)
|
||||
total - Required : nombre total avant complétion (eg: len(List))
|
||||
prefix - Optional : Préfix -> écrit à gauche de la barre (Str)
|
||||
suffix - Optional : Suffix -> écrit à droite de la barre (Str)
|
||||
decimals - Optional : nombres de chiffres après la virgule (Int)
|
||||
length - Optional : taille de la barre en nombre de caractères (Int)
|
||||
fill - Optional : charactère de remplissange de la barre (Str)
|
||||
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
|
||||
"""
|
||||
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||
color = ProgressBarColors.RED
|
||||
if 50 >= float(percent) > 25:
|
||||
color = ProgressBarColors.MAGENTA
|
||||
if 75 >= float(percent) > 50:
|
||||
color = ProgressBarColors.BLUE
|
||||
if 90 >= float(percent) > 75:
|
||||
color = ProgressBarColors.CYAN
|
||||
if 100 >= float(percent) > 90:
|
||||
color = ProgressBarColors.GREEN
|
||||
styling = f"{prefix} |{fill}| {percent}% {suffix}"
|
||||
if autosize:
|
||||
cols, _ = get_terminal_size(fallback=(length, 1))
|
||||
length = cols - len(styling)
|
||||
filledLength = int(length * iteration // total)
|
||||
bar = fill * filledLength + "-" * (length - filledLength)
|
||||
print(f"\r{color}{styling.replace(fill, bar)}{ProgressBarColors.RESET}", end="\r")
|
||||
# Affiche une nouvelle ligne vide
|
||||
if iteration == total:
|
||||
print(f"\n{finish_msg}")
|
||||
|
||||
|
||||
class ProgressBarColors:
|
||||
BLUE = "\033[94m"
|
||||
CYAN = "\033[96m"
|
||||
GREEN = "\033[92m"
|
||||
MAGENTA = "\033[95m"
|
||||
RED = "\033[91m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
class BiDirectionalEnum(Enum):
|
||||
"""Permet la recherche inverse d'un enum
|
||||
Condition : les clés et les valeurs doivent être uniques
|
||||
|
@ -1,77 +1,33 @@
|
||||
# Script de migration des données de la base "absences" -> "assiduites"/"justificatifs"
|
||||
import shutil
|
||||
|
||||
from app import db
|
||||
|
||||
from app.profiler import Profiler
|
||||
from app.models import (
|
||||
Assiduite,
|
||||
Justificatif,
|
||||
Absence,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
ModuleImplInscription,
|
||||
Departement,
|
||||
)
|
||||
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
localize_datetime,
|
||||
ProgressBarColors,
|
||||
printProgressBar,
|
||||
)
|
||||
from datetime import time, datetime, date
|
||||
from json import dump
|
||||
|
||||
|
||||
class glob:
|
||||
class _glob:
|
||||
DUPLICATIONS_ASSIDUITES: dict[tuple[date, bool, int], Assiduite] = {}
|
||||
DUPLICATIONS_JUSTIFICATIFS: dict[tuple[date, bool, int], Justificatif] = {}
|
||||
DUPLICATED: list[Justificatif] = []
|
||||
|
||||
|
||||
class bcolors:
|
||||
BLUE = "\033[94m"
|
||||
CYAN = "\033[96m"
|
||||
GREEN = "\033[92m"
|
||||
MAGENTA = "\033[95m"
|
||||
RED = "\033[91m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
def printProgressBar(
|
||||
iteration,
|
||||
total,
|
||||
prefix="",
|
||||
suffix="",
|
||||
finish_msg="",
|
||||
decimals=1,
|
||||
length=100,
|
||||
fill="█",
|
||||
autosize=False,
|
||||
):
|
||||
"""
|
||||
Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique)
|
||||
@params:
|
||||
iteration - Required : index du point donné (Int)
|
||||
total - Required : nombre total avant complétion (eg: len(List))
|
||||
prefix - Optional : Préfix -> écrit à gauche de la barre (Str)
|
||||
suffix - Optional : Suffix -> écrit à droite de la barre (Str)
|
||||
decimals - Optional : nombres de chiffres après la virgule (Int)
|
||||
length - Optional : taille de la barre en nombre de caractères (Int)
|
||||
fill - Optional : charactère de remplissange de la barre (Str)
|
||||
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
|
||||
"""
|
||||
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||
color = bcolors.RED
|
||||
if 50 > float(percent) > 25:
|
||||
color = bcolors.MAGENTA
|
||||
if 75 > float(percent) > 50:
|
||||
color = bcolors.BLUE
|
||||
if 90 > float(percent) > 75:
|
||||
color = bcolors.CYAN
|
||||
if 100 >= float(percent) > 90:
|
||||
color = bcolors.GREEN
|
||||
styling = f"{prefix} |{fill}| {percent}% {suffix}"
|
||||
if autosize:
|
||||
cols, _ = shutil.get_terminal_size(fallback=(length, 1))
|
||||
length = cols - len(styling)
|
||||
filledLength = int(length * iteration // total)
|
||||
bar = fill * filledLength + "-" * (length - filledLength)
|
||||
print(f"\r{color}{styling.replace(fill, bar)}{bcolors.RESET}", end="\r")
|
||||
# Affiche une nouvelle ligne vide
|
||||
if iteration == total:
|
||||
print(f"\n{finish_msg}")
|
||||
PROBLEMS: dict[int, list[str]] = {}
|
||||
CURRENT_ETU: list = []
|
||||
MODULES: list[tuple[int, int]] = []
|
||||
COMPTE: list[int, int] = []
|
||||
|
||||
|
||||
def migrate_abs_to_assiduites(
|
||||
@ -95,6 +51,11 @@ def migrate_abs_to_assiduites(
|
||||
.entry_date: datetime -> timestamp d'entrée de l'abs
|
||||
.etudid: relation -> Identite
|
||||
"""
|
||||
Profiler.clear()
|
||||
|
||||
time_elapsed: Profiler = Profiler("migration")
|
||||
time_elapsed.start()
|
||||
|
||||
if morning is None:
|
||||
pref_time_morning = time(8, 0)
|
||||
else:
|
||||
@ -115,42 +76,77 @@ def migrate_abs_to_assiduites(
|
||||
|
||||
absences_query = Absence.query
|
||||
if dept is not None:
|
||||
depts_id = [dep.id for dep in Departement.query.filter_by(acronym=dept).all()]
|
||||
absences_query = absences_query.filter(Absence.etudid.in_(depts_id))
|
||||
absences: list[Absence] = absences_query.order_by(Absence.jour).all()
|
||||
|
||||
glob.DUPLICATED = []
|
||||
glob.DUPLICATIONS_ASSIDUITES = {}
|
||||
dept: Departement = Departement.query.filter_by(acronym=dept).first()
|
||||
if dept is not None:
|
||||
etuds_id: list[int] = [etud.id for etud in dept.etudiants]
|
||||
absences_query = absences_query.filter(Absence.etudid.in_(etuds_id))
|
||||
absences: Absence = absences_query.order_by(Absence.etudid)
|
||||
|
||||
absences_len: int = len(absences)
|
||||
_glob.DUPLICATED = []
|
||||
_glob.DUPLICATIONS_ASSIDUITES = {}
|
||||
_glob.DUPLICATIONS_JUSTIFICATIFS = {}
|
||||
_glob.PROBLEMS = {}
|
||||
_glob.CURRENT_ETU = []
|
||||
_glob.MODULES = []
|
||||
_glob.COMPTE = [0, 0]
|
||||
|
||||
absences_len: int = absences.count()
|
||||
|
||||
print(
|
||||
f"{ProgressBarColors.BLUE}{absences_len} absences vont être migrées{ProgressBarColors.RESET}"
|
||||
)
|
||||
|
||||
printProgressBar(0, absences_len, "Progression", "effectué", autosize=True)
|
||||
|
||||
for i, abs in enumerate(absences):
|
||||
|
||||
if abs.estabs:
|
||||
generated = _from_abs_to_assiduite(
|
||||
abs, pref_time_morning, pref_time_noon, pref_time_evening
|
||||
)
|
||||
try:
|
||||
if abs.estabs:
|
||||
generated = _from_abs_to_assiduite(
|
||||
abs, pref_time_morning, pref_time_noon, pref_time_evening
|
||||
)
|
||||
if not isinstance(generated, str):
|
||||
db.session.add(generated)
|
||||
_glob.COMPTE[0] += 1
|
||||
except Exception as e:
|
||||
if abs.id not in _glob.PROBLEMS:
|
||||
_glob.PROBLEMS[abs.id] = []
|
||||
_glob.PROBLEMS[abs.id].append(e.args[0])
|
||||
|
||||
if abs.estjust:
|
||||
generated = _from_abs_to_justificatif(
|
||||
abs, pref_time_morning, pref_time_noon, pref_time_evening
|
||||
try:
|
||||
if abs.estjust:
|
||||
generated = _from_abs_to_justificatif(
|
||||
abs, pref_time_morning, pref_time_noon, pref_time_evening
|
||||
)
|
||||
if not isinstance(generated, str):
|
||||
db.session.add(generated)
|
||||
_glob.COMPTE[1] += 1
|
||||
|
||||
except Exception as e:
|
||||
if abs.id not in _glob.PROBLEMS:
|
||||
_glob.PROBLEMS[abs.id] = []
|
||||
_glob.PROBLEMS[abs.id].append(e.args[0])
|
||||
|
||||
if i % 10 == 0:
|
||||
printProgressBar(
|
||||
i,
|
||||
absences_len,
|
||||
"Progression",
|
||||
"effectué",
|
||||
autosize=True,
|
||||
)
|
||||
if not isinstance(generated, str):
|
||||
db.session.add(generated)
|
||||
|
||||
printProgressBar(
|
||||
i,
|
||||
absences_len,
|
||||
"Progression",
|
||||
"effectué",
|
||||
autosize=True,
|
||||
)
|
||||
if i % 1000 == 0:
|
||||
printProgressBar(
|
||||
i,
|
||||
absences_len,
|
||||
"Progression",
|
||||
"effectué",
|
||||
autosize=True,
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
dup_assi = glob.DUPLICATED
|
||||
dup_assi = _glob.DUPLICATED
|
||||
assi: Assiduite
|
||||
for assi in dup_assi:
|
||||
assi.moduleimpl_id = None
|
||||
@ -164,9 +160,28 @@ def migrate_abs_to_assiduites(
|
||||
"Progression",
|
||||
"effectué",
|
||||
autosize=True,
|
||||
finish_msg=f"{bcolors.GREEN}Les absences ont bien été migrées.{bcolors.RESET}",
|
||||
finish_msg=f"{ProgressBarColors.GREEN}Les absences ont bien été migrées.{ProgressBarColors.RESET}",
|
||||
)
|
||||
|
||||
time_elapsed.stop()
|
||||
print(
|
||||
f"{ProgressBarColors.GREEN}La migration a pris {time_elapsed.elapsed():.2f} secondes {ProgressBarColors.RESET}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"{ProgressBarColors.RED}Il y a eu {len(_glob.PROBLEMS)} absences qui n'ont pas pu être migrée."
|
||||
)
|
||||
print(
|
||||
f"Vous retrouverez un fichier json {ProgressBarColors.GREEN}/tmp/scodoc_migration_abs.json{ProgressBarColors.RED} contenant les ids des absences ainsi que les erreurs liées."
|
||||
)
|
||||
with open("/tmp/scodoc_migration_abs.json", "w", encoding="utf-8") as file:
|
||||
dump(_glob.PROBLEMS, file)
|
||||
|
||||
print(
|
||||
f"{ProgressBarColors.CYAN}{_glob.COMPTE[0]} assiduités et {_glob.COMPTE[1]} justificatifs ont été générés.{ProgressBarColors.RESET}"
|
||||
)
|
||||
# afficher nombre justificatifs généré par rapport au nombre de justificatifs
|
||||
|
||||
|
||||
def _from_abs_to_assiduite(
|
||||
_abs: Absence, morning: time, noon: time, evening: time
|
||||
@ -174,6 +189,7 @@ def _from_abs_to_assiduite(
|
||||
etat = EtatAssiduite.ABSENT
|
||||
date_deb: datetime = None
|
||||
date_fin: datetime = None
|
||||
|
||||
if _abs.matin:
|
||||
date_deb = datetime.combine(_abs.jour, morning)
|
||||
date_fin = datetime.combine(_abs.jour, noon)
|
||||
@ -183,37 +199,54 @@ def _from_abs_to_assiduite(
|
||||
|
||||
date_deb = localize_datetime(date_deb)
|
||||
date_fin = localize_datetime(date_fin)
|
||||
duplicata: Assiduite = glob.DUPLICATIONS_ASSIDUITES.get(
|
||||
|
||||
duplicata: Assiduite = _glob.DUPLICATIONS_ASSIDUITES.get(
|
||||
(_abs.jour, _abs.matin, _abs.etudid)
|
||||
)
|
||||
if duplicata is not None:
|
||||
glob.DUPLICATED.append(duplicata)
|
||||
_glob.DUPLICATED.append(duplicata)
|
||||
return "Duplicated"
|
||||
|
||||
desc: str = _abs.description
|
||||
entry_date: datetime = _abs.entry_date
|
||||
|
||||
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
|
||||
moduleimpl: ModuleImpl = ModuleImpl.query.filter_by(id=_abs.moduleimpl_id).first()
|
||||
if _abs.etudid not in _glob.CURRENT_ETU:
|
||||
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
|
||||
if etud is None:
|
||||
return "No Etud"
|
||||
_glob.CURRENT_ETU.append(_abs.etudid)
|
||||
|
||||
retour = Assiduite.create_assiduite(
|
||||
etud=etud,
|
||||
moduleimpl_id: int = _abs.moduleimpl_id
|
||||
|
||||
if (
|
||||
moduleimpl_id is not None
|
||||
and (_abs.etudid, _abs.moduleimpl_id) not in _glob.MODULES
|
||||
):
|
||||
moduleimpl_inscription: ModuleImplInscription = (
|
||||
ModuleImplInscription.query.filter_by(
|
||||
moduleimpl_id=_abs.moduleimpl_id, etudid=_abs.etudid
|
||||
).first()
|
||||
)
|
||||
if moduleimpl_inscription is None:
|
||||
raise Exception("Moduleimpl_id incorrect ou étudiant non inscrit")
|
||||
|
||||
retour = Assiduite.fast_create_assiduite(
|
||||
etudid=_abs.etudid,
|
||||
date_debut=date_deb,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
moduleimpl=moduleimpl,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
description=desc,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
glob.DUPLICATIONS_ASSIDUITES[(_abs.jour, _abs.matin, _abs.etudid)] = retour
|
||||
|
||||
_glob.DUPLICATIONS_ASSIDUITES[(_abs.jour, _abs.matin, _abs.etudid)] = retour
|
||||
return retour
|
||||
|
||||
|
||||
def _from_abs_to_justificatif(
|
||||
_abs: Absence, morning: time, noon: time, evening: time
|
||||
) -> Justificatif:
|
||||
|
||||
etat = EtatJustificatif.VALIDE
|
||||
date_deb: datetime = None
|
||||
date_fin: datetime = None
|
||||
@ -227,13 +260,23 @@ def _from_abs_to_justificatif(
|
||||
date_deb = localize_datetime(date_deb)
|
||||
date_fin = localize_datetime(date_fin)
|
||||
|
||||
duplicata: Justificatif = _glob.DUPLICATIONS_JUSTIFICATIFS.get(
|
||||
(_abs.jour, _abs.matin, _abs.etudid)
|
||||
)
|
||||
if duplicata is not None:
|
||||
return "Duplicated"
|
||||
|
||||
desc: str = _abs.description
|
||||
entry_date: datetime = _abs.entry_date
|
||||
|
||||
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
|
||||
if _abs.etudid not in _glob.CURRENT_ETU:
|
||||
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
|
||||
if etud is None:
|
||||
return "No Etud"
|
||||
_glob.CURRENT_ETU.append(_abs.etudid)
|
||||
|
||||
retour = Justificatif.create_justificatif(
|
||||
etud=etud,
|
||||
retour = Justificatif.fast_create_justificatif(
|
||||
etudid=_abs.etudid,
|
||||
date_debut=date_deb,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
@ -241,4 +284,5 @@ def _from_abs_to_justificatif(
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
_glob.DUPLICATIONS_JUSTIFICATIFS[(_abs.jour, _abs.matin, _abs.etudid)] = retour
|
||||
return retour
|
||||
|
Loading…
Reference in New Issue
Block a user