Skip to content
Snippets Groups Projects
Commit 94347657 authored by iziram's avatar iziram
Browse files

Assiduites : script de migration et de suppression

parent e748973a
No related branches found
No related tags found
No related merge requests found
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("")
...@@ -642,3 +642,63 @@ def profile(host, port, length, profile_dir): ...@@ -642,3 +642,63 @@ def profile(host, port, length, profile_dir):
run_simple( run_simple(
host, port, app, use_debugger=False host, port, app, use_debugger=False
) # use run_simple instead of app.run() ) # use run_simple instead of app.run()
# <== Gestion de l'assiduité ==>
@app.cli.command()
@click.option(
"-d", "--dept", help="Restreint la migration au dept sélectionné (ACRONYME)"
)
@click.option(
"-m",
"--morning",
help="Spécifie l'heure de début des cours format `hh:mm`",
default="08h00",
show_default=True,
)
@click.option(
"-n",
"--noon",
help="Spécifie l'heure de fin du matin (et donc début de l'après-midi) format `hh:mm`",
default="12h00",
show_default=True,
)
@click.option(
"-e",
"--evening",
help="Spécifie l'heure de fin des cours format `hh:mm`",
default="18h00",
show_default=True,
)
@with_appcontext
def migrate_abs_to_assiduites(
dept: str = None, morning: str = None, noon: str = None, evening: str = None
): # migrate-abs-to-assiduites
"""Permet de migrer les absences vers le nouveau module d'assiduités"""
tools.migrate_abs_to_assiduites(dept, morning, noon, evening)
@app.cli.command()
@click.option(
"-d", "--dept", help="Restreint la suppression au dept sélectionné (ACRONYME)"
)
@click.option(
"-a",
"--assiduites",
is_flag=True,
help="Supprime les assiduités de scodoc",
)
@click.option(
"-j",
"--justificatifs",
is_flag=True,
help="Supprime les justificatifs de scodoc",
)
@with_appcontext
def downgrade_assiduites_module(
dept: str = None, assiduites: bool = False, justificatifs: bool = False
):
"""Supprime les assiduites et/ou les justificatifs de tous les départements ou du département sélectionné"""
tools.downgrade_module(dept, assiduites, justificatifs)
...@@ -8,3 +8,5 @@ from tools.import_scodoc7_user_db import import_scodoc7_user_db ...@@ -8,3 +8,5 @@ from tools.import_scodoc7_user_db import import_scodoc7_user_db
from tools.import_scodoc7_dept import import_scodoc7_dept from tools.import_scodoc7_dept import import_scodoc7_dept
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives
from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos
from tools.migrate_abs_to_assiduites import migrate_abs_to_assiduites
from tools.downgrade_assiduites import downgrade_module
"""
Commande permettant de supprimer les assiduités et les justificatifs
Ecrit par Matthias HARTMANN
"""
from app import db
from app.models import Justificatif, Assiduite, Departement
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
from app.scodoc.sco_utils import TerminalColor
def downgrade_module(
dept: str = None, assiduites: bool = False, justificatifs: bool = False
):
"""
Supprime les assiduités et/ou justificatifs du dept sélectionné ou de tous les départements
Args:
dept (str, optional): l'acronym du département. Par défaut tous les départements.
assiduites (bool, optional): suppression des assiduités. Par défaut : Non
justificatifs (bool, optional): supression des justificatifs. Par défaut : Non
"""
dept_etudid: list[int] = None
dept_id: int = None
if dept is not None:
departement: Departement = Departement.query.filter_by(acronym=dept).first()
assert departement is not None, "Le département n'existe pas."
dept_etudid = [etud.id for etud in departement.etudiants]
dept_id = departement.id
if assiduites:
_remove_assiduites(dept_etudid)
if justificatifs:
_remove_justificatifs(dept_etudid)
_remove_justificatifs_archive(dept_id)
if dept is None:
if assiduites:
db.session.execute("ALTER SEQUENCE assiduites_id_seq RESTART WITH 1")
if justificatifs:
db.session.execute("ALTER SEQUENCE justificatifs_id_seq RESTART WITH 1")
db.session.commit()
print(
f"{TerminalColor.GREEN}Le module assiduité a bien été remis à zero.{TerminalColor.RESET}"
)
def _remove_assiduites(dept_etudid: str = None):
if dept_etudid is None:
Assiduite.query.delete()
else:
Assiduite.query.filter(Assiduite.etudid.in_(dept_etudid)).delete()
def _remove_justificatifs(dept_etudid: str = None):
if dept_etudid is None:
Justificatif.query.delete()
else:
Justificatif.query.filter(Justificatif.etudid.in_(dept_etudid)).delete()
def _remove_justificatifs_archive(dept_id: int = None):
JustificatifArchiver().remove_dept_archive(dept_id)
"""
Script de migration des données de la base "absences" -> "assiduites"/"justificatifs"
Ecrit par Matthias HARTMANN
"""
from datetime import date, datetime, time, timedelta
from json import dump, dumps
from sqlalchemy import not_
from app import db
from app.models import (
Absence,
Assiduite,
Departement,
Identite,
Justificatif,
ModuleImplInscription,
)
from app.models.assiduites import (
compute_assiduites_justified,
)
from app.profiler import Profiler
from app.scodoc.sco_utils import (
EtatAssiduite,
EtatJustificatif,
TerminalColor,
localize_datetime,
print_progress_bar,
)
class _Merger:
"""pour typage"""
class _glob:
"""variables globales du script"""
DEBUG: bool = False
PROBLEMS: dict[int, list[str]] = {}
CURRENT_ETU: list = []
MODULES: list[tuple[int, int]] = []
COMPTE: list[int, int] = []
ERR_ETU: list[int] = []
MERGER_ASSI: _Merger = None
MERGER_JUST: _Merger = None
MORNING: time = None
NOON: time = None
EVENING: time = None
class _Merger:
def __init__(self, abs_: Absence, est_abs: bool) -> None:
self.deb = (abs_.jour, abs_.matin)
self.fin = (abs_.jour, abs_.matin)
self.moduleimpl = abs_.moduleimpl_id
self.etudid = abs_.etudid
self.est_abs = est_abs
self.raison = abs_.description
self.entry_date = abs_.entry_date
def merge(self, abs_: Absence) -> bool:
"""Fusionne les absences"""
if self.etudid != abs_.etudid:
return False
# Cas d'une même absence enregistrée plusieurs fois
if self.fin == (abs_.jour, abs_.matin):
self.moduleimpl = None
else:
if self.fin[1]:
if abs_.jour != self.fin[0]:
return False
else:
day_after: date = abs_.jour - timedelta(days=1) == self.fin[0]
if not (day_after and abs_.matin):
return False
self.fin = (abs_.jour, abs_.matin)
return True
@staticmethod
def _tuple_to_date(couple: tuple[date, bool], end=False):
if couple[1]:
time_ = _glob.NOON if end else _glob.MORNING
date_ = datetime.combine(couple[0], time_)
else:
time_ = _glob.EVENING if end else _glob.NOON
date_ = datetime.combine(couple[0], time_)
d = localize_datetime(date_)
return d
def _to_justif(self):
date_deb = _Merger._tuple_to_date(self.deb)
date_fin = _Merger._tuple_to_date(self.fin, end=True)
retour = Justificatif.fast_create_justificatif(
etudid=self.etudid,
date_debut=date_deb,
date_fin=date_fin,
etat=EtatJustificatif.VALIDE,
raison=self.raison,
entry_date=self.entry_date,
)
return retour
def _to_assi(self):
date_deb = _Merger._tuple_to_date(self.deb)
date_fin = _Merger._tuple_to_date(self.fin, end=True)
retour = Assiduite.fast_create_assiduite(
etudid=self.etudid,
date_debut=date_deb,
date_fin=date_fin,
etat=EtatAssiduite.ABSENT,
moduleimpl_id=self.moduleimpl,
description=self.raison,
entry_date=self.entry_date,
)
return retour
def export(self):
"""Génère un nouvel objet Assiduité ou Justificatif"""
obj: Assiduite or Justificatif = None
if self.est_abs:
_glob.COMPTE[0] += 1
obj = self._to_assi()
else:
_glob.COMPTE[1] += 1
obj = self._to_justif()
db.session.add(obj)
class _Statistics:
def __init__(self) -> None:
self.object: dict[str, dict or int] = {"total": 0}
self.year: int = None
def __set_year(self, year: int):
if year not in self.object:
self.object[year] = {
"etuds_inexistant": [],
"abs_invalide": {},
}
self.year = year
return self
def __add_etud(self, etudid: int):
if etudid not in self.object[self.year]["etuds_inexistant"]:
self.object[self.year]["etuds_inexistant"].append(etudid)
return self
def __add_abs(self, abs_: int, err: str):
if abs_ not in self.object[self.year]["abs_invalide"]:
self.object[self.year]["abs_invalide"][abs_] = [err]
else:
self.object[self.year]["abs_invalide"][abs_].append(err)
return self
def add_problem(self, abs_: Absence, err: str):
"""Ajoute un nouveau problème dans les statistiques"""
abs_.jour: date
pivot: date = date(abs_.jour.year, 9, 15)
year: int = abs_.jour.year
if pivot < abs_.jour:
year += 1
self.__set_year(year)
if err == "Etudiant inexistant":
self.__add_etud(abs_.etudid)
else:
self.__add_abs(abs_.id, err)
self.object["total"] += 1
def compute_stats(self) -> dict:
"""Comptage des statistiques"""
stats: dict = {"total": self.object["total"]}
for year, item in self.object.items():
if year == "total":
continue
stats[year] = {}
stats[year]["etuds_inexistant"] = len(item["etuds_inexistant"])
stats[year]["abs_invalide"] = len(item["abs_invalide"])
return stats
def export(self, file):
"""Sérialise les statistiques dans un fichier"""
dump(self.object, file, indent=2)
def migrate_abs_to_assiduites(
dept: str = None,
morning: str = None,
noon: str = None,
evening: str = None,
debug: bool = False,
):
"""
une absence à 3 états:
|.estabs|.estjust|
|1|0| -> absence non justifiée
|1|1| -> absence justifiée
|0|1| -> justifié
dualité des temps :
.matin: bool (0:00 -> time_pref | time_pref->23:59:59)
.jour : date (jour de l'absence/justificatif)
.moduleimpl_id: relation -> moduleimpl_id
description:str -> motif abs / raision justif
.entry_date: datetime -> timestamp d'entrée de l'abs
.etudid: relation -> Identite
"""
Profiler.clear()
_glob.DEBUG = debug
if morning is None:
_glob.MORNING = time(8, 0)
else:
morning: list[str] = morning.split("h")
_glob.MORNING = time(int(morning[0]), int(morning[1]))
if noon is None:
_glob.NOON = time(12, 0)
else:
noon: list[str] = noon.split("h")
_glob.NOON = time(int(noon[0]), int(noon[1]))
if evening is None:
_glob.EVENING = time(18, 0)
else:
evening: list[str] = evening.split("h")
_glob.EVENING = time(int(evening[0]), int(evening[1]))
if dept is None:
prof_total = Profiler("MigrationTotal")
prof_total.start()
depart: Departement
for depart in Departement.query.order_by(Departement.id):
migrate_dept(
depart.acronym, _Statistics(), Profiler(f"Migration_{depart.acronym}")
)
prof_total.stop()
print(
TerminalColor.GREEN
+ f"Fin de la migration, elle a durée {prof_total.elapsed():.2f}"
+ TerminalColor.RESET
)
else:
migrate_dept(dept, _Statistics(), Profiler("Migration"))
def migrate_dept(dept_name: str, stats: _Statistics, time_elapsed: Profiler):
time_elapsed.start()
absences_query = Absence.query
dept: Departement = Departement.query.filter_by(acronym=dept_name).first()
if dept is None:
return
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, Absence.jour, not_(Absence.matin)
)
absences_len: int = absences.count()
if absences_len == 0:
print(
f"{TerminalColor.BLUE}Le département {dept_name} ne possède aucune absence.{TerminalColor.RESET}"
)
return
_glob.CURRENT_ETU = []
_glob.MODULES = []
_glob.COMPTE = [0, 0]
_glob.ERR_ETU = []
_glob.MERGER_ASSI = None
_glob.MERGER_JUST = None
print(
f"{TerminalColor.BLUE}{absences_len} absences du département {dept_name} vont être migrées{TerminalColor.RESET}"
)
print_progress_bar(0, absences_len, "Progression", "effectué", autosize=True)
for i, abs_ in enumerate(absences):
try:
_from_abs_to_assiduite_justificatif(abs_)
except ValueError as e:
stats.add_problem(abs_, e.args[0])
if i % 10 == 0:
print_progress_bar(
i,
absences_len,
"Progression",
"effectué",
autosize=True,
)
if i % 1000 == 0:
print_progress_bar(
i,
absences_len,
"Progression",
"effectué",
autosize=True,
)
db.session.commit()
_glob.MERGER_ASSI.export()
_glob.MERGER_JUST.export()
db.session.commit()
justifs: Justificatif = Justificatif.query
if dept_name is not None:
justifs.filter(Justificatif.etudid.in_(etuds_id))
print_progress_bar(
absences_len,
absences_len,
"Progression",
"effectué",
autosize=True,
)
print(
TerminalColor.RED
+ f"Justification des absences du département {dept_name}, veuillez patienter, ceci peut prendre un certain temps."
+ TerminalColor.RESET
)
compute_assiduites_justified(justifs, reset=True)
time_elapsed.stop()
statistiques: dict = stats.compute_stats()
print(
f"{TerminalColor.GREEN}La migration a pris {time_elapsed.elapsed():.2f} secondes {TerminalColor.RESET}"
)
print(
f"{TerminalColor.RED}{statistiques['total']} absences qui n'ont pas pu être migrées."
)
print(
f"Vous retrouverez un fichier json {TerminalColor.GREEN}/opt/scodoc-data/log/scodoc_migration_abs_{dept_name}.json{TerminalColor.RED} contenant les problèmes de migrations"
)
with open(
f"/opt/scodoc-data/log/scodoc_migration_abs_{dept_name}.json",
"w",
encoding="utf-8",
) as file:
stats.export(file)
print(
f"{TerminalColor.CYAN}{_glob.COMPTE[0]} assiduités et {_glob.COMPTE[1]} justificatifs ont été générés pour le département {dept_name}.{TerminalColor.RESET}"
)
if _glob.DEBUG:
print(dumps(statistiques, indent=2))
def _from_abs_to_assiduite_justificatif(_abs: Absence):
if _abs.etudid not in _glob.CURRENT_ETU:
etud: Identite = Identite.query.filter_by(id=_abs.etudid).first()
if etud is None:
raise ValueError("Etudiant inexistant")
_glob.CURRENT_ETU.append(_abs.etudid)
if _abs.estabs:
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 ValueError("Moduleimpl_id incorrect ou étudiant non inscrit")
if _glob.MERGER_ASSI is None:
_glob.MERGER_ASSI = _Merger(_abs, True)
return True
elif _glob.MERGER_ASSI.merge(_abs):
return True
else:
_glob.MERGER_ASSI.export()
_glob.MERGER_ASSI = _Merger(_abs, True)
return False
if _glob.MERGER_JUST is None:
_glob.MERGER_JUST = _Merger(_abs, False)
return True
elif _glob.MERGER_JUST.merge(_abs):
return True
else:
_glob.MERGER_JUST.export()
_glob.MERGER_JUST = _Merger(_abs, False)
return False
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment