From 687ac3cf135affe818a48769765e203b4a33bc1d Mon Sep 17 00:00:00 2001 From: Iziram Date: Mon, 6 May 2024 17:29:21 +0200 Subject: [PATCH 1/5] =?UTF-8?q?Assiduit=C3=A9=20:=20G=C3=A9n=C3=A9ralisati?= =?UTF-8?q?on=20du=20Calendrier=20WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_gen_cal.py | 119 ++++++++++++++++++++++++++++++++++++ app/templates/calendrier.j2 | 42 +++++++++++++ app/views/assiduites.py | 24 ++++++++ 3 files changed, 185 insertions(+) create mode 100644 app/scodoc/sco_gen_cal.py create mode 100644 app/templates/calendrier.j2 diff --git a/app/scodoc/sco_gen_cal.py b/app/scodoc/sco_gen_cal.py new file mode 100644 index 000000000..b29f7d948 --- /dev/null +++ b/app/scodoc/sco_gen_cal.py @@ -0,0 +1,119 @@ +""" +Génération d'un calendrier +(Classe abstraite à implémenter dans les classes filles) +""" + +import datetime +from flask import render_template + +import app.scodoc.sco_utils as scu +from app import g + + +class Jour: + """ + Représente un jour dans le calendrier + Permet d'obtenir les informations sur le jour + et générer une représentation html + """ + + def __init__(self, date: datetime.date): + self.date = date + + def get_nom(self, short=True): + """ + Renvoie le nom du jour + "M19" ou "Mer 19" + + par défaut en version courte + """ + str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize() + return ( + f"{str_jour[0] if short or self.is_non_work() else str_jour[:3]+' '}" + + f"{self.date.day}" + ) + + 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_date(self) -> str: + """ + Renvoie la date du jour au format "dd/mm/yyyy" + """ + return self.date.strftime(scu.DATE_FMT) + + def get_html(self): + """ + Renvoie le code html du jour + à surcharger dans les classes filles + """ + raise NotImplementedError("Méthode à implémenter dans les classes filles") + + +class Calendrier: + """ + Représente un calendrier + Permet d'obtenir les informations sur les jours + et générer une représentation html + """ + + def __init__(self, date_debut: datetime.date, date_fin: datetime.date): + self.date_debut = date_debut + 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 + + Returns: + list[datetime.date]: liste des dates entre date_debut et date_fin + """ + resultat = [] + date_actuelle = self.date_debut + while date_actuelle <= self.date_fin: + resultat.append(date_actuelle) + date_actuelle += datetime.timedelta(days=1) + return resultat + + def organize_by_month(self): + """ + Organise les jours par mois + Instancie un objet Jour pour chaque jour + + met à jour self.jours + """ + organized = {} + for date in self._get_dates_between(): + # 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] = [] + + jour: Jour = self.instanciate_jour(date) + + organized[month].append(jour) + + self.jours = organized + + def instanciate_jour(self, date: datetime.date) -> Jour: + """ + Instancie un objet Jour pour chaque jour + A surcharger dans les classes filles si besoin + """ + raise NotImplementedError("Méthode à implémenter dans les classes filles") + + def get_html(self): + """ + get_html Renvoie le code html du calendrier + """ + return render_template("calendrier.j2", calendrier=self.jours) diff --git a/app/templates/calendrier.j2 b/app/templates/calendrier.j2 new file mode 100644 index 000000000..eb5f0e8f0 --- /dev/null +++ b/app/templates/calendrier.j2 @@ -0,0 +1,42 @@ +
+ {% for mois,jours in calendrier.items() %} +
+

{{mois}}

+
+ {% for jour in jours %} +
+ {{jour.get_html() | safe}} +
+ {% endfor %} +
+
+ {% endfor %} +
+ + \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 82990fcbc..8dc9d1499 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -2873,3 +2873,27 @@ 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() From 1ac35d04c2fa3f35475b6a311428a7639a2ce207 Mon Sep 17 00:00:00 2001 From: Iziram Date: Tue, 7 May 2024 15:16:40 +0200 Subject: [PATCH 2/5] =?UTF-8?q?Assiduit=C3=A9=20:=20utilisation=20sco=5Fge?= =?UTF-8?q?n=5Fcal=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() From a4db8c4ff8458a113792794ec5d86458ac9bf9d3 Mon Sep 17 00:00:00 2001 From: Iziram Date: Tue, 7 May 2024 16:47:08 +0200 Subject: [PATCH 3/5] utilisation sco_gen_cal pour calendrier evaluations #875 --- app/scodoc/sco_evaluations.py | 155 +++++++++++++++++++++++----------- 1 file changed, 105 insertions(+), 50 deletions(-) diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index ef2e8526b..ee11aa6c6 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -25,8 +25,8 @@ # ############################################################################## -"""Evaluations -""" +"""Evaluations""" + import collections import datetime import operator @@ -50,6 +50,7 @@ from app.scodoc import sco_cal from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups +from app.scodoc import sco_gen_cal from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences from app.scodoc import sco_users @@ -360,6 +361,103 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl): return etat +class JourEval(sco_gen_cal.Jour): + """ + Représentation d'un jour dans un calendrier d'évaluations + """ + + COLOR_INCOMPLETE = "#FF6060" + COLOR_COMPLETE = "#A0FFA0" + COLOR_FUTUR = "#70E0FF" + + def __init__( + self, + date: datetime.date, + evaluations: list[Evaluation], + parent: "CalendrierEval", + ): + super().__init__(date) + + self.evaluations: list[Evaluation] = evaluations + self.evaluations.sort(key=lambda e: e.date_debut) + + self.parent: "CalendrierEval" = parent + + def get_html(self) -> str: + html: str = "" + + for e in self.evaluations: + url: str = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=e.moduleimpl_id, + ) + title: str = ( + e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval." + ) + html += f""" + {title}
""" + + return html + + def _get_eval_style(self, e: Evaluation) -> str: + color: str = "" + # Etat (notes completes) de l'évaluation: + modimpl_result = self.parent.nt.modimpls_results[e.moduleimpl.id] + if modimpl_result.evaluations_etat[e.id].is_complete: + color = JourEval.COLOR_COMPLETE + else: + color = JourEval.COLOR_INCOMPLETE + if e.date_debut > datetime.datetime.now(scu.TIME_ZONE): + color = JourEval.COLOR_FUTUR + + return f"background-color: {color};" + + def _get_eval_title(self, e: Evaluation) -> str: + heure_debut_txt, heure_fin_txt = "", "" + if e.date_debut != e.date_fin: + heure_debut_txt = ( + e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else "" + ) + heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else "" + + title = f"{e.description or e.moduleimpl.module.titre_str()}" + if heure_debut_txt: + title += f" de {heure_debut_txt} à {heure_fin_txt}" + + return title + + +class CalendrierEval(sco_gen_cal.Calendrier): + """ + Représentation des évaluations d'un semestre dans un calendrier + """ + + def __init__(self, year: int, evals: list[Evaluation], nt: NotesTableCompat): + # On prend du 01/09 au 31/08 + date_debut: datetime.datetime = datetime.datetime(year, 9, 1, 0, 0) + date_fin: datetime.datetime = datetime.datetime(year + 1, 8, 31, 23, 59) + super().__init__(date_debut, date_fin) + + # évalutions du semestre + self.evals: dict[datetime.date, list[Evaluation]] = {} + for e in evals: + if e.date_debut is not None: + day = e.date_debut.date() + if day not in self.evals: + self.evals[day] = [] + self.evals[day].append(e) + + self.nt: NotesTableCompat = nt + + def instanciate_jour(self, date: datetime.date) -> JourEval: + return JourEval(date, self.evals.get(date, []), parent=self) + + # View def formsemestre_evaluations_cal(formsemestre_id): """Page avec calendrier de toutes les evaluations de ce semestre""" @@ -369,52 +467,9 @@ def formsemestre_evaluations_cal(formsemestre_id): evaluations = formsemestre.get_evaluations() nb_evals = len(evaluations) - color_incomplete = "#FF6060" - color_complete = "#A0FFA0" - color_futur = "#70E0FF" - year = formsemestre.annee_scolaire() - events_by_day = collections.defaultdict(list) # date_iso : event - for e in evaluations: - if e.date_debut is None: - continue # éval. sans date - if e.date_debut == e.date_fin: - heure_debut_txt, heure_fin_txt = "", "" - else: - heure_debut_txt = ( - e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else "" - ) - heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else "" - - # Etat (notes completes) de l'évaluation: - modimpl_result = nt.modimpls_results[e.moduleimpl.id] - if modimpl_result.evaluations_etat[e.id].is_complete: - color = color_complete - else: - color = color_incomplete - if e.date_debut > datetime.datetime.now(scu.TIME_ZONE): - color = color_futur - day = e.date_debut.date().isoformat() # yyyy-mm-dd - event = { - "color": color, - "date_iso": day, - "title": e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval.", - "description": f"""{e.description or e.moduleimpl.module.titre_str()}""" - + ( - f""" de {heure_debut_txt} à {heure_fin_txt}""" - if heure_debut_txt - else "" - ), - "href": url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=e.moduleimpl_id, - ), - "modimpl": e.moduleimpl, - } - events_by_day[day].append(event) - - cal_html = sco_cal.YearTable(year, events_by_day=events_by_day) + cal = CalendrierEval(year, evaluations, nt) + cal_html = cal.get_html() return f""" { @@ -430,15 +485,15 @@ def formsemestre_evaluations_cal(formsemestre_id):

  • en rouge + "background-color: {JourEval.COLOR_INCOMPLETE}">rouge les évaluations passées auxquelles il manque des notes
  • en vert + "background-color: {JourEval.COLOR_COMPLETE}">vert les évaluations déjà notées
  • en bleu + "background-color: {JourEval.COLOR_FUTUR}">bleu les évaluations futures
From fa6f83722e5afa8c64f4260d53e34d579fe4db39 Mon Sep 17 00:00:00 2001 From: Iziram Date: Mon, 13 May 2024 15:38:56 +0200 Subject: [PATCH 4/5] sco_gen_cal : ajout style semaine courante --- app/scodoc/sco_gen_cal.py | 9 +++++++++ app/templates/calendrier.j2 | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/app/scodoc/sco_gen_cal.py b/app/scodoc/sco_gen_cal.py index 44695c3fc..45bfa21f5 100644 --- a/app/scodoc/sco_gen_cal.py +++ b/app/scodoc/sco_gen_cal.py @@ -20,8 +20,11 @@ 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") + if self.is_current_week(): + self.class_list.append("sem-courante") def get_nom(self, short=True): """ @@ -45,6 +48,12 @@ class Jour: dept_id=g.scodoc_dept_id ) + def is_current_week(self): + """ + Renvoie True si le jour est dans la semaine courante + """ + return self.date.isocalendar()[1] == datetime.date.today().isocalendar()[1] + def get_date(self) -> str: """ Renvoie la date du jour au format "dd/mm/yyyy" diff --git a/app/templates/calendrier.j2 b/app/templates/calendrier.j2 index 701ddc0a2..ce13a4f81 100644 --- a/app/templates/calendrier.j2 +++ b/app/templates/calendrier.j2 @@ -69,4 +69,11 @@ border: 1px solid #d5d5d5; position: relative; } + + .sem-courante{ + --couleur : #ee752c; + border-left: solid 3px var(--couleur); + border-right: solid 3px var(--couleur); + } + \ No newline at end of file From 778fecabb645259aa4c88ed3292f4579c7c2920f Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 15 May 2024 14:16:11 +0200 Subject: [PATCH 5/5] =?UTF-8?q?sco=5Fgen=5Fcal=20:=20correction=20affichag?= =?UTF-8?q?e=20semaine/ann=C3=A9e=20courante?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_gen_cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_gen_cal.py b/app/scodoc/sco_gen_cal.py index 45bfa21f5..54a66e9de 100644 --- a/app/scodoc/sco_gen_cal.py +++ b/app/scodoc/sco_gen_cal.py @@ -52,7 +52,7 @@ class Jour: """ Renvoie True si le jour est dans la semaine courante """ - return self.date.isocalendar()[1] == datetime.date.today().isocalendar()[1] + return self.date.isocalendar()[0:2] == datetime.date.today().isocalendar()[0:2] def get_date(self) -> str: """