from datetime import date, datetime, time, timedelta import app.scodoc.sco_utils as scu from app.models.assiduites import Assiduite, Justificatif from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.profiler import Profiler class CountCalculator: def __init__( self, morning: time = time(8, 0), noon: time = time(12, 0), after_noon: time = time(14, 00), evening: time = time(18, 0), skip_saturday: bool = True, ) -> None: self.morning: time = morning self.noon: time = noon self.after_noon: time = after_noon self.evening: time = evening self.skip_saturday: bool = skip_saturday delta_total: timedelta = datetime.combine(date.min, evening) - datetime.combine( date.min, morning ) delta_lunch: timedelta = datetime.combine( date.min, after_noon ) - datetime.combine(date.min, noon) self.hour_per_day: float = (delta_total - delta_lunch).total_seconds() / 3600 self.days: list[date] = [] self.half_days: list[tuple[date, bool]] = [] # tuple -> (date, morning:bool) self.hours: float = 0.0 self.count: int = 0 def reset(self): self.days = [] self.half_days = [] self.hours = 0.0 self.count = 0 def add_half_day(self, day: date, is_morning: bool = True): key: tuple[date, bool] = (day, is_morning) if key not in self.half_days: self.half_days.append(key) def add_day(self, day: date): if day not in self.days: self.days.append(day) def check_in_morning(self, period: tuple[datetime, datetime]) -> bool: interval_morning: tuple[datetime, datetime] = ( scu.localize_datetime(datetime.combine(period[0].date(), self.morning)), scu.localize_datetime(datetime.combine(period[0].date(), self.noon)), ) in_morning: bool = scu.is_period_overlapping( period, interval_morning, bornes=False ) return in_morning def check_in_evening(self, period: tuple[datetime, datetime]) -> bool: interval_evening: tuple[datetime, datetime] = ( scu.localize_datetime(datetime.combine(period[0].date(), self.after_noon)), scu.localize_datetime(datetime.combine(period[0].date(), self.evening)), ) in_evening: bool = scu.is_period_overlapping(period, interval_evening) return in_evening def compute_long_assiduite(self, assi: Assiduite): pointer_date: date = assi.date_debut.date() + timedelta(days=1) start_hours: timedelta = assi.date_debut - scu.localize_datetime( datetime.combine(assi.date_debut, self.morning) ) finish_hours: timedelta = assi.date_fin - scu.localize_datetime( datetime.combine(assi.date_fin, self.morning) ) self.add_day(assi.date_debut.date()) self.add_day(assi.date_fin.date()) start_period: tuple[datetime, datetime] = ( assi.date_debut, scu.localize_datetime( datetime.combine(assi.date_debut.date(), self.evening) ), ) finish_period: tuple[datetime, datetime] = ( scu.localize_datetime(datetime.combine(assi.date_fin.date(), self.morning)), assi.date_fin, ) hours = 0.0 for period in (start_period, finish_period): if self.check_in_evening(period): self.add_half_day(period[0].date(), False) if self.check_in_morning(period): self.add_half_day(period[0].date()) while pointer_date < assi.date_fin.date(): if pointer_date.weekday() < (6 - self.skip_saturday): self.add_day(pointer_date) self.add_half_day(pointer_date) self.add_half_day(pointer_date, False) self.hours += self.hour_per_day hours += self.hour_per_day pointer_date += timedelta(days=1) self.hours += finish_hours.total_seconds() / 3600 self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600) def compute_assiduites(self, assiduites: Assiduite): assi: Assiduite assiduites: list[Assiduite] = ( assiduites.all() if isinstance(assiduites, Assiduite) else assiduites ) for assi in assiduites: self.count += 1 delta: timedelta = assi.date_fin - assi.date_debut if delta.days > 0: # raise Exception(self.hours) self.compute_long_assiduite(assi) continue period: tuple[datetime, datetime] = (assi.date_debut, assi.date_fin) deb_date: date = assi.date_debut.date() if self.check_in_morning(period): self.add_half_day(deb_date) if self.check_in_evening(period): self.add_half_day(deb_date, False) self.add_day(deb_date) self.hours += delta.total_seconds() / 3600 def to_dict(self) -> dict[str, object]: return { "compte": self.count, "journee": len(self.days), "demi": len(self.half_days), "heure": round(self.hours, 2), } def get_assiduites_stats( assiduites: Assiduite, metric: str = "all", filtered: dict[str, object] = None ) -> Assiduite: if filtered is not None: deb, fin = None, None for key in filtered: if key == "etat": assiduites = filter_assiduites_by_etat(assiduites, filtered[key]) elif key == "date_fin": fin = filtered[key] elif key == "date_debut": deb = filtered[key] elif key == "moduleimpl_id": assiduites = filter_by_module_impl(assiduites, filtered[key]) elif key == "formsemestre": assiduites = filter_by_formsemestre(assiduites, filtered[key]) if (deb, fin) != (None, None): assiduites = filter_by_date(assiduites, Assiduite, deb, fin) calculator: CountCalculator = CountCalculator() calculator.compute_assiduites(assiduites) count: dict = calculator.to_dict() metrics: list[str] = metric.split(",") output: dict = {} for key, val in count.items(): if key in metrics: output[key] = val return output if output else count def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite: """ Filtrage d'une collection d'assiduites en fonction de leur état """ etats: list[str] = list(etat.split(",")) etats = [scu.EtatAssiduite.get(e, -1) for e in etats] return assiduites.filter(Assiduite.etat.in_(etats)) def filter_by_date( collection: Assiduite or Justificatif, collection_cls: Assiduite or Justificatif, date_deb: datetime = None, date_fin: datetime = None, strict: bool = False, ): """ Filtrage d'une collection d'assiduites en fonction d'une date """ if date_deb is None: date_deb = datetime.min if date_fin is None: date_fin = datetime.max date_deb = scu.localize_datetime(date_deb) date_fin = scu.localize_datetime(date_fin) if not strict: return collection.filter( collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb ) return collection.filter( collection_cls.date_debut < date_fin, collection_cls.date_fin > date_deb ) def filter_justificatifs_by_etat( justificatifs: Justificatif, etat: str ) -> Justificatif: """ Filtrage d'une collection de justificatifs en fonction de leur état """ etats: list[str] = list(etat.split(",")) etats = [scu.EtatJustificatif.get(e, -1) for e in etats] return justificatifs.filter(Justificatif.etat.in_(etats)) def filter_by_module_impl( assiduites: Assiduite, module_impl_id: int or None ) -> Assiduite: """ Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl """ return assiduites.filter(Assiduite.moduleimpl_id == module_impl_id) def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemestre): """ Filtrage d'une collection d'assiduites en fonction d'un formsemestre """ if formsemestre is None: return assiduites_query.filter(False) assiduites_query = ( assiduites_query.join(Identite, Assiduite.etudid == Identite.id) .join( FormSemestreInscription, Identite.id == FormSemestreInscription.etudid, ) .filter(FormSemestreInscription.formsemestre_id == formsemestre.id) ) assiduites_query = assiduites_query.filter( Assiduite.date_debut >= formsemestre.date_debut ) return assiduites_query.filter(Assiduite.date_fin <= formsemestre.date_fin) def justifies(justi: Justificatif, obj: bool = False) -> list[int]: """ Retourne la liste des assiduite_id qui sont justifié par la justification Une assiduité est justifiée si elle est STRICTEMENT comprise dans la plage du justificatif et que l'état du justificatif est "validé" renvoie des id si obj == False, sinon les Assiduités """ if justi.etat != scu.EtatJustificatif.VALIDE: return [] assiduites_query: Assiduite = ( Assiduite.query.join(Justificatif, Assiduite.etudid == Justificatif.etudid) .filter(Assiduite.etat != scu.EtatAssiduite.PRESENT) .filter( Assiduite.date_debut >= justi.date_debut, Assiduite.date_debut <= justi.date_fin, Assiduite.date_fin >= justi.date_debut, Assiduite.date_fin <= justi.date_fin, ) ) if not obj: return [assi.id for assi in assiduites_query.all()] return assiduites_query def get_all_justified( justificatifs: Justificatif, date_deb: datetime = None, date_fin: datetime = None ) -> list[Assiduite]: if date_deb is None: date_deb = datetime.min if date_fin is None: date_fin = datetime.max date_deb = scu.localize_datetime(date_deb) date_fin = scu.localize_datetime(date_fin) assiduites: list[Assiduite] = [] for justi in justificatifs: assis: list[Assiduite] = justifies(justi, obj=True) assiduites.extend(assis) return list(assiduites)