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 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 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) 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 for assi in assiduites.all(): 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 big_counter( # interval: tuple[datetime], # pref_time: time = time(12, 0), # ): # curr_date: datetime # if interval[0].time() >= pref_time: # curr_date = scu.localize_datetime( # datetime.combine(interval[0].date(), pref_time) # ) # else: # curr_date = scu.localize_datetime( # datetime.combine(interval[0].date(), time(0, 0)) # ) # def next_(curr: datetime, journee): # if curr.time() != pref_time: # next_time = scu.localize_datetime(datetime.combine(curr.date(), pref_time)) # else: # next_time = scu.localize_datetime( # datetime.combine(curr.date() + timedelta(days=1), time(0, 0)) # ) # journee += 1 # return next_time, journee # demi: int = 0 # j: int = 0 # while curr_date <= interval[1]: # next_time: datetime # next_time, j = next_(curr_date, j) # if scu.is_period_overlapping((curr_date, next_time), interval, True): # demi += 1 # curr_date = next_time # delta: timedelta = interval[1] - interval[0] # heures: float = delta.total_seconds() / 3600 # if delta.days >= 1: # heures -= delta.days * 16 # return (demi, j, heures) # def get_count( # assiduites: Assiduite, noon: time = time(hour=12) # ) -> dict[str, int or float]: # """Fonction permettant de compter les assiduites # -> seul "compte" est correcte lorsque les assiduites viennent de plusieurs étudiants # """ # # TODO: Comptage demi journée / journée d'assiduité longue # output: dict[str, int or float] = {} # compte: int = assiduites.count() # heure: float = 0.0 # journee: int = 0 # demi: int = 0 # all_assiduites: list[Assiduite] = assiduites.order_by(Assiduite.date_debut).all() # current_day: date = None # current_time: str = None # midnight: time = time(hour=0) # def time_check(dtime): # return midnight <= dtime.time() <= noon # for ass in all_assiduites: # delta: timedelta = ass.date_fin - ass.date_debut # if delta.days > 0: # computed_values: tuple[int, int, float] = big_counter( # (ass.date_debut, ass.date_fin), noon # ) # demi += computed_values[0] - 1 # journee += computed_values[1] - 1 # heure += computed_values[2] # current_day = ass.date_fin.date() # continue # heure += delta.total_seconds() / 3600 # ass_time: str = time_check(ass.date_debut) # if current_day != ass.date_debut.date(): # current_day = ass.date_debut.date() # current_time = ass_time # demi += 1 # journee += 1 # if current_time != ass_time: # current_time = ass_time # demi += 1 # heure = round(heure, 2) # return {"compte": compte, "journee": journee, "heure": heure, "demi": demi} 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_justificatifs_by_date( justificatifs: Justificatif, date_: datetime, sup: bool = True ) -> Assiduite: """ Filtrage d'une collection d'assiduites en fonction d'une date Sup == True -> les assiduites doivent débuter après 'date'\n Sup == False -> les assiduites doivent finir avant 'date' """ if date_.tzinfo is None: first_justificatif: Justificatif = justificatifs.first() if first_justificatif is not None: date_: datetime = date_.replace(tzinfo=first_justificatif.date_debut.tzinfo) if sup: return justificatifs.filter(Justificatif.date_debut >= date_) return justificatifs.filter(Justificatif.date_fin <= date_) 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) -> 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é" """ justified: list[int] = [] if justi.etat != scu.EtatJustificatif.VALIDE: return justified assiduites_query: Assiduite = Assiduite.query.join( Justificatif, Assiduite.etudid == Justificatif.etudid ).filter(Assiduite.etat != scu.EtatAssiduite.PRESENT) assiduites_query = filter_by_date( assiduites_query, Assiduite, justi.date_debut, justi.date_fin ) justified = [assi.id for assi in assiduites_query.all()] return justified