From 1f6f3620a2bc19f72b7b64ec290c7d4c5cf3fcfd Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 30 Jan 2024 10:54:00 +0100 Subject: [PATCH 1/5] Bul. BUT: traitement etud noin inscrit --- app/but/bulletin_but.py | 21 ++++++++++----------- sco_version.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 5ec30c5e3..918d14fd2 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -349,19 +349,12 @@ class BulletinBUT: raise ScoValueError("bulletin_etud: version de bulletin demandée invalide") res = self.res formsemestre = res.formsemestre - etat_inscription = etud.inscription_etat(formsemestre.id) - nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT] - published = (not formsemestre.bul_hide_xml) or force_publishing - if formsemestre.formation.referentiel_competence is None: - etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)} - else: - etud_ues_ids = res.etud_ues_ids(etud.id) - d = { "version": "0", "type": "BUT", "date": datetime.datetime.utcnow().isoformat() + "Z", "publie": not formsemestre.bul_hide_xml, + "etat_inscription": etud.inscription_etat(formsemestre.id), "etudiant": etud.to_dict_bul(), "formation": { "id": formsemestre.formation.id, @@ -370,14 +363,20 @@ class BulletinBUT: "titre": formsemestre.formation.titre, }, "formsemestre_id": formsemestre.id, - "etat_inscription": etat_inscription, "options": sco_preferences.bulletin_option_affichage( formsemestre, self.prefs ), } - if not published: + published = (not formsemestre.bul_hide_xml) or force_publishing + if not published or d["etat_inscription"] is False: return d + nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT] + if formsemestre.formation.referentiel_competence is None: + etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)} + else: + etud_ues_ids = res.etud_ues_ids(etud.id) + nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) etud_groups = sco_groups.get_etud_formsemestre_groups( etud, formsemestre, only_to_show=True @@ -410,7 +409,7 @@ class BulletinBUT: semestre_infos.update( sco_bulletins_json.dict_decision_jury(etud, formsemestre) ) - if etat_inscription == scu.INSCRIT: + if d["etat_inscription"] == scu.INSCRIT: # moyenne des moyennes générales du semestre semestre_infos["notes"] = { "value": fmt_note(res.etud_moy_gen[etud.id]), diff --git a/sco_version.py b/sco_version.py index 35a788bfb..65feb9eb6 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.88" +SCOVERSION = "9.6.89" SCONAME = "ScoDoc" From 5446ac0ed27d22cd89b3e250e7eeea9ad8dbaa77 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 30 Jan 2024 11:02:28 +0100 Subject: [PATCH 2/5] PE: ajout coloness etudid, INE, NIP + some code cleaning --- app/pe/pe_etudiant.py | 86 +++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/app/pe/pe_etudiant.py b/app/pe/pe_etudiant.py index 40f2c45cf..811180df1 100644 --- a/app/pe/pe_etudiant.py +++ b/app/pe/pe_etudiant.py @@ -38,27 +38,24 @@ Created on 17/01/2024 import pandas as pd from app.models import FormSemestre, Identite -import app.pe.pe_affichage as pe_affichage -import app.pe.pe_comp as pe_comp from app.pe import pe_comp, pe_affichage class EtudiantsJuryPE: + """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE""" def __init__(self, annee_diplome: int): """ - Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE - Args: annee_diplome: L'année de diplomation """ self.annee_diplome = annee_diplome """L'année du diplôme""" - self.identites = {} # ex. ETUDINFO_DICT + self.identites: dict[int, Identite] = {} # ex. ETUDINFO_DICT "Les identités des étudiants traités pour le jury" - self.cursus = {} + self.cursus: dict[int, dict] = {} "Les cursus (semestres suivis, abandons) des étudiants" self.trajectoires = {} @@ -67,15 +64,17 @@ class EtudiantsJuryPE: (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)""" self.etudiants_diplomes = {} - """Les identités des étudiants à considérer au jury (ceux qui seront effectivement diplômés)""" + """Les identités des étudiants à considérer au jury (ceux qui seront effectivement + diplômés)""" self.diplomes_ids = {} """Les etudids des étudiants diplômés""" self.etudiants_ids = {} - """Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons). - Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant - été réorientés ou ayant abandonnés)""" + """Les etudids des étudiants dont il faut calculer les moyennes/classements + (même si d'éventuels abandons). + Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi + d'autres ayant été réorientés ou ayant abandonnés)""" self.cosemestres: dict[int, FormSemestre] = None "Les cosemestres donnant lieu à même année de diplome" @@ -107,18 +106,15 @@ class EtudiantsJuryPE: pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres") self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) pe_affichage.pe_print( - " => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids) + f" => {len(self.etudiants_ids)} étudiants trouvés dans les cosemestres" ) # Analyse des parcours étudiants pour déterminer leur année effective de diplome # avec prise en compte des redoublements, des abandons, .... pe_affichage.pe_print("3) Analyse des parcours individuels des étudiants") - no_etud = 0 - for no_etud, etudid in enumerate(self.etudiants_ids): - identite = Identite.get_etud(etudid) - self.identites[etudid] = identite - """identités des étudiants""" + for etudid in self.etudiants_ids: + self.identites[etudid] = Identite.get_etud(etudid) # Analyse son cursus self.analyse_etat_etudiant(etudid, cosemestres) @@ -131,7 +127,6 @@ class EtudiantsJuryPE: self.diplomes_ids = set(self.etudiants_diplomes.keys()) self.etudiants_ids = set(self.identites.keys()) - """Les étudiants dont il faut calculer les moyennes""" self.formsemestres_jury_ids = self.get_formsemestres() """Les formsemestres (des étudiants) dont il faut calculer les moyennes""" @@ -162,8 +157,6 @@ class EtudiantsJuryPE: # + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) # ) - - def get_etudiants_diplomes(self) -> dict[int, Identite]: """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` qui vont être à traiter au jury PE pour @@ -175,9 +168,9 @@ class EtudiantsJuryPE: """ etudids = [ etudid - for etudid in self.cursus - if self.cursus[etudid]["diplome"] == self.annee_diplome - and self.cursus[etudid]["abandon"] is False + for etudid, cursus_etud in self.cursus.items() + if cursus_etud["diplome"] == self.annee_diplome + and cursus_etud["abandon"] is False ] etudiants = {etudid: self.identites[etudid] for etudid in etudids} return etudiants @@ -192,9 +185,9 @@ class EtudiantsJuryPE: """ etudids = [ etudid - for etudid in self.cursus - if self.cursus[etudid]["diplome"] != self.annee_diplome - or self.cursus[etudid]["abandon"] is True + for etudid, cursus_etud in self.cursus.items() + if cursus_etud["diplome"] != self.annee_diplome + or cursus_etud["abandon"] is True ] etudiants = {etudid: self.identites[etudid] for etudid in etudids} return etudiants @@ -224,9 +217,9 @@ class EtudiantsJuryPE: formsemestres = identite.get_formsemestres() semestres_etudiant = { - frmsem.formsemestre_id: frmsem - for frmsem in formsemestres - if frmsem.formation.is_apc() + formsemestre.formsemestre_id: formsemestre + for formsemestre in formsemestres + if formsemestre.formation.is_apc() } self.cursus[etudid] = { @@ -282,11 +275,12 @@ class EtudiantsJuryPE: # Tri des semestres par numéro de semestre for nom_sem in pe_comp.TOUS_LES_SEMESTRES: i = int(nom_sem[1]) # le n° du semestre + # les semestres de n°i de l'étudiant: semestres_i = { - fid: semestres_significatifs[fid] - for fid in semestres_significatifs - if semestres_significatifs[fid].semestre_id == i - } # les semestres de n°i de l'étudiant + fid: sem_sig + for fid, sem_sig in semestres_significatifs.items() + if sem_sig.semestre_id == i + } self.cursus[etudid][nom_sem] = semestres_i def get_trajectoire( @@ -323,7 +317,7 @@ class EtudiantsJuryPE: int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["aggregat"] ] assert numero_semestre_terminal in numero_semestres_possibles - else: # les xS = tous les semestres jusqu'à Sx (pax ex: des S1, S2, S3 pour un S3 terminal) + else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal) numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1)) semestres_aggreges = {} @@ -380,23 +374,23 @@ class EtudiantsJuryPE: * '3S', '4S' : pour obtenir les combinaisons de semestres définies par les aggrégats Returns: - Un dictionnaire de la forme ``{fid: FormSemestre(fid)}`` + Un dictionnaire de la forme `{fid: FormSemestre(fid)}` Remarque: - Une liste de la forme ``[ 'Si', 'iA' , ... ]`` (combinant les formats précédents) est possible. + Une liste de la forme `[ 'Si', 'iA' , ... ]` (combinant les formats précédents) est possible. """ if semestres_recherches is None: # Appel récursif pour obtenir tous les semestres (validants) semestres = self.get_formsemestres(pe_comp.AGGREGAT_DIPLOMANT) return semestres - elif isinstance(semestres_recherches, list): + if isinstance(semestres_recherches, list): # Appel récursif sur tous les éléments de la liste semestres = {} for elmt in semestres_recherches: semestres_elmt = self.get_formsemestres(elmt) semestres = semestres | semestres_elmt return semestres - elif ( + if ( isinstance(semestres_recherches, str) and semestres_recherches in pe_comp.TOUS_LES_AGGREGATS ): @@ -405,7 +399,7 @@ class EtudiantsJuryPE: pe_comp.PARCOURS[semestres_recherches]["aggregat"] ) return semestres - elif ( + if ( isinstance(semestres_recherches, str) and semestres_recherches in pe_comp.TOUS_LES_SEMESTRES ): @@ -418,8 +412,8 @@ class EtudiantsJuryPE: if self.cursus[etudid][nom_sem]: semestres = semestres | self.cursus[etudid][nom_sem] return semestres - else: - raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") + + raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int: """Partant d'un ensemble d'étudiants, @@ -433,8 +427,7 @@ class EtudiantsJuryPE: nbres_semestres.append(self.cursus[etudid]["nb_semestres"]) if not nbres_semestres: return 0 - else: - return max(nbres_semestres) + return max(nbres_semestres) def df_administratif(self, etudids: list[int]) -> pd.DataFrame: """Synthétise toutes les données administratives d'un groupe @@ -461,13 +454,16 @@ class EtudiantsJuryPE: diplome = "indéterminé" administratif[etudid] = { + "etudid": etudiant.id, + "INE": etudiant.code_ine or "", + "NIP": etudiant.code_nip or "", "Nom": etudiant.nom, "Prenom": etudiant.prenom, "Civilite": etudiant.civilite_str, "Age": pe_comp.calcul_age(etudiant.date_naissance), - "Date d'entree": cursus["entree"], - "Date de diplome": diplome, - "Nbre de semestres": len(formsemestres), + "Date entree": cursus["entree"], + "Date diplome": diplome, + "Nb semestres": len(formsemestres), } # Ajout des noms de semestres parcourus From eefbe709440e0ac6428c0ec6e061f5e2e9c8b124 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 30 Jan 2024 11:47:57 +0100 Subject: [PATCH 3/5] =?UTF-8?q?Fix:=20Assiduit=C3=A9:=20saisie/=C3=A9ditii?= =?UTF-8?q?on=20date=20d=C3=A9p=C3=B4t=20justif.=20Fix=20#852?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/forms/assiduite/ajout_assiduite_etud.py | 12 +++++++++++- app/models/assiduites.py | 2 +- app/scodoc/sco_archives_justificatifs.py | 4 ++-- .../assiduites/pages/ajout_assiduite_etud.j2 | 2 +- .../assiduites/pages/ajout_justificatif_etud.j2 | 6 +++--- app/views/assiduites.py | 16 ++++++++++++++-- 6 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/forms/assiduite/ajout_assiduite_etud.py b/app/forms/assiduite/ajout_assiduite_etud.py index d5783e2d0..4ece2b5bd 100644 --- a/app/forms/assiduite/ajout_assiduite_etud.py +++ b/app/forms/assiduite/ajout_assiduite_etud.py @@ -102,7 +102,7 @@ class AjoutAssiOrJustForm(FlaskForm): ) entry_date = StringField( - "Date de dépot ou saisie", + "Date de dépôt ou saisie", validators=[validators.Length(max=10)], render_kw={ "class": "datepicker", @@ -110,6 +110,16 @@ class AjoutAssiOrJustForm(FlaskForm): "id": "entry_date", }, ) + entry_time = StringField( + "Heure dépôt", + default="", + validators=[validators.Length(max=5)], + render_kw={ + "class": "timepicker", + "size": 5, + "id": "assi_heure_fin", + }, + ) submit = SubmitField("Enregistrer") cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 89ab7fe67..e08a92111 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -418,7 +418,7 @@ class Justificatif(ScoDocModel): entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) "date de création de l'élément: date de saisie" - # pourrait devenir date de dépot au secrétariat, si différente + # pourrait devenir date de dépôt au secrétariat, si différente user_id = db.Column( db.Integer, diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index abdb9fff4..0b3ff9137 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -19,7 +19,7 @@ class Trace: """gestionnaire de la trace des fichiers justificatifs Role des fichiers traces : - - Sauvegarder la date de dépot du fichier + - Sauvegarder la date de dépôt du fichier - Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif) - Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView) @@ -116,7 +116,7 @@ class JustificatifArchiver(BaseArchiver): TOTALK: - oid -> etudid - - archive_id -> date de création de l'archive (une archive par dépot de document) + - archive_id -> date de création de l'archive (une archive par dépôt de document) justificatif └── diff --git a/app/templates/assiduites/pages/ajout_assiduite_etud.j2 b/app/templates/assiduites/pages/ajout_assiduite_etud.j2 index 7871bf903..59ba761f2 100644 --- a/app/templates/assiduites/pages/ajout_assiduite_etud.j2 +++ b/app/templates/assiduites/pages/ajout_assiduite_etud.j2 @@ -100,7 +100,7 @@ div.submit > input { {{ form.description() }} {{ render_field_errors(form, 'description') }} - {# Date dépot #} + {# Date dépôt #} {{ form.entry_date.label }} : {{ form.entry_date }} laisser vide pour date courante {{ render_field_errors(form, 'entry_date') }} diff --git a/app/templates/assiduites/pages/ajout_justificatif_etud.j2 b/app/templates/assiduites/pages/ajout_justificatif_etud.j2 index 3aee67d09..4088bbac2 100644 --- a/app/templates/assiduites/pages/ajout_justificatif_etud.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif_etud.j2 @@ -123,9 +123,9 @@ div.submit > input { {{ render_field_errors(form, 'fichiers') }} - {# Date dépot #} - {{ form.entry_date.label }} : {{ form.entry_date }} - laisser vide pour date courante + {# Date dépôt #} + {{ form.entry_date.label }} : {{ form.entry_date }} à {{ form.entry_time }} + laisser vide pour date courante {{ render_field_errors(form, 'entry_date') }} {# Submit #} diff --git a/app/views/assiduites.py b/app/views/assiduites.py index bd6a1b129..019906581 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -334,7 +334,7 @@ def _get_dates_from_assi_form( dt_fin = datetime.datetime.combine(date_fin or date_debut, heure_fin) if dt_fin <= dt_debut: form.set_error("dates début/fin incohérentes") - # La date de dépot (si vide, la date actuelle) + # La date de dépôt (si vide, la date actuelle) try: dt_entry_date = ( datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y") @@ -344,7 +344,16 @@ def _get_dates_from_assi_form( except ValueError: dt_entry_date = None form.set_error("format de date de dépôt invalide", form.entry_date) - + # L'heure de dépôt + try: + entry_time = datetime.time.fromisoformat( + form.entry_time.data or datetime.datetime.now().time().isoformat("seconds") + ) + except ValueError: + dt_entry_date = None + form.set_error("format d'heure de dépôt invalide", form.entry_date) + if dt_entry_date: + dt_entry_date = datetime.datetime.combine(dt_entry_date, entry_time) # Ajoute time zone serveur dt_debut_tz_server = scu.TIME_ZONE.localize(dt_debut) dt_fin_tz_server = scu.TIME_ZONE.localize(dt_fin) @@ -576,6 +585,9 @@ def edit_justificatif_etud(justif_id: int): form.entry_date.data = ( justif.entry_date.strftime("%d/%m/%Y") if justif.entry_date else "" ) + form.entry_time.data = ( + justif.entry_date.strftime("%H:%M") if justif.entry_date else "" + ) form.etat.data = str(justif.etat) redirect_url = url_for( From f7e41dc7fe1d41a2a8ec3ea19fb785b1726bb3e7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 30 Jan 2024 14:18:27 +0100 Subject: [PATCH 4/5] Fix: api formsemestres_courants date fin incluse --- app/api/departements.py | 2 +- app/models/formsemestre.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/departements.py b/app/api/departements.py index 04cb8e75e..7d056e468 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -295,7 +295,7 @@ def dept_formsemestres_courants_by_id(dept_id: int): if date_courante: test_date = datetime.fromisoformat(date_courante) else: - test_date = app.db.func.now() + test_date = db.func.current_date() # Les semestres en cours de ce département formsemestres = FormSemestre.query.filter( FormSemestre.dept_id == dept.id, diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 322958165..6cdaef8af 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -673,7 +673,7 @@ class FormSemestre(db.Model): ) -> db.Query: """Liste (query) ordonnée des formsemestres courants, c'est à dire contenant la date courant (si None, la date actuelle)""" - date_courante = date_courante or db.func.now() + date_courante = date_courante or db.func.current_date() # Les semestres en cours de ce département formsemestres = FormSemestre.query.filter( FormSemestre.dept_id == dept.id, From 90bf31fc03c3b6a8772c8d134a941390da443bdf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 30 Jan 2024 22:12:55 +0100 Subject: [PATCH 5/5] Tag module formation selon leur type --- app/models/modules.py | 23 ++++++- app/scodoc/sco_edit_ue.py | 13 +++- app/scodoc/sco_formation_recap.py | 3 + app/scodoc/sco_tag_module.py | 96 ++++++++---------------------- app/static/js/module_tag_editor.js | 39 +++++++++--- app/views/notes.py | 23 +++++++ 6 files changed, 117 insertions(+), 80 deletions(-) diff --git a/app/models/modules.py b/app/models/modules.py index 430835a91..2c3a91311 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -1,6 +1,6 @@ """ScoDoc 9 models : Modules """ -from flask import current_app +from flask import current_app, g from app import db from app.models import APO_CODE_STR_LEN @@ -310,6 +310,14 @@ class Module(db.Model): return [] return self.parcours + def add_tag(self, tag: "NotesTag"): + """Add tag to module. Check if already has it.""" + if tag.id in {t.id for t in self.tags}: + return + self.tags.append(tag) + db.session.add(self) + db.session.flush() + class ModuleUECoef(db.Model): """Coefficients des modules vers les UE (APC, BUT) @@ -372,6 +380,19 @@ class NotesTag(db.Model): dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) title = db.Column(db.Text(), nullable=False) + @classmethod + def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag": + """Get tag, or create it if it doesn't yet exists. + If dept_id unspecified, use current dept. + """ + dept_id = dept_id if dept_id is not None else g.scodoc_dept_id + tag = NotesTag.query.filter_by(dept_id=dept_id, title=title).first() + if tag is None: + tag = NotesTag(dept_id=dept_id, title=title) + db.session.add(tag) + db.session.flush() + return tag + # Association tag <-> module notes_modules_tags = db.Table( diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index f33eeacd5..dd95dfd37 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -670,6 +670,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list semestre_idx = None else: semestre_idx = int(semestre_idx) + show_tags = scu.to_bool(request.args.get("show_tags", 0)) locked = formation.has_locked_sems(semestre_idx) semestre_ids = range(1, parcours.NB_SEM + 1) # transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7 @@ -875,11 +876,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); ) # Description des UE/matières/modules H.append( - """ + f"""
Programme pédagogique:
- montrer les tags des modules + montrer les tags des modules
""" ) @@ -978,6 +981,11 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); formation_id=formation_id) }">Table récapitulative de la formation +
  • Tagguer tous les modules par leur type (tag res, sae). +
  • + """ return htm diff --git a/app/scodoc/sco_formation_recap.py b/app/scodoc/sco_formation_recap.py index fbd72b54c..3b5a923e9 100644 --- a/app/scodoc/sco_formation_recap.py +++ b/app/scodoc/sco_formation_recap.py @@ -103,6 +103,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response: "heures_cours": mod.heures_cours, "heures_td": mod.heures_td, "heures_tp": mod.heures_tp, + "tags": ", ".join(t.title for t in mod.tags if t.title), "_css_row_class": f"mod {mod.type_abbrv()}", } ) @@ -117,6 +118,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response: "heures_cours", "heures_td", "heures_tp", + "tags", ] if not formation.is_apc(): columns_ids.insert(columns_ids.index("ects"), "coef") @@ -132,6 +134,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response: "heures_cours": "Cours (h)", "heures_td": "TD (h)", "heures_tp": "TP (h)", + "tags": "Tags", "ects": "ECTS", } diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index 3c62fd553..e5f6a5d88 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -38,17 +38,14 @@ import re from flask import g -from app.comp import res_sem -from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app import db, log +from app.models import Formation, NotesTag +from app.scodoc import sco_edit_module import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb -from app import log -from app.scodoc import sco_edit_module -from app.scodoc import sco_etud from app.scodoc.sco_exceptions import ScoValueError -# Opérations à implementer: +# Opérations implementées: # + liste des modules des formations de code donné (formation_code) avec ce tag # + liste de tous les noms de tag # + tag pour un nom @@ -62,6 +59,7 @@ from app.scodoc.sco_exceptions import ScoValueError # module_tag_set( module_id, taglist ) -> modifie les tags +# NOTA: ancien code, n'utile pas de modèles SQLAlchemy class ScoTag(object): """Generic tags for ScoDoc""" @@ -232,7 +230,7 @@ def module_tag_search(term: str | int): return scu.sendJSON(data) -def module_tag_list(module_id=""): +def module_tag_list(module_id="") -> list[str]: """les noms de tags associés à ce module""" r = ndb.SimpleDictFetch( """SELECT t.title @@ -249,6 +247,7 @@ def module_tag_set(module_id="", taglist=None): """taglist may either be: a string with tag names separated by commas ("un,deux") or a list of strings (["un", "deux"]) + Remplace les tags existants """ if not taglist: taglist = [] @@ -284,34 +283,6 @@ def module_tag_set(module_id="", taglist=None): return "", http.HTTPStatus.NO_CONTENT -def get_etud_tagged_modules(etudid, tagname): - """Liste d'infos sur les modules de ce semestre avec ce tag. - Cherche dans tous les semestres dans lesquel l'étudiant est ou a été inscrit. - Construit la liste des modules avec le tag donné par tagname - """ - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - R = [] - for sem in etud["sems"]: - formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - modimpls = nt.get_modimpls_dict() - for modimpl in modimpls: - tags = module_tag_list(module_id=modimpl["module_id"]) - if tagname in tags: - moy = nt.get_etud_mod_moy( - modimpl["moduleimpl_id"], etudid - ) # ou NI si non inscrit - R.append( - { - "sem": sem, - "moy": moy, # valeur réelle, ou NI (non inscrit au module ou NA (pas de note) - "moduleimpl": modimpl, - "tags": tags, - } - ) - return R - - def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]: """Découpage d'un tag, tel que saisi par un utilisateur dans le programme, pour en extraire : @@ -335,42 +306,27 @@ def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]: try: pond = float(temp[1]) return (temp[0], pond) - except: - """Renvoie tout le tag si le découpage à échouer""" + except (IndexError, ValueError, TypeError): + # Renvoie tout le tag si le découpage a échoué return (tag, 1.0) else: - """initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag""" + # initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération + # n'ait indiqué dans le tag return (tag, 1.0) -"""Tests: -from debug import * -from app.scodoc.sco_tag_module import * -_ = go_dept(app, 'RT').Notes - -t = ModuleTag( 'essai') -t.tag_module('totoro') # error (module invalide) -t.tag_module('MOD21460') -t.delete() # detruit tag et assoc -t = ModuleTag( 'essai2') -t.tag_module('MOD21460') -t.tag_module('MOD21464') -t.list_modules() -t.list_modules(formation_code='ccc') # empty list -t.list_modules(formation_code='FCOD2') - - -Un essai de get_etud_tagged_modules: -from debug import * -from app.scodoc.sco_tag_module import * -_ = go_dept(app, 'GEA').Notes - -etudid='GEAEID80687' -etud = sco_etud.get_etud_info( etudid=etudid, filled=True)[0] -sem = etud['sems'][0] - -[ tm['moy'] for tm in get_etud_tagged_modules( etudid, 'allo') ] - -# si besoin après modif par le Web: -# sco_cache.invalidate_formsemestre() -""" +def formation_tag_modules_by_type(formation: Formation): + """Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus' + Ne taggue pas les modules standards. + """ + tag_titles = { + m.type_abbrv() for m in formation.modules + } # usually {'res', 'mod', 'sae'} + tag_by_type = { + tag_title: NotesTag.get_or_create(title=tag_title, dept_id=formation.dept_id) + for tag_title in tag_titles + } + for module in formation.modules: + if module.module_type != scu.ModuleType.STANDARD: + module.add_tag(tag_by_type[module.type_abbrv()]) + db.session.commit() diff --git a/app/static/js/module_tag_editor.js b/app/static/js/module_tag_editor.js index e7927fa7a..89484c266 100644 --- a/app/static/js/module_tag_editor.js +++ b/app/static/js/module_tag_editor.js @@ -23,11 +23,36 @@ $(function () { // version readonly readOnlyTags($(".module_tag_editor_ro")); - $(".sco_tag_checkbox").click(function () { - if ($(this).is(":checked")) { - $(".sco_tag_edit").show(); - } else { - $(".sco_tag_edit").hide(); - } - }); + // $(".sco_tag_checkbox").click(function () { + // if ($(this).is(":checked")) { + // $(".sco_tag_edit").show(); + // } else { + // $(".sco_tag_edit").hide(); + // } + // }); }); + +// tags +function toggleEditDisplay(checkbox) { + const isChecked = checkbox.checked; + document.querySelectorAll('.sco_tag_edit').forEach(el => { + el.style.display = isChecked ? 'block' : 'none'; + }); + // form semection de semestres: + const showTagsInput = document.querySelector('input[name="show_tags"]'); + if (showTagsInput) { + showTagsInput.value = isChecked ? '1' : '0'; + } +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.sco_tag_checkbox').forEach(checkbox => { + // Set up initial state for each checkbox + toggleEditDisplay(checkbox); + + // Add click event listener to each checkbox + checkbox.addEventListener('click', function() { + toggleEditDisplay(this); + }); + }); +}); \ No newline at end of file diff --git a/app/views/notes.py b/app/views/notes.py index b29fd965b..0572353bc 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -623,6 +623,29 @@ sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) +@bp.route("/formation_tag_modules_by_type//") +@scodoc +@permission_required(Permission.EditFormationTags) +def formation_tag_modules_by_type(formation_id: int, semestre_idx: int): + """Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus' + Ne taggue pas les modules standards. + """ + formation = Formation.query.filter_by( + id=formation_id, dept_id=g.scodoc_dept_id + ).first_or_404() + sco_tag_module.formation_tag_modules_by_type(formation) + flash("Formation tagguée") + return flask.redirect( + url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + semestre_idx=semestre_idx, + formation_id=formation.id, + show_tags=1, + ) + ) + + @bp.route("/module_tag_set", methods=["POST"]) @scodoc @permission_required(Permission.EditFormationTags)