From 841ae1c7abb5feca24d7220054b85f18509295fe Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 4 Apr 2022 08:59:22 +0200 Subject: [PATCH 01/37] Fix: table recap style col. vide --- app/comp/res_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 1f19fbfd9..ed2232713 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -629,7 +629,7 @@ class ResultatsSemestre(ResultatsCache): c_class = f"_{col_id}_class" if "col_empty" in bottom_infos["moy"].get(c_class, ""): for row in rows: - row[c_class] += " col_empty" + row[c_class] = row.get(c_class, "") + " col_empty" titles[c_class] += " col_empty" for row in bottom_infos.values(): row[c_class] = row.get(c_class, "") + " col_empty" From 6b49c8472d3fe60b1f24bf47742d786a1e067b42 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 4 Apr 2022 09:35:52 +0200 Subject: [PATCH 02/37] =?UTF-8?q?Export=20excel=20depuis=20table=20recap:?= =?UTF-8?q?=20closes=20#351.=20Export=20codes=20Apo.=20Export=20notes=20?= =?UTF-8?q?=C3=A9valuations.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_common.py | 106 ++++++++++++++++++++++------ app/scodoc/sco_formsemestre_edit.py | 2 +- app/scodoc/sco_recapcomplet.py | 41 ++++++----- app/static/css/scodoc.css | 25 +++++++ app/static/js/table_recap.js | 32 +++++++-- 5 files changed, 160 insertions(+), 46 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index ed2232713..112cc6493 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -23,6 +23,7 @@ from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM +from app.scodoc import sco_evaluation_db from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_groups from app.scodoc import sco_users @@ -387,7 +388,7 @@ class ResultatsSemestre(ResultatsCache): # --- TABLEAU RECAP - def get_table_recap(self, convert_values=False): + def get_table_recap(self, convert_values=False, include_evaluations=False): """Result: tuple avec - rows: liste de dicts { column_id : value } - titles: { column_id : title } @@ -457,6 +458,11 @@ class ResultatsSemestre(ResultatsCache): idx = 0 # index de la colonne etud = Identite.query.get(etudid) row = {"etudid": etudid} + # --- Codes (seront cachés, mais exportés en excel) + idx = add_cell(row, "etudid", "etudid", etudid, "codes", idx) + idx = add_cell( + row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx + ) # --- Rang idx = add_cell( row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx @@ -618,11 +624,14 @@ class ResultatsSemestre(ResultatsCache): self._recap_add_partitions(rows, titles) self._recap_add_admissions(rows, titles) + # tri par rang croissant rows.sort(key=lambda e: e["_rang_order"]) # INFOS POUR FOOTER bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note) + if include_evaluations: + self._recap_add_evaluations(rows, titles, bottom_infos) # Ajoute style "col_empty" aux colonnes de modules vides for col_id in titles: @@ -641,7 +650,9 @@ class ResultatsSemestre(ResultatsCache): row["moy_gen"] = row.get("moy_gen", "") row["_moy_gen_class"] = "col_moy_gen" # titre de la ligne: - row["prenom"] = row["nom_short"] = bottom_line.capitalize() + row["prenom"] = row["nom_short"] = ( + row.get(f"_title", "") or bottom_line.capitalize() + ) row["_tr_class"] = bottom_line.lower() + ( (" " + row["_tr_class"]) if "_tr_class" in row else "" ) @@ -656,53 +667,58 @@ class ResultatsSemestre(ResultatsCache): def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict: """Les informations à mettre en bas de la table: min, max, moy, ECTS""" - row_min, row_max, row_moy, row_coef, row_ects = ( - {"_tr_class": "bottom_info"}, + row_min, row_max, row_moy, row_coef, row_ects, row_apo = ( + {"_tr_class": "bottom_info", "_title": "Min."}, {"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"}, + {"_tr_class": "bottom_info", "_title": "Code Apogée"}, ) # --- ECTS for ue in ues: - row_ects[f"moy_ue_{ue.id}"] = ue.ects - row_ects[f"_moy_ue_{ue.id}_class"] = "col_ue" + colid = f"moy_ue_{ue.id}" + row_ects[colid] = ue.ects + row_ects[f"_{colid}_class"] = "col_ue" # style cases vides pour borders verticales - row_coef[f"moy_ue_{ue.id}"] = "" - row_coef[f"_moy_ue_{ue.id}_class"] = "col_ue" + row_coef[colid] = "" + row_coef[f"_{colid}_class"] = "col_ue" + # row_apo[colid] = ue.code_apogee or "" row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]) row_ects["_moy_gen_class"] = "col_moy_gen" - # --- MIN, MAX, MOY + # --- MIN, MAX, MOY, APO row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min()) row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max()) row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean()) for ue in ues: - col_id = f"moy_ue_{ue.id}" - row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min()) - row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max()) - row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean()) - row_min[f"_{col_id}_class"] = "col_ue" - row_max[f"_{col_id}_class"] = "col_ue" - row_moy[f"_{col_id}_class"] = "col_ue" + colid = f"moy_ue_{ue.id}" + row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min()) + row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max()) + row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean()) + row_min[f"_{colid}_class"] = "col_ue" + row_max[f"_{colid}_class"] = "col_ue" + row_moy[f"_{colid}_class"] = "col_ue" + row_apo[colid] = ue.code_apogee or "" for modimpl in self.formsemestre.modimpls_sorted: if modimpl.id in modimpl_ids: - col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" + colid = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" if self.is_apc: coef = self.modimpl_coefs_df[modimpl.id][ue.id] else: coef = modimpl.module.coefficient or 0 - row_coef[col_id] = fmt_note(coef) + row_coef[colid] = fmt_note(coef) notes = self.modimpl_notes(modimpl.id, ue.id) - row_min[col_id] = fmt_note(np.nanmin(notes)) - row_max[col_id] = fmt_note(np.nanmax(notes)) + row_min[colid] = fmt_note(np.nanmin(notes)) + row_max[colid] = fmt_note(np.nanmax(notes)) moy = np.nanmean(notes) - row_moy[col_id] = fmt_note(moy) + row_moy[colid] = fmt_note(moy) if np.isnan(moy): # aucune note dans ce module - row_moy[f"_{col_id}_class"] = "col_empty" + row_moy[f"_{colid}_class"] = "col_empty" + row_apo[colid] = modimpl.module.code_apogee or "" return { # { key : row } avec key = min, max, moy, coef "min": row_min, @@ -710,6 +726,7 @@ class ResultatsSemestre(ResultatsCache): "moy": row_moy, "coef": row_coef, "ects": row_ects, + "apo": row_apo, } def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict): @@ -803,3 +820,48 @@ class ResultatsSemestre(ResultatsCache): row[f"{cid}"] = gr_name row[f"_{cid}_class"] = klass first_partition = False + + def _recap_add_evaluations( + self, rows: list[dict], titles: dict, bottom_infos: dict + ): + """Ajoute les colonnes avec les notes aux évaluations + rows est une liste de dict avec une clé "etudid" + Les colonnes ont la classe css "evaluation" + """ + # nouvelle ligne pour description évaluations: + bottom_infos["descr_evaluation"] = { + "_tr_class": "bottom_info", + "_title": "Description évaluation", + } + first = True + for modimpl in self.formsemestre.modimpls_sorted: + evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl) + eval_index = len(evals) - 1 + inscrits = {i.etudid for i in modimpl.inscriptions} + klass = "evaluation first" if first else "evaluation" + first = False + for i, e in enumerate(evals): + cid = f"eval_{e.id}" + titles[ + cid + ] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}' + titles[f"_{cid}_class"] = klass + titles[f"_{cid}_col_order"] = 9000 + i # à droite + eval_index -= 1 + notes_db = sco_evaluation_db.do_evaluation_get_all_notes( + e.evaluation_id + ) + for row in rows: + etudid = row["etudid"] + if etudid in inscrits: + if etudid in notes_db: + val = notes_db[etudid]["value"] + else: + # Note manquante mais prise en compte immédiate: affiche ATT + val = scu.NOTES_ATTENTE + row[cid] = scu.fmt_note(val) + row[f"_{cid}_class"] = klass + bottom_infos["coef"][cid] = e.coefficient + bottom_infos["min"][cid] = "0" + bottom_infos["max"][cid] = scu.fmt_note(e.note_max) + bottom_infos["descr_evaluation"][cid] = e.description or "" diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 685707b99..7d903f6cf 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -1314,7 +1314,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new) def formsemestre_delete(formsemestre_id): """Delete a formsemestre (affiche avertissements)""" - sem = sco_formsemestre.get_formsemestre(formsemestre_id) + sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] H = [ html_sco_header.html_sem_header("Suppression du semestre"), diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index a00e7df39..f7b365ed0 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -130,12 +130,10 @@ def formsemestre_recapcomplet( '' - % formsemestre_id, - '', - ] - + H.append( + f"""
+ + """ + ) if modejury: H.append( - '' - % modejury + f'' ) H.append( '") H.append( - f""" (cliquer sur un nom pour afficher son bulletin ou - ici avoir le classeur papier) + f""" (cliquer sur un nom pour afficher son bulletin ou ici avoir le classeur papier) """ ) data = do_formsemestre_recapcomplet( formsemestre_id, format=tabformat, - hidemodules=hidemodules, - hidebac=hidebac, modejury=modejury, sortcol=sortcol, xml_with_decisions=xml_with_decisions, @@ -168,24 +160,27 @@ def formsemestre_recapcomplet( return response H.append(data) - if not isFile: + if not is_file: if len(formsemestre.inscriptions) > 0: H.append("
") H.append( - """

Voir les décisions du jury

""" - % formsemestre_id + f"""

Voir les décisions du jury

""" ) if sco_permissions_check.can_validate_sem(formsemestre_id): H.append("

") if modejury: H.append( - """Calcul automatique des décisions du jury

""" - % (formsemestre_id,) + f"""Calcul automatique des décisions du jury

""" ) else: H.append( - """Saisie des décisions du jury""" - % formsemestre_id + f"""Saisie des décisions du jury""" ) H.append("

") if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): @@ -219,684 +214,59 @@ def do_formsemestre_recapcomplet( ): """Calcule et renvoie le tableau récapitulatif.""" formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + + filename = scu.sanitize_filename( + f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + if (format == "html" or format == "evals") and not modejury: res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - data, filename = gen_formsemestre_recapcomplet_html( - formsemestre, res, include_evaluations=(format == "evals") + data = gen_formsemestre_recapcomplet_html( + formsemestre, + res, + include_evaluations=(format == "evals"), + filename=filename, ) - else: - data, filename, format = make_formsemestre_recapcomplet( - formsemestre_id=formsemestre_id, - format=format, - hidemodules=hidemodules, - hidebac=hidebac, - xml_nodate=xml_nodate, - modejury=modejury, - sortcol=sortcol, - xml_with_decisions=xml_with_decisions, - disable_etudlink=disable_etudlink, - rank_partition_id=rank_partition_id, - force_publishing=force_publishing, - ) - # --- - if format == "xml" or format == "html" or format == "evals": return data - elif format == "csv": - return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE) - elif format.startswith("xls") or format.startswith("xlsx"): - return scu.send_file(data, filename=filename, mime=scu.XLSX_MIMETYPE) - elif format == "json": - js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder) - return scu.send_file( - js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE + elif format.startswith("xls") or format == "csv": + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + include_evaluations = format in {"xlsall", "csv "} + if format != "csv": + format = "xlsx" + data, filename = gen_formsemestre_recapcomplet_excel( + formsemestre, + res, + include_evaluations=include_evaluations, + format=format, + filename=filename, ) - else: - raise ValueError(f"unknown format {format}") - - -def make_formsemestre_recapcomplet( - formsemestre_id=None, - format="html", # html, evals, xml, json - hidemodules=False, # ne pas montrer les modules (ignoré en XML) - hidebac=False, # pas de colonne Bac (ignoré en XML) - xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML) - modejury=False, # saisie décisions jury - sortcol=None, # indice colonne a trier dans table T - xml_with_decisions=False, - disable_etudlink=False, - rank_partition_id=None, # si None, calcul rang global - force_publishing=True, # donne bulletins JSON/XML meme si non publiés -): - """Grand tableau récapitulatif avec toutes les notes de modules - pour tous les étudiants, les moyennes par UE et générale, - trié par moyenne générale décroissante. - """ - civ_nom_prenom = False # 3 colonnes différentes ou une seule avec prénom abrégé ? - if format == "xml": - return _formsemestre_recapcomplet_xml( + return scu.send_file(data, filename=filename, mime=scu.get_mime_suffix(format)) + elif format == "xml": + data = gen_formsemestre_recapcomplet_xml( formsemestre_id, xml_nodate, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, ) + return scu.send_file(data, filename=filename, suffix=scu.XML_SUFFIX) elif format == "json": - return _formsemestre_recapcomplet_json( + data = gen_formsemestre_recapcomplet_json( formsemestre_id, xml_nodate=xml_nodate, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, ) - if format[:3] == "xls": - civ_nom_prenom = True # 3 cols: civilite, nom, prenom - keep_numeric = True # pas de conversion des notes en strings - else: - keep_numeric = False + return scu.sendJSON(data, filename=filename) - if hidebac: - admission_extra_cols = [] - else: - admission_extra_cols = [ - "type_admission", - "classement", - "apb_groupe", - "apb_classement_gr", - ] - - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - # A ré-écrire XXX - sem = sco_formsemestre.do_formsemestre_list( - args={"formsemestre_id": formsemestre_id} - )[0] - parcours = formsemestre.formation.get_parcours() - - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - modimpls = formsemestre.modimpls_sorted - ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport - - partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups( - formsemestre_id - ) - if rank_partition_id and format == "html": - # Calcul rang sur une partition et non sur l'ensemble - # seulement en format HTML (car colonnes rangs toujours presentes en xls) - rank_partition = sco_groups.get_partition(rank_partition_id) - rank_label = "Rg (%s)" % rank_partition["partition_name"] - else: - rank_partition = sco_groups.get_default_partition(formsemestre_id) - rank_label = "Rg" - - T = nt.get_table_moyennes_triees() - if not T: - return "", "", format - - # Construit une liste de listes de chaines: le champs du tableau resultat (HTML ou CSV) - F = [] - h = [rank_label] - if civ_nom_prenom: - h += ["Civilité", "Nom", "Prénom"] - else: - h += ["Nom"] - if not hidebac: - h.append("Bac") - - # Si CSV ou XLS, indique tous les groupes - if format[:3] == "xls" or format == "csv": - for partition in partitions: - h.append("%s" % partition["partition_name"]) - else: - h.append("Gr") - - h.append("Moy") - # Ajoute rangs dans groupe seulement si CSV ou XLS - if format[:3] == "xls" or format == "csv": - for partition in partitions: - h.append("rang_%s" % partition["partition_name"]) - - cod2mod = {} # code : moduleimpl - mod_evals = {} # moduleimpl_id : liste de toutes les evals de ce module - for ue in ues: - if ue["type"] != UE_SPORT: - h.append(ue["acronyme"]) - else: # UE_SPORT: - # n'affiche pas la moyenne d'UE dans ce cas - # mais laisse col. vide si modules affichés (pour séparer les UE) - if not hidemodules: - h.append("") - pass - if not hidemodules and not ue["is_external"]: - for modimpl in modimpls: - if modimpl.module.ue_id == ue["ue_id"]: - code = modimpl.module.code - h.append(code) - cod2mod[code] = modimpl # pour fabriquer le lien - if format == "xlsall": - evals = nt.modimpls_results[ - modimpl.id - ].get_evaluations_completes(modimpl) - # evals = nt.get_mod_evaluation_etat_list(... - mod_evals[modimpl.id] = evals - h += _list_notes_evals_titles(code, evals) - - h += admission_extra_cols - h += ["code_nip", "etudid"] - F.append(h) - - def fmtnum(val): # conversion en nombre pour cellules excel - if keep_numeric: - try: - return float(val) - except: - return val - else: - return val - - # Compte les decisions de jury - codes_nb = scu.DictDefault(defaultvalue=0) - # - is_dem = {} # etudid : bool - for t in T: - etudid = t[-1] - dec = nt.get_etud_decision_sem(etudid) - if dec: - codes_nb[dec["code"]] += 1 - etud_etat = nt.get_etud_etat(etudid) - if etud_etat == "D": - gr_name = "Dém." - is_dem[etudid] = True - elif etud_etat == DEF: - gr_name = "Déf." - is_dem[etudid] = False - else: - group = sco_groups.get_etud_main_group(etudid, formsemestre_id) - gr_name = group["group_name"] or "" - is_dem[etudid] = False - if rank_partition_id: - rang_gr, _, rank_gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt - ) - if rank_gr_name[rank_partition_id]: - rank = "%s %s" % ( - rank_gr_name[rank_partition_id], - rang_gr[rank_partition_id], - ) - else: - rank = "" - else: - rank = nt.get_etud_rang(etudid) - - e = nt.identdict[etudid] - if civ_nom_prenom: - sco_etud.format_etud_ident(e) - l = [rank, e["civilite_str"], e["nom_disp"], e["prenom"]] # civ, nom prenom - else: - l = [rank, nt.get_nom_short(etudid)] # rang, nom, - - e["admission"] = {} - if not hidebac: - e["admission"] = nt.etuds_dict[etudid].admission.first() - if e["admission"]: - bac = nt.etuds_dict[etudid].admission[0].get_bac() - l.append(bac.abbrev()) - else: - l.append("") - - if format[:3] == "xls" or format == "csv": # tous les groupes - for partition in partitions: - group = partitions_etud_groups[partition["partition_id"]].get( - etudid, None - ) - if group: - l.append(group["group_name"]) - else: - l.append("") - else: - l.append(gr_name) # groupe - - # Moyenne générale - l.append(fmtnum(scu.fmt_note(t[0], keep_numeric=keep_numeric))) - # Ajoute rangs dans groupes seulement si CSV ou XLS - if format[:3] == "xls" or format == "csv": - rang_gr, _, gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt - ) - - for partition in partitions: - l.append(rang_gr[partition["partition_id"]]) - - # Nombre d'UE au dessus de 10 - # t[i] est une chaine :-) - # nb_ue_ok = sum( - # [t[i] > 10 for i, ue in enumerate(ues, start=1) if ue["type"] != UE_SPORT] - # ) - ue_index = [] # indices des moy UE dans l (pour appliquer style css) - for i, ue in enumerate(ues, start=1): - if ue["type"] != UE_SPORT: - l.append( - fmtnum(scu.fmt_note(t[i], keep_numeric=keep_numeric)) - ) # moyenne etud dans ue - else: # UE_SPORT: - # n'affiche pas la moyenne d'UE dans ce cas - if not hidemodules: - l.append("") - ue_index.append(len(l) - 1) - if not hidemodules and not ue["is_external"]: - j = 0 - for modimpl in modimpls: - if modimpl.module.ue_id == ue["ue_id"]: - l.append( - fmtnum( - scu.fmt_note( - t[j + len(ues) + 1], keep_numeric=keep_numeric - ) - ) - ) # moyenne etud dans module - if format == "xlsall": - l += _list_notes_evals(mod_evals[modimpl.id], etudid) - j += 1 - if not hidebac: - for k in admission_extra_cols: - l.append(getattr(e["admission"], k, "") or "") - l.append( - nt.identdict[etudid]["code_nip"] or "" - ) # avant-derniere colonne = code_nip - l.append(etudid) # derniere colonne = etudid - F.append(l) - - # Dernière ligne: moyennes, min et max des UEs et modules - if not hidemodules: # moy/min/max dans chaque module - mods_stats = {} # moduleimpl_id : stats - for modimpl in modimpls: - mods_stats[modimpl.id] = nt.get_mod_stats(modimpl.id) - - def add_bottom_stat(key, title, corner_value=""): - l = ["", title] - if civ_nom_prenom: - l += ["", ""] - if not hidebac: - l.append("") - if format[:3] == "xls" or format == "csv": - l += [""] * len(partitions) - else: - l += [""] - l.append(corner_value) - if format[:3] == "xls" or format == "csv": - for _ in partitions: - l += [""] # rangs dans les groupes - for ue in ues: - if ue["type"] != UE_SPORT: - if key == "nb_valid_evals": - l.append("") - elif key == "coef": - if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): - l.append("%2.3f" % ue["coefficient"]) - else: - l.append("") - else: - if key == "ects": - if keep_numeric: - l.append(ue[key]) - else: - l.append(str(ue[key])) - else: - l.append(scu.fmt_note(ue[key], keep_numeric=keep_numeric)) - else: # UE_SPORT: - # n'affiche pas la moyenne d'UE dans ce cas - if not hidemodules: - l.append("") - # ue_index.append(len(l) - 1) - if not hidemodules and not ue["is_external"]: - for modimpl in modimpls: - if modimpl.module.ue_id == ue["ue_id"]: - if key == "coef": - coef = modimpl.module.coefficient - if format[:3] != "xls": - coef = str(coef) - l.append(coef) - elif key == "ects": - l.append("") # ECTS module ? - else: - val = mods_stats[modimpl.id][key] - if key == "nb_valid_evals": - if ( - format[:3] != "xls" - ): # garde val numerique pour excel - val = str(val) - else: # moyenne du module - val = scu.fmt_note(val, keep_numeric=keep_numeric) - l.append(val) - - if format == "xlsall": - l += _list_notes_evals_stats(mod_evals[modimpl.id], key) - if modejury: - l.append("") # case vide sur ligne "Moyennes" - - l += [""] * len(admission_extra_cols) # infos admission vides ici - F.append(l + ["", ""]) # ajoute cellules code_nip et etudid inutilisees ici - - add_bottom_stat( - "min", "Min", corner_value=scu.fmt_note(nt.moy_min, keep_numeric=keep_numeric) - ) - add_bottom_stat( - "max", "Max", corner_value=scu.fmt_note(nt.moy_max, keep_numeric=keep_numeric) - ) - add_bottom_stat( - "moy", - "Moyennes", - corner_value=scu.fmt_note(nt.moy_moy, keep_numeric=keep_numeric), - ) - add_bottom_stat("coef", "Coef") - add_bottom_stat("nb_valid_evals", "Nb évals") - add_bottom_stat("ects", "ECTS") - - # Génération de la table au format demandé - if format == "html": - # Table format HTML - H = [ - """ - - - """ - ] - if sortcol: # sort table using JS sorttable - H.append( - """ - """ - % (int(sortcol)) - ) - - ligne_titres_head = _ligne_titres( - ue_index, F, cod2mod, modejury, with_modules_links=False - ) - ligne_titres_foot = _ligne_titres( - ue_index, F, cod2mod, modejury, with_modules_links=True - ) - - H.append("\n" + ligne_titres_head + "\n\n\n") - if disable_etudlink: - etudlink = "%(name)s" - else: - etudlink = """%(name)s""" - ir = 0 - nblines = len(F) - 1 - for l in F[1:]: - etudid = l[-1] - if ir == nblines - 6: - H.append("") - H.append("") - if ir >= nblines - 6: - # dernieres lignes: - el = l[1] - styl = ( - "recap_row_min", - "recap_row_max", - "recap_row_moy", - "recap_row_coef", - "recap_row_nbeval", - "recap_row_ects", - )[ir - nblines + 6] - cells = f'' - else: - el = etudlink % { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - "name": l[1], - } - if ir % 2 == 0: - cells = f'' - else: - cells = f'' - ir += 1 - # XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ] - # notes sans le NA: - nsn = l[:-2] # copy - for i, _ in enumerate(nsn): - if nsn[i] == "NA": - nsn[i] = "-" - try: - order = int(nsn[0].split()[0]) - except: - order = 99999 - cells += ( - f'' # rang - ) - cells += '' % el # nom etud (lien) - if not hidebac: - cells += '' % nsn[2] # bac - idx_col_gr = 3 - else: - idx_col_gr = 2 - cells += '' % nsn[idx_col_gr] # group name - - # Style si moyenne generale < barre - idx_col_moy = idx_col_gr + 1 - cssclass = "recap_col_moy" - try: - if float(nsn[idx_col_moy]) < (parcours.BARRE_MOY - scu.NOTES_TOLERANCE): - cssclass = "recap_col_moy_inf" - except: - pass - cells += '' % (cssclass, nsn[idx_col_moy]) - ue_number = 0 - for i in range(idx_col_moy + 1, len(nsn)): - if i in ue_index: - cssclass = "recap_col_ue" - # grise si moy UE < barre - ue = ues[ue_number] - ue_number += 1 - - if (ir < (nblines - 4)) or (ir == nblines - 3): - try: - if float(nsn[i]) < parcours.get_barre_ue( - ue["type"] - ): # NOTES_BARRE_UE - cssclass = "recap_col_ue_inf" - elif float(nsn[i]) >= parcours.NOTES_BARRE_VALID_UE: - cssclass = "recap_col_ue_val" - except: - pass - else: - cssclass = "recap_col" - if ( - ir == nblines - 3 - ): # si moyenne generale module < barre ue, surligne: - try: - if float(nsn[i]) < parcours.get_barre_ue(ue["type"]): - cssclass = "recap_col_moy_inf" - except: - pass - cells += '' % (cssclass, nsn[i]) - if modejury and etudid: - decision_sem = nt.get_etud_decision_sem(etudid) - if is_dem[etudid]: - code = "DEM" - act = "" - elif decision_sem: - code = decision_sem["code"] - act = "(modifier)" - else: - code = "" - act = "saisir" - cells += '" - H.append(cells + "") - - H.append(ligne_titres_foot) - H.append("") - H.append("
{nsn[0]}%s%s%s%s%s%s' % code - if act: - # cells += ' %s' % (formsemestre_id, etudid, act) - cells += ( - """ %s""" - % (formsemestre_id, etudid, act) - ) - cells += "
") - - # Form pour choisir partition de classement: - if not modejury and partitions: - H.append("Afficher le rang des groupes de: ") - if not rank_partition_id: - checked = "checked" - else: - checked = "" - H.append( - 'tous ' - % (checked) - ) - for p in partitions: - if p["partition_id"] == rank_partition_id: - checked = "checked" - else: - checked = "" - H.append( - '%s ' - % (p["partition_id"], checked, p["partition_name"]) - ) - - # recap des decisions jury (nombre dans chaque code): - if codes_nb: - H.append("

Décisions du jury

") - cods = list(codes_nb.keys()) - cods.sort() - for cod in cods: - H.append("" % (cod, codes_nb[cod])) - H.append("
%s%d
") - # Avertissements - if formsemestre.formation.is_apc(): - H.append( - """

Pour les formations par compétences (comme le BUT), la moyenne générale est purement indicative et ne devrait pas être communiquée aux étudiants.

""" - ) - return "\n".join(H), "", "html" - elif format == "csv": - CSV = scu.CSV_LINESEP.join( - [scu.CSV_FIELDSEP.join([str(x) for x in l]) for l in F] - ) - semname = sem["titre_num"].replace(" ", "_") - date = time.strftime("%d-%m-%Y") - filename = "notes_modules-%s-%s.csv" % (semname, date) - return CSV, filename, "csv" - elif format[:3] == "xls": - semname = sem["titre_num"].replace(" ", "_") - date = time.strftime("%d-%m-%Y") - if format == "xls": - filename = "notes_modules-%s-%s%s" % (semname, date, scu.XLSX_SUFFIX) - else: - filename = "notes_modules_evals-%s-%s%s" % (semname, date, scu.XLSX_SUFFIX) - sheet_name = "notes %s %s" % (semname, date) - if len(sheet_name) > 31: - sheet_name = "notes %s %s" % ("...", date) - xls = sco_excel.excel_simple_table( - titles=["etudid", "code_nip"] + F[0][:-2], - lines=[ - [x[-1], x[-2]] + x[:-2] for x in F[1:] - ], # reordonne cols (etudid et nip en 1er), - sheet_name=sheet_name, - ) - return xls, filename, "xls" - else: - raise ValueError("unknown format %s" % format) + raise ScoValueError(f"Format demandé invalide: {format}") -def _ligne_titres(ue_index, F, cod2mod, modejury, with_modules_links=True): - """Cellules de la ligne de titre (haut ou bas)""" - cells = '' - for i in range(len(F[0]) - 2): - if i in ue_index: - cls = "recap_tit_ue" - else: - cls = "recap_tit" - attr = f'class="{cls}"' - if i == 0 or F[0][i] == "classement": # Rang: force tri numerique - try: - order = int(F[0][i].split()[0]) - except: - order = 99999 - attr += f' data-order="{order:05d}"' - if F[0][i] in cod2mod: # lien vers etat module - modimpl = cod2mod[F[0][i]] - if with_modules_links: - href = url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ) - else: - href = "" - cells += f"""{F[0][i]}""" - else: - cells += f"{F[0][i]}" - if modejury: - cells += 'Décision' - return cells + "" - - -def _list_notes_evals(evals: list[Evaluation], etudid: int) -> list[str]: - """Liste des notes des evaluations completes de ce module - (pour table xls avec evals) - """ - L = [] - for e in evals: - notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id) - if etudid in notes_db: - val = notes_db[etudid]["value"] - else: - # Note manquante mais prise en compte immédiate: affiche ATT - val = scu.NOTES_ATTENTE - val_fmt = scu.fmt_note(val, keep_numeric=True) - L.append(val_fmt) - return L - - -def _list_notes_evals_titles(codemodule: str, evals: list[Evaluation]) -> list[str]: - """Liste des titres des evals completes""" - L = [] - eval_index = len(evals) - 1 - for e in evals: - L.append( - codemodule - + "-" - + str(eval_index) - + "-" - + (e.jour.isoformat() if e.jour else "") - ) - eval_index -= 1 - return L - - -def _list_notes_evals_stats(evals: list[Evaluation], key: str) -> list[str]: - """Liste des stats (moy, ou rien!) des evals completes""" - L = [] - for e in evals: - if key == "moy": - # TODO #sco92 - # val = e["etat"]["moy_num"] - # L.append(scu.fmt_note(val, keep_numeric=True)) - L.append("") - elif key == "max": - L.append(e.note_max) - elif key == "min": - L.append(0.0) - elif key == "coef": - L.append(e.coefficient) - else: - L.append("") # on n'a pas sous la main min/max - return L - - -def _formsemestre_recapcomplet_xml( +def gen_formsemestre_recapcomplet_xml( formsemestre_id, xml_nodate, xml_with_decisions=False, force_publishing=True, -): +) -> str: "XML export: liste tous les bulletins XML." formsemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) @@ -931,23 +301,19 @@ def _formsemestre_recapcomplet_xml( xml_nodate=xml_nodate, xml_with_decisions=xml_with_decisions, ) - return ( - sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING), - "", - "xml", - ) + return ElementTree.tostring(doc).decode(scu.SCO_ENCODING) -def _formsemestre_recapcomplet_json( +def gen_formsemestre_recapcomplet_json( formsemestre_id, xml_nodate=False, xml_with_decisions=False, force_publishing=True, -): +) -> dict: """JSON export: liste tous les bulletins JSON :param xml_nodate(bool): indique la date courante (attribut docdate) :param force_publishing: donne les bulletins même si non "publiés sur portail" - :returns: dict, "", "json" + :returns: dict """ formsemestre = FormSemestre.query.get_or_404(formsemestre_id) is_apc = formsemestre.formation.is_apc() @@ -957,7 +323,7 @@ def _formsemestre_recapcomplet_json( else: docdate = datetime.datetime.now().isoformat() evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) - J = { + js_data = { "docdate": docdate, "formsemestre_id": formsemestre_id, "evals_info": { @@ -968,7 +334,7 @@ def _formsemestre_recapcomplet_json( }, "bulletins": [], } - bulletins = J["bulletins"] + bulletins = js_data["bulletins"] formsemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) T = nt.get_table_moyennes_triees() @@ -986,7 +352,7 @@ def _formsemestre_recapcomplet_json( xml_with_decisions=xml_with_decisions, ) bulletins.append(bul) - return J, "", "json" + return js_data def formsemestres_bulletins(annee_scolaire): @@ -994,16 +360,16 @@ def formsemestres_bulletins(annee_scolaire): :param annee_scolaire(int): année de début de l'année scolaire :returns: JSON """ - jslist = [] + js_list = [] sems = sco_formsemestre.list_formsemestre_by_etape(annee_scolaire=annee_scolaire) log("formsemestres_bulletins(%s): %d sems" % (annee_scolaire, len(sems))) for sem in sems: - J, _, _ = _formsemestre_recapcomplet_json( + js_data = gen_formsemestre_recapcomplet_json( sem["formsemestre_id"], force_publishing=False ) - jslist.append(J) + js_list.append(js_data) - return scu.sendJSON(jslist) + return scu.sendJSON(js_list) def _gen_cell(key: str, row: dict, elt="td"): @@ -1029,15 +395,16 @@ def _gen_row(keys: list[str], row, elt="td"): def gen_formsemestre_recapcomplet_html( - formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False + formsemestre: FormSemestre, + res: NotesTableCompat, + include_evaluations=False, + filename="", ): """Construit table recap pour le BUT Cache le résultat pour le semestre. Return: data, filename + data est une chaine, le
...
incluant le tableau. """ - filename = scu.sanitize_filename( - f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" - ) if include_evaluations: table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) else: @@ -1051,7 +418,7 @@ def gen_formsemestre_recapcomplet_html( else: sco_cache.TableRecapCache.set(formsemestre.id, table_html) - return table_html, filename + return table_html def _gen_formsemestre_recapcomplet_html( @@ -1066,8 +433,7 @@ def _gen_formsemestre_recapcomplet_html( ) if not rows: return ( - '
aucun étudiant !
', - "", + '
aucun étudiant !
' ) H = [ f"""
tuple: + """Génère le tableau recap en excel (xlsx) ou CSV. + Utilisé pour archives et autres besoins particuliers (API). + Attention: le tableau exporté depuis la page html est celui généré en js par DataTables, + et non celui-ci. + """ + suffix = scu.CSV_SUFFIX if format == "csv" else scu.XLSX_SUFFIX + filename += suffix + rows, footer_rows, titles, column_ids = res.get_table_recap( + convert_values=False, include_evaluations=include_evaluations + ) + + tab = GenTable( + columns_ids=column_ids, + titles=titles, + rows=rows + footer_rows, + preferences=sco_preferences.SemPreferences(formsemestre_id=formsemestre.id), + ) + + return tab.gen(format=format), filename diff --git a/sco_version.py b/sco_version.py index aced26cfe..4e9e9ad14 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.91" +SCOVERSION = "9.1.92" SCONAME = "ScoDoc" From ec57ba4ef75cfac158b2942a5c81403988a8ee2e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Apr 2022 09:46:25 +0200 Subject: [PATCH 11/37] =?UTF-8?q?Mise=20=C3=A0=20jour=20bonus=20B=C3=A9thu?= =?UTF-8?q?ne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/bonus_spo.py | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 5a549132b..416a0d0ee 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -418,17 +418,46 @@ class BonusAmiens(BonusSportAdditif): class BonusBethune(BonusSportMultiplicatif): - """Calcul bonus modules optionnels (sport), règle IUT de Béthune. - - Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre. - Ce bonus est égal au nombre de points divisé par 200 et multiplié par la - moyenne générale du semestre de l'étudiant. + """ + Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune. +

+ Pour le BUT : + La note de sport est sur 20, et on calcule une bonification (en %) + qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant + la formule : bonification (en %) = max(note-10, 0)*(1/500). +

+ La bonification ne s'applique que si la note est supérieure à 10. +

+ (Une note de 10 donne donc 0% de bonif, + 1 point au dessus de 10 augmente la moyenne des UE de 0.2%) +

+

+ Pour le DUT/LP : + La note de sport est sur 20, et on calcule une bonification (en %) + qui va s'appliquer à la moyenne générale du semestre en appliquant + la formule : bonification (en %) = max(note-10, 0)*(1/200). +

+ La bonification ne s'applique que si la note est supérieure à 10. +

+ (Une note de 10 donne donc 0% de bonif, + 1 point au dessus de 10 augmente la moyenne des UE de 0.5%) +

""" name = "bonus_iutbethune" displayed_name = "IUT de Béthune" - seuil_moy_gen = 10.0 - amplitude = 0.005 + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + if self.formsemestre.formation.is_apc(): + self.amplitude = 0.002 + else: + self.amplitude = 0.005 + + return super().compute_bonus( + sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + ) class BonusBezier(BonusSportAdditif): From 7a8c77add43ac1efe78fbe3ce53996720104c738 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Apr 2022 23:31:08 +0200 Subject: [PATCH 12/37] =?UTF-8?q?Tableau=20saisie=20jury=20bas=C3=A9=20sur?= =?UTF-8?q?=20recap=5Fcomplet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_common.py | 21 +- app/scodoc/sco_formsemestre_status.py | 3 - app/scodoc/sco_formsemestre_validation.py | 93 ++++++--- app/scodoc/sco_recapcomplet.py | 242 +++++++++++----------- app/static/css/scodoc.css | 12 ++ app/static/js/table_recap.js | 86 ++++---- 6 files changed, 260 insertions(+), 197 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index d7240598a..7245439f4 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -18,7 +18,7 @@ from app.auth.models import User from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, FormSemestreUECoef +from app.models import FormSemestre, FormSemestreUECoef, formsemestre from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns @@ -388,7 +388,9 @@ class ResultatsSemestre(ResultatsCache): # --- TABLEAU RECAP - def get_table_recap(self, convert_values=False, include_evaluations=False): + def get_table_recap( + self, convert_values=False, include_evaluations=False, modejury=False + ): """Result: tuple avec - rows: liste de dicts { column_id : value } - titles: { column_id : title } @@ -538,6 +540,9 @@ class ResultatsSemestre(ResultatsCache): titles_bot[ f"_{col_id}_target_attrs" ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ + if modejury: + # pas d'autre colonnes de résultats + continue # Bonus (sport) dans cette UE ? # Le bonus sport appliqué sur cette UE if (self.bonus_ues is not None) and (ue.id in self.bonus_ues): @@ -632,6 +637,18 @@ class ResultatsSemestre(ResultatsCache): elif nb_ues_validables < len(ues_sans_bonus): row["_ues_validables_class"] += " moy_inf" row["_ues_validables_order"] = nb_ues_validables # pour tri + if modejury: + idx = add_cell( + row, + "jury_link", + "", + f"""saisir décision""", + "col_jury_link", + 1000, + ) rows.append(row) self._recap_add_partitions(rows, titles) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 275605d88..50b11f6fa 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -404,9 +404,6 @@ def formsemestre_status_menubar(sem): "args": { "formsemestre_id": formsemestre_id, "modejury": 1, - "hidemodules": 1, - "hidebac": 1, - "pref_override": 0, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), }, diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index ba517e6ae..f4896a8dc 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -31,6 +31,7 @@ import time import flask from flask import url_for, g, request +from app.models.etudiants import Identite import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -107,29 +108,57 @@ def formsemestre_validation_etud_form( if not Se.sem["etat"]: raise ScoValueError("validation: semestre verrouille") + url_tableau = url_for( + "notes.formsemestre_recapcomplet", + scodoc_dept=g.scodoc_dept, + modejury=1, + formsemestre_id=formsemestre_id, + selected_etudid=etudid, # va a la bonne ligne + ) + H = [ html_sco_header.sco_header( - page_title="Parcours %(nomprenom)s" % etud, + page_title=f"Parcours {etud['nomprenom']}", javascripts=["js/recap_parcours.js"], ) ] - Footer = ["

"] # Navigation suivant/precedent - if etud_index_prev != None: - etud_p = sco_etud.get_etud_info(etudid=T[etud_index_prev][-1], filled=True)[0] - Footer.append( - 'Etud. précédent (%s)' - % (formsemestre_id, etud_index_prev, etud_p["nomprenom"]) + if etud_index_prev is not None: + etud_prev = Identite.query.get(T[etud_index_prev][-1]) + url_prev = url_for( + "notes.formsemestre_validation_etud_form", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etud_index=etud_index_prev, ) - if etud_index_next != None: - etud_n = sco_etud.get_etud_info(etudid=T[etud_index_next][-1], filled=True)[0] - Footer.append( - 'Etud. suivant (%s)' - % (formsemestre_id, etud_index_next, etud_n["nomprenom"]) + else: + url_prev = None + if etud_index_next is not None: + etud_next = Identite.query.get(T[etud_index_next][-1]) + url_next = url_for( + "notes.formsemestre_validation_etud_form", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etud_index=etud_index_next, ) - Footer.append("

") - Footer.append(html_sco_header.sco_footer()) + else: + url_next = None + footer = ["""") + + footer.append(html_sco_header.sco_footer()) H.append('
{"".join([_gen_cell(key, row, elt) for key in keys])}' + tr_id = ( + f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else "" + ) + return f'{"".join([_gen_cell(key, row, elt) for key in keys])}' def gen_formsemestre_recapcomplet_html( formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False, + modejury=False, filename="", + selected_etudid=None, ): """Construit table recap pour le BUT - Cache le résultat pour le semestre. + Cache le résultat pour le semestre (sauf en mode jury). + + Si modejury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury + Return: data, filename data est une chaine, le
...
incluant le tableau. """ - if include_evaluations: - table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) - else: - table_html = sco_cache.TableRecapCache.get(formsemestre.id) - if table_html is None: - table_html = _gen_formsemestre_recapcomplet_html( - formsemestre, res, include_evaluations, filename - ) + table_html = None + if not (modejury or selected_etudid): if include_evaluations: - sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html) + table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) else: - sco_cache.TableRecapCache.set(formsemestre.id, table_html) + table_html = sco_cache.TableRecapCache.get(formsemestre.id) + if modejury or (table_html is None): + table_html = _gen_formsemestre_recapcomplet_html( + formsemestre, + res, + include_evaluations, + modejury, + filename, + selected_etudid=selected_etudid, + ) + if not modejury: + if include_evaluations: + sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html) + else: + sco_cache.TableRecapCache.set(formsemestre.id, table_html) return table_html @@ -425,11 +426,13 @@ def _gen_formsemestre_recapcomplet_html( formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False, + modejury=False, filename: str = "", + selected_etudid=None, ) -> str: """Génère le html""" rows, footer_rows, titles, column_ids = res.get_table_recap( - convert_values=True, include_evaluations=include_evaluations + convert_values=True, include_evaluations=include_evaluations, modejury=modejury ) if not rows: return ( @@ -437,7 +440,8 @@ def _gen_formsemestre_recapcomplet_html( ) H = [ f"""
') if not check: @@ -171,7 +200,7 @@ def formsemestre_validation_etud_form( """ ) ) - return "\n".join(H + Footer) + return "\n".join(H + footer) H.append( formsemestre_recap_parcours_table( @@ -180,18 +209,10 @@ def formsemestre_validation_etud_form( ) if check: if not desturl: - desturl = url_for( - "notes.formsemestre_recapcomplet", - scodoc_dept=g.scodoc_dept, - modejury=1, - formsemestre_id=formsemestre_id, - sortcol=sortcol - or None, # pour refaire tri sorttable du tableau de notes - _anchor="etudid%s" % etudid, # va a la bonne ligne - ) + desturl = url_tableau H.append(f'') - return "\n".join(H + Footer) + return "\n".join(H + footer) decision_jury = Se.nt.get_etud_decision_sem(etudid) @@ -207,7 +228,7 @@ def formsemestre_validation_etud_form( """ ) ) - return "\n".join(H + Footer) + return "\n".join(H + footer) # Infos si pas de semestre précédent if not Se.prev: @@ -345,7 +366,7 @@ def formsemestre_validation_etud_form( else: H.append("sans semestres décalés

") - return "".join(H + Footer) + return "".join(H + footer) def formsemestre_validation_etud( @@ -937,19 +958,23 @@ def do_formsemestre_validation_auto(formsemestre_id): ) if conflicts: H.append( - """

Attention: %d étudiants non modifiés car décisions différentes - déja saisies :

    """ - % len(conflicts) + f"""

    Attention: {len(conflicts)} étudiants non modifiés + car décisions différentes déja saisies : +

    ") H.append( - 'continuer' - % formsemestre_id + f"""continuer""" ) H.append(html_sco_header.sco_footer()) return "\n".join(H) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 72d16012c..0dc6b19c4 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -32,7 +32,7 @@ import time from xml.etree import ElementTree from flask import g, request -from flask import make_response, url_for +from flask import url_for from app import log from app.but import bulletin_but @@ -40,39 +40,29 @@ from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.etudiants import Identite -from app.models.evaluations import Evaluation from app.scodoc.gen_tables import GenTable import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_xml -from app.scodoc import sco_bulletins, sco_excel from app.scodoc import sco_cache -from app.scodoc import sco_codes_parcours from app.scodoc import sco_evaluations -from app.scodoc import sco_evaluation_db from app.scodoc.sco_exceptions import ScoValueError -from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_status -from app.scodoc import sco_groups from app.scodoc import sco_permissions_check from app.scodoc import sco_preferences -from app.scodoc import sco_etud -from app.scodoc import sco_users -from app.scodoc import sco_xml -from app.scodoc.sco_codes_parcours import DEF, UE_SPORT def formsemestre_recapcomplet( formsemestre_id=None, - modejury=False, # affiche lien saisie decision jury + modejury=False, tabformat="html", sortcol=None, - xml_with_decisions=False, # XML avec decisions - rank_partition_id=None, # si None, calcul rang global - force_publishing=True, # publie les XML/JSON meme si bulletins non publiés + xml_with_decisions=False, + force_publishing=True, + selected_etudid=None, ): """Page récapitulant les notes d'un semestre. Grand tableau récapitulatif avec toutes les notes de modules @@ -89,7 +79,9 @@ def formsemestre_recapcomplet( pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable) modejury: cache modules, affiche lien saisie decision jury - + xml_with_decisions: publie décisions de jury dans xml et json + force_publishing: publie les xml et json même si bulletins non publiés + selected_etudid: etudid sélectionné (pour scroller au bon endroit) """ formsemestre = FormSemestre.query.get_or_404(formsemestre_id) @@ -98,98 +90,92 @@ def formsemestre_recapcomplet( xml_with_decisions = int(xml_with_decisions) force_publishing = int(force_publishing) is_file = tabformat in {"csv", "json", "xls", "xlsx", "xlsall", "xml"} - H = [] - if not is_file: - H += [ - html_sco_header.sco_header( - page_title="Récapitulatif", - no_side_bar=True, - init_qtip=True, - javascripts=["js/etud_info.js", "js/table_recap.js"], - ), - sco_formsemestre_status.formsemestre_status_head( - formsemestre_id=formsemestre_id - ), - ] - if len(formsemestre.inscriptions) > 0: - H.append( - f"""
    - - """ - ) - if modejury: - H.append( - f'' - ) - H.append( - '") - - H.append( - f""" (cliquer sur un nom pour afficher son bulletin ou ici avoir le classeur papier) - """ - ) - - data = do_formsemestre_recapcomplet( + data = _do_formsemestre_recapcomplet( formsemestre_id, format=tabformat, modejury=modejury, sortcol=sortcol, xml_with_decisions=xml_with_decisions, - rank_partition_id=rank_partition_id, force_publishing=force_publishing, + selected_etudid=selected_etudid, ) - if tabformat == "xml": - response = make_response(data) - response.headers["Content-Type"] = scu.XML_MIMETYPE - return response + if is_file: + return data + H = [ + html_sco_header.sco_header( + page_title="Récapitulatif", + no_side_bar=True, + init_qtip=True, + javascripts=["js/etud_info.js", "js/table_recap.js"], + ), + sco_formsemestre_status.formsemestre_status_head( + formsemestre_id=formsemestre_id + ), + ] + if len(formsemestre.inscriptions) > 0: + H.append( + f""" + + """ + ) + if modejury: + H.append( + f'' + ) + H.append( + '") + + H.append( + f""" (cliquer sur un nom pour afficher son bulletin ou ici avoir le classeur papier) + """ + ) H.append(data) - if not is_file: - if len(formsemestre.inscriptions) > 0: - H.append("
    ") - H.append( - f"""

    Voir les décisions du jury

    """ - ) - if sco_permissions_check.can_validate_sem(formsemestre_id): - H.append("

    ") - if modejury: - H.append( - f"""Calcul automatique des décisions du jury

    """ - ) - else: - H.append( - f"""Saisie des décisions du jury""" - ) - H.append("

    ") - if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): + if len(formsemestre.inscriptions) > 0: + H.append("") + H.append( + f"""

    Voir les décisions du jury

    """ + ) + if sco_permissions_check.can_validate_sem(formsemestre_id): + H.append("

    ") + if modejury: H.append( - """ -

    utilise les coefficients d'UE pour calculer la moyenne générale.

    - """ + f"""Calcul automatique des décisions du jury

    """ ) - H.append(html_sco_header.sco_footer()) + else: + H.append( + f"""Saisie des décisions du jury""" + ) + H.append("

    ") + if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): + H.append( + """ +

    utilise les coefficients d'UE pour calculer la moyenne générale.

    + """ + ) + H.append(html_sco_header.sco_footer()) # HTML or binary data ? if len(H) > 1: return "".join(H) @@ -199,18 +185,15 @@ def formsemestre_recapcomplet( return H -def do_formsemestre_recapcomplet( +def _do_formsemestre_recapcomplet( formsemestre_id=None, format="html", # html, xml, xls, xlsall, json - hidemodules=False, # ne pas montrer les modules (ignoré en XML) - hidebac=False, # pas de colonne Bac (ignoré en XML) xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML) modejury=False, # saisie décisions jury sortcol=None, # indice colonne a trier dans table T xml_with_decisions=False, - disable_etudlink=False, - rank_partition_id=None, # si None, calcul rang global force_publishing=True, + selected_etudid=None, ): """Calcule et renvoie le tableau récapitulatif.""" formsemestre = FormSemestre.query.get_or_404(formsemestre_id) @@ -219,13 +202,15 @@ def do_formsemestre_recapcomplet( f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" ) - if (format == "html" or format == "evals") and not modejury: + if format == "html" or format == "evals": res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) data = gen_formsemestre_recapcomplet_html( formsemestre, res, include_evaluations=(format == "evals"), + modejury=modejury, filename=filename, + selected_etudid=selected_etudid, ) return data elif format.startswith("xls") or format == "csv": @@ -388,35 +373,51 @@ def _gen_cell(key: str, row: dict, elt="td"): return f"<{elt} {attrs}>{content}" -def _gen_row(keys: list[str], row, elt="td"): +def _gen_row(keys: list[str], row, elt="td", selected_etudid=None): klass = row.get("_tr_class") tr_class = f'class="{klass}"' if klass else "" - return f'
""" ] # header @@ -451,7 +455,7 @@ def _gen_formsemestre_recapcomplet_html( # body H.append("") for row in rows: - H.append(f"{_gen_row(column_ids, row)}\n") + H.append(f"{_gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") H.append("\n") # footer H.append("") diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index e9712ba77..3625d5f01 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1146,6 +1146,18 @@ span.jurylink a { text-decoration: underline; } +div.jury_footer { + display: flex; + justify-content: space-evenly; +} + +div.jury_footer>span { + border: 2px solid rgb(90, 90, 90); + border-radius: 4px; + padding: 4px; + background-color: rgb(230, 242, 230); +} + .eval_description p { margin-left: 15px; margin-bottom: 2px; diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 174fb186b..4dcd0c23a 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -21,43 +21,54 @@ $(function () { dt.columns(".partition_aux").visible(!visible); dt.buttons('toggle_partitions:name').text(visible ? "Toutes les partitions" : "Cacher les partitions"); } - }, - $('table.table_recap').hasClass("apc") ? - { - name: "toggle_res", - text: "Cacher les ressources", - action: function (e, dt, node, config) { - let visible = dt.columns(".col_res").visible()[0]; - dt.columns(".col_res").visible(!visible); - dt.columns(".col_ue_bonus").visible(!visible); - dt.columns(".col_malus").visible(!visible); - dt.buttons('toggle_res:name').text(visible ? "Montrer les ressources" : "Cacher les ressources"); + }]; + if (!$('table.table_recap').hasClass("jury")) { + buttons.push( + $('table.table_recap').hasClass("apc") ? + { + name: "toggle_res", + text: "Cacher les ressources", + action: function (e, dt, node, config) { + let visible = dt.columns(".col_res").visible()[0]; + dt.columns(".col_res").visible(!visible); + dt.columns(".col_ue_bonus").visible(!visible); + dt.columns(".col_malus").visible(!visible); + dt.buttons('toggle_res:name').text(visible ? "Montrer les ressources" : "Cacher les ressources"); + } + } : { + name: "toggle_mod", + text: "Cacher les modules", + action: function (e, dt, node, config) { + let visible = dt.columns(".col_mod:not(.col_empty)").visible()[0]; + dt.columns(".col_mod:not(.col_empty)").visible(!visible); + dt.columns(".col_ue_bonus").visible(!visible); + dt.columns(".col_malus").visible(!visible); + dt.buttons('toggle_mod:name').text(visible ? "Montrer les modules" : "Cacher les modules"); + visible = dt.columns(".col_empty").visible()[0]; + dt.buttons('toggle_col_empty:name').text(visible ? "Cacher mod. vides" : "Montrer mod. vides"); + } } - } : { - name: "toggle_mod", - text: "Cacher les modules", + ); + if ($('table.table_recap').hasClass("apc")) { + buttons.push({ + name: "toggle_sae", + text: "Cacher les SAÉs", action: function (e, dt, node, config) { - let visible = dt.columns(".col_mod:not(.col_empty)").visible()[0]; - dt.columns(".col_mod:not(.col_empty)").visible(!visible); - dt.columns(".col_ue_bonus").visible(!visible); - dt.columns(".col_malus").visible(!visible); - dt.buttons('toggle_mod:name').text(visible ? "Montrer les modules" : "Cacher les modules"); - visible = dt.columns(".col_empty").visible()[0]; - dt.buttons('toggle_col_empty:name').text(visible ? "Cacher mod. vides" : "Montrer mod. vides"); + let visible = dt.columns(".col_sae").visible()[0]; + dt.columns(".col_sae").visible(!visible); + dt.buttons('toggle_sae:name').text(visible ? "Montrer les SAÉs" : "Cacher les SAÉs"); } - } - ]; - if ($('table.table_recap').hasClass("apc")) { + }) + } buttons.push({ - name: "toggle_sae", - text: "Cacher les SAÉs", + name: "toggle_col_empty", + text: "Montrer mod. vides", action: function (e, dt, node, config) { - let visible = dt.columns(".col_sae").visible()[0]; - dt.columns(".col_sae").visible(!visible); - dt.buttons('toggle_sae:name').text(visible ? "Montrer les SAÉs" : "Cacher les SAÉs"); + let visible = dt.columns(".col_empty").visible()[0]; + dt.columns(".col_empty").visible(!visible); + dt.buttons('toggle_col_empty:name').text(visible ? "Montrer mod. vides" : "Cacher mod. vides"); } }) - } buttons.push({ name: "toggle_admission", @@ -68,15 +79,6 @@ $(function () { dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission"); } }) - buttons.push({ - name: "toggle_col_empty", - text: "Montrer mod. vides", - action: function (e, dt, node, config) { - let visible = dt.columns(".col_empty").visible()[0]; - dt.columns(".col_empty").visible(!visible); - dt.buttons('toggle_col_empty:name').text(visible ? "Montrer mod. vides" : "Cacher mod. vides"); - } - }) $('table.table_recap').DataTable( { paging: false, @@ -143,4 +145,10 @@ $(function () { $(this).addClass('selected'); } }); + // Pour montrer et highlihter l'étudiant sélectionné: + $(function () { + document.querySelector("#row_selected").scrollIntoView(); + window.scrollBy(0, -50); + document.querySelector("#row_selected").classList.add("selected"); + }); }); From 6ac096d29dc874ccefc4290a5bde34b5d5790426 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 8 Apr 2022 07:56:58 +0200 Subject: [PATCH 13/37] Fix link, closes #354 --- app/templates/bul_foot.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/bul_foot.html b/app/templates/bul_foot.html index 09a8d2006..e2085cb75 100644 --- a/app/templates/bul_foot.html +++ b/app/templates/bul_foot.html @@ -32,7 +32,7 @@ {% if can_edit_appreciations %}

Ajouter une appréciation

{% endif %} From 1153bc9a7c41664c3b5376adea0c1affd67d6daf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 8 Apr 2022 13:01:47 +0200 Subject: [PATCH 14/37] Trombino: export en doc. Closes #344 --- app/scodoc/sco_trombino.py | 44 ++++++++++++-------- app/scodoc/sco_trombino_doc.py | 76 ++++++++++++++++++++++++++++++++++ app/scodoc/sco_utils.py | 19 +++++++++ 3 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 app/scodoc/sco_trombino_doc.py diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index e9102fc67..9534cfb29 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -55,19 +55,18 @@ from app.scodoc.sco_pdf import SU from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_import_etuds +from app.scodoc import sco_etud from app.scodoc import sco_excel -from app.scodoc import sco_formsemestre -from app.scodoc import sco_groups from app.scodoc import sco_groups_view from app.scodoc import sco_pdf from app.scodoc import sco_photos from app.scodoc import sco_portal_apogee from app.scodoc import sco_preferences -from app.scodoc import sco_etud +from app.scodoc import sco_trombino_doc def trombino( - group_ids=[], # liste des groupes à afficher + group_ids=(), # liste des groupes à afficher formsemestre_id=None, # utilisé si pas de groupes selectionné etat=None, format="html", @@ -93,6 +92,8 @@ def trombino( return _trombino_pdf(groups_infos) elif format == "pdflist": return _listeappel_photos_pdf(groups_infos) + elif format == "doc": + return sco_trombino_doc.trombino_doc(groups_infos) else: raise Exception("invalid format") # return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer() @@ -176,8 +177,13 @@ def trombino_html(groups_infos): H.append("") H.append( - '' - % groups_infos.groups_query_args + f"""
+ Version PDF +    + Version doc +
""" ) return "\n".join(H) @@ -234,7 +240,7 @@ def _trombino_zip(groups_infos): Z.writestr(filename, img) Z.close() size = data.tell() - log("trombino_zip: %d bytes" % size) + log(f"trombino_zip: {size} bytes") data.seek(0) return send_file( data, @@ -470,7 +476,7 @@ def _listeappel_photos_pdf(groups_infos): # --------------------- Upload des photos de tout un groupe -def photos_generate_excel_sample(group_ids=[]): +def photos_generate_excel_sample(group_ids=()): """Feuille excel pour import fichiers photos""" fmt = sco_import_etuds.sco_import_format() data = sco_import_etuds.sco_import_generate_excel_sample( @@ -492,31 +498,33 @@ def photos_generate_excel_sample(group_ids=[]): # return sco_excel.send_excel_file(data, "ImportPhotos" + scu.XLSX_SUFFIX) -def photos_import_files_form(group_ids=[]): +def photos_import_files_form(group_ids=()): """Formulaire pour importation photos""" if not group_ids: raise ScoValueError("paramètre manquant !") groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) - back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args + back_url = f"groups_view?{groups_infos.groups_query_args}&curtab=tab-photos" H = [ html_sco_header.sco_header(page_title="Import des photos des étudiants"), - """

Téléchargement des photos des étudiants

-

Vous pouvez aussi charger les photos individuellement via la fiche de chaque étudiant (menu "Etudiant" / "Changer la photo").

-

Cette page permet de charger en une seule fois les photos de plusieurs étudiants.
- Il faut d'abord remplir une feuille excel donnant les noms + f"""

Téléchargement des photos des étudiants

+

Vous pouvez aussi charger les photos individuellement via la fiche + de chaque étudiant (menu "Etudiant" / "Changer la photo"). +

+

Cette page permet de charger en une seule fois les photos + de plusieurs étudiants.
+ Il faut d'abord remplir une feuille excel donnant les noms des fichiers images (une image par étudiant).

-

Ensuite, réunir vos images dans un fichier zip, puis télécharger +

Ensuite, réunir vos images dans un fichier zip, puis télécharger simultanément le fichier excel et le fichier zip.

    -
  1. +
  2. Obtenir la feuille excel à remplir
  3. - """ - % groups_infos.groups_query_args, + """, ] F = html_sco_header.sco_footer() vals = scu.get_request_args() diff --git a/app/scodoc/sco_trombino_doc.py b/app/scodoc/sco_trombino_doc.py new file mode 100644 index 000000000..038832e5a --- /dev/null +++ b/app/scodoc/sco_trombino_doc.py @@ -0,0 +1,76 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Génération d'un trombinoscope en doc +""" + +import docx +from docx.shared import Mm +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.table import WD_ALIGN_VERTICAL + +from app.scodoc import sco_etud +from app.scodoc import sco_photos +import app.scodoc.sco_utils as scu +import sco_version + + +def trombino_doc(groups_infos): + "Send photos as docx document" + filename = f"trombino_{groups_infos.groups_filename}.docx" + sem = groups_infos.formsemestre # suppose 1 seul semestre + PHOTO_WIDTH = Mm(25) + N_PER_ROW = 5 # XXX should be in ScoDoc preferences + + document = docx.Document() + document.add_heading( + f"Trombinoscope {sem['titreannee']} {groups_infos.groups_titles}", 1 + ) + section = document.sections[0] + footer = section.footer + footer.paragraphs[ + 0 + ].text = f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}" + + nb_images = len(groups_infos.members) + table = document.add_table(rows=2 * (nb_images // N_PER_ROW + 1), cols=N_PER_ROW) + table.allow_autofit = False + + for i, t in enumerate(groups_infos.members): + li = i // N_PER_ROW + co = i % N_PER_ROW + img_path = ( + sco_photos.photo_pathname(t["photo_filename"], size="small") + or sco_photos.UNKNOWN_IMAGE_PATH + ) + cell = table.rows[2 * li].cells[co] + cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP + cell_p, cell_f, cell_r = _paragraph_format_run(cell) + cell_r.add_picture(img_path, width=PHOTO_WIDTH) + + # le nom de l'étudiant: cellules de lignes impaires + cell = table.rows[2 * li + 1].cells[co] + cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP + cell_p, cell_f, cell_r = _paragraph_format_run(cell) + cell_r.add_text(sco_etud.format_nomprenom(t)) + cell_f.space_after = Mm(8) + + return scu.send_docx(document, filename) + + +def _paragraph_format_run(cell): + "parag. dans cellule tableau" + # inspired by https://stackoverflow.com/questions/64218305/problem-with-python-docx-putting-pictures-in-a-table + paragraph = cell.paragraphs[0] + fmt = paragraph.paragraph_format + run = paragraph.add_run() + + fmt.space_before = Mm(0) + fmt.space_after = Mm(0) + fmt.line_spacing = 1.0 + fmt.alignment = WD_ALIGN_PARAGRAPH.CENTER + + return paragraph, fmt, run diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 0a4da781a..8cba3a59c 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -33,6 +33,7 @@ import bisect import copy import datetime from enum import IntEnum +import io import json from hashlib import md5 import numbers @@ -49,6 +50,7 @@ from PIL import Image as PILImage import pydot import requests +import flask from flask import g, request from flask import flash, url_for, make_response, jsonify @@ -379,6 +381,10 @@ CSV_FIELDSEP = ";" CSV_LINESEP = "\n" CSV_MIMETYPE = "text/comma-separated-values" CSV_SUFFIX = ".csv" +DOCX_MIMETYPE = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" +) +DOCX_SUFFIX = ".docx" JSON_MIMETYPE = "application/json" JSON_SUFFIX = ".json" PDF_MIMETYPE = "application/pdf" @@ -398,6 +404,7 @@ def get_mime_suffix(format_code: str) -> tuple[str, str]: """ d = { "csv": (CSV_MIMETYPE, CSV_SUFFIX), + "docx": (DOCX_MIMETYPE, DOCX_SUFFIX), "xls": (XLSX_MIMETYPE, XLSX_SUFFIX), "xlsx": (XLSX_MIMETYPE, XLSX_SUFFIX), "pdf": (PDF_MIMETYPE, PDF_SUFFIX), @@ -740,6 +747,18 @@ def send_file(data, filename="", suffix="", mime=None, attached=None): return response +def send_docx(document, filename): + "Send a python-docx document" + buffer = io.BytesIO() # in-memory document, no disk file + document.save(buffer) + buffer.seek(0) + return flask.send_file( + buffer, + attachment_filename=sanitize_filename(filename), + mimetype=DOCX_MIMETYPE, + ) + + def get_request_args(): """returns a dict with request (POST or GET) arguments converted to suit legacy Zope style (scodoc7) functions. From a9f0fcdd6d33bb9e2cc4645f1be2d301ef90cfb2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 8 Apr 2022 16:36:56 +0200 Subject: [PATCH 15/37] Pages saisies absence + refonte tableau bord semestre. Close #342 --- app/models/groups.py | 4 + app/scodoc/html_sidebar.py | 6 +- app/scodoc/sco_abs.py | 8 +- app/scodoc/sco_find_etud.py | 4 +- app/scodoc/sco_formsemestre_status.py | 33 +++---- app/scodoc/sco_utils.py | 1 + app/static/js/scodoc.js | 6 +- app/templates/sidebar.html | 6 +- app/views/absences.py | 119 ++++++++++++++------------ sco_version.py | 2 +- 10 files changed, 94 insertions(+), 95 deletions(-) diff --git a/app/models/groups.py b/app/models/groups.py index f6452cf7c..9cf5f2364 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -74,6 +74,10 @@ class GroupDescr(db.Model): f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">""" ) + def get_nom_with_part(self) -> str: + "Nom avec partition: 'TD A'" + return f"{self.partition.partition_name or ''} {self.group_name or '-'}" + group_membership = db.Table( "group_membership", diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index 53efbfc95..c700c1b80 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -86,9 +86,9 @@ def sidebar(): f"""
+ """ else: @@ -788,7 +795,7 @@ def _make_listes_sem(sem, with_absences=True): # Genere liste pour chaque partition (categorie de groupes) for partition in sco_groups.get_partitions_list(sem["formsemestre_id"]): if not partition["partition_name"]: - H.append("

Tous les étudiants

" % partition) + H.append("

Tous les étudiants

") else: H.append("

Groupes de %(partition_name)s

" % partition) groups = sco_groups.get_partition_groups(partition) @@ -818,20 +825,6 @@ def _make_listes_sem(sem, with_absences=True): ) }">{group["label"]} """ diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 8cba3a59c..543d97074 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -178,6 +178,7 @@ MONTH_NAMES = ( "novembre", "décembre", ) +DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche") def fmt_note(val, note_max=None, keep_numeric=False): diff --git a/app/static/js/scodoc.js b/app/static/js/scodoc.js index 25f4b26dd..eae42d0a4 100644 --- a/app/static/js/scodoc.js +++ b/app/static/js/scodoc.js @@ -3,14 +3,14 @@ $(function () { // Autocomplete recherche etudiants par nom - $("#in-expnom").autocomplete( + $(".in-expnom").autocomplete( { delay: 300, // wait 300ms before suggestions minLength: 2, // min nb of chars before suggest position: { collision: 'flip' }, // automatic menu position up/down - source: "search_etud_by_name", + source: SCO_URL + "/search_etud_by_name", select: function (event, ui) { - $("#in-expnom").val(ui.item.value); + $(".in-expnom").val(ui.item.value); $("#form-chercheetud").submit(); } }); diff --git a/app/templates/sidebar.html b/app/templates/sidebar.html index 5ed8ee883..e0282c4f2 100644 --- a/app/templates/sidebar.html +++ b/app/templates/sidebar.html @@ -15,7 +15,7 @@

Dépt. {{ sco.prefs["DeptName"] }}

Accueil
{% if sco.prefs["DeptIntranetURL"] %} - + {{ sco.prefs["DeptIntranetTitle"] }} {% endif %}
@@ -41,7 +41,7 @@
- +
@@ -49,7 +49,7 @@
{% if sco.etud %}

+ 'scolar.ficheEtud', scodoc_dept=g.scodoc_dept, etudid=sco.etud.id )}}" class="sidebar"> {{sco.etud.nomprenom}}

Absences diff --git a/app/views/absences.py b/app/views/absences.py index ae95d7eba..f664333af 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -69,7 +69,7 @@ from app.decorators import ( permission_required, permission_required_compat_scodoc7, ) -from app.models import FormSemestre +from app.models import FormSemestre, GroupDescr from app.models.absences import BilletAbsence from app.views import absences_bp as bp @@ -119,63 +119,71 @@ def sco_publish(route, function, permission, methods=["GET"]): @scodoc7func def index_html(): """Gestionnaire absences, page principale""" - # crude portage from 1999 DTML - sems = sco_formsemestre.do_formsemestre_list() - authuser = current_user H = [ html_sco_header.sco_header( - page_title="Gestion des absences", + page_title="Saisie des absences", cssstyles=["css/calabs.css"], javascripts=["js/calabs.js"], ), - """

Gestion des Absences

""", + """

Traitement des absences

+

+ Pour saisir des absences ou consulter les états, il est recommandé par passer par + le semestre concerné (saisie par jours nommés ou par semaines). +

+ """, ] - if not sems: + H.append( + """

Pour signaler, annuler ou justifier une absence pour un seul étudiant, + choisissez d'abord concerné:

""" + ) + H.append(sco_find_etud.form_search_etud()) + if current_user.has_permission( + Permission.ScoAbsChange + ) and sco_preferences.get_preference("handle_billets_abs"): H.append( - """

Aucun semestre défini (ou aucun groupe d'étudiant)

""" - ) - else: - H.append( - """
  • Afficher l'état des absences (pour tout un groupe)
  • """ - ) - if sco_preferences.get_preference("handle_billets_abs"): - H.append( - """
  • Traitement des billets d'absence en attente
  • """ - ) - H.append( - """

    Pour signaler, annuler ou justifier une absence, choisissez d'abord l'étudiant concerné:

    """ - ) - H.append(sco_find_etud.form_search_etud()) - if authuser.has_permission(Permission.ScoAbsChange): - H.extend( - ( - """
    -
    - -

    - - Saisie par semaine - Choix du groupe: - """ - % request.base_url, - sco_abs_views.formChoixSemestreGroupe(), - "

    ", - cal_select_week(), - """

    Sélectionner le groupe d'étudiants, puis cliquez sur une semaine pour -saisir les absences de toute cette semaine.

    - """, - ) - ) - else: - H.append( - """

    Vous n'avez pas l'autorisation d'ajouter, justifier ou supprimer des absences.

    """ - ) - +

    Billets d'absence

    + + """ + ) H.append(html_sco_header.sco_footer()) return "\n".join(H) +@bp.route("/choix_semaine") +@scodoc +@permission_required(Permission.ScoAbsChange) +@scodoc7func +def choix_semaine(group_id): + """Page choix semaine sur calendrier pour saisie absences d'un groupe""" + group = GroupDescr.query.get_or_404(group_id) + H = [ + html_sco_header.sco_header( + page_title="Saisie des absences", + cssstyles=["css/calabs.css"], + javascripts=["js/calabs.js"], + ), + f""" +

    Saisie des Absences

    +
    +

    + + Saisie par semaine - Groupe: {group.get_nom_with_part()} + + +

    + """, + cal_select_week(), + """

    Sélectionner le groupe d'étudiants, puis cliquez sur une semaine pour + saisir les absences de toute cette semaine.

    + + """, + html_sco_header.sco_footer(), + ] + return "\n".join(H) + + def cal_select_week(year=None): "display calendar allowing week selection" if not year: @@ -479,7 +487,7 @@ def SignaleAbsenceGrSemestre( datedebut, datefin, destination="", - group_ids=[], # list of groups to display + group_ids=(), # list of groups to display nbweeks=4, # ne montre que les nbweeks dernieres semaines moduleimpl_id=None, ): @@ -566,7 +574,7 @@ def SignaleAbsenceGrSemestre( url_link_semaines += "&moduleimpl_id=" + str(moduleimpl_id) # dates = [x.ISO() for x in dates] - dayname = sco_abs.day_names()[jourdebut.weekday] + day_name = sco_abs.day_names()[jourdebut.weekday] if groups_infos.tous_les_etuds_du_sem: gr_tit = "en" @@ -579,19 +587,18 @@ def SignaleAbsenceGrSemestre( H = [ html_sco_header.sco_header( - page_title="Saisie des absences", + page_title=f"Saisie des absences du {day_name}", init_qtip=True, javascripts=["js/etud_info.js", "js/abs_ajax.js"], no_side_bar=1, ), - """
absences + + - + - état + + saisie par semaine - (tableur) - Photos ({n_members} étudiants)
- - - {% else %} - - - - {% endif %} - - - - - - + + + + {% endmacro %} {% macro render_logos(dept_form) %} -
-

Saisie des absences %s %s, - les %s

+ f"""{"".join([_gen_cell(key, row, elt) for key in keys])}' - - def gen_formsemestre_recapcomplet_html( formsemestre: FormSemestre, res: NotesTableCompat, @@ -448,20 +423,20 @@ def _gen_formsemestre_recapcomplet_html( H.append( f""" - {_gen_row(column_ids, titles, "th")} + {scu.gen_row(column_ids, titles, "th")} """ ) # body H.append("") for row in rows: - H.append(f"{_gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") + H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") H.append("\n") # footer H.append("") idx_last = len(footer_rows) - 1 for i, row in enumerate(footer_rows): - H.append(f'{_gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') + H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') H.append( """ diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 543d97074..c7422ecbc 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -1077,6 +1077,36 @@ def objects_renumber(db, obj_list) -> None: db.session.commit() +def gen_cell(key: str, row: dict, elt="td", with_col_class=False): + "html table cell" + klass = row.get(f"_{key}_class", "") + if with_col_class: + klass = key + " " + klass + attrs = f'class="{klass}"' if klass else "" + order = row.get(f"_{key}_order") + if order: + attrs += f' data-order="{order}"' + content = row.get(key, "") + target = row.get(f"_{key}_target") + target_attrs = row.get(f"_{key}_target_attrs", "") + if target or target_attrs: # avec lien + href = f'href="{target}"' if target else "" + content = f"{content}" + return f"<{elt} {attrs}>{content}" + + +def gen_row( + keys: list[str], row, elt="td", selected_etudid=None, with_col_classes=False +): + "html table row" + klass = row.get("_tr_class") + tr_class = f'class="{klass}"' if klass else "" + tr_id = ( + f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else "" + ) + return f"""{"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) for key in keys if not key.startswith('_')])}""" + + # Pour accès depuis les templates jinja def is_entreprises_enabled(): from app.models import ScoDocSiteConfig diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 470b929a8..57a951b3a 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1076,7 +1076,7 @@ tr.etuddem td { td.etudabs, td.etudabs a.discretelink, tr.etudabs td.moyenne a.discretelink { - color: rgb(180, 0, 0); + color: rgb(195, 0, 0); } tr.moyenne td { @@ -1103,6 +1103,17 @@ table.notes_evaluation th.eval_attente { width: 80px; } +table.notes_evaluation td.att a { + color: rgb(255, 0, 217); + font-weight: bold; +} + +table.notes_evaluation td.exc a { + font-style: italic; + color: rgb(0, 131, 0); +} + + table.notes_evaluation tr td a.discretelink:hover { text-decoration: none; } @@ -3554,7 +3565,7 @@ table.dataTable td.group { text-align: left; } -/* Nouveau tableau recap */ +/* ------------- Nouveau tableau recap ------------ */ div.table_recap { margin-top: 6px; } @@ -3756,4 +3767,78 @@ table.table_recap tr.apo td { table.table_recap td.evaluation.first, table.table_recap th.evaluation.first { border-left: 2px solid rgb(4, 16, 159); +} + +table.table_recap td.evaluation.first_of_mod, +table.table_recap th.evaluation.first_of_mod { + border-left: 1px dashed rgb(4, 16, 159); +} + + +table.table_recap td.evaluation.att { + color: rgb(255, 0, 217); + font-weight: bold; +} + +table.table_recap td.evaluation.abs { + color: rgb(231, 0, 0); + font-weight: bold; +} + +table.table_recap td.evaluation.exc { + font-style: italic; + color: rgb(0, 131, 0); +} + +table.table_recap td.evaluation.non_inscrit { + font-style: italic; + color: rgb(101, 101, 101); +} + +/* ------------- Tableau etat evals ------------ */ + +div.evaluations_recap table.evaluations_recap { + width: auto !important; + border: 1px solid black; +} + +table.evaluations_recap tr.odd td { + background-color: #fff4e4; +} + +table.evaluations_recap tr.res td { + background-color: #f7d372; +} + +table.evaluations_recap tr.sae td { + background-color: #d8fcc8; +} + + +table.evaluations_recap tr.module td { + font-weight: bold; +} + +table.evaluations_recap tr.evaluation td.titre { + font-style: italic; + padding-left: 2em; +} + +table.evaluations_recap td.titre, +table.evaluations_recap th.titre { + max-width: 350px; +} + +table.evaluations_recap td.complete, +table.evaluations_recap th.complete { + text-align: center; +} + +table.evaluations_recap tr.evaluation.incomplete td, +table.evaluations_recap tr.evaluation.incomplete td a { + color: red; +} + +table.evaluations_recap tr.evaluation.incomplete td a.incomplete { + font-weight: bold; } \ No newline at end of file diff --git a/app/static/js/evaluations_recap.js b/app/static/js/evaluations_recap.js new file mode 100644 index 000000000..b3b60c3fb --- /dev/null +++ b/app/static/js/evaluations_recap.js @@ -0,0 +1,38 @@ +// Tableau recap evaluations du semestre +$(function () { + $('table.evaluations_recap').DataTable( + { + paging: false, + searching: true, + info: false, + autoWidth: false, + fixedHeader: { + header: true, + footer: false + }, + orderCellsTop: true, // cellules ligne 1 pour tri + aaSorting: [], // Prevent initial sorting + colReorder: true, + "columnDefs": [ + { + // colonne date, triable (XXX ne fonctionne pas) + targets: ["date"], + "type": "string", + }, + ], + dom: 'Bfrtip', + buttons: [ + { + extend: 'copyHtml5', + text: 'Copier', + exportOptions: { orthogonal: 'export' } + }, + { + extend: 'excelHtml5', + exportOptions: { orthogonal: 'export' }, + title: document.querySelector('table.evaluations_recap').dataset.filename + }, + ], + + }) +}); diff --git a/app/views/notes.py b/app/views/notes.py index 35f92e0b6..82c455393 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -100,6 +100,7 @@ from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_check_abs from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_edit +from app.scodoc import sco_evaluation_recap from app.scodoc import sco_export_results from app.scodoc import sco_formations from app.scodoc import sco_formsemestre @@ -109,22 +110,18 @@ from app.scodoc import sco_formsemestre_exterieurs from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_validation -from app.scodoc import sco_groups from app.scodoc import sco_inscr_passage from app.scodoc import sco_liste_notes from app.scodoc import sco_lycee from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_status -from app.scodoc import sco_news -from app.scodoc import sco_parcours_dut from app.scodoc import sco_permissions_check from app.scodoc import sco_placement from app.scodoc import sco_poursuite_dut from app.scodoc import sco_preferences from app.scodoc import sco_prepajury from app.scodoc import sco_pvjury -from app.scodoc import sco_pvpdf from app.scodoc import sco_recapcomplet from app.scodoc import sco_report from app.scodoc import sco_saisie_notes @@ -136,7 +133,6 @@ from app.scodoc import sco_undo_notes from app.scodoc import sco_users from app.scodoc import sco_xml from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_pdf import PDFLOCK from app.scodoc.sco_permissions import Permission from app.scodoc.TrivialFormulator import TrivialFormulator from app.views import ScoData @@ -235,6 +231,11 @@ sco_publish( sco_recapcomplet.formsemestre_recapcomplet, Permission.ScoView, ) +sco_publish( + "/evaluations_recap", + sco_evaluation_recap.evaluations_recap, + Permission.ScoView, +) sco_publish( "/formsemestres_bulletins", sco_recapcomplet.formsemestres_bulletins, From 4d257e63e87fecc4b786db998f9858a379a6c78d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Apr 2022 18:17:57 +0200 Subject: [PATCH 18/37] formsemestre_status: bulle coefs --- app/scodoc/sco_formsemestre_status.py | 2 ++ app/static/css/scodoc.css | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 802399f2c..83314cf85 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1186,6 +1186,7 @@ def formsemestre_tableau_modules( H.append("") if mod.module_type in ( None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 57a951b3a..9811b3045 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2432,6 +2432,12 @@ span.bul_minmax:before { content: " "; } +a.invisible_link, +a.invisible_link:hover { + text-decoration: none; + color: rgb(20, 30, 30); +} + a.bull_link { text-decoration: none; color: rgb(20, 30, 30); From 4d7349403d5fe5c3a5c89e51158aaaf6e9acefba Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Apr 2022 08:03:27 +0200 Subject: [PATCH 19/37] typos --- app/comp/bonus_spo.py | 2 +- app/scodoc/sco_news.py | 14 ++++++-------- app/views/notes.py | 13 +++++++------ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 416a0d0ee..d4712098f 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -1011,7 +1011,7 @@ class BonusTarbes(BonusSportAdditif): """ name = "bonus_tarbes" - displayed_name = "IUT de Tazrbes" + displayed_name = "IUT de Tarbes" seuil_moy_gen = 10.0 proportion_point = 1 / 30.0 classic_use_bonus_ues = True diff --git a/app/scodoc/sco_news.py b/app/scodoc/sco_news.py index e0977a951..a516ddbaa 100644 --- a/app/scodoc/sco_news.py +++ b/app/scodoc/sco_news.py @@ -112,7 +112,6 @@ def scolar_news_summary(n=5): """Return last n news. News are "compressed", ie redondant events are joined. """ - from app.scodoc import sco_etud from app.scodoc import sco_users cnx = ndb.GetDBConnexion() @@ -152,12 +151,12 @@ def scolar_news_summary(n=5): # date resumee j, m = n["date"].split("/")[:2] mois = scu.MONTH_NAMES_ABBREV[int(m) - 1] - n["formatted_date"] = "%s %s %s" % (j, mois, n["hm"]) + n["formatted_date"] = f'{j} {mois} {n["hm"]}' # indication semestre si ajout notes: infos = _get_formsemestre_infos_from_news(n) if infos: n["text"] += ( - ' (%(descr_sem)s)' + ' (%(descr_sem)s)' % infos ) n["text"] += ( @@ -179,21 +178,20 @@ def _get_formsemestre_infos_from_news(n): mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id) if not mods: return {} # module does not exists anymore - return {} # pas d'indication du module - mod = mods[0] - formsemestre_id = mod["formsemestre_id"] + mod = mods[0] + formsemestre_id = mod["formsemestre_id"] if not formsemestre_id: return {} try: sem = sco_formsemestre.get_formsemestre(formsemestre_id) - except: + except ValueError: # semestre n'existe plus return {} if sem["semestre_id"] > 0: - descr_sem = "S%d" % sem["semestre_id"] + descr_sem = f'S{sem["semestre_id"]}' else: descr_sem = "" if sem["modalite"]: diff --git a/app/views/notes.py b/app/views/notes.py index 82c455393..688811c09 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1738,12 +1738,13 @@ def evaluation_listenotes(): evaluation_id = None moduleimpl_id = None vals = scu.get_request_args() - if "evaluation_id" in vals: - evaluation_id = int(vals["evaluation_id"]) - mode = "eval" - if "moduleimpl_id" in vals and vals["moduleimpl_id"]: - moduleimpl_id = int(vals["moduleimpl_id"]) - mode = "module" + try: + if "evaluation_id" in vals: + evaluation_id = int(vals["evaluation_id"]) + if "moduleimpl_id" in vals and vals["moduleimpl_id"]: + moduleimpl_id = int(vals["moduleimpl_id"]) + except ValueError as exc: + raise ScoValueError("adresse invalide !") from exc format = vals.get("format", "html") html_content, page_title = sco_liste_notes.do_evaluation_listenotes( From e51b09e7f6b202575fe35f266a6a8fe521cc8152 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Apr 2022 08:12:37 +0200 Subject: [PATCH 20/37] =?UTF-8?q?Changement=20r=C3=A8gle=20bonus=20Cachan?= =?UTF-8?q?=20(sur=20les=20DUT).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/bonus_spo.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index d4712098f..bc6dca945 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -531,10 +531,11 @@ class BonusCachan1(BonusSportAdditif):
  • DUT/LP : la meilleure note d'option, si elle est supérieure à 10, - bonifie les moyennes d'UE (sauf l'UE41 dont le code est UE41_E) à raison + bonifie les moyennes d'UE (uniquement UE13_E pour le semestre 1, UE23_E + pour le semestre 2, UE33_E pour le semestre 3 et UE43_E pour le semestre + 4) à raison de bonus = (option - 10)/10.
  • -
  • BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie les moyennes d'UE à raison de bonus = (option - 10) * 3%.
@@ -545,6 +546,7 @@ class BonusCachan1(BonusSportAdditif): seuil_moy_gen = 10.0 # tous les points sont comptés proportion_point = 0.03 classic_use_bonus_ues = True + ues_bonifiables_cachan = {"UE13_E", "UE23_E", "UE33_E", "UE43_E"} def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): """calcul du bonus, avec réglage différent suivant le type de formation""" @@ -569,7 +571,7 @@ class BonusCachan1(BonusSportAdditif): dtype=float, ) else: # --- DUT - # pareil mais proportion différente et exclusion d'une UE + # pareil mais proportion différente et application à certaines UEs proportion_point = 0.1 bonus_moy_arr = np.where( note_bonus_max > self.seuil_moy_gen, @@ -582,10 +584,10 @@ class BonusCachan1(BonusSportAdditif): columns=ues_idx, dtype=float, ) - # Pas de bonus sur la ou les ue de code "UE41_E" - ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"] - for ue in ue_exclues: - self.bonus_ues[ue.id] = 0.0 + # Applique bonus seulement sur certaines UE de code connu: + for ue in ues: + if ue.ue_code not in self.ues_bonifiables_cachan: + self.bonus_ues[ue.id] = 0.0 # annule class BonusCalais(BonusSportAdditif): From 70db38bbb4e870bb26886eb2b32c48f1396bac8e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 12 Apr 2022 11:32:13 +0200 Subject: [PATCH 21/37] added python-docx --- app/scodoc/sco_news.py | 274 ----------------------------------------- requirements-3.9.txt | 4 +- 2 files changed, 3 insertions(+), 275 deletions(-) delete mode 100644 app/scodoc/sco_news.py diff --git a/app/scodoc/sco_news.py b/app/scodoc/sco_news.py deleted file mode 100644 index a516ddbaa..000000000 --- a/app/scodoc/sco_news.py +++ /dev/null @@ -1,274 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -"""Gestion des "nouvelles" -""" -import re -import time - - -from operator import itemgetter - -from flask import g -from flask_login import current_user - -import app.scodoc.sco_utils as scu -import app.scodoc.notesdb as ndb -from app import log -from app.scodoc import sco_formsemestre -from app.scodoc import sco_moduleimpl -from app.scodoc import sco_preferences -from app import email - - -_scolar_news_editor = ndb.EditableTable( - "scolar_news", - "news_id", - ("date", "authenticated_user", "type", "object", "text", "url"), - filter_dept=True, - sortkey="date desc", - output_formators={"date": ndb.DateISOtoDMY}, - input_formators={"date": ndb.DateDMYtoISO}, - html_quote=False, # no user supplied data, needed to store html links -) - -NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id) -NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id) -NEWS_FORM = "FORM" # modification formation (object=formation_id) -NEWS_SEM = "SEM" # creation semestre (object=None) -NEWS_MISC = "MISC" # unused -NEWS_MAP = { - NEWS_INSCR: "inscription d'étudiants", - NEWS_NOTE: "saisie note", - NEWS_FORM: "modification formation", - NEWS_SEM: "création semestre", - NEWS_MISC: "opération", # unused -} -NEWS_TYPES = list(NEWS_MAP.keys()) - -scolar_news_create = _scolar_news_editor.create -scolar_news_list = _scolar_news_editor.list - -_LAST_NEWS = {} # { (authuser_name, type, object) : time } - - -def add(typ, object=None, text="", url=None, max_frequency=False): - """Ajoute une nouvelle. - Si max_frequency, ne genere pas 2 nouvelles identiques à moins de max_frequency - secondes d'intervalle. - """ - from app.scodoc import sco_users - - authuser_name = current_user.user_name - cnx = ndb.GetDBConnexion() - args = { - "authenticated_user": authuser_name, - "user_info": sco_users.user_info(authuser_name), - "type": typ, - "object": object, - "text": text, - "url": url, - } - t = time.time() - if max_frequency: - last_news_time = _LAST_NEWS.get((authuser_name, typ, object), False) - if last_news_time and (t - last_news_time < max_frequency): - # log("not recording") - return - - log("news: %s" % args) - - _LAST_NEWS[(authuser_name, typ, object)] = t - - _send_news_by_mail(args) - return scolar_news_create(cnx, args) - - -def scolar_news_summary(n=5): - """Return last n news. - News are "compressed", ie redondant events are joined. - """ - from app.scodoc import sco_users - - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - """SELECT id AS news_id, * - FROM scolar_news - WHERE dept_id=%(dept_id)s - ORDER BY date DESC LIMIT 100 - """, - {"dept_id": g.scodoc_dept_id}, - ) - selected_news = {} # (type,object) : news dict - news = cursor.dictfetchall() # la plus récente d'abord - - for r in reversed(news): # la plus ancienne d'abord - # si on a deja une news avec meme (type,object) - # et du meme jour, on la remplace - dmy = ndb.DateISOtoDMY(r["date"]) # round - key = (r["type"], r["object"], dmy) - selected_news[key] = r - - news = list(selected_news.values()) - # sort by date, descending - news.sort(key=itemgetter("date"), reverse=True) - news = news[:n] - # mimic EditableTable.list output formatting: - for n in news: - n["date822"] = n["date"].strftime("%a, %d %b %Y %H:%M:%S %z") - # heure - n["hm"] = n["date"].strftime("%Hh%M") - for k in n.keys(): - if n[k] is None: - n[k] = "" - if k in _scolar_news_editor.output_formators: - n[k] = _scolar_news_editor.output_formators[k](n[k]) - # date resumee - j, m = n["date"].split("/")[:2] - mois = scu.MONTH_NAMES_ABBREV[int(m) - 1] - n["formatted_date"] = f'{j} {mois} {n["hm"]}' - # indication semestre si ajout notes: - infos = _get_formsemestre_infos_from_news(n) - if infos: - n["text"] += ( - ' (%(descr_sem)s)' - % infos - ) - n["text"] += ( - " par " + sco_users.user_info(n["authenticated_user"])["nomcomplet"] - ) - return news - - -def _get_formsemestre_infos_from_news(n): - """Informations sur le semestre concerné par la nouvelle n - {} si inexistant - """ - formsemestre_id = None - if n["type"] == NEWS_INSCR: - formsemestre_id = n["object"] - elif n["type"] == NEWS_NOTE: - moduleimpl_id = n["object"] - if n["object"]: - mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id) - if not mods: - return {} # module does not exists anymore - mod = mods[0] - formsemestre_id = mod["formsemestre_id"] - - if not formsemestre_id: - return {} - - try: - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - except ValueError: - # semestre n'existe plus - return {} - - if sem["semestre_id"] > 0: - descr_sem = f'S{sem["semestre_id"]}' - else: - descr_sem = "" - if sem["modalite"]: - descr_sem += " " + sem["modalite"] - return {"formsemestre_id": formsemestre_id, "sem": sem, "descr_sem": descr_sem} - - -def scolar_news_summary_html(n=5): - """News summary, formated in HTML""" - news = scolar_news_summary(n=n) - if not news: - return "" - H = ['
Dernières opérations'] - H.append('
    ') - - for n in news: - H.append( - '
  • %(formatted_date)s%(text)s
  • ' - % n - ) - H.append("
") - - # Informations générales - H.append( - """
- Pour être informé des évolutions de ScoDoc, - vous pouvez vous - - abonner à la liste de diffusion. -
- """ - % scu.SCO_ANNONCES_WEBSITE - ) - - H.append("
") - return "\n".join(H) - - -def _send_news_by_mail(n): - """Notify by email""" - infos = _get_formsemestre_infos_from_news(n) - formsemestre_id = infos.get("formsemestre_id", None) - prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id) - destinations = prefs["emails_notifications"] or "" - destinations = [x.strip() for x in destinations.split(",")] - destinations = [x for x in destinations if x] - if not destinations: - return - # - txt = n["text"] - if infos: - txt += "\n\nSemestre %(titremois)s\n\n" % infos["sem"] - txt += ( - """%(descr_sem)s - """ - % infos - ) - txt += "\n\nEffectué par: %(nomcomplet)s\n" % n["user_info"] - - txt = ( - "\n" - + txt - + """\n ---- Ceci est un message de notification automatique issu de ScoDoc ---- vous recevez ce message car votre adresse est indiquée dans les paramètres de ScoDoc. -""" - ) - - # Transforme les URL en URL absolue - base = scu.ScoURL() - txt = re.sub('href=.*?"', 'href="' + base + "/", txt) - - # Transforme les liens HTML en texte brut: 'texte' devient 'texte: url' - # (si on veut des messages non html) - txt = re.sub(r'(.*?)', r"\2: \1", txt) - - subject = "[ScoDoc] " + NEWS_MAP.get(n["type"], "?") - sender = prefs["email_from_addr"] - - email.send_email(subject, sender, destinations, txt) diff --git a/requirements-3.9.txt b/requirements-3.9.txt index b4a10ac16..2f91f07a7 100755 --- a/requirements-3.9.txt +++ b/requirements-3.9.txt @@ -1,5 +1,5 @@ alembic==1.7.5 -astroid==2.9.1 +astroid==2.11.2 attrs==21.4.0 Babel==2.9.1 blinker==1.4 @@ -35,6 +35,7 @@ isort==5.10.1 itsdangerous==2.0.1 Jinja2==3.0.3 lazy-object-proxy==1.7.1 +lxml==4.8.0 Mako==1.1.6 MarkupSafe==2.0.1 mccabe==0.6.1 @@ -53,6 +54,7 @@ pyOpenSSL==21.0.0 pyparsing==3.0.6 pytest==6.2.5 python-dateutil==2.8.2 +python-docx==0.8.11 python-dotenv==0.19.2 python-editor==1.0.4 pytz==2021.3 From 721a15d5ec59599a0a1fc6f540cdecc97f04aa3c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 12 Apr 2022 17:12:51 +0200 Subject: [PATCH 22/37] =?UTF-8?q?R=C3=A9-=C3=A9criture=20des=20news.=20Clo?= =?UTF-8?q?se=20#117?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/etudiants.py | 4 +- app/models/events.py | 220 +++++++++++++++++++++++++++- app/models/formsemestre.py | 10 ++ app/scodoc/sco_dept.py | 5 +- app/scodoc/sco_edit_formation.py | 18 +-- app/scodoc/sco_edit_matiere.py | 18 +-- app/scodoc/sco_edit_module.py | 16 +- app/scodoc/sco_edit_ue.py | 17 +-- app/scodoc/sco_etud.py | 7 +- app/scodoc/sco_evaluation_db.py | 16 +- app/scodoc/sco_evaluations.py | 26 ++-- app/scodoc/sco_exceptions.py | 11 ++ app/scodoc/sco_formations.py | 19 ++- app/scodoc/sco_formsemestre.py | 6 +- app/scodoc/sco_formsemestre_edit.py | 8 +- app/scodoc/sco_import_etuds.py | 11 +- app/scodoc/sco_preferences.py | 2 +- app/scodoc/sco_saisie_notes.py | 31 ++-- app/scodoc/sco_synchro_etuds.py | 13 +- app/static/css/scodoc.css | 10 ++ app/templates/dept_news.html | 47 ++++++ app/views/scolar.py | 64 ++++++++ scodoc.py | 1 + 23 files changed, 461 insertions(+), 119 deletions(-) create mode 100644 app/templates/dept_news.html diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 962e7baaf..3aacb66a9 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -16,7 +16,7 @@ from app import models from app.scodoc import notesdb as ndb from app.scodoc.sco_bac import Baccalaureat -from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_exceptions import ScoInvalidParamError import app.scodoc.sco_utils as scu @@ -358,7 +358,7 @@ def make_etud_args( try: args = {"etudid": int(etudid)} except ValueError as exc: - raise ScoValueError("Adresse invalide") from exc + raise ScoInvalidParamError() from exc elif code_nip: args = {"code_nip": code_nip} elif use_request: # use form from current request (Flask global) diff --git a/app/models/events.py b/app/models/events.py index 55b34d38d..ccb6396e5 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -2,9 +2,21 @@ """Evenements et logs divers """ +import datetime +import re + +from flask import g, url_for +from flask_login import current_user from app import db +from app import email +from app import log +from app.auth.models import User from app.models import SHORT_STR_LEN +from app.models.formsemestre import FormSemestre +from app.models.moduleimpls import ModuleImpl +import app.scodoc.sco_utils as scu +from app.scodoc import sco_preferences class Scolog(db.Model): @@ -24,13 +36,213 @@ class Scolog(db.Model): class ScolarNews(db.Model): """Nouvelles pour page d'accueil""" + NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id) + NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id) + NEWS_FORM = "FORM" # modification formation (object=formation_id) + NEWS_SEM = "SEM" # creation semestre (object=None) + NEWS_ABS = "ABS" # saisie absence + NEWS_MISC = "MISC" # unused + NEWS_MAP = { + NEWS_INSCR: "inscription d'étudiants", + NEWS_NOTE: "saisie note", + NEWS_FORM: "modification formation", + NEWS_SEM: "création semestre", + NEWS_MISC: "opération", # unused + } + NEWS_TYPES = list(NEWS_MAP.keys()) + __tablename__ = "scolar_news" id = db.Column(db.Integer, primary_key=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) - date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - authenticated_user = db.Column(db.Text) # login, sans contrainte + date = db.Column( + db.DateTime(timezone=True), server_default=db.func.now(), index=True + ) + authenticated_user = db.Column(db.Text, index=True) # login, sans contrainte # type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC' - type = db.Column(db.String(SHORT_STR_LEN)) - object = db.Column(db.Integer) # moduleimpl_id, formation_id, formsemestre_id + type = db.Column(db.String(SHORT_STR_LEN), index=True) + object = db.Column( + db.Integer, index=True + ) # moduleimpl_id, formation_id, formsemestre_id text = db.Column(db.Text) url = db.Column(db.Text) + + def __repr__(self): + return ( + f"<{self.__class__.__name__}(id={self.id}, date='{self.date.isoformat()}')>" + ) + + def __str__(self): + "'Chargement notes dans Stage (S3 FI) par Aurélie Dupont'" + formsemestre = self.get_news_formsemestre() + user = User.query.filter_by(user_name=self.authenticated_user).first() + + sem_text = ( + f"""({formsemestre.sem_modalite()})""" + if formsemestre + else "" + ) + author = f"par {user.get_nomcomplet()}" if user else "" + return f"{self.text} {sem_text} {author}" + + def formatted_date(self) -> str: + "06 Avr 14h23" + mois = scu.MONTH_NAMES_ABBREV[self.date.month - 1] + return f"{self.date.day} {mois} {self.date.hour:02d}h{self.date.minute:02d}" + + def to_dict(self): + return { + "date": { + "display": self.date.strftime("%d/%m/%Y %H:%M"), + "timestamp": self.date.timestamp(), + }, + "type": self.NEWS_MAP.get(self.type, "?"), + "authenticated_user": self.authenticated_user, + "text": self.text, + } + + @classmethod + def last_news(cls, n=1) -> list: + "The most recent n news. Returns list of ScolarNews instances." + return cls.query.order_by(cls.date.desc()).limit(n).all() + + @classmethod + def add(cls, typ, obj=None, text="", url=None, max_frequency=0): + """Enregistre une nouvelle + Si max_frequency, ne génère pas 2 nouvelles "identiques" + à moins de max_frequency secondes d'intervalle. + Deux nouvelles sont considérées comme "identiques" si elles ont + même (obj, typ, user). + La nouvelle enregistrée est aussi envoyée par mail. + """ + if max_frequency: + last_news = ( + cls.query.filter_by( + dept_id=g.scodoc_dept_id, + authenticated_user=current_user.user_name, + type=typ, + object=obj, + ) + .order_by(cls.date.desc()) + .limit(1) + .first() + ) + if last_news: + now = datetime.datetime.now(tz=last_news.date.tzinfo) + if (now - last_news.date) < datetime.timedelta(seconds=max_frequency): + # on n'enregistre pas + return + + news = ScolarNews( + dept_id=g.scodoc_dept_id, + authenticated_user=current_user.user_name, + type=typ, + object=obj, + text=text, + url=url, + ) + db.session.add(news) + db.session.commit() + log(f"news: {news}") + news.notify_by_mail() + + def get_news_formsemestre(self) -> FormSemestre: + """formsemestre concerné par la nouvelle + None si inexistant + """ + formsemestre_id = None + if self.type == self.NEWS_INSCR: + formsemestre_id = self.object + elif self.type == self.NEWS_NOTE: + moduleimpl_id = self.object + if moduleimpl_id: + modimpl = ModuleImpl.query.get(moduleimpl_id) + if modimpl is None: + return None # module does not exists anymore + formsemestre_id = modimpl.formsemestre_id + + if not formsemestre_id: + return None + formsemestre = FormSemestre.query.get(formsemestre_id) + return formsemestre + + def notify_by_mail(self): + """Notify by email""" + formsemestre = self.get_news_formsemestre() + + prefs = sco_preferences.SemPreferences( + formsemestre_id=formsemestre.id if formsemestre else None + ) + destinations = prefs["emails_notifications"] or "" + destinations = [x.strip() for x in destinations.split(",")] + destinations = [x for x in destinations if x] + if not destinations: + return + # + txt = self.text + if formsemestre: + txt += f"""\n\nSemestre {formsemestre.titre_mois()}\n\n""" + txt += f"""{formsemestre.sem_modalite()} + """ + user = User.query.filter_by(user_name=self.authenticated_user).first() + if user: + txt += f"\n\nEffectué par: {user.get_nomcomplet()}\n" + + txt = ( + "\n" + + txt + + """\n + --- Ceci est un message de notification automatique issu de ScoDoc + --- vous recevez ce message car votre adresse est indiquée dans les paramètres de ScoDoc. + """ + ) + + # Transforme les URL en URL absolues + base = scu.ScoURL() + txt = re.sub('href=/.*?"', 'href="' + base + "/", txt) + + # Transforme les liens HTML en texte brut: 'texte' devient 'texte: url' + # (si on veut des messages non html) + txt = re.sub(r'(.*?)', r"\2: \1", txt) + + subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?") + sender = prefs["email_from_addr"] + + email.send_email(subject, sender, destinations, txt) + + @classmethod + def scolar_news_summary_html(cls, n=5) -> str: + """News summary, formated in HTML""" + news_list = cls.last_news(n=n) + if not news_list: + return "" + H = [ + f"""
Dernières opérations +
    """ + ] + + for news in news_list: + H.append( + f"""
  • {news.formatted_date()}{news}
  • """ + ) + + H.append("
") + + # Informations générales + H.append( + f"""
+ Pour être informé des évolutions de ScoDoc, + vous pouvez vous + + abonner à la liste de diffusion. +
+ """ + ) + + H.append("
") + return "\n".join(H) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 4ec90052b..edf5fa68d 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -374,6 +374,16 @@ class FormSemestre(db.Model): return self.titre return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" + def sem_modalite(self) -> str: + """Le semestre et la modialité, ex "S2 FI" ou "S3 APP" """ + if self.semestre_id > 0: + descr_sem = f"S{self.semestre_id}" + else: + descr_sem = "" + if self.modalite: + descr_sem += " " + self.modalite + return descr_sem + def get_abs_count(self, etudid): """Les comptes d'absences de cet étudiant dans ce semestre: tuple (nb abs, nb abs justifiées) diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py index 453aa2f6c..c19f93608 100644 --- a/app/scodoc/sco_dept.py +++ b/app/scodoc/sco_dept.py @@ -32,6 +32,7 @@ from flask import g, request from flask_login import current_user import app +from app.models import ScolarNews import app.scodoc.sco_utils as scu from app.scodoc.gen_tables import GenTable from app.scodoc.sco_permissions import Permission @@ -40,9 +41,7 @@ import app.scodoc.notesdb as ndb from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_modalites -from app.scodoc import sco_news from app.scodoc import sco_preferences -from app.scodoc import sco_up_to_date from app.scodoc import sco_users @@ -53,7 +52,7 @@ def index_html(showcodes=0, showsemtable=0): H = [] # News: - H.append(sco_news.scolar_news_summary_html()) + H.append(ScolarNews.scolar_news_summary_html()) # Avertissement de mise à jour: H.append("""
""") diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index 48caf2d5a..aabfaddce 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -37,6 +37,7 @@ from app.models import SHORT_STR_LEN from app.models.formations import Formation from app.models.modules import Module from app.models.ues import UniteEns +from app.models import ScolarNews import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -44,13 +45,10 @@ from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError from app.scodoc import html_sco_header -from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours -from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_formations from app.scodoc import sco_formsemestre -from app.scodoc import sco_news def formation_delete(formation_id=None, dialog_confirmed=False): @@ -117,11 +115,10 @@ def do_formation_delete(oid): sco_formations._formationEditor.delete(cnx, oid) # news - sco_news.add( - typ=sco_news.NEWS_FORM, - object=oid, - text="Suppression de la formation %(acronyme)s" % F, - max_frequency=3, + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=oid, + text=f"Suppression de la formation {F['acronyme']}", ) @@ -281,10 +278,9 @@ def do_formation_create(args): # r = sco_formations._formationEditor.create(cnx, args) - sco_news.add( - typ=sco_news.NEWS_FORM, + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, text="Création de la formation %(titre)s (%(acronyme)s)" % args, - max_frequency=3, ) return r diff --git a/app/scodoc/sco_edit_matiere.py b/app/scodoc/sco_edit_matiere.py index f691e350a..2ae1c15ab 100644 --- a/app/scodoc/sco_edit_matiere.py +++ b/app/scodoc/sco_edit_matiere.py @@ -30,6 +30,7 @@ """ import flask from flask import g, url_for, request +from app.models.events import ScolarNews from app.models.formations import Matiere import app.scodoc.notesdb as ndb @@ -78,8 +79,7 @@ def do_matiere_edit(*args, **kw): def do_matiere_create(args): "create a matiere" from app.scodoc import sco_edit_ue - from app.scodoc import sco_formations - from app.scodoc import sco_news + from app.models import ScolarNews cnx = ndb.GetDBConnexion() # check @@ -89,9 +89,9 @@ def do_matiere_create(args): # news formation = Formation.query.get(ue["formation_id"]) - sco_news.add( - typ=sco_news.NEWS_FORM, - object=ue["formation_id"], + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=ue["formation_id"], text=f"Modification de la formation {formation.acronyme}", max_frequency=3, ) @@ -174,10 +174,8 @@ def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]: def do_matiere_delete(oid): "delete matiere and attached modules" - from app.scodoc import sco_formations from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_module - from app.scodoc import sco_news cnx = ndb.GetDBConnexion() # check @@ -197,9 +195,9 @@ def do_matiere_delete(oid): # news formation = Formation.query.get(ue["formation_id"]) - sco_news.add( - typ=sco_news.NEWS_FORM, - object=ue["formation_id"], + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=ue["formation_id"], text=f"Modification de la formation {formation.acronyme}", max_frequency=3, ) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 2f6b3f430..6d1547491 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -38,6 +38,7 @@ from app import models from app.models import APO_CODE_STR_LEN from app.models import Formation, Matiere, Module, UniteEns from app.models import FormSemestre, ModuleImpl +from app.models import ScolarNews import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -53,7 +54,6 @@ from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_matiere from app.scodoc import sco_moduleimpl -from app.scodoc import sco_news _moduleEditor = ndb.EditableTable( "notes_modules", @@ -98,16 +98,14 @@ def module_list(*args, **kw): def do_module_create(args) -> int: "Create a module. Returns id of new object." # create - from app.scodoc import sco_formations - cnx = ndb.GetDBConnexion() r = _moduleEditor.create(cnx, args) # news formation = Formation.query.get(args["formation_id"]) - sco_news.add( - typ=sco_news.NEWS_FORM, - object=formation.id, + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=formation.id, text=f"Modification de la formation {formation.acronyme}", max_frequency=3, ) @@ -396,9 +394,9 @@ def do_module_delete(oid): # news formation = module.formation - sco_news.add( - typ=sco_news.NEWS_FORM, - object=mod["formation_id"], + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=mod["formation_id"], text=f"Modification de la formation {formation.acronyme}", max_frequency=3, ) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index ffe4d64fc..ec4245765 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -37,6 +37,7 @@ from app import db from app import log from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import Formation, UniteEns, ModuleImpl, Module +from app.models import ScolarNews from app.models.formations import Matiere import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -55,15 +56,11 @@ from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_apc -from app.scodoc import sco_edit_formation from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module -from app.scodoc import sco_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl -from app.scodoc import sco_news -from app.scodoc import sco_permissions from app.scodoc import sco_preferences from app.scodoc import sco_tag_module @@ -138,9 +135,9 @@ def do_ue_create(args): ue = UniteEns.query.get(ue_id) flash(f"UE créée (code {ue.ue_code})") formation = Formation.query.get(args["formation_id"]) - sco_news.add( - typ=sco_news.NEWS_FORM, - object=args["formation_id"], + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=args["formation_id"], text=f"Modification de la formation {formation.acronyme}", max_frequency=3, ) @@ -222,9 +219,9 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): sco_cache.invalidate_formsemestre() # news F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0] - sco_news.add( - typ=sco_news.NEWS_FORM, - object=ue.formation_id, + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=ue.formation_id, text="Modification de la formation %(acronyme)s" % F, max_frequency=3, ) diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index 710f85bc4..598ec96b0 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -637,7 +637,7 @@ def create_etud(cnx, args={}): Returns: etud, l'étudiant créé. """ - from app.scodoc import sco_news + from app.models import ScolarNews # creation d'un etudiant etudid = etudident_create(cnx, args) @@ -671,9 +671,8 @@ def create_etud(cnx, args={}): etud = etudident_list(cnx, {"etudid": etudid})[0] fill_etuds_info([etud]) etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) - sco_news.add( - typ=sco_news.NEWS_INSCR, - object=None, # pas d'object pour ne montrer qu'un etudiant + ScolarNews.add( + typ=ScolarNews.NEWS_INSCR, text='Nouvel étudiant %(nomprenom)s' % etud, url=etud["url"], ) diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 59dbc5fe8..7aec90889 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -28,7 +28,6 @@ """Gestion evaluations (ScoDoc7, sans SQlAlchemy) """ -import datetime import pprint import flask @@ -37,6 +36,7 @@ from flask_login import current_user from app import log +from app.models import ScolarNews from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -44,9 +44,7 @@ from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc import sco_cache from app.scodoc import sco_edit_module -from app.scodoc import sco_formsemestre from app.scodoc import sco_moduleimpl -from app.scodoc import sco_news from app.scodoc import sco_permissions_check @@ -179,9 +177,9 @@ def do_evaluation_create( mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] mod["moduleimpl_id"] = M["moduleimpl_id"] mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod - sco_news.add( - typ=sco_news.NEWS_NOTE, - object=moduleimpl_id, + ScolarNews.add( + typ=ScolarNews.NEWS_NOTE, + obj=moduleimpl_id, text='Création d\'une évaluation dans %(titre)s' % mod, url=mod["url"], ) @@ -240,9 +238,9 @@ def do_evaluation_delete(evaluation_id): mod["url"] = ( scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod ) - sco_news.add( - typ=sco_news.NEWS_NOTE, - object=moduleimpl_id, + ScolarNews.add( + typ=ScolarNews.NEWS_NOTE, + obj=moduleimpl_id, text='Suppression d\'une évaluation dans %(titre)s' % mod, url=mod["url"], ) diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 43b585907..6152e8810 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -36,32 +36,27 @@ from flask import g from flask_login import current_user from flask import request -from app import log - from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre +from app.models import ScolarNews import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb -from app.scodoc.sco_exceptions import AccessDenied, ScoValueError -import sco_version from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_evaluation_db from app.scodoc import sco_abs -from app.scodoc import sco_cache from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue -from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl -from app.scodoc import sco_news from app.scodoc import sco_permissions_check from app.scodoc import sco_preferences from app.scodoc import sco_users +import sco_version # -------------------------------------------------------------------- @@ -633,13 +628,16 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): 'voir toutes les notes du module' % moduleimpl_id ) - mod_descr = '%s %s (resp. %s) %s' % ( - moduleimpl_id, - Mod["code"] or "", - Mod["titre"] or "?", - nomcomplet, - resp, - link, + mod_descr = ( + '%s %s (resp. %s) %s' + % ( + moduleimpl_id, + Mod["code"] or "", + Mod["titre"] or "?", + nomcomplet, + resp, + link, + ) ) etit = E["description"] or "" diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 35d2d9d6e..17d39a4c2 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -63,6 +63,17 @@ class ScoFormatError(ScoValueError): pass +class ScoInvalidParamError(ScoValueError): + """Paramètres requete invalides. + A utilisée lorsqu'une route est appelée avec des paramètres invalides + (id strings, ...) + """ + + def __init__(self, msg=None, dest_url=None): + msg = msg or "Adresse invalide. Vérifiez vos signets." + super().__init__(msg, dest_url=dest_url) + + class ScoPDFFormatError(ScoValueError): "erreur génération PDF (templates platypus, ...)" diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 8b99a0678..178138b22 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -39,12 +39,12 @@ import app.scodoc.notesdb as ndb from app import db from app import log from app.models import Formation, Module +from app.models import ScolarNews from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_formsemestre -from app.scodoc import sco_news from app.scodoc import sco_preferences from app.scodoc import sco_tag_module from app.scodoc import sco_xml @@ -351,10 +351,13 @@ def formation_list_table(formation_id=None, args={}): else: but_locked = '' if editable and not locked: - but_suppr = '%s' % ( - f["formation_id"], - f["acronyme"].lower().replace(" ", "-"), - suppricon, + but_suppr = ( + '%s' + % ( + f["formation_id"], + f["acronyme"].lower().replace(" ", "-"), + suppricon, + ) ) else: but_suppr = '' @@ -422,9 +425,9 @@ def formation_create_new_version(formation_id, redirect=True): new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data) # news F = formation_list(args={"formation_id": new_id})[0] - sco_news.add( - typ=sco_news.NEWS_FORM, - object=new_id, + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=new_id, text="Nouvelle version de la formation %(acronyme)s" % F, ) if redirect: diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index e5f8129e8..5d8510ac2 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -229,7 +229,7 @@ def etapes_apo_str(etapes): def do_formsemestre_create(args, silent=False): "create a formsemestre" from app.scodoc import sco_groups - from app.scodoc import sco_news + from app.models import ScolarNews cnx = ndb.GetDBConnexion() formsemestre_id = _formsemestreEditor.create(cnx, args) @@ -254,8 +254,8 @@ def do_formsemestre_create(args, silent=False): args["formsemestre_id"] = formsemestre_id args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args if not silent: - sco_news.add( - typ=sco_news.NEWS_SEM, + ScolarNews.add( + typ=ScolarNews.NEWS_SEM, text='Création du semestre %(titre)s' % args, url=args["url"], ) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 7d903f6cf..3c88ff706 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -1493,11 +1493,9 @@ def do_formsemestre_delete(formsemestre_id): sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id) # news - from app.scodoc import sco_news - - sco_news.add( - typ=sco_news.NEWS_SEM, - object=formsemestre_id, + ScolarNews.add( + typ=ScolarNews.NEWS_SEM, + obj=formsemestre_id, text="Suppression du semestre %(titre)s" % sem, ) diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index da117b5d0..32b2530d4 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -40,6 +40,8 @@ from flask import g, url_for import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log +from app.models import ScolarNews + from app.scodoc.sco_excel import COLORS from app.scodoc.sco_formsemestre_inscriptions import ( do_formsemestre_inscription_with_modules, @@ -54,14 +56,13 @@ from app.scodoc.sco_exceptions import ( ScoLockedFormError, ScoGenError, ) + from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import sco_etud -from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_excel from app.scodoc import sco_groups_view -from app.scodoc import sco_news from app.scodoc import sco_preferences # format description (in tools/) @@ -472,11 +473,11 @@ def scolars_import_excel_file( diag.append("Import et inscription de %s étudiants" % len(created_etudids)) - sco_news.add( - typ=sco_news.NEWS_INSCR, + ScolarNews.add( + typ=ScolarNews.NEWS_INSCR, text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents % len(created_etudids), - object=formsemestre_id, + obj=formsemestre_id, ) log("scolars_import_excel_file: completing transaction") diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 112b0da50..9ccb63628 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -350,7 +350,7 @@ class BasePreferences(object): "initvalue": "", "title": "e-mails à qui notifier les opérations", "size": 70, - "explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc). (vous pouvez préférer utiliser le flux rss)", + "explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc).", "category": "general", "only_global": False, # peut être spécifique à un semestre }, diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index fbef92458..23847c994 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -39,6 +39,7 @@ from flask_login import current_user from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre +from app.models import ScolarNews import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb @@ -48,6 +49,7 @@ from app.scodoc.sco_exceptions import ( InvalidNoteValue, NoteProcessError, ScoGenError, + ScoInvalidParamError, ScoValueError, ) from app.scodoc.TrivialFormulator import TrivialFormulator, TF @@ -64,7 +66,6 @@ from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_groups_view from app.scodoc import sco_moduleimpl -from app.scodoc import sco_news from app.scodoc import sco_permissions_check from app.scodoc import sco_undo_notes from app.scodoc import sco_etud @@ -274,9 +275,9 @@ def do_evaluation_upload_xls(): moduleimpl_id=mod["moduleimpl_id"], _external=True, ) - sco_news.add( - typ=sco_news.NEWS_NOTE, - object=M["moduleimpl_id"], + ScolarNews.add( + typ=ScolarNews.NEWS_NOTE, + obj=M["moduleimpl_id"], text='Chargement notes dans %(titre)s' % mod, url=mod["url"], ) @@ -359,9 +360,9 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"], ) - sco_news.add( - typ=sco_news.NEWS_NOTE, - object=M["moduleimpl_id"], + ScolarNews.add( + typ=ScolarNews.NEWS_NOTE, + obj=M["moduleimpl_id"], text='Initialisation notes dans %(titre)s' % mod, url=mod["url"], ) @@ -451,9 +452,9 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] mod["moduleimpl_id"] = M["moduleimpl_id"] mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod - sco_news.add( - typ=sco_news.NEWS_NOTE, - object=M["moduleimpl_id"], + ScolarNews.add( + typ=ScolarNews.NEWS_NOTE, + obj=M["moduleimpl_id"], text='Suppression des notes d\'une évaluation dans %(titre)s' % mod, url=mod["url"], @@ -893,10 +894,12 @@ def has_existing_decision(M, E, etudid): def saisie_notes(evaluation_id, group_ids=[]): """Formulaire saisie notes d'une évaluation pour un groupe""" + if not isinstance(evaluation_id, int): + raise ScoInvalidParamError() group_ids = [int(group_id) for group_id in group_ids] evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: - raise ScoValueError("invalid evaluation_id") + raise ScoValueError("évaluation inexistante") E = evals[0] M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] formsemestre_id = M["formsemestre_id"] @@ -1283,9 +1286,9 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""): nbchanged, _, existing_decisions = notes_add( authuser, evaluation_id, L, comment=comment, do_it=True ) - sco_news.add( - typ=sco_news.NEWS_NOTE, - object=M["moduleimpl_id"], + ScolarNews.add( + typ=ScolarNews.NEWS_NOTE, + obj=M["moduleimpl_id"], text='Chargement notes dans %(titre)s' % Mod, url=Mod["url"], max_frequency=30 * 60, # 30 minutes diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index db775438c..6483ffcff 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -29,12 +29,14 @@ """ import time -import pprint from operator import itemgetter from flask import g, url_for from flask_login import current_user +from app import log +from app.models import ScolarNews + import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import html_sco_header @@ -43,11 +45,8 @@ from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_inscr_passage -from app.scodoc import sco_news -from app.scodoc import sco_excel from app.scodoc import sco_portal_apogee from app.scodoc import sco_etud -from app import log from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission @@ -701,10 +700,10 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident): sco_cache.invalidate_formsemestre() raise - sco_news.add( - typ=sco_news.NEWS_INSCR, + ScolarNews.add( + typ=ScolarNews.NEWS_INSCR, text="Import Apogée de %d étudiants en " % len(created_etudids), - object=sem["formsemestre_id"], + obj=sem["formsemestre_id"], ) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 9811b3045..a153ecc02 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -487,6 +487,16 @@ div.news { border-radius: 8px; } +div.news a { + color: black; + text-decoration: none; +} + +div.news a:hover { + color: rgb(153, 51, 51); + text-decoration: underline; +} + span.newstitle { font-weight: bold; } diff --git a/app/templates/dept_news.html b/app/templates/dept_news.html new file mode 100644 index 000000000..23e3f26c8 --- /dev/null +++ b/app/templates/dept_news.html @@ -0,0 +1,47 @@ +{# -*- mode: jinja-html -*- #} +{% extends "sco_page.html" %} +{% block styles %} +{{super()}} +{% endblock %} + +{% block app_content %} +

Opérations dans le département {{g.scodoc_dept}}

+ +
+

Saisie des absences {gr_tit} {sem["titre_num"]}, + les {day_name}s

- %s + {msg}

- """ - % (gr_tit, sem["titre_num"], dayname, url_link_semaines, msg), + """, ] # if etuds: @@ -820,7 +827,6 @@ def _gen_form_saisie_groupe( # version pour formulaire avec AJAX (Yann LB) H.append( """ -

@@ -831,8 +837,7 @@ def _gen_form_saisie_groupe(

Si vous "décochez" une case, l'absence correspondante sera supprimée. Attention, les modifications sont automatiquement entregistrées au fur et à mesure.

- """ - % destination + """ ) return H diff --git a/sco_version.py b/sco_version.py index 4e9e9ad14..15814bf76 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.92" +SCOVERSION = "9.2.0a" SCONAME = "ScoDoc" From 705aa54d775b395194f4f823f59ad854bc2b8502 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 9 Apr 2022 14:20:56 +0200 Subject: [PATCH 16/37] =?UTF-8?q?Table=20recap:=20liens=20vers=20=C3=A9val?= =?UTF-8?q?uations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_common.py | 7 ++++++- app/static/css/scodoc.css | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 7245439f4..60c7547db 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -680,7 +680,7 @@ class ResultatsSemestre(ResultatsCache): row["_moy_gen_class"] = "col_moy_gen" # titre de la ligne: row["prenom"] = row["nom_short"] = ( - row.get(f"_title", "") or bottom_line.capitalize() + row.get("_title", "") or bottom_line.capitalize() ) row["_tr_class"] = bottom_line.lower() + ( (" " + row["_tr_class"]) if "_tr_class" in row else "" @@ -894,3 +894,8 @@ class ResultatsSemestre(ResultatsCache): bottom_infos["min"][cid] = "0" bottom_infos["max"][cid] = scu.fmt_note(e.note_max) bottom_infos["descr_evaluation"][cid] = e.description or "" + bottom_infos["descr_evaluation"][f"_{cid}_target"] = url_for( + "notes.evaluation_listenotes", + scodoc_dept=g.scodoc_dept, + evaluation_id=e.id, + ) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 3625d5f01..470b929a8 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -3730,6 +3730,15 @@ table.table_recap tr.descr_evaluation { color: rgb(4, 16, 159); } +table.table_recap tr.descr_evaluation a { + color: rgb(4, 16, 159); + text-decoration: none; +} + +table.table_recap tr.descr_evaluation a:hover { + color: red; +} + table.table_recap tr.descr_evaluation { vertical-align: top; } From 570e2dc308533aa819dd978e34ecc99ff448e37b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Apr 2022 17:38:59 +0200 Subject: [PATCH 17/37] =?UTF-8?q?Page=20=C3=A9tat=20de=20=C3=A9valuations?= =?UTF-8?q?=20(closes=20#142).=20Am=C3=A9liore=20tableau=20recap.=20Cosm?= =?UTF-8?q?=C3=A9tique.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_mod.py | 11 +- app/comp/res_common.py | 27 +++-- app/models/moduleimpls.py | 1 - app/scodoc/sco_evaluation_check_abs.py | 2 +- app/scodoc/sco_evaluation_recap.py | 144 +++++++++++++++++++++++++ app/scodoc/sco_formsemestre_status.py | 5 + app/scodoc/sco_liste_notes.py | 22 ++-- app/scodoc/sco_recapcomplet.py | 31 +----- app/scodoc/sco_utils.py | 30 ++++++ app/static/css/scodoc.css | 89 ++++++++++++++- app/static/js/evaluations_recap.js | 38 +++++++ app/views/notes.py | 11 +- 12 files changed, 359 insertions(+), 52 deletions(-) create mode 100644 app/scodoc/sco_evaluation_recap.py create mode 100644 app/static/js/evaluations_recap.js diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index eea357e8f..13829a392 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -92,6 +92,8 @@ class ModuleImplResults: ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) ne donnent pas de coef vers cette UE. """ + self.evals_etudids_sans_note = {} + """dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions.""" self.load_notes() self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index) """1 bool par etud, indique si sa moyenne de module vient de la session2""" @@ -142,12 +144,13 @@ class ModuleImplResults: # ou évaluation déclarée "à prise en compte immédiate" # Les évaluations de rattrapage et 2eme session sont toujours incomplètes # car on calcule leur moyenne à part. + etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem. is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and ( - evaluation.publish_incomplete - or (not (inscrits_module - set(eval_df.index))) + evaluation.publish_incomplete or (not etudids_sans_note) ) self.evaluations_completes.append(is_complete) self.evaluations_completes_dict[evaluation.id] = is_complete + self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note # NULL en base => ABS (= -999) eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) @@ -193,7 +196,9 @@ class ModuleImplResults: return eval_df def _etudids(self): - """L'index du dataframe est la liste de tous les étudiants inscrits au semestre""" + """L'index du dataframe est la liste de tous les étudiants inscrits au semestre + (incluant les DEM et DEF) + """ return [ inscr.etudid for inscr in ModuleImpl.query.get( diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 60c7547db..7bc199ead 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -862,20 +862,27 @@ class ResultatsSemestre(ResultatsCache): "_tr_class": "bottom_info", "_title": "Description évaluation", } - first = True + first_eval = True + index_col = 9000 # à droite for modimpl in self.formsemestre.modimpls_sorted: evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl) eval_index = len(evals) - 1 inscrits = {i.etudid for i in modimpl.inscriptions} - klass = "evaluation first" if first else "evaluation" - first = False - for i, e in enumerate(evals): + first_eval_of_mod = True + for e in evals: cid = f"eval_{e.id}" titles[ cid ] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}' + klass = "evaluation" + if first_eval: + klass += " first" + elif first_eval_of_mod: + klass += " first_of_mod" titles[f"_{cid}_class"] = klass - titles[f"_{cid}_col_order"] = 9000 + i # à droite + first_eval_of_mod = first_eval = False + titles[f"_{cid}_col_order"] = index_col + index_col += 1 eval_index -= 1 notes_db = sco_evaluation_db.do_evaluation_get_all_notes( e.evaluation_id @@ -889,7 +896,15 @@ class ResultatsSemestre(ResultatsCache): # Note manquante mais prise en compte immédiate: affiche ATT val = scu.NOTES_ATTENTE row[cid] = scu.fmt_note(val) - row[f"_{cid}_class"] = klass + row[f"_{cid}_class"] = klass + { + "ABS": " abs", + "ATT": " att", + "EXC": " exc", + }.get(row[cid], "") + else: + row[cid] = "ni" + row[f"_{cid}_class"] = klass + " non_inscrit" + bottom_infos["coef"][cid] = e.coefficient bottom_infos["min"][cid] = "0" bottom_infos["max"][cid] = scu.fmt_note(e.note_max) diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 292ec8ffd..7574ed7e8 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -9,7 +9,6 @@ from app.comp import df_cache from app.models.etudiants import Identite from app.models.modules import Module -import app.scodoc.notesdb as ndb from app.scodoc import sco_utils as scu diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py index b8287c25d..b464cd673 100644 --- a/app/scodoc/sco_evaluation_check_abs.py +++ b/app/scodoc/sco_evaluation_check_abs.py @@ -25,7 +25,7 @@ # ############################################################################## -"""Vérification des abasneces à une évaluation +"""Vérification des absences à une évaluation """ from flask import url_for, g diff --git a/app/scodoc/sco_evaluation_recap.py b/app/scodoc/sco_evaluation_recap.py new file mode 100644 index 000000000..168b463b8 --- /dev/null +++ b/app/scodoc/sco_evaluation_recap.py @@ -0,0 +1,144 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Tableau recap. de toutes les évaluations d'un semestre +avec leur état. + +Sur une idée de Pascal Bouron, de Lyon. +""" +import time +from flask import g, url_for + +from app.models import Evaluation, FormSemestre +from app.comp import res_sem +from app.comp.res_compat import NotesTableCompat +from app.comp.moy_mod import ModuleImplResults +from app.scodoc import html_sco_header +import app.scodoc.sco_utils as scu + + +def evaluations_recap(formsemestre_id: int) -> str: + """Page récap. de toutes les évaluations d'un semestre""" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + rows, titles = evaluations_recap_table(formsemestre) + column_ids = titles.keys() + filename = scu.sanitize_filename( + f"""evaluations-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + if not rows: + return '
aucune évaluation
' + H = [ + html_sco_header.sco_header( + page_title="Évaluations du semestre", + javascripts=["js/evaluations_recap.js"], + ), + f"""
""", + ] + # header + H.append( + f""" + + {scu.gen_row(column_ids, titles, "th", with_col_classes=True)} + + """ + ) + # body + H.append("") + for row in rows: + H.append(f"{scu.gen_row(column_ids, row, with_col_classes=True)}\n") + + H.append("""
""") + H.append( + html_sco_header.sco_footer(), + ) + return "".join(H) + + +def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]: + """Tableau recap. de toutes les évaluations d'un semestre + Colonnes: + - code (UE ou module), + - titre + - complete + - publiée + - inscrits (non dem. ni def.) + - nb notes manquantes + - nb ATT + - nb ABS + - nb EXC + """ + rows = [] + titles = { + "type": "", + "code": "Code", + "titre": "", + "date": "Date", + "complete": "Comptée", + "inscrits": "Inscrits", + "manquantes": "Manquantes", # notes eval non entrées + "nb_abs": "ABS", + "nb_att": "ATT", + "nb_exc": "EXC", + } + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + line_idx = 0 + for modimpl in nt.formsemestre.modimpls_sorted: + modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id] + row = { + "type": modimpl.module.type_abbrv().upper(), + "_type_order": f"{line_idx:04d}", + "code": modimpl.module.code, + "_code_target": url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, + ), + "titre": modimpl.module.titre, + "_titre_class": "titre", + "inscrits": modimpl_results.nb_inscrits_module, + "date": "-", + "_date_order": "", + "_tr_class": f"module {modimpl.module.type_abbrv()}", + } + rows.append(row) + line_idx += 1 + for evaluation_id in modimpl_results.evals_notes: + e = Evaluation.query.get(evaluation_id) + eval_etat = modimpl_results.evaluations_etat[evaluation_id] + row = { + "type": "", + "_type_order": f"{line_idx:04d}", + "titre": e.description or "sans titre", + "_titre_target": url_for( + "notes.evaluation_listenotes", + scodoc_dept=g.scodoc_dept, + evaluation_id=evaluation_id, + ), + "_titre_target_attrs": 'class="discretelink"', + "date": e.jour.strftime("%d/%m/%Y") if e.jour else "", + "_date_order": e.jour.isoformat() if e.jour else "", + "complete": "oui" if eval_etat.is_complete else "non", + "_complete_target": "#", + "_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"' + if eval_etat.is_complete + else 'class="bull_link incomplete" title="il manque des notes"', + "manquantes": len(modimpl_results.evals_etudids_sans_note[e.id]), + "inscrits": modimpl_results.nb_inscrits_module, + "nb_abs": sum(modimpl_results.evals_notes[e.id] == scu.NOTES_ABSENCE), + "nb_att": eval_etat.nb_attente, + "nb_exc": sum( + modimpl_results.evals_notes[e.id] == scu.NOTES_NEUTRALISE + ), + "_tr_class": "evaluation" + + (" incomplete" if not eval_etat.is_complete else ""), + } + rows.append(row) + line_idx += 1 + + return rows, titles diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 0a36697b7..802399f2c 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -357,6 +357,11 @@ def formsemestre_status_menubar(sem): "endpoint": "notes.formsemestre_recapcomplet", "args": {"formsemestre_id": formsemestre_id}, }, + { + "title": "État des évaluations", + "endpoint": "notes.evaluations_recap", + "args": {"formsemestre_id": formsemestre_id}, + }, { "title": "Saisie des notes", "endpoint": "notes.formsemestre_status", diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index fd9b7e592..cee1e8493 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -698,7 +698,7 @@ def _add_eval_columns( # calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES if ( (etudid in inscrits) - and val != None + and val is not None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE ): @@ -721,16 +721,26 @@ def _add_eval_columns( comment, ) else: - explanation = "" - val_fmt = "" - val = None + if (etudid in inscrits) and e["publish_incomplete"]: + # Note manquante mais prise en compte immédiate: affiche ATT + val = scu.NOTES_ATTENTE + val_fmt = "ATT" + explanation = "non saisie mais prise en compte immédiate" + else: + explanation = "" + val_fmt = "" + val = None + + cell_class = klass + {"ATT": " att", "ABS": " abs", "EXC": " exc"}.get( + val_fmt, "" + ) if val is None: - row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {klass}" ' + row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {cell_class}" ' if not row.get("_css_row_class", ""): row["_css_row_class"] = "etudabs" else: - row[f"_{evaluation_id}_td_attrs"] = f'class="{klass}" ' + row[f"_{evaluation_id}_td_attrs"] = f'class="{cell_class}" ' # regroupe les commentaires if explanation: if explanation in K: diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 0dc6b19c4..83dd79d66 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -357,31 +357,6 @@ def formsemestres_bulletins(annee_scolaire): return scu.sendJSON(js_list) -def _gen_cell(key: str, row: dict, elt="td"): - "html table cell" - klass = row.get(f"_{key}_class") - attrs = f'class="{klass}"' if klass else "" - order = row.get(f"_{key}_order") - if order: - attrs += f' data-order="{order}"' - content = row.get(key, "") - target = row.get(f"_{key}_target") - target_attrs = row.get(f"_{key}_target_attrs", "") - if target or target_attrs: # avec lien - href = f'href="{target}"' if target else "" - content = f"{content}" - return f"<{elt} {attrs}>{content}" - - -def _gen_row(keys: list[str], row, elt="td", selected_etudid=None): - klass = row.get("_tr_class") - tr_class = f'class="{klass}"' if klass else "" - tr_id = ( - f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else "" - ) - return f'
") if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE): coefs = mod.ue_coefs_list() + H.append(f'') for coef in coefs: if coef[1] > 0: H.append( @@ -1197,6 +1198,7 @@ def formsemestre_tableau_modules( ) else: H.append(f"""""") + H.append("") H.append("
+ + + + + + + + + + +
DateTypeAuteurDétail
+{% endblock %} + + +{% block scripts %} +{{super()}} + +{% endblock %} diff --git a/app/views/scolar.py b/app/views/scolar.py index 1b88575e4..de987cb17 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -41,6 +41,7 @@ from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed from wtforms import SubmitField +from app import db from app import log from app.decorators import ( scodoc, @@ -52,8 +53,10 @@ from app.decorators import ( ) from app.models.etudiants import Identite from app.models.etudiants import make_etud_args +from app.models.events import ScolarNews from app.views import scolar_bp as bp +from app.views import ScoData import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -339,6 +342,67 @@ def install_info(): return sco_up_to_date.is_up_to_date() +@bp.route("/dept_news") +@scodoc +@permission_required(Permission.ScoView) +def dept_news(): + "Affiche table des dernières opérations" + return render_template( + "dept_news.html", title=f"Opérations {g.scodoc_dept}", sco=ScoData() + ) + + +@bp.route("/dept_news_json") +@scodoc +@permission_required(Permission.ScoView) +def dept_news_json(): + "Table des news du département" + start = request.args.get("start", type=int) + length = request.args.get("length", type=int) + + log(f"dept_news_json( start={start}, length={length})") + query = ScolarNews.query.filter_by(dept_id=g.scodoc_dept_id) + # search + search = request.args.get("search[value]") + if search: + query = query.filter( + db.or_( + ScolarNews.authenticated_user.like(f"%{search}%"), + ScolarNews.text.like(f"%{search}%"), + ) + ) + total_filtered = query.count() + # sorting + order = [] + i = 0 + while True: + col_index = request.args.get(f"order[{i}][column]") + if col_index is None: + break + col_name = request.args.get(f"columns[{col_index}][data]") + if col_name not in ["date", "type", "authenticated_user"]: + col_name = "date" + descending = request.args.get(f"order[{i}][dir]") == "desc" + col = getattr(ScolarNews, col_name) + if descending: + col = col.desc() + order.append(col) + i += 1 + if order: + query = query.order_by(*order) + + # pagination + query = query.offset(start).limit(length) + data = [news.to_dict() for news in query] + # response + return { + "data": data, + "recordsFiltered": total_filtered, + "recordsTotal": ScolarNews.query.count(), + "draw": request.args.get("draw", type=int), + } + + sco_publish( "/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"] ) diff --git a/scodoc.py b/scodoc.py index c728bed98..1d3b17262 100755 --- a/scodoc.py +++ b/scodoc.py @@ -77,6 +77,7 @@ def make_shell_context(): "pp": pp, "Role": Role, "scolar": scolar, + "ScolarNews": models.ScolarNews, "scu": scu, "UniteEns": UniteEns, "User": User, From 68001714f0485e36c8816b577a0d723faa103a51 Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Tue, 12 Apr 2022 18:07:49 +0200 Subject: [PATCH 23/37] add folding on logo editor --- app/static/css/scodoc.css | 52 +++++++++++- app/templates/config_logos.html | 142 ++++++++++++++++---------------- 2 files changed, 118 insertions(+), 76 deletions(-) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 9811b3045..ba00717a1 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -987,9 +987,34 @@ span.wtf-field ul.errors li { font-weight: bold; } -.configuration_logo div.img {} +.configuration_logo summary { + display: list-item !important; +} -.configuration_logo div.img-container { +.configuration_logo h1 { + display: inline-block; +} + +.configuration_logo h2 { + display: inline-block; +} + +.configuration_logo h3 { + display: inline-block; +} + +.configuration_logo details > *:not(summary) { + margin-left: 32px; +} + +.configuration_logo .content { + display : grid; + grid-template-columns: auto auto 1fr; +} + +.configuration_logo .image_logo { + vertical-align: top; + grid-column: 1/2; width: 256px; } @@ -997,8 +1022,27 @@ span.wtf-field ul.errors li { max-width: 100%; } -.configuration_logo div.img-data { - vertical-align: top; +.configuration_logo .infos_logo { + grid-column: 2/3; +} + +.configuration_logo .actions_logo { + grid-column: 3/5; + display:grid; + grid-template-columns: auto auto; + grid-column-gap: 10px; + align-self: start; + grid-row-gap: 10px; +} + +.configuration_logo .actions_logo .action_label { + grid-column: 1/2; + grid-template-columns: auto auto; +} + +.configuration_logo .actions_logo .action_button { + grid-column: 2/3; + align-self: start; } .configuration_logo logo-edit titre { diff --git a/app/templates/config_logos.html b/app/templates/config_logos.html index f4bd543cb..a4974ca78 100644 --- a/app/templates/config_logos.html +++ b/app/templates/config_logos.html @@ -20,73 +20,70 @@ {% endmacro %} {% macro render_add_logo(add_logo_form) %} -
-

Ajouter un logo

- {{ add_logo_form.hidden_tag() }} - {{ render_field(add_logo_form.name) }} - {{ render_field(add_logo_form.upload) }} - {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }} -
+
+ +

Ajouter un logo

+
+
+ {{ render_field(add_logo_form.name) }} + {{ render_field(add_logo_form.upload) }} + {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }} +
+
{% endmacro %} {% macro render_logo(dept_form, logo_form) %} -
- {{ logo_form.hidden_tag() }} - {% if logo_form.titre %} -
-
-

{{ logo_form.titre }}

-
-
{{ logo_form.description or "" }}
-
- -

Logo personalisé: {{ logo_form.logo_id.data }}

-
- {{ logo_form.description or "" }} -
-
+
+ {{ logo_form.hidden_tag() }} + + {% if logo_form.titre %} + + {% if logo_form.description %} +
{{ logo_form.description }}
+ {% endif %} + {% else %} + + {% if logo_form.description %} +
{{ logo_form.description }}
+ {% endif %} + {% endif %} +
+
+ -
-

{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})

- Taille: {{ logo_form.logo.size }} px - {% if logo_form.logo.mm %}   /   {{ logo_form.logo.mm }} mm {% endif %}
- Aspect ratio: {{ logo_form.logo.aspect_ratio }}
- Usage: {{ logo_form.logo.get_usage() }} -
-

Modifier l'image

- {{ render_field(logo_form.upload, False, onchange="submit_form()") }} - {% if logo_form.can_delete %} -

Supprimer l'image

- {{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }} - {% endif %} -
{% for logo_entry in dept_form.logos.entries %} {% set logo_form = logo_entry.form %} {{ render_logo(dept_form, logo_form) }} {% else %} -

-

Aucun logo défini en propre à ce département

+

{% endfor %} -
{% endmacro %} {% block app_content %} @@ -100,25 +97,26 @@ From 7e5ccfb2d83b7ae4da63fbc21317962b6eb09f82 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 12 Apr 2022 17:21:58 +0200 Subject: [PATCH 24/37] Fixes #359 --- app/scodoc/sco_formsemestre_edit.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 3c88ff706..c6d9de0ee 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -191,10 +191,10 @@ def do_formsemestre_createwithmodules(edit=False): modimpl.responsable_id, f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !", ) - - initvalues["responsable_id"] = uid2display.get( - sem["responsables"][0], sem["responsables"][0] - ) + if sem["responsables"]: + initvalues["responsable_id"] = uid2display.get( + sem["responsables"][0], sem["responsables"][0] + ) if len(sem["responsables"]) > 1: initvalues["responsable_id2"] = uid2display.get( sem["responsables"][1], sem["responsables"][1] From c4b45e11b3bf16ef8d94c84e0d08ef0fee367163 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 12 Apr 2022 17:27:52 +0200 Subject: [PATCH 25/37] =?UTF-8?q?Table=20recap.=20=C3=A9valuations:=20cosm?= =?UTF-8?q?etic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_evaluation_recap.py | 6 +++++- app/static/css/scodoc.css | 14 +++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_evaluation_recap.py b/app/scodoc/sco_evaluation_recap.py index 168b463b8..7f1120b78 100644 --- a/app/scodoc/sco_evaluation_recap.py +++ b/app/scodoc/sco_evaluation_recap.py @@ -53,7 +53,11 @@ def evaluations_recap(formsemestre_id: int) -> str: for row in rows: H.append(f"{scu.gen_row(column_ids, row, with_col_classes=True)}\n") - H.append("""
""") + H.append( + """ +
Les étudiants démissionnaires ou défaillants ne sont pas pris en compte dans cette table.
+ """ + ) H.append( html_sco_header.sco_footer(), ) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index db6be386d..5a244863d 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1013,12 +1013,12 @@ span.wtf-field ul.errors li { display: inline-block; } -.configuration_logo details > *:not(summary) { +.configuration_logo details>*:not(summary) { margin-left: 32px; } .configuration_logo .content { - display : grid; + display: grid; grid-template-columns: auto auto 1fr; } @@ -1038,7 +1038,7 @@ span.wtf-field ul.errors li { .configuration_logo .actions_logo { grid-column: 3/5; - display:grid; + display: grid; grid-template-columns: auto auto; grid-column-gap: 10px; align-self: start; @@ -3901,4 +3901,12 @@ table.evaluations_recap tr.evaluation.incomplete td a { table.evaluations_recap tr.evaluation.incomplete td a.incomplete { font-weight: bold; +} + +table.evaluations_recap td.inscrits, +table.evaluations_recap td.manquantes, +table.evaluations_recap td.nb_abs, +table.evaluations_recap td.nb_att, +table.evaluations_recap td.nb_exc { + text-align: center; } \ No newline at end of file From ea3b67852efdb2c690f034505d3068b6959fdfc5 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Apr 2022 00:09:20 +0200 Subject: [PATCH 26/37] Version 9.2.0 --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index 15814bf76..db42b50d5 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.0a" +SCOVERSION = "9.2.0" SCONAME = "ScoDoc" From fb5425c3f6452b848cfc085f4079fbd3cc390467 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Apr 2022 00:32:00 +0200 Subject: [PATCH 27/37] missing import --- app/scodoc/sco_formsemestre_edit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index c6d9de0ee..3ccba7728 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -36,6 +36,7 @@ from app import db from app.auth.models import User from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteEns +from app.models import ScolarNews from app.models.formations import Formation from app.models.formsemestre import FormSemestre import app.scodoc.notesdb as ndb From 424d376692b1301eeb89c76ff1a21b591a90a6cd Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Apr 2022 00:32:31 +0200 Subject: [PATCH 28/37] Modif freq. max. news --- app/scodoc/sco_edit_matiere.py | 4 ++-- app/scodoc/sco_edit_module.py | 4 ++-- app/scodoc/sco_edit_ue.py | 4 ++-- app/scodoc/sco_saisie_notes.py | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/scodoc/sco_edit_matiere.py b/app/scodoc/sco_edit_matiere.py index 2ae1c15ab..dd9ffa9bd 100644 --- a/app/scodoc/sco_edit_matiere.py +++ b/app/scodoc/sco_edit_matiere.py @@ -93,7 +93,7 @@ def do_matiere_create(args): typ=ScolarNews.NEWS_FORM, obj=ue["formation_id"], text=f"Modification de la formation {formation.acronyme}", - max_frequency=3, + max_frequency=10 * 60, ) formation.invalidate_cached_sems() return r @@ -199,7 +199,7 @@ def do_matiere_delete(oid): typ=ScolarNews.NEWS_FORM, obj=ue["formation_id"], text=f"Modification de la formation {formation.acronyme}", - max_frequency=3, + max_frequency=10 * 60, ) formation.invalidate_cached_sems() diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 6d1547491..ece30a345 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -107,7 +107,7 @@ def do_module_create(args) -> int: typ=ScolarNews.NEWS_FORM, obj=formation.id, text=f"Modification de la formation {formation.acronyme}", - max_frequency=3, + max_frequency=10 * 60, ) formation.invalidate_cached_sems() return r @@ -398,7 +398,7 @@ def do_module_delete(oid): typ=ScolarNews.NEWS_FORM, obj=mod["formation_id"], text=f"Modification de la formation {formation.acronyme}", - max_frequency=3, + max_frequency=10 * 60, ) formation.invalidate_cached_sems() diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index ec4245765..be2523bbc 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -139,7 +139,7 @@ def do_ue_create(args): typ=ScolarNews.NEWS_FORM, obj=args["formation_id"], text=f"Modification de la formation {formation.acronyme}", - max_frequency=3, + max_frequency=10 * 60, ) formation.invalidate_cached_sems() return ue_id @@ -223,7 +223,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): typ=ScolarNews.NEWS_FORM, obj=ue.formation_id, text="Modification de la formation %(acronyme)s" % F, - max_frequency=3, + max_frequency=10 * 60, ) # if not force: diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 23847c994..9cda23725 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -280,6 +280,7 @@ def do_evaluation_upload_xls(): obj=M["moduleimpl_id"], text='Chargement notes dans %(titre)s' % mod, url=mod["url"], + max_frequency=30 * 60, # 30 minutes ) msg = ( @@ -365,6 +366,7 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): obj=M["moduleimpl_id"], text='Initialisation notes dans %(titre)s' % mod, url=mod["url"], + max_frequency=30 * 60, ) return ( html_sco_header.sco_header() From 68723f63ee0f5fbc494577169c75a297a64018dd Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Apr 2022 00:42:31 +0200 Subject: [PATCH 29/37] 9.2.1 / version --- sco_version.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index db42b50d5..0f7ec6c57 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,13 +1,21 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.0" +SCOVERSION = "9.2.1" SCONAME = "ScoDoc" SCONEWS = """

Année 2021

    +
  • ScoDoc 9.2: +
      +
    • Tableau récap. complet pour BUT et autres formations.
    • +
    • Tableau état évaluations
    • +
    • Version alpha du module "relations entreprises"
    • +
    • Export des trombinoscope en document docx
    • +
    • Très nombreux correctifs
    • +
  • ScoDoc 9.1.75: bulletins BUT pdf
  • ScoDoc 9.1.50: nombreuses amélioration gestion BUT
  • ScoDoc 9.1: gestion des formations par compétences, type BUT.
  • From 93556ca3724baeed10d297716211225069e9682b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 13 Apr 2022 13:53:53 +0200 Subject: [PATCH 30/37] =?UTF-8?q?Filtre=20nouvelles=20par=20d=C3=A9parteme?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/events.py | 10 ++++++++-- app/scodoc/sco_edit_ue.py | 2 +- sco_version.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/models/events.py b/app/models/events.py index ccb6396e5..9725f3c8a 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -102,9 +102,15 @@ class ScolarNews(db.Model): } @classmethod - def last_news(cls, n=1) -> list: + def last_news(cls, n=1, dept_id=None, filter_dept=True) -> list: "The most recent n news. Returns list of ScolarNews instances." - return cls.query.order_by(cls.date.desc()).limit(n).all() + query = cls.query + if filter_dept: + if dept_id is None: + dept_id = g.scodoc_dept_id + query = query.filter_by(dept_id=dept_id) + + return query.order_by(cls.date.desc()).limit(n).all() @classmethod def add(cls, typ, obj=None, text="", url=None, max_frequency=0): diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index be2523bbc..c6811d454 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -222,7 +222,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): ScolarNews.add( typ=ScolarNews.NEWS_FORM, obj=ue.formation_id, - text="Modification de la formation %(acronyme)s" % F, + text=f"Modification de la formation {F['acronyme']}", max_frequency=10 * 60, ) # diff --git a/sco_version.py b/sco_version.py index 0f7ec6c57..4380a1fa5 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.1" +SCOVERSION = "9.2.2" SCONAME = "ScoDoc" From 954a8c8e81c36b7f15ee226da3bd756ab53093ae Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 14 Apr 2022 06:16:12 +0200 Subject: [PATCH 31/37] Bonus IUT vannes --- app/comp/bonus_spo.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index bc6dca945..da8cce5d7 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -1072,6 +1072,29 @@ class BonusTours(BonusDirect): ) +class BonusIUTvannes(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT Vannes + +

    Ne concerne actuellement que les DUT et LP

    +

    Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'U.B.S. (sports, musique, deuxième langue, culture, etc) non + rattachés à une unité d'enseignement. +

    + Les points au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés. +

    + 3% de ces points cumulés s'ajoutent à la moyenne générale du semestre + déjà obtenue par l'étudiant. +

    + """ + + name = "bonus_iutvannes" + displayed_name = "IUT de Vannes" + seuil_moy_gen = 10.0 + proportion_point = 0.03 # 3% + classic_use_bonus_ues = False # seulement sur moy gen. + + class BonusVilleAvray(BonusSport): """Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray. From dab0f78279cc930537ade4f76e01ea713e26b1d1 Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Thu, 14 Apr 2022 10:52:38 +0200 Subject: [PATCH 32/37] fix ad local header ; deployed errored fields --- app/forms/main/config_logos.py | 23 +++++++++++++++++++- app/scodoc/sco_config_actions.py | 2 +- app/templates/config_logos.html | 36 +++++++++++++++++++++----------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/app/forms/main/config_logos.py b/app/forms/main/config_logos.py index db69ae35b..daea5de1b 100644 --- a/app/forms/main/config_logos.py +++ b/app/forms/main/config_logos.py @@ -151,7 +151,7 @@ class AddLogoForm(FlaskForm): dept_id = dept_key_to_id(self.dept_key.data) if dept_id == GLOBAL: dept_id = None - if find_logo(logoname=name.data, dept_id=dept_id) is not None: + if find_logo(logoname=name.data, dept_id=dept_id, strict=True) is not None: raise validators.ValidationError("Un logo de même nom existe déjà") def select_action(self): @@ -160,6 +160,14 @@ class AddLogoForm(FlaskForm): return LogoInsert.build_action(self.data) return None + def errors(self): + if self.do_insert.data: + if self.name.errors: + return True + if self.upload.errors: + return True + return False + class LogoForm(FlaskForm): """Embed both presentation of a logo (cf. template file configuration.html) @@ -211,6 +219,11 @@ class LogoForm(FlaskForm): return LogoUpdate.build_action(self.data) return None + def errors(self): + if self.upload.data and self.upload.errors: + return True + return False + class DeptForm(FlaskForm): dept_key = HiddenField() @@ -244,6 +257,14 @@ class DeptForm(FlaskForm): return self return self.index.get(logoname, None) + def errors(self): + if self.add_logo.errors(): + return True + for logo_form in self.logos: + if logo_form.errors(): + return True + return False + def _make_dept_id_name(): """Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ) diff --git a/app/scodoc/sco_config_actions.py b/app/scodoc/sco_config_actions.py index f6ea96371..e0e9133ec 100644 --- a/app/scodoc/sco_config_actions.py +++ b/app/scodoc/sco_config_actions.py @@ -136,7 +136,7 @@ class LogoInsert(Action): parameters["dept_id"] = None if parameters["upload"] and parameters["name"]: logo = find_logo( - logoname=parameters["name"], dept_id=parameters["dept_key"] + logoname=parameters["name"], dept_id=parameters["dept_key"], strict=True ) if logo is None: return LogoInsert(parameters) diff --git a/app/templates/config_logos.html b/app/templates/config_logos.html index a4974ca78..04d4d2663 100644 --- a/app/templates/config_logos.html +++ b/app/templates/config_logos.html @@ -20,20 +20,28 @@ {% endmacro %} {% macro render_add_logo(add_logo_form) %} -
    - -

    Ajouter un logo

    -
    -
    - {{ render_field(add_logo_form.name) }} - {{ render_field(add_logo_form.upload) }} - {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }} -
    -
    + {% if add_logo_form.errors() %} +
    + {% else %} +
    + {% endif %} + +

    Ajouter un logo

    +
    +
    + {{ render_field(add_logo_form.name) }} + {{ render_field(add_logo_form.upload) }} + {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }} +
    +
    {% endmacro %} {% macro render_logo(dept_form, logo_form) %} -
    + {% if logo_form.errors() %} +
    + {% else %} +
    + {% endif %} {{ logo_form.hidden_tag() }} {% if logo_form.titre %} @@ -97,9 +105,13 @@