From 1ac35d04c2fa3f35475b6a311428a7639a2ce207 Mon Sep 17 00:00:00 2001 From: Iziram Date: Tue, 7 May 2024 15:16:40 +0200 Subject: [PATCH] =?UTF-8?q?Assiduit=C3=A9=20:=20utilisation=20sco=5Fgen=5F?= =?UTF-8?q?cal=20closes=20#877?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_gen_cal.py | 33 +- app/static/css/minitimeline.css | 12 +- .../assiduites/pages/calendrier_assi_etud.j2 | 81 +--- app/templates/calendrier.j2 | 36 +- app/views/assiduites.py | 368 ++++++++---------- 5 files changed, 252 insertions(+), 278 deletions(-) diff --git a/app/scodoc/sco_gen_cal.py b/app/scodoc/sco_gen_cal.py index b29f7d948..44695c3fc 100644 --- a/app/scodoc/sco_gen_cal.py +++ b/app/scodoc/sco_gen_cal.py @@ -19,6 +19,9 @@ class Jour: def __init__(self, date: datetime.date): self.date = date + self.class_list: list[str] = [] + if self.is_non_work(): + self.class_list.append("non-travail") def get_nom(self, short=True): """ @@ -52,9 +55,29 @@ class Jour: """ Renvoie le code html du jour à surcharger dans les classes filles + + l'html final ressemblera à : + +
+ {{jour.get_nom()}} +
+ {{jour.get_html() | safe}} +
+
+ """ raise NotImplementedError("Méthode à implémenter dans les classes filles") + def get_class(self): + """ + Renvoie la classe css du jour + + utilise self.class_list + -> fait un join de la liste + + """ + return " ".join(self.class_list) + class Calendrier: """ @@ -68,8 +91,6 @@ class Calendrier: self.date_fin = date_fin self.jours: dict[str, list[Jour]] = {} - self.organize_by_month() - def _get_dates_between(self) -> list[datetime.date]: """ get_dates_between Renvoie la liste des dates entre date_debut et date_fin @@ -78,9 +99,12 @@ class Calendrier: list[datetime.date]: liste des dates entre date_debut et date_fin """ resultat = [] - date_actuelle = self.date_debut + date_actuelle: datetime.date = self.date_debut while date_actuelle <= self.date_fin: - resultat.append(date_actuelle) + if isinstance(date_actuelle, datetime.datetime): + resultat.append(date_actuelle.date()) + elif isinstance(date_actuelle, datetime.date): + resultat.append(date_actuelle) date_actuelle += datetime.timedelta(days=1) return resultat @@ -116,4 +140,5 @@ class Calendrier: """ get_html Renvoie le code html du calendrier """ + self.organize_by_month() return render_template("calendrier.j2", calendrier=self.jours) diff --git a/app/static/css/minitimeline.css b/app/static/css/minitimeline.css index 3c7347002..b1707f667 100644 --- a/app/static/css/minitimeline.css +++ b/app/static/css/minitimeline.css @@ -1,19 +1,21 @@ -.day .dayline { +.jour .dayline { position: absolute; display: none; top: 100%; z-index: 50; width: max-content; - height: 75px; background-color: #dedede; - border-radius: 15px; - padding: 5px; + border-radius: 8px; + padding: 5px 5px 15px 5px; + transform: translateX(-50%); + border: 2px solid #333; } -.day:hover .dayline { +.jour:hover .dayline { display: block; } + .dayline .mini-timeline { margin-top: 10%; } diff --git a/app/templates/assiduites/pages/calendrier_assi_etud.j2 b/app/templates/assiduites/pages/calendrier_assi_etud.j2 index a3f2c93ec..d78222944 100644 --- a/app/templates/assiduites/pages/calendrier_assi_etud.j2 +++ b/app/templates/assiduites/pages/calendrier_assi_etud.j2 @@ -23,47 +23,8 @@ Calendrier de l'assiduité for="mode_demi">mode demi journée -
- {% for mois,jours in calendrier.items() %} -
-

{{mois}}

-
- {% for jour in jours %} - {% if jour.is_non_work() %} -
- {{jour.get_nom()}} - {% else %} -
- {% endif %} - {% if mode_demi %} - {% if not jour.is_non_work() %} - {{jour.get_nom()}} - - - {% endif %} - {% else %} - {% if not jour.is_non_work() %} - {{jour.get_nom(False)}} - {% endif %} - {% endif %} - - {% if not jour.is_non_work() and jour.has_assiduites()%} - -
-
- {{jour.get_date()}} - {{jour.generate_minitimeline() | safe}} -
-
- - {% endif %} -
- - {% endfor %} -
-
- {% endfor %} -
+
+ {{calendrier|safe}}
Année scolaireChanger année: @@ -134,36 +95,26 @@ Calendrier de l'assiduité margin-bottom: 12px; } - .month h3 { - text-align: center; - } - - .day, - .demi .day.color.nonwork { - text-align: left; - margin: 2px; - cursor: default; - font-size: 13px; - position: relative; - font-weight: normal; - min-width: 6em; + .assi_case { display: flex; - justify-content: start; + width: 100%; + height: 100%; } - - - - - .demi .day.nonwork>span { - flex: none; - border: none; + .assi_case > span { + flex: 1; } - .demi .day { - border-radius: 0; + .assi_case>span:last-of-type { + border-left: #d5d5d5 solid 1px; + } + .assi_case>span:first-of-type { + border-right: #d5d5d5 solid 1px; } + .dayline{ + display: none; + } @media print { @@ -294,7 +245,5 @@ Calendrier de l'assiduité window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`); }) }); - - {% endblock app_content %} \ No newline at end of file diff --git a/app/templates/calendrier.j2 b/app/templates/calendrier.j2 index eb5f0e8f0..701ddc0a2 100644 --- a/app/templates/calendrier.j2 +++ b/app/templates/calendrier.j2 @@ -4,8 +4,11 @@

{{mois}}

{% for jour in jours %} -
- {{jour.get_html() | safe}} +
+ {{jour.get_nom()}} +
+ {{jour.get_html() | safe}} +
{% endfor %}
@@ -37,6 +40,33 @@ min-width: 6em; display: flex; justify-content: start; - background-color: whitesmoke; + } + + .jour>.contenu { + background-color: white; + width: 100%; + } + + .jour.jour.non-travail>.nom, + .jour.jour.non-travail>.contenu { + border: 0; + background-color: #badfff; + } + + .jour>.nom { + width: 3em; + min-width: 3em; + + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + } + + .jour>.contenu, + .jour>.nom { + text-align: center; + border: 1px solid #d5d5d5; + position: relative; } \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 8dc9d1499..e9a32454c 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -63,6 +63,7 @@ from app.models import ( Scolog, ) from app.scodoc.codes_cursus import UE_STANDARD + from app.auth.models import User from app.models.assiduites import get_assiduites_justif from app.tables.list_etuds import RowEtud, TableEtud @@ -82,6 +83,7 @@ from app.scodoc import sco_find_etud from app.scodoc import sco_assiduites as scass from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc import sco_gen_cal from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids @@ -757,8 +759,6 @@ def _verif_date_form_justif( deb = deb.replace(hour=0, minute=0) fin = fin.replace(hour=23, minute=59) - print(f"DEBUG {cas=}") - return deb, fin @@ -925,7 +925,14 @@ def calendrier_assi_etud(): # (sera utilisé pour générer le selecteur d'année) annees_str: str = json.dumps(annees) - calendrier: dict[str, list["Jour"]] = generate_calendar(etud, annee) + cal = CalendrierAssi( + annee, + etud, + mode_demi=mode_demi, + show_pres=show_pres, + show_reta=show_reta, + ) + calendrier: str = cal.get_html() # Peuplement du template jinja return render_template( @@ -2517,79 +2524,74 @@ def _get_etuds_dem_def(formsemestre) -> str: # --- Gestion du calendrier --- -def generate_calendar( - etudiant: Identite, - annee: int = None, -) -> dict[str, list["Jour"]]: +class JourAssi(sco_gen_cal.Jour): """ - Génère le calendrier d'assiduité de l'étudiant pour une année scolaire donnée - """ - # Si pas d'année alors on prend l'année scolaire en cours - if annee is None: - annee = scu.annee_scolaire() - - # On prend du 01/09 au 31/08 - date_debut: datetime.datetime = datetime.datetime(annee, 9, 1, 0, 0) - date_fin: datetime.datetime = datetime.datetime(annee + 1, 8, 31, 23, 59) - - # Filtrage des assiduités et des justificatifs en fonction de la periode / année - etud_assiduites: Query = scass.filter_by_date( - etudiant.assiduites, - Assiduite, - date_deb=date_debut, - date_fin=date_fin, - ) - etud_justificatifs: Query = scass.filter_by_date( - etudiant.justificatifs, - Justificatif, - date_deb=date_debut, - date_fin=date_fin, - ) - - # Récupération des jours de l'année et de leurs assiduités/justificatifs - annee_par_mois: dict[str, list[Jour]] = _organize_by_month( - _get_dates_between( - deb=date_debut.date(), - fin=date_fin.date(), - ), - etud_assiduites, - etud_justificatifs, - ) - - return annee_par_mois - - -class Jour: - """Jour - Jour du calendrier - get_nom : retourne le numéro et le nom du Jour (ex: M19 / Mer 19) + Représente un jour d'assiduité """ - def __init__(self, date: datetime.date, assiduites: Query, justificatifs: Query): - self.date = date + def __init__( + self, + date: datetime.date, + assiduites: Query, + justificatifs: Query, + parent: "CalendrierAssi", + ): + super().__init__(date) + + # assiduités et justificatifs du jour self.assiduites = assiduites self.justificatifs = justificatifs - def get_nom(self, mode_demi: bool = True) -> str: - """ - Renvoie le nom du jour - "M19" ou "Mer 19" - """ - str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize() - return ( - f"{str_jour[0] if mode_demi or self.is_non_work() else str_jour[:3]+' '}" - + f"{self.date.day}" + self.parent = parent + + def get_html(self) -> str: + # si non travaillé on renvoie une case vide + if self.is_non_work(): + return "" + + html: str = ( + self._get_html_demi() if self.parent.mode_demi else self._get_html_normal() ) + html = f'
{html}
' - def get_date(self) -> str: - """ - Renvoie la date du jour au format "dd/mm/yyyy" - """ - return self.date.strftime(scu.DATE_FMT) + if self.has_assiduite(): + minitimeline: str = f""" +
+
+ {self.get_date()} + {self._generate_minitimeline()} +
+
+ """ + html += minitimeline - def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str: + return html + + def has_assiduite(self) -> bool: + """Renvoie True si le jour a une assiduité""" + return self.assiduites.count() > 0 + + def _get_html_normal(self) -> str: """ - Retourne la classe css du jour (mode normal) + Renvoie l'html de la case du calendrier + (version journee normale (donc une couleur)) + """ + class_name = self._get_color_normal() + return f'' + + def _get_html_demi(self) -> str: + """ + Renvoie l'html de la case du calendrier + (version journee divisée en demi-journées (donc 2 couleurs)) + """ + matin = self._get_color_demi(True) + aprem = self._get_color_demi(False) + return f'' + + def _get_color_normal(self) -> str: + """renvoie la classe css correspondant + à la case du calendrier + (version journee normale) """ etat = "" est_just = "" @@ -2599,8 +2601,8 @@ class Jour: etat = self._get_color_assiduites_cascade( self._get_etats_from_assiduites(self.assiduites), - show_pres=show_pres, - show_reta=show_reta, + show_pres=self.parent.show_pres, + show_reta=self.parent.show_reta, ) est_just = self._get_color_justificatifs_cascade( @@ -2609,13 +2611,11 @@ class Jour: return f"color {etat} {est_just}" - def get_demi_class( - self, matin: bool, show_pres: bool = False, show_reta: bool = False - ) -> str: + def _get_color_demi(self, matin: bool) -> str: + """renvoie la classe css correspondant + à la case du calendrier + (version journee divisée en demi-journees) """ - Renvoie la class css de la demi journée - """ - heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00")) if matin: @@ -2647,8 +2647,8 @@ class Jour: etat = self._get_color_assiduites_cascade( self._get_etats_from_assiduites(assiduites_matin), - show_pres=show_pres, - show_reta=show_reta, + show_pres=self.parent.show_pres, + show_reta=self.parent.show_reta, ) est_just = self._get_color_justificatifs_cascade( @@ -2687,8 +2687,8 @@ class Jour: etat = self._get_color_assiduites_cascade( self._get_etats_from_assiduites(assiduites_aprem), - show_pres=show_pres, - show_reta=show_reta, + show_pres=self.parent.show_pres, + show_reta=self.parent.show_reta, ) est_just = self._get_color_justificatifs_cascade( @@ -2697,77 +2697,6 @@ class Jour: return f"color {etat} {est_just}" - def has_assiduites(self) -> bool: - """ - Renverra True si le jour a des assiduités - """ - return self.assiduites.count() > 0 - - def generate_minitimeline(self) -> str: - """ - Génère la minitimeline du jour - """ - # Récupérer le référenciel de la timeline - heure_matin: datetime.timedelta = _time_to_timedelta( - scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08:00")) - ) - heure_soir: datetime.timedelta = _time_to_timedelta( - scass.str_to_time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00")) - ) - # longueur_timeline = heure_soir - heure_matin - longueur_timeline: datetime.timedelta = heure_soir - heure_matin - - # chaque block d'assiduité est défini par: - # longueur = ( (fin-deb) / longueur_timeline ) * 100 - # emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100 - # longueur + emplacement = 100% sinon on réduit longueur - - assiduite_blocks: list[dict[str, float | str]] = [] - - for assi in self.assiduites: - deb: datetime.timedelta = _time_to_timedelta( - assi.date_debut.time() - if assi.date_debut.date() == self.date - else heure_matin - ) - fin: datetime.timedelta = _time_to_timedelta( - assi.date_fin.time() - if assi.date_fin.date() == self.date - else heure_soir - ) - - emplacement: float = max(((deb - heure_matin) / longueur_timeline) * 100, 0) - longueur: float = ((fin - deb) / longueur_timeline) * 100 - if longueur + emplacement > 100: - longueur = 100 - emplacement - etat: str = scu.EtatAssiduite(assi.etat).name.lower() - est_just: str = "est_just" if assi.est_just else "" - - assiduite_blocks.append( - { - "longueur": longueur, - "emplacement": emplacement, - "etat": etat, - "est_just": est_just, - "bubble": _generate_assiduite_bubble(assi), - "id": assi.assiduite_id, - } - ) - - return render_template( - "assiduites/widgets/minitimeline_simple.j2", - assi_blocks=assiduite_blocks, - ) - - def is_non_work(self): - """ - Renvoie True si le jour est un jour non travaillé - (en fonction de la préférence du département) - """ - return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days( - dept_id=g.scodoc_dept_id - ) - def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]: return list(set(scu.EtatAssiduite(assi.etat) for assi in assiduites)) @@ -2806,47 +2735,110 @@ class Jour: return "" + def _generate_minitimeline(self) -> str: + """ + Génère la minitimeline du jour + """ + # Récupérer le référenciel de la timeline + heure_matin: datetime.timedelta = _time_to_timedelta( + scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08:00")) + ) + heure_soir: datetime.timedelta = _time_to_timedelta( + scass.str_to_time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00")) + ) + # longueur_timeline = heure_soir - heure_matin + longueur_timeline: datetime.timedelta = heure_soir - heure_matin -def _get_dates_between(deb: datetime.date, fin: datetime.date) -> list[datetime.date]: - resultat = [] - date_actuelle = deb - while date_actuelle <= fin: - resultat.append(date_actuelle) - date_actuelle += datetime.timedelta(days=1) - return resultat + # chaque block d'assiduité est défini par: + # longueur = ( (fin-deb) / longueur_timeline ) * 100 + # emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100 + # longueur + emplacement = 100% sinon on réduit longueur + + assiduite_blocks: list[dict[str, float | str]] = [] + + for assi in self.assiduites: + deb: datetime.timedelta = _time_to_timedelta( + assi.date_debut.time() + if assi.date_debut.date() == self.date + else heure_matin + ) + fin: datetime.timedelta = _time_to_timedelta( + assi.date_fin.time() + if assi.date_fin.date() == self.date + else heure_soir + ) + + emplacement: float = max(((deb - heure_matin) / longueur_timeline) * 100, 0) + longueur: float = ((fin - deb) / longueur_timeline) * 100 + if longueur + emplacement > 100: + longueur = 100 - emplacement + + etat: str = scu.EtatAssiduite(assi.etat).name.lower() + est_just: str = "est_just" if assi.est_just else "" + + assiduite_blocks.append( + { + "longueur": longueur, + "emplacement": emplacement, + "etat": etat, + "est_just": est_just, + "bubble": _generate_assiduite_bubble(assi), + "id": assi.assiduite_id, + } + ) + + return render_template( + "assiduites/widgets/minitimeline_simple.j2", + assi_blocks=assiduite_blocks, + ) -def _organize_by_month(days, assiduites, justificatifs) -> dict[str, list[Jour]]: +class CalendrierAssi(sco_gen_cal.Calendrier): """ - Organiser les dates par mois. + Représente un calendrier d'assiduité d'un étudiant """ - organized = {} - for date in days: - # Récupérer le mois en français - month = scu.MONTH_NAMES_ABBREV[date.month - 1] - # Ajouter le jour à la liste correspondante au mois - if month not in organized: - organized[month] = [] - date_assiduites: Query = scass.filter_by_date( - assiduites, + def __init__(self, annee: int, etudiant: Identite, **options): + # On prend du 01/09 au 31/08 + date_debut: datetime.datetime = datetime.datetime(annee, 9, 1, 0, 0) + date_fin: datetime.datetime = datetime.datetime(annee + 1, 8, 31, 23, 59) + super().__init__(date_debut, date_fin) + + # On récupère les assiduités et les justificatifs + self.etud_assiduites: Query = scass.filter_by_date( + etudiant.assiduites, + Assiduite, + date_deb=date_debut, + date_fin=date_fin, + ) + self.etud_justificatifs: Query = scass.filter_by_date( + etudiant.justificatifs, + Justificatif, + date_deb=date_debut, + date_fin=date_fin, + ) + + # Ajout des options (exemple : mode_demi, show_pres, show_reta, ...) + for key, value in options.items(): + setattr(self, key, value) + + def instanciate_jour(self, date: datetime.date) -> JourAssi: + """ + Instancie un jour d'assiduité + """ + assiduites: Query = scass.filter_by_date( + self.etud_assiduites, Assiduite, date_deb=datetime.datetime.combine(date, datetime.time(0, 0)), date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)), ) - - date_justificatifs: Query = scass.filter_by_date( - justificatifs, + justificatifs: Query = scass.filter_by_date( + self.etud_justificatifs, Justificatif, date_deb=datetime.datetime.combine(date, datetime.time(0, 0)), date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)), ) - # On génère un `Jour` composé d'une date, et des assiduités/justificatifs du jour - jour: Jour = Jour(date, date_assiduites, date_justificatifs) - - organized[month].append(jour) - - return organized + return JourAssi(date, assiduites, justificatifs, parent=self) def _time_to_timedelta(t: datetime.time) -> datetime.timedelta: @@ -2873,27 +2865,3 @@ def _generate_assiduite_bubble(assiduite: Assiduite) -> str: saisie=saisie, motif=motif, ) - - -from app.scodoc import sco_gen_cal - - -class JourTest(sco_gen_cal.Jour): - def get_html(self): - return f"{self.get_nom()}" - - -class CalendrierTest(sco_gen_cal.Calendrier): - def instanciate_jour(self, date: datetime.date): - return JourTest(date) - - -@bp.route("/generate_cal") -@scodoc -@permission_required(Permission.AbsJustifView) -def test(): - cal = CalendrierTest( - datetime.date(2021, 9, 1), - datetime.date(2022, 8, 31), - ) - return cal.get_html()