2023-04-17 15:43:58 +02:00
"""
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_
2023-07-19 10:37:56 +02:00
from flask import g
2023-04-17 15:43:58 +02:00
from app import db
from app . models import (
Absence ,
Assiduite ,
Departement ,
Identite ,
Justificatif ,
ModuleImplInscription ,
)
2023-04-25 22:59:06 +02:00
from app . models . config import ScoDocSiteConfig
2023-04-17 15:43:58 +02:00
from app . profiler import Profiler
from app . scodoc . sco_utils import (
EtatAssiduite ,
EtatJustificatif ,
TerminalColor ,
localize_datetime ,
print_progress_bar ,
)
2023-07-19 10:37:56 +02:00
from app . scodoc import notesdb as ndb
2023-04-17 15:43:58 +02:00
class _glob :
""" variables globales du script """
DEBUG : bool = False
PROBLEMS : dict [ int , list [ str ] ] = { }
2023-07-18 08:35:18 +02:00
DEPT_ETUDIDS : dict [ int , Identite ] = { }
2023-04-17 15:43:58 +02:00
COMPTE : list [ int , int ] = [ ]
ERR_ETU : list [ int ] = [ ]
2023-07-18 08:35:18 +02:00
MERGER_ASSI : " _Merger " = None
MERGER_JUST : " _Merger " = None
2023-04-17 15:43:58 +02:00
2023-07-21 19:21:29 +02:00
JUSTIFS : dict [ int , list [ tuple [ datetime , datetime ] ] ] = { }
2023-04-17 15:43:58 +02:00
MORNING : time = None
NOON : time = None
2023-08-22 16:18:58 +02:00
AFTERNOON : time = None
2023-04-17 15:43:58 +02:00
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
2023-07-21 19:21:29 +02:00
self . est_just = abs_ . estjust
2023-04-17 15:43:58 +02:00
def merge ( self , abs_ : Absence ) - > bool :
2023-07-18 08:35:18 +02:00
""" Fusionne les absences.
Return False si pas de fusion .
"""
2023-04-17 15:43:58 +02:00
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 ]
2023-07-21 19:21:29 +02:00
if not ( day_after and abs_ . matin and self . est_just == abs_ . estjust ) :
2023-04-17 15:43:58 +02:00
return False
2023-07-21 19:21:29 +02:00
self . est_just = self . est_just or abs_ . estjust
2023-04-17 15:43:58 +02:00
self . fin = ( abs_ . jour , abs_ . matin )
2023-07-21 19:21:29 +02:00
2023-04-17 15:43:58 +02:00
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 :
2023-08-22 16:18:58 +02:00
time_ = _glob . EVENING if end else _glob . AFTERNOON
2023-04-17 15:43:58 +02:00
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 )
2023-07-21 19:21:29 +02:00
_glob . JUSTIFS [ self . etudid ] . append ( ( date_deb , date_fin ) )
2023-07-19 10:37:56 +02:00
_glob . cursor . execute (
""" INSERT INTO justificatifs
( etudid , date_debut , date_fin , etat , raison , entry_date )
VALUES ( % ( etudid ) s , % ( date_debut ) s , % ( date_fin ) s , % ( etat ) s , % ( raison ) s , % ( entry_date ) s )
""" ,
{
" etudid " : self . etudid ,
" date_debut " : date_deb ,
" date_fin " : date_fin ,
" etat " : EtatJustificatif . VALIDE ,
" raison " : self . raison ,
" entry_date " : self . entry_date ,
} ,
2023-04-17 15:43:58 +02:00
)
def _to_assi ( self ) :
date_deb = _Merger . _tuple_to_date ( self . deb )
date_fin = _Merger . _tuple_to_date ( self . fin , end = True )
2023-07-21 19:21:29 +02:00
self . est_just = (
_assi_in_justifs ( date_deb , date_fin , self . etudid ) or self . est_just
)
if _glob . MERGER_JUST is not None and not self . est_just :
justi_date_deb = _Merger . _tuple_to_date ( _glob . MERGER_JUST . deb )
justi_date_fin = _Merger . _tuple_to_date ( _glob . MERGER_JUST . fin , end = True )
justifiee = date_deb > = justi_date_deb and date_fin < = justi_date_fin
self . est_just = justifiee
2023-07-19 10:37:56 +02:00
_glob . cursor . execute (
""" INSERT INTO assiduites
2023-07-21 19:21:29 +02:00
( etudid , date_debut , date_fin , etat , moduleimpl_id , description , entry_date , est_just )
VALUES ( % ( etudid ) s , % ( date_debut ) s , % ( date_fin ) s , % ( etat ) s , % ( moduleimpl_id ) s , % ( description ) s , % ( entry_date ) s , % ( est_just ) s )
2023-07-19 10:37:56 +02:00
""" ,
{
" etudid " : self . etudid ,
" date_debut " : date_deb ,
" date_fin " : date_fin ,
" etat " : EtatAssiduite . ABSENT ,
" moduleimpl_id " : self . moduleimpl ,
2023-07-20 13:10:26 +02:00
" description " : self . raison ,
2023-07-19 10:37:56 +02:00
" entry_date " : self . entry_date ,
2023-07-21 19:21:29 +02:00
" est_just " : self . est_just ,
2023-07-19 10:37:56 +02:00
} ,
2023-04-17 15:43:58 +02:00
)
2023-07-19 10:37:56 +02:00
2023-04-17 15:43:58 +02:00
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
2023-07-19 10:37:56 +02:00
self . _to_assi ( )
2023-04-17 15:43:58 +02:00
else :
_glob . COMPTE [ 1 ] + = 1
2023-07-19 10:37:56 +02:00
self . _to_justif ( )
2023-04-17 15:43:58 +02:00
2023-07-21 19:21:29 +02:00
def _assi_in_justifs ( deb , fin , etudid ) :
return any ( deb > = j [ 0 ] and fin < = j [ 1 ] for j in _glob . JUSTIFS [ etudid ] )
2023-04-17 15:43:58 +02:00
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 ,
2023-08-22 16:18:58 +02:00
afternoon : str = None ,
2023-04-17 15:43:58 +02:00
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
2023-07-18 08:35:18 +02:00
description : str - > motif abs / raison justif
2023-04-17 15:43:58 +02:00
. entry_date : datetime - > timestamp d ' entrée de l ' abs
. etudid : relation - > Identite
"""
Profiler . clear ( )
_glob . DEBUG = debug
if morning is None :
2023-06-28 17:15:24 +02:00
morning = ScoDocSiteConfig . get ( " assi_morning_time " , time ( 8 , 0 ) )
2023-07-04 15:04:58 +02:00
morning : list [ str ] = str ( morning ) . split ( " : " )
2023-06-28 17:15:24 +02:00
_glob . MORNING = time ( int ( morning [ 0 ] ) , int ( morning [ 1 ] ) )
2023-04-17 15:43:58 +02:00
if noon is None :
2023-06-28 17:15:24 +02:00
noon = ScoDocSiteConfig . get ( " assi_lunch_time " , time ( 13 , 0 ) )
2023-07-04 15:04:58 +02:00
noon : list [ str ] = str ( noon ) . split ( " : " )
2023-06-28 17:15:24 +02:00
_glob . NOON = time ( int ( noon [ 0 ] ) , int ( noon [ 1 ] ) )
2023-04-17 15:43:58 +02:00
2023-08-22 16:18:58 +02:00
if afternoon is None :
afternoon = ScoDocSiteConfig . get ( " assi_lunch_time " , time ( 13 , 0 ) )
afternoon : list [ str ] = str ( afternoon ) . split ( " : " )
_glob . AFTERNOON = time ( int ( afternoon [ 0 ] ) , int ( afternoon [ 1 ] ) )
2023-04-17 15:43:58 +02:00
if evening is None :
2023-06-28 17:15:24 +02:00
evening = ScoDocSiteConfig . get ( " assi_afternoon_time " , time ( 18 , 0 ) )
2023-07-04 15:04:58 +02:00
evening : list [ str ] = str ( evening ) . split ( " : " )
2023-06-28 17:15:24 +02:00
_glob . EVENING = time ( int ( evening [ 0 ] ) , int ( evening [ 1 ] ) )
2023-04-17 15:43:58 +02:00
2023-07-19 10:37:56 +02:00
ndb . open_db_connection ( )
_glob . cnx = g . db_conn
_glob . cursor = _glob . cnx . cursor ( )
2023-04-17 15:43:58 +02:00
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 :
2023-07-18 08:35:18 +02:00
raise ValueError ( f " Département inexistant: { dept_name } " )
2023-04-17 15:43:58 +02:00
etuds_id : list [ int ] = [ etud . id for etud in dept . etudiants ]
2023-07-21 19:21:29 +02:00
for etudid in etuds_id :
_glob . JUSTIFS [ etudid ] = [ ]
2023-04-17 15:43:58 +02:00
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
2023-07-18 08:35:18 +02:00
_glob . DEPT_ETUDIDS = { e . id for e in Identite . query . filter_by ( dept_id = dept . id ) }
2023-04-17 15:43:58 +02:00
_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 )
2023-07-19 10:37:56 +02:00
etuds_modimpl_ids = { }
2023-04-17 15:43:58 +02:00
for i , abs_ in enumerate ( absences ) :
2023-07-19 10:37:56 +02:00
etud_modimpl_ids = etuds_modimpl_ids . get ( abs_ . etudid )
if etud_modimpl_ids is None :
etud_modimpl_ids = {
ins . moduleimpl_id
for ins in ModuleImplInscription . query . filter_by ( etudid = abs_ . etudid )
}
etuds_modimpl_ids [ abs_ . etudid ] = etud_modimpl_ids
2023-04-17 15:43:58 +02:00
try :
2023-07-19 10:37:56 +02:00
_from_abs_to_assiduite_justificatif ( abs_ , etud_modimpl_ids )
2023-04-17 15:43:58 +02:00
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 ,
)
2023-07-19 10:37:56 +02:00
_glob . cnx . commit ( )
2023-04-17 15:43:58 +02:00
2023-07-18 08:35:18 +02:00
if _glob . MERGER_ASSI is not None :
_glob . MERGER_ASSI . export ( )
if _glob . MERGER_JUST is not None :
_glob . MERGER_JUST . export ( )
2023-04-17 15:43:58 +02:00
2023-07-19 10:37:56 +02:00
_glob . cnx . commit ( )
2023-04-17 15:43:58 +02:00
print_progress_bar (
absences_len ,
absences_len ,
" Progression " ,
" effectué " ,
autosize = True ,
)
2023-07-21 19:21:29 +02:00
# print(
# TerminalColor.RED
# + f"Justification des absences du département {dept_name}, veuillez patienter, ceci peut prendre un certain temps."
# + TerminalColor.RESET
# )
2023-04-17 15:43:58 +02:00
2023-07-21 19:21:29 +02:00
# justifs: Justificatif = Justificatif.query.join(Identite).filter_by(dept_id=dept.id)
# compute_assiduites_justified(justifs, reset=True)
2023-04-17 15:43:58 +02:00
time_elapsed . stop ( )
statistiques : dict = stats . compute_stats ( )
print (
f " { TerminalColor . GREEN } La migration a pris { time_elapsed . elapsed ( ) : .2f } secondes { TerminalColor . RESET } "
)
2023-07-10 18:30:25 +02:00
filename = f " /opt/scodoc-data/log/ { datetime . now ( ) . strftime ( ' % Y- % m- %d T % H: % M: % S ' ) } scodoc_migration_abs_ { dept_name } .json "
2023-07-18 08:35:18 +02:00
if statistiques [ " total " ] > 0 :
print (
f " { TerminalColor . RED } { statistiques [ ' total ' ] } absences qui n ' ont pas pu être migrées. "
)
print (
f " Vous retrouverez un fichier json { TerminalColor . GREEN } { filename } { TerminalColor . RED } contenant les problèmes de migrations "
)
2023-04-17 15:43:58 +02:00
with open (
2023-07-10 18:30:25 +02:00
filename ,
2023-04-17 15:43:58 +02:00
" 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 ) )
2023-07-19 10:37:56 +02:00
def _from_abs_to_assiduite_justificatif ( _abs : Absence , etud_modimpl_ids : set [ int ] ) :
2023-07-18 08:35:18 +02:00
if _abs . etudid not in _glob . DEPT_ETUDIDS :
raise ValueError ( " Etudiant inexistant " )
2023-04-17 15:43:58 +02:00
if _abs . estabs :
2023-07-19 10:37:56 +02:00
if ( _abs . moduleimpl_id is not None ) and (
_abs . moduleimpl_id not in etud_modimpl_ids
2023-04-17 15:43:58 +02:00
) :
2023-07-19 10:37:56 +02:00
raise ValueError ( " Moduleimpl_id incorrect ou étudiant non inscrit " )
2023-04-17 15:43:58 +02:00
if _glob . MERGER_ASSI is None :
_glob . MERGER_ASSI = _Merger ( _abs , True )
elif _glob . MERGER_ASSI . merge ( _abs ) :
2023-07-21 19:21:29 +02:00
pass
2023-04-17 15:43:58 +02:00
else :
_glob . MERGER_ASSI . export ( )
_glob . MERGER_ASSI = _Merger ( _abs , True )
2023-07-21 19:21:29 +02:00
if _abs . estjust :
if _glob . MERGER_JUST is None :
_glob . MERGER_JUST = _Merger ( _abs , False )
elif _glob . MERGER_JUST . merge ( _abs ) :
pass
else :
_glob . MERGER_JUST . export ( )
_glob . MERGER_JUST = _Merger ( _abs , False )