From a05801b78a8f324dba04a75fb1142e26a403ee24 Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 11 Jan 2024 17:19:56 +0100 Subject: [PATCH 1/6] Assiduites : calendrier python fixes #812 --- app/models/assiduites.py | 13 + app/templates/assiduites/pages/calendrier2.j2 | 596 ++++++++++++++++++ .../assiduites/widgets/assiduite_bubble.j2 | 7 + .../assiduites/widgets/minitimeline_simple.j2 | 7 + app/views/assiduites.py | 392 +++++++++++- 5 files changed, 999 insertions(+), 16 deletions(-) create mode 100644 app/templates/assiduites/pages/calendrier2.j2 create mode 100644 app/templates/assiduites/widgets/assiduite_bubble.j2 create mode 100644 app/templates/assiduites/widgets/minitimeline_simple.j2 diff --git a/app/models/assiduites.py b/app/models/assiduites.py index ccfdd81e..76ec9ea1 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -338,6 +338,19 @@ class Assiduite(ScoDocModel): return "Non spécifié" if traduire else None + def get_saisie(self) -> str: + """ + retourne le texte "saisie le par " + """ + + date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M") + utilisateur: str = "" + if self.user != None: + self.user: User + utilisateur = f"par {self.user.get_prenomnom()}" + + return f"saisie le {date} {utilisateur}" + class Justificatif(ScoDocModel): """ diff --git a/app/templates/assiduites/pages/calendrier2.j2 b/app/templates/assiduites/pages/calendrier2.j2 new file mode 100644 index 00000000..3eaedb52 --- /dev/null +++ b/app/templates/assiduites/pages/calendrier2.j2 @@ -0,0 +1,596 @@ +{% block pageContent %} +{% include "assiduites/widgets/alert.j2" %} + +
+

Assiduité de {{sco.etud.html_link_fiche()|safe}}

+ +
+ + + +
+ +
+ {% 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()%} + +
+
+ Assiduité du +
+ {{jour.get_date()}} + {{jour.generate_minitimeline() | safe}} +
+
+ + {% endif %} +
+ + {% endfor %} +
+
+ {% endfor %} +
+
+ Année scolaire 2022-2023Changer + année: + + + Assiduité de {{sco.etud.nomprenom}} +
+ +
+

Calendrier

+

Code couleur

+
    +
  • → présence de l'étudiant lors de la période +
  • +
  • → la période n'est pas travaillée +
  • +
  • → absence de l'étudiant lors de la période +
  • +
  • → absence justifiée +
  • +
  • → retard de l'étudiant lors de la période +
  • +
  • → retard justifié +
  • + +
  • → la période est couverte par un + justificatif valide
  • +
  • → la période est + couverte par un justificatif non valide +
  • +
  • → la période + a un justificatif en attente de validation +
  • +
+ + +

Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires

+
+
    +
  • présence +
  • +
  • non travaillé +
  • +
  • absence +
  • +
  • absence justifiée +
  • +
  • retard +
  • +
  • retard justifié +
  • +
  • + justificatif valide
  • +
  • justificatif non valide +
  • +
+
+ + + + +{% endblock pageContent %} diff --git a/app/templates/assiduites/widgets/assiduite_bubble.j2 b/app/templates/assiduites/widgets/assiduite_bubble.j2 new file mode 100644 index 00000000..3db6ea25 --- /dev/null +++ b/app/templates/assiduites/widgets/assiduite_bubble.j2 @@ -0,0 +1,7 @@ +
+
{{moduleimpl}}
+
{{date_debut}}
+
{{date_fin}}
+
État: {{etat}}
+
{{saisie}}
+
\ No newline at end of file diff --git a/app/templates/assiduites/widgets/minitimeline_simple.j2 b/app/templates/assiduites/widgets/minitimeline_simple.j2 new file mode 100644 index 00000000..5842505f --- /dev/null +++ b/app/templates/assiduites/widgets/minitimeline_simple.j2 @@ -0,0 +1,7 @@ +
+{% for assi in assi_blocks %} +
+ {{assi.bubble | safe }} +
+{% endfor %} +
\ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 73f99790..a6108af2 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -80,6 +80,7 @@ from app.scodoc.sco_exceptions import ScoValueError from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids from app.scodoc.sco_archives_justificatifs import JustificatifArchiver +from flask_sqlalchemy.query import Query CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS @@ -837,19 +838,11 @@ def calendrier_assi_etud(): if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") - # Préparation de la page - header: str = html_sco_header.sco_header( - page_title="Calendrier de l'assiduité", - init_qtip=True, - javascripts=[ - "js/assiduites.js", - "js/date_utils.js", - ], - cssstyles=CSSSTYLES - + [ - "css/assiduites.css", - ], - ) + # Options + mode_demi: bool = scu.to_bool(request.args.get("mode_demi", "t")) + show_pres: bool = scu.to_bool(request.args.get("show_pres", "f")) + show_reta: bool = scu.to_bool(request.args.get("show_reta", "f")) + annee: int = int(request.args.get("annee", scu.annee_scolaire())) # Récupération des années d'étude de l'étudiant annees: list[int] = [] @@ -866,16 +859,34 @@ def calendrier_assi_etud(): annees_str += f"{ann}," annees_str += "]" + # Préparation de la page + header: str = html_sco_header.sco_header( + page_title="Calendrier de l'assiduité", + init_qtip=True, + javascripts=[ + "js/assiduites.js", + "js/date_utils.js", + ], + cssstyles=CSSSTYLES + + [ + "css/assiduites.css", + ], + ) + + calendrier = generate_calendar(etud, annee) # Peuplement du template jinja return HTMLBuilder( header, render_template( - "assiduites/pages/calendrier.j2", + "assiduites/pages/calendrier2.j2", sco=ScoData(etud), - annee=scu.annee_scolaire(), + annee=annee, nonworkdays=_non_work_days(), - minitimeline=_mini_timeline(), annees=annees_str, + calendrier=calendrier, + mode_demi=mode_demi, + show_pres=show_pres, + show_reta=show_reta, ), ).build() @@ -2239,3 +2250,352 @@ def _get_etuds_dem_def(formsemestre) -> str: ) + "}" ) + + +# --- Gestion du calendrier --- + + +def generate_calendar( + etudiant: Identite, + annee: int = None, +): + # 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[int, list[datetime.date]] = _organize_by_month( + _get_dates_between( + deb=date_debut.date(), + fin=date_fin.date(), + ), + etud_assiduites, + etud_justificatifs, + ) + + return annee_par_mois + + +WEEKDAYS = { + 0: "Lun ", + 1: "Mar ", + 2: "Mer ", + 3: "Jeu ", + 4: "Ven ", + 5: "Sam ", + 6: "Dim ", +} + +MONTHS = { + 1: "Janv.", + 2: "Févr.", + 3: "Mars", + 4: "Avr.", + 5: "Mai", + 6: "Juin", + 7: "Juil.", + 8: "Août", + 9: "Sept.", + 10: "Oct.", + 11: "Nov.", + 12: "Déc.", +} + + +class Jour: + """Jour + Jour du calendrier + get_nom : retourne le numéro et le nom du Jour (ex: M19 / Mer 19) + """ + + def __init__(self, date: datetime.date, assiduites: Query, justificatifs: Query): + self.date = date + self.assiduites = assiduites + self.justificatifs = justificatifs + + def get_nom(self, mode_demi: bool = True) -> str: + str_jour: str = WEEKDAYS.get(self.date.weekday()) + return f"{str_jour[0] if mode_demi or self.is_non_work() else str_jour}{self.date.day}" + + def get_date(self) -> str: + return self.date.strftime("%d/%m/%Y") + + def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str: + etat = "" + est_just = "" + + if self.is_non_work(): + return "color nonwork" + + etat = self._get_color_assiduites_cascade( + self._get_etats_from_assiduites(self.assiduites), + show_pres=show_pres, + show_reta=show_reta, + ) + + est_just = self._get_color_justificatifs_cascade( + self._get_etats_from_justificatifs(self.justificatifs), + ) + + return f"color {etat} {est_just}" + + def get_demi_class( + self, matin: bool, show_pres: bool = False, show_reta: bool = False + ) -> str: + # Transformation d'une heure "HH:MM" en time(h,m) + STR_TIME = lambda x: datetime.time(*list(map(int, x.split(":")))) + + heure_midi = STR_TIME(ScoDocSiteConfig.get("assi_lunch_time", "13:00")) + + if matin: + heure_matin = STR_TIME(ScoDocSiteConfig.get("assi_morning_time", "08:00")) + matin = ( + # date debut + scu.localize_datetime( + datetime.datetime.combine(self.date, heure_matin) + ), + # date fin + scu.localize_datetime(datetime.datetime.combine(self.date, heure_midi)), + ) + assiduites_matin = [ + assi + for assi in self.assiduites + if scu.is_period_overlapping((assi.date_debut, assi.date_fin), matin) + ] + justificatifs_matin = [ + justi + for justi in self.justificatifs + if scu.is_period_overlapping((justi.date_debut, justi.date_fin), matin) + ] + + etat = self._get_color_assiduites_cascade( + self._get_etats_from_assiduites(assiduites_matin), + show_pres=show_pres, + show_reta=show_reta, + ) + + est_just = self._get_color_justificatifs_cascade( + self._get_etats_from_justificatifs(justificatifs_matin), + ) + + return f"color {etat} {est_just}" + + heure_soir = STR_TIME(ScoDocSiteConfig.get("assi_afternoon_time", "17:00")) + + # séparation en demi journées + aprem = ( + # date debut + scu.localize_datetime(datetime.datetime.combine(self.date, heure_midi)), + # date fin + scu.localize_datetime(datetime.datetime.combine(self.date, heure_soir)), + ) + + assiduites_aprem = [ + assi + for assi in self.assiduites + if scu.is_period_overlapping((assi.date_debut, assi.date_fin), aprem) + ] + + justificatifs_aprem = [ + justi + for justi in self.justificatifs + if scu.is_period_overlapping((justi.date_debut, justi.date_fin), aprem) + ] + + etat = self._get_color_assiduites_cascade( + self._get_etats_from_assiduites(assiduites_aprem), + show_pres=show_pres, + show_reta=show_reta, + ) + + est_just = self._get_color_justificatifs_cascade( + self._get_etats_from_justificatifs(justificatifs_aprem), + ) + + return f"color {etat} {est_just}" + + def has_assiduites(self) -> bool: + return self.assiduites.count() > 0 + + def generate_minitimeline(self) -> str: + # Récupérer le référenciel de la timeline + STR_TIME = lambda x: _time_to_timedelta( + datetime.time(*list(map(int, x.split(":")))) + ) + + heure_matin: datetime.timedelta = STR_TIME( + ScoDocSiteConfig.get("assi_morning_time", "08:00") + ) + heure_midi: datetime.timedelta = STR_TIME( + ScoDocSiteConfig.get("assi_lun_time", "13:00") + ) + heure_soir: datetime.timedelta = STR_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 + + 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 + ) + + longueur: float = ((fin - deb) / longueur_timeline) * 100 + emplacement: float = ((deb - heure_matin) / longueur_timeline) * 100 + 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): + 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])) + + def _get_etats_from_justificatifs( + self, justificatifs: Query + ) -> list[scu.EtatJustificatif]: + return list(set([scu.EtatJustificatif(justi.etat) for justi in justificatifs])) + + def _get_color_assiduites_cascade( + self, + etats: list[scu.EtatAssiduite], + show_pres: bool = False, + show_reta: bool = False, + ) -> str: + if scu.EtatAssiduite.ABSENT in etats: + return "absent" + if scu.EtatAssiduite.RETARD in etats and show_reta: + return "retard" + if scu.EtatAssiduite.PRESENT in etats and show_pres: + return "present" + + return "sans_etat" + + def _get_color_justificatifs_cascade( + self, + etats: list[scu.EtatJustificatif], + ) -> str: + if scu.EtatJustificatif.VALIDE in etats: + return "est_just" + if scu.EtatJustificatif.ATTENTE in etats: + return "attente" + if scu.EtatJustificatif.MODIFIE in etats: + return "modifie" + if scu.EtatJustificatif.NON_VALIDE in etats: + return "invalide" + + return "" + + +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 + + +def _organize_by_month(days, assiduites, justificatifs): + """ + Organiser les dates par mois. + """ + organized = {} + for date in days: + # Utiliser le numéro du mois comme clé + month = MONTHS.get(date.month) + # Ajouter le jour à la liste correspondante au mois + if month not in organized: + organized[month] = [] + + date_assiduites: Query = scass.filter_by_date( + 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, + 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 + + +def _time_to_timedelta(t: datetime.time) -> datetime.timedelta: + if isinstance(t, datetime.timedelta): + return t + return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) + + +def _generate_assiduite_bubble(assiduite: Assiduite) -> str: + # Récupérer informations modules impl + moduleimpl_infos: str = assiduite.get_module(traduire=True) + + # Récupérer informations saisie + saisie: str = assiduite.get_saisie() + + return render_template( + "assiduites/widgets/assiduite_bubble.j2", + moduleimpl=moduleimpl_infos, + etat=scu.EtatAssiduite(assiduite.etat).name.lower(), + date_debut=assiduite.date_debut.strftime("%d/%m/%Y %H:%M"), + date_fin=assiduite.date_fin.strftime("%d/%m/%Y %H:%M"), + saisie=saisie, + ) From bcb01089ca4affdc2bdc4aed56b259548d317043 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 12 Jan 2024 09:08:46 +0100 Subject: [PATCH 2/6] =?UTF-8?q?Assiduites=20:=20fix=20bug=20api=20count=20?= =?UTF-8?q?etat=20mal=20orthographi=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_assiduites.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 487556ab..5f8287e6 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -394,6 +394,10 @@ def get_assiduites_stats( if "etat" in filtered else ["absent", "present", "retard"] ) + + # être sur que les états sont corrects + etats = [etat for etat in etats if etat in ["absent", "present", "retard"]] + # Préparation du dictionnaire de retour avec les valeurs du calcul count: dict = calculator.to_dict(only_total=False) for etat in etats: From 0fa35708f9545f3cc55d2ab62659ce4d4b74b915 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 12 Jan 2024 10:00:53 +0100 Subject: [PATCH 3/6] Assiduites : bug moduleimpl / autre fixes #827 --- app/models/assiduites.py | 100 +++++++++++++++++++++++++-------------- app/views/assiduites.py | 18 ++----- 2 files changed, 69 insertions(+), 49 deletions(-) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 76ec9ea1..3d6a296c 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -4,6 +4,7 @@ from datetime import datetime from flask_login import current_user from flask_sqlalchemy.query import Query +from sqlalchemy.exc import DataError from app import db, log, g, set_sco_dept from app.models import ( @@ -249,50 +250,58 @@ class Assiduite(ScoDocModel): sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut) return nouv_assiduite - def set_moduleimpl(self, moduleimpl_id: int | str) -> bool: - """TODO""" - # je ne comprend pas cette fonction WIP - # moduleimpl_id peut être == "autre", ce qui plante - # ci-dessous un fix temporaire en attendant explication de @iziram - if moduleimpl_id is None: - raise ScoValueError("invalid moduleimpl_id") + def set_moduleimpl(self, moduleimpl_id: int | str): + """Mise à jour du moduleimpl_id + Les valeurs du champs "moduleimpl_id" possibles sont : + - (un id classique) + - ("autre" ou "") + - None (pas de moduleimpl_id) + Si la valeur est "autre" il faut: + - mettre à None assiduité.moduleimpl_id + - mettre à jour assiduite.external_data["module"] = "autre" + En fonction de la configuration du semestre la valeur `None` peut-être considérée comme invalide. + - Il faudra donc vérifier que ce n'est pas le cas avant de mettre à jour l'assiduité + """ + moduleimpl: ModuleImpl = None try: - moduleimpl_id_int = int(moduleimpl_id) - except ValueError as exc: - raise ScoValueError("invalid moduleimpl_id") from exc - # /fix - moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id_int) - if moduleimpl is not None: - # Vérification de l'inscription de l'étudiant + # ne lève une erreur que si moduleimpl_id est une chaine de caractère non parsable (parseInt) + moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id) + # moduleImpl est soit : + # - None si moduleimpl_id==None + # - None si moduleimpl_id== non reconnu + # - ModuleImpl si valide + + # Vérification ModuleImpl not None (raise ScoValueError) + if moduleimpl is None and self._check_force_module(moduleimpl): + # Ici uniquement si on est autorisé à ne pas avoir de module + self.moduleimpl_id = None + return + + # Vérification Inscription ModuleImpl (raise ScoValueError) if moduleimpl.est_inscrit(self.etudiant): self.moduleimpl_id = moduleimpl.id else: raise ScoValueError("L'étudiant n'est pas inscrit au module") - elif isinstance(moduleimpl_id, str): + + except DataError: + # On arrive ici si moduleimpl_id == "autre" ou moduleimpl_id == non parsé + + if moduleimpl_id != "autre": + raise ScoValueError("Module non reconnu") + + # Configuration de external_data pour Module Autre + # Si self.external_data None alors on créé un dictionnaire {"module": "autre"} + # Sinon on met à jour external_data["module"] à "autre" + if self.external_data is None: - self.external_data = {"module": moduleimpl_id} + self.external_data = {"module": "autre"} else: - self.external_data["module"] = moduleimpl_id + self.external_data["module"] = "autre" + + # Dans tous les cas une fois fait, assiduite.moduleimpl_id doit être None self.moduleimpl_id = None - else: - # Vérification si module forcé - formsemestre: FormSemestre = get_formsemestre_from_data( - { - "etudid": self.etudid, - "date_debut": self.date_debut, - "date_fin": self.date_fin, - } - ) - force: bool - if formsemestre: - force = is_assiduites_module_forced(formsemestre_id=formsemestre.id) - else: - force = is_assiduites_module_forced(dept_id=self.etudiant.dept_id) - - if force: - raise ScoValueError("Module non renseigné") - return True + # Ici pas de vérification du force module car on l'a mis dans "external_data" def supprime(self): "Supprime l'assiduité. Log et commit." @@ -351,6 +360,27 @@ class Assiduite(ScoDocModel): return f"saisie le {date} {utilisateur}" + def _check_force_module(self, moduleimpl: ModuleImpl) -> bool: + # Vérification si module forcé + formsemestre: FormSemestre = get_formsemestre_from_data( + { + "etudid": self.etudid, + "date_debut": self.date_debut, + "date_fin": self.date_fin, + } + ) + force: bool + + if formsemestre: + force = is_assiduites_module_forced(formsemestre_id=formsemestre.id) + else: + force = is_assiduites_module_forced(dept_id=self.etudiant.dept_id) + + if force: + raise ScoValueError("Module non renseigné") + + return True + class Justificatif(ScoDocModel): """ diff --git a/app/views/assiduites.py b/app/views/assiduites.py index a6108af2..bc9760b4 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1570,20 +1570,10 @@ def _action_modifier_assiduite(assi: Assiduite): # Gestion de la description assi.description = form["description"] - module: str = form["moduleimpl_select"] - - if module == "": - module = None - else: - try: - module = int(module) - except ValueError: - pass - # TODO revoir, documenter (voir set_moduleimpl) - # ne pas appeler module ici un paramètre qui s'appelle moduleimpl_id dans la fonction - # module == instance de Module - # moduleimpl_id : id, toujours integer - assi.set_moduleimpl(module) + possible_moduleimpl_id: str = form["moduleimpl_select"] + + # Raise ScoValueError (si None et force module | Etudiant non inscrit | Module non reconnu) + assi.set_moduleimpl(possible_moduleimpl_id) db.session.add(assi) db.session.commit() From 7d441b1c4d56221602cadff0c51581796fe19a70 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 12 Jan 2024 10:52:40 +0100 Subject: [PATCH 4/6] Assiduites : choix date quand date courante hors semestre fixes #837 --- app/forms/assiduite/ajout_assiduite_etud.py | 27 ++++++++ app/templates/assiduites/pages/choix_date.j2 | 30 +++++++++ app/views/assiduites.py | 71 +++++++++++++++++--- 3 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 app/templates/assiduites/pages/choix_date.j2 diff --git a/app/forms/assiduite/ajout_assiduite_etud.py b/app/forms/assiduite/ajout_assiduite_etud.py index 44b7f9c7..deeec72c 100644 --- a/app/forms/assiduite/ajout_assiduite_etud.py +++ b/app/forms/assiduite/ajout_assiduite_etud.py @@ -161,3 +161,30 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm): validators=[DataRequired(message="This field is required.")], ) fichiers = MultipleFileField(label="Ajouter des fichiers") + + +class ChoixDateForm(FlaskForm): + def __init__(self, *args, **kwargs): + "Init form, adding a filed for our error messages" + super().__init__(*args, **kwargs) + self.ok = True + self.error_messages: list[str] = [] # used to report our errors + + def set_error(self, err_msg, field=None): + "Set error message both in form and field" + self.ok = False + self.error_messages.append(err_msg) + if field: + field.errors.append(err_msg) + + date = StringField( + "Date", + validators=[validators.Length(max=10)], + render_kw={ + "class": "datepicker", + "size": 10, + "id": "date", + }, + ) + submit = SubmitField("Enregistrer") + cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/templates/assiduites/pages/choix_date.j2 b/app/templates/assiduites/pages/choix_date.j2 new file mode 100644 index 00000000..d743ac15 --- /dev/null +++ b/app/templates/assiduites/pages/choix_date.j2 @@ -0,0 +1,30 @@ +{% extends "sco_page.j2" %} +{% import 'wtf.j2' as wtf %} + +{% block styles %} + {{super()}} + +{% endblock %} + +{% block app_content %} + {% for err_msg in form.error_messages %} +
+ {{ err_msg }} +
+ {% endfor %} +

La date courante n'est pas dans le semestre ({{deb}} -> {{fin}})

+

Choissez une autre date

+ +
+ {{ form.hidden_tag() }} + {{ form.date.label }} : {{ form.date }} +
+ {{ form.submit }} {{ form.cancel }} +
+
+ +{% endblock app_content %} +{% block scripts %} +{{ super() }} + +{% endblock scripts %} \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index bc9760b4..152c266b 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -43,6 +43,7 @@ from app.forms.assiduite.ajout_assiduite_etud import ( AjoutAssiOrJustForm, AjoutAssiduiteEtudForm, AjoutJustificatifEtudForm, + ChoixDateForm, ) from app.models import ( Assiduite, @@ -891,6 +892,56 @@ def calendrier_assi_etud(): ).build() +@bp.route("/choix_date", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.AbsChange) +def choix_date() -> str: + formsemestre_id = request.args.get("formsemestre_id") + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + + group_ids = request.args.get("group_ids") + moduleimpl_id = request.args.get("moduleimpl_id") + form = ChoixDateForm(request.form) + + if form.validate_on_submit(): + if form.cancel.data: + return redirect(url_for("scodoc.index")) + # Vérifier si date dans semestre + ok: bool = False + try: + date: datetime.date = datetime.datetime.strptime( + form.date.data, "%d/%m/%Y" + ).date() + if date < formsemestre.date_debut or date > formsemestre.date_fin: + form.set_error( + "La date sélectionnée n'est pas dans le semestre.", form.date + ) + else: + ok = True + except ValueError: + form.set_error("Date invalide", form.date) + + if ok: + return redirect( + url_for( + "assiduites.signal_assiduites_group", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + group_ids=group_ids, + moduleimpl_id=moduleimpl_id, + jour=date.isoformat(), + ) + ) + + return render_template( + "assiduites/pages/choix_date.j2", + form=form, + sco=ScoData(formsemestre=formsemestre), + deb=formsemestre.date_debut.strftime("%d/%m/%Y"), + fin=formsemestre.date_fin.strftime("%d/%m/%Y"), + ) + + @bp.route("/signal_assiduites_group") @scodoc @permission_required(Permission.AbsChange) @@ -962,15 +1013,15 @@ def signal_assiduites_group(): real_date = scu.is_iso_formated(date, True).date() if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin: - # Si le jour est hors semestre, indiquer une erreur - - # Formatage des dates pour le message d'erreur - real_str = real_date.strftime("%d/%m/%Y") - form_deb = formsemestre.date_debut.strftime("%d/%m/%Y") - form_fin = formsemestre.date_fin.strftime("%d/%m/%Y") - raise ScoValueError( - f"Impossible de saisir l'assiduité pour le {real_str}" - + f" : Jour en dehors du semestre ( {form_deb} → {form_fin}) " + # Si le jour est hors semestre, renvoyer vers choix date + return redirect( + url_for( + "assiduites.choix_date", + formsemestre_id=formsemestre_id, + group_ids=group_ids, + moduleimpl_id=moduleimpl_id, + scodoc_dept=g.scodoc_dept, + ) ) # --- Restriction en fonction du moduleimpl_id --- @@ -1571,7 +1622,7 @@ def _action_modifier_assiduite(assi: Assiduite): assi.description = form["description"] possible_moduleimpl_id: str = form["moduleimpl_select"] - + # Raise ScoValueError (si None et force module | Etudiant non inscrit | Module non reconnu) assi.set_moduleimpl(possible_moduleimpl_id) From c639778b787bbde3515c230e1d8e313d8d692d5b Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 12 Jan 2024 11:36:00 +0100 Subject: [PATCH 5/6] Assiduites : VisuAssiduiteGr logique page fixes #804 --- app/static/js/assiduites.js | 21 +++++++++------- .../pages/signal_assiduites_etud.j2 | 3 +++ .../pages/signal_assiduites_group.j2 | 24 +++++++++++++------ .../assiduites/widgets/minitimeline.j2 | 3 ++- app/templates/assiduites/widgets/timeline.j2 | 4 ++-- app/views/assiduites.py | 2 +- 6 files changed, 37 insertions(+), 20 deletions(-) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 55d0c6af..9f7e69e7 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -110,11 +110,12 @@ function validateSelectors(btn) { getAssiduitesFromEtuds(true); - document.querySelector(".selectors").disabled = true; - $("#tl_date").datepicker("option", "disabled", true); + // document.querySelector(".selectors").disabled = true; + // $("#tl_date").datepicker("option", "disabled", true); generateMassAssiduites(); generateAllEtudRow(); - btn.remove(); + // btn.remove(); + btn.textContent = "Actualiser"; onlyAbs(); }; @@ -533,6 +534,7 @@ function massAction() { * puis on ajoute les événements associés */ function generateMassAssiduites() { + if (readOnly || document.querySelector(".mass-selection") != null) return; const content = document.getElementById("content"); const mass = document.createElement("div"); @@ -1411,7 +1413,8 @@ function generateEtudRow( assi += ``; } }); - const conflit = assiduite.type == "conflit" ? "conflit" : ""; + if (readOnly) assi = ""; + const conflit = assiduite.type == "conflit" && !readOnly ? "conflit" : ""; const pdp_url = `${getUrl()}/api/etudiant/etudid/${etud.id}/photo?size=small`; let defdem = ""; @@ -1543,11 +1546,11 @@ function generateAllEtudRow() { return; } - if (!document.querySelector(".selectors")?.disabled) { - return; - } - - document.querySelector(".etud_holder").innerHTML = ""; + // if (!document.querySelector(".selectors")?.disabled) { + // return; + // } + const etud_hodler = document.querySelector(".etud_holder"); + if (etud_hodler) etud_hodler.innerHTML = ""; etuds_ids = Object.keys(etuds).sort((a, b) => etuds[a].nom > etuds[b].nom ? 1 : etuds[b].nom > etuds[a].nom ? -1 : 0 ); diff --git a/app/templates/assiduites/pages/signal_assiduites_etud.j2 b/app/templates/assiduites/pages/signal_assiduites_etud.j2 index 2284604a..ea453095 100644 --- a/app/templates/assiduites/pages/signal_assiduites_etud.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_etud.j2 @@ -102,6 +102,9 @@ setupTimeLine(() => { + if(document.querySelector('.etud_holder .placeholder') != null){ + generateAllEtudRow(); + } updateJustifyBtn(); }); diff --git a/app/templates/assiduites/pages/signal_assiduites_group.j2 b/app/templates/assiduites/pages/signal_assiduites_group.j2 index 15fd6a48..238dd25c 100644 --- a/app/templates/assiduites/pages/signal_assiduites_group.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_group.j2 @@ -26,11 +26,9 @@
+ {% if readonly == "false" %} {{timeline|safe}} - - - {% if readonly == "false" %}