From bcbace01208bfc91680b4406b503d645f1db9481 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 3 Mar 2022 23:02:24 +0100 Subject: [PATCH 01/81] N'affiche pas les UE sans inscriptions sur les buleltins classiques --- app/__init__.py | 4 ++-- app/comp/res_classic.py | 7 ++++++- app/scodoc/sco_bulletins.py | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 76f9471bb..237f53cea 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -201,7 +201,7 @@ def create_app(config_class=DevConfig): app.register_blueprint(auth_bp, url_prefix="/auth") from app.entreprises import bp as entreprises_bp - + app.register_blueprint(entreprises_bp, url_prefix="/ScoDoc/entreprises") from app.views import scodoc_bp @@ -297,7 +297,7 @@ def create_app(config_class=DevConfig): from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC - # l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements. + # l'ordre est important, le premier sera le "défaut" pour les nouveaux départements. sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC) diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index b36eaaf6c..91614935d 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -146,7 +146,12 @@ class ResultatsSemestreClassic(NotesTableCompat): """La moyenne de l'étudiant dans le moduleimpl Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM) """ - return self.modimpls_results[moduleimpl_id].etuds_moy_module.get(etudid, "NI") + try: + if self.modimpl_inscr_df[moduleimpl_id][etudid]: + return self.modimpls_results[moduleimpl_id].etuds_moy_module[etudid] + except KeyError: + pass + return "NI" def get_mod_stats(self, moduleimpl_id: int) -> dict: """Stats sur les notes obtenues dans un modimpl""" diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 47947bd36..5f6f91e5b 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -441,7 +441,9 @@ def _sort_mod_by_matiere(modlist, nt, etudid): return matmod -def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version): +def _ue_mod_bulletin( + etudid, formsemestre_id, ue_id, modimpls, nt: NotesTableCompat, version +): """Infos sur les modules (et évaluations) dans une UE (ajoute les informations aux modimpls) Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit). From e04a187a01009c35facc7e9561020498cb85845c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 4 Mar 2022 18:55:45 +0100 Subject: [PATCH 02/81] Exception si archive introuvable --- app/scodoc/sco_archives.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 159d3305a..0103caa24 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -70,13 +70,13 @@ from app.scodoc.sco_exceptions import ( ) from app.scodoc import html_sco_header from app.scodoc import sco_bulletins_pdf -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_permissions_check from app.scodoc import sco_pvjury from app.scodoc import sco_pvpdf +from app.scodoc.sco_exceptions import ScoValueError class BaseArchiver(object): @@ -254,7 +254,7 @@ class BaseArchiver(object): self.initialize() if not scu.is_valid_filename(filename): log('Archiver.get: invalid filename "%s"' % filename) - raise ValueError("invalid filename") + raise ScoValueError("archive introuvable (déjà supprimée ?)") fname = os.path.join(archive_id, filename) log("reading archive file %s" % fname) with open(fname, "rb") as f: From b4a7749e5ae8b1a75ab44fbacf134865022caa8d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 4 Mar 2022 20:02:50 +0100 Subject: [PATCH 03/81] Mode test pour les mails. Closes #326 --- app/__init__.py | 23 ++++++------- app/email.py | 65 ++++++++++++++++++++++++++++++++--- app/scodoc/html_sco_header.py | 5 +-- app/scodoc/sco_bulletins.py | 14 +++++--- app/scodoc/sco_preferences.py | 14 ++++++++ sco_version.py | 2 +- 6 files changed, 98 insertions(+), 25 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 237f53cea..74585a7b2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -13,7 +13,7 @@ from logging.handlers import SMTPHandler, WatchedFileHandler from flask import current_app, g, request from flask import Flask -from flask import abort, has_request_context, jsonify +from flask import abort, flash, has_request_context, jsonify from flask import render_template from flask.logging import default_handler from flask_sqlalchemy import SQLAlchemy @@ -457,15 +457,12 @@ from app.models import Departement from app.scodoc import notesdb as ndb, sco_preferences from app.scodoc import sco_cache -# admin_role = Role.query.filter_by(name="SuperAdmin").first() -# if admin_role: -# admin = ( -# User.query.join(UserRole) -# .filter((UserRole.user_id == User.id) & (UserRole.role_id == admin_role.id)) -# .first() -# ) -# else: -# click.echo( -# "Warning: user database not initialized !\n (use: flask user-db-init)" -# ) -# admin = None + +def scodoc_flash_status_messages(): + """Should be called on each page: flash messages indicating specific ScoDoc status""" + email_test_mode_address = sco_preferences.get_preference("email_test_mode_address") + if email_test_mode_address: + flash( + f"Mode test: mails redirigés vers {email_test_mode_address}", + category="warning", + ) diff --git a/app/email.py b/app/email.py index 226429df2..1fc7632b6 100644 --- a/app/email.py +++ b/app/email.py @@ -1,8 +1,17 @@ # -*- coding: UTF-8 -* +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + from threading import Thread -from flask import current_app + +from flask import current_app, g from flask_mail import Message + from app import mail +from app.scodoc import sco_preferences def send_async_email(app, msg): @@ -11,20 +20,66 @@ def send_async_email(app, msg): def send_email( - subject: str, sender: str, recipients: list, text_body: str, html_body="" + subject: str, + sender: str, + recipients: list, + text_body: str, + html_body="", + bcc=(), + attachments=(), ): """ - Send an email + Send an email. _All_ ScoDoc mails SHOULD be sent using this function. + If html_body is specified, build a multipart message with HTML content, else send a plain text email. + + attachements: list of dict { 'filename', 'mimetype', 'data' } """ - msg = Message(subject, sender=sender, recipients=recipients) + msg = Message(subject, sender=sender, recipients=recipients, bcc=bcc) msg.body = text_body msg.html = html_body + if attachments: + for attachment in attachments: + msg.attach( + attachment["filename"], attachment["mimetype"], attachment["data"] + ) + send_message(msg) -def send_message(msg): +def send_message(msg: Message): + """Send a message. + All ScoDoc emails MUST be sent by this function. + + In mail debug mode, addresses are discarded and all mails are sent to the + specified debugging address. + """ + if hasattr(g, "scodoc_dept"): + # on est dans un département, on peut accéder aux préférences + email_test_mode_address = sco_preferences.get_preference( + "email_test_mode_address" + ) + if email_test_mode_address: + # Mode spécial test: remplace les adresses de destination + orig_to = msg.recipients + orig_cc = msg.cc + orig_bcc = msg.bcc + msg.recipients = [email_test_mode_address] + msg.cc = None + msg.bcc = None + msg.subject = "[TEST SCODOC] " + msg.subject + msg.body = ( + f"""--- Message ScoDoc dérouté pour tests --- +Adresses d'origine: + to : {orig_to} + cc : {orig_cc} + bcc: {orig_bcc} +--- + \n\n""" + + msg.body + ) + Thread( target=send_async_email, args=(current_app._get_current_object(), msg) ).start() diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 653cdb80d..8ce3d2cdd 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -35,7 +35,7 @@ from flask import request from flask_login import current_user import app.scodoc.sco_utils as scu -from app import log +from app import scodoc_flash_status_messages from app.scodoc import html_sidebar import sco_version @@ -153,13 +153,14 @@ def sco_header( "Main HTML page header for ScoDoc" from app.scodoc.sco_formsemestre_status import formsemestre_page_title + scodoc_flash_status_messages() + # Get head message from http request: if not head_message: if request.method == "POST": head_message = request.form.get("head_message", "") elif request.method == "GET": head_message = request.args.get("head_message", "") - params = { "page_title": page_title or sco_version.SCONAME, "no_side_bar": no_side_bar, diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 5f6f91e5b..bb0597e59 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -1040,13 +1040,19 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): bcc = copy_addr.strip() else: bcc = "" - msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc]) - msg.body = hea # Attach pdf - msg.attach(filename, scu.PDF_MIMETYPE, pdfdata) log("mail bulletin a %s" % recipient_addr) - email.send_message(msg) + email.send_email( + subject, + sender, + recipients, + bcc=[bcc], + text_body=hea, + attachments=[ + {"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata} + ], + ) def _formsemestre_bulletinetud_header_html( diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index aab148e70..7476be6b9 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -245,6 +245,7 @@ PREF_CATEGORIES = ( ), ("pe", {"title": "Avis de poursuites d'études"}), ("edt", {"title": "Connexion avec le logiciel d'emplois du temps"}), + ("debug", {"title": "Tests / mise au point"}), ) @@ -1859,6 +1860,19 @@ class BasePreferences(object): "category": "edt", }, ), + ( + "email_test_mode_address", + { + "title": "Adresse de test", + "initvalue": "", + "explanation": """si cette adresse est indiquée, TOUS les mails + envoyés par ScoDoc de ce département vont aller vers elle + AU LIEU DE LEUR DESTINATION NORMALE !""", + "size": 30, + "category": "debug", + "only_global": True, + }, + ), ) self.prefs_name = set([x[0] for x in self.prefs_definition]) diff --git a/sco_version.py b/sco_version.py index 55ce771df..4ab7caa51 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.71" +SCOVERSION = "9.1.72" SCONAME = "ScoDoc" From 3d0509de64d143eb5c53d7707e65668cbc28651c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 4 Mar 2022 20:51:44 +0100 Subject: [PATCH 04/81] =?UTF-8?q?Bonus=20Saint-Brieuc=20(=C3=A0=20valider:?= =?UTF-8?q?=20semestres=20=C3=A0=20affecter=3F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/bonus_spo.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index b27219413..2feb05776 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -824,6 +824,27 @@ class BonusRoanne(BonusSportAdditif): proportion_point = 1 +class BonusStBrieuc(BonusSportAdditif): + """IUT de Saint Brieuc + + Ne s'applique qu'aux semestres pairs (S2, S4, S6), et bonifie les moyennes d'UE: +
    +
  • Bonus = (S - 10)/20
  • +
+
(XXX vérifier si S6 est éligible au bonus, et le S2 du DUT XXX)
+ """ + + name = "bonus_iut_stbrieuc" + displayed_name = "IUT de Saint-Brieuc" + proportion_point = 1 / 20.0 + classic_use_bonus_ues = True + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + if self.formsemestre.semestre_id % 2 == 0: + super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan) + + class BonusStDenis(BonusSportAdditif): """Calcul bonus modules optionnels (sport, culture), règle IUT Saint-Denis From c923a5015b42b02c2dbed1621e62f0154a1ac46d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 5 Mar 2022 12:47:08 +0100 Subject: [PATCH 05/81] Infos pour bulletins BUT pdf --- app/but/bulletin_but.py | 32 +++++++- app/scodoc/sco_abs_notification.py | 56 +++++++------ app/scodoc/sco_bulletins.py | 128 ++++++++++++----------------- app/scodoc/sco_find_etud.py | 4 +- app/scodoc/sco_groups.py | 16 ++-- app/scodoc/sco_import_etuds.py | 2 +- app/scodoc/sco_inscr_passage.py | 5 +- app/scodoc/sco_page_etud.py | 4 +- app/views/scolar.py | 2 +- 9 files changed, 134 insertions(+), 115 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 87ec62a28..c0c29bf15 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -16,7 +16,7 @@ from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_preferences -from app.scodoc.sco_codes_parcours import UE_SPORT +from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_utils import fmt_note @@ -318,19 +318,42 @@ class BulletinBUT: return d def bulletin_etud_complet(self, etud: Identite) -> dict: - """Bulletin dict complet avec toutes les infos pour les bulletins pdf""" + """Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf + Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict + """ d = self.bulletin_etud(etud, self.res.formsemestre, force_publishing=True) d["etudid"] = etud.id d["etud"] = d["etudiant"] d["etud"]["nomprenom"] = etud.nomprenom d.update(self.res.sem) + etud_etat = self.res.get_etud_etat(etud.id) d["filigranne"] = sco_bulletins_pdf.get_filigranne( - self.res.get_etud_etat(etud.id), + etud_etat, self.prefs, decision_sem=d["semestre"].get("decision_sem"), ) + if etud_etat == scu.DEMISSION: + d["demission"] = "(Démission)" + elif etud_etat == DEF: + d["demission"] = "(Défaillant)" + else: + d["demission"] = "" + # --- Absences d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id) + + # --- Decision Jury + infos, dpv = sco_bulletins.etud_descr_situation_semestre( + etud.id, + self.res.formsemestre.id, + format="html", + show_date_inscr=self.prefs["bul_show_date_inscr"], + show_decisions=self.prefs["bul_show_decision"], + show_uevalid=self.prefs["bul_show_uevalid"], + show_mention=self.prefs["bul_show_mention"], + ) + + d.update(infos) # --- Rangs d[ "rang_nt" @@ -341,5 +364,6 @@ class BulletinBUT: d.update( sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id) ) - # XXX TODO A COMPLETER ? + d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"])) + return d diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index f15e7d4c8..466d13df1 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -35,6 +35,7 @@ import datetime from flask import g, url_for from flask_mail import Message +from app.models.formsemestre import FormSemestre import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -55,27 +56,30 @@ def abs_notify(etudid, date): """ from app.scodoc import sco_abs - sem = retreive_current_formsemestre(etudid, date) - if not sem: + formsemestre = retreive_current_formsemestre(etudid, date) + if not formsemestre: return # non inscrit a la date, pas de notification - nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) - do_abs_notify(sem, etudid, date, nbabs, nbabsjust) + nbabs, nbabsjust = sco_abs.get_abs_count_in_interval( + etudid, formsemestre.date_debut.isoformat(), formsemestre.date_fin.isoformat() + ) + do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust) -def do_abs_notify(sem, etudid, date, nbabs, nbabsjust): +def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust): """Given new counts of absences, check if notifications are requested and send them.""" # prefs fallback to global pref if sem is None: - if sem: - formsemestre_id = sem["formsemestre_id"] + if formsemestre: + formsemestre_id = formsemestre.id else: formsemestre_id = None - prefs = sco_preferences.SemPreferences(formsemestre_id=sem["formsemestre_id"]) + prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id) destinations = abs_notify_get_destinations( - sem, prefs, etudid, date, nbabs, nbabsjust + formsemestre, prefs, etudid, date, nbabs, nbabsjust ) - msg = abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust) + + msg = abs_notification_message(formsemestre, prefs, etudid, nbabs, nbabsjust) if not msg: return # abort @@ -131,19 +135,19 @@ def abs_notify_send(destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id ) -def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust): +def abs_notify_get_destinations( + formsemestre: FormSemestre, prefs, etudid, date, nbabs, nbabsjust +) -> set: """Returns set of destination emails to be notified""" - formsemestre_id = sem["formsemestre_id"] destinations = [] # list of email address to notify - if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id): - if sem and prefs["abs_notify_respsem"]: + if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre.id): + if prefs["abs_notify_respsem"]: # notifie chaque responsable du semestre - for responsable_id in sem["responsables"]: - u = sco_users.user_info(responsable_id) - if u["email"]: - destinations.append(u["email"]) + for responsable in formsemestre.responsables: + if responsable.email: + destinations.append(responsable.email) if prefs["abs_notify_chief"] and prefs["email_chefdpt"]: destinations.append(prefs["email_chefdpt"]) if prefs["abs_notify_email"]: @@ -156,7 +160,7 @@ def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust): # Notification (à chaque fois) des resp. de modules ayant des évaluations # à cette date # nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas - if sem and prefs["abs_notify_respeval"]: + if prefs["abs_notify_respeval"]: mods = mod_with_evals_at_date(date, etudid) for mod in mods: u = sco_users.user_info(mod["responsable_id"]) @@ -232,7 +236,9 @@ def user_nbdays_since_last_notif(email_addr, etudid): return None -def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust): +def abs_notification_message( + formsemestre: FormSemestre, prefs, etudid, nbabs, nbabsjust +): """Mime notification message based on template. returns a Message instance or None if sending should be canceled (empty template). @@ -242,7 +248,7 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust): etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] # Variables accessibles dans les balises du template: %(nom_variable)s : - values = sco_bulletins.make_context_dict(sem, etud) + values = sco_bulletins.make_context_dict(formsemestre, etud) values["nbabs"] = nbabs values["nbabsjust"] = nbabsjust @@ -264,9 +270,11 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust): return msg -def retreive_current_formsemestre(etudid, cur_date): +def retreive_current_formsemestre(etudid: int, cur_date) -> FormSemestre: """Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée date est une chaine au format ISO (yyyy-mm-dd) + + Result: FormSemestre ou None si pas inscrit à la date indiquée """ req = """SELECT i.formsemestre_id FROM notes_formsemestre_inscription i, notes_formsemestre sem @@ -278,8 +286,8 @@ def retreive_current_formsemestre(etudid, cur_date): if not r: return None # s'il y a plusieurs semestres, prend le premier (rarissime et non significatif): - sem = sco_formsemestre.get_formsemestre(r[0]["formsemestre_id"]) - return sem + formsemestre = FormSemestre.query.get(r[0]["formsemestre_id"]) + return formsemestre def mod_with_evals_at_date(date_abs, etudid): diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 60cf4931d..6b1afc4a2 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -78,33 +78,20 @@ from app.scodoc import sco_bulletins_legacy from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun -def make_context_dict(sem, etud): +def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict: """Construit dictionnaire avec valeurs pour substitution des textes (preferences bul_pdf_*) """ - C = sem.copy() - C["responsable"] = " ,".join( - [ - sco_users.user_info(responsable_id)["prenomnom"] - for responsable_id in sem["responsables"] - ] - ) - - annee_debut = sem["date_debut"].split("/")[2] - annee_fin = sem["date_fin"].split("/")[2] - if annee_debut != annee_fin: - annee = "%s - %s" % (annee_debut, annee_fin) - else: - annee = annee_debut - C["anneesem"] = annee + C = formsemestre.get_infos_dict() + C["responsable"] = formsemestre.responsables_str() + C["anneesem"] = C["annee"] # backward compat C.update(etud) # copie preferences - # XXX devrait acceder directement à un dict de preferences, à revoir for name in sco_preferences.get_base_preferences().prefs_name: - C[name] = sco_preferences.get_preference(name, sem["formsemestre_id"]) + C[name] = sco_preferences.get_preference(name, formsemestre.id) # ajoute groupes et group_0, group_1, ... - sco_groups.etud_add_group_infos(etud, sem) + sco_groups.etud_add_group_infos(etud, formsemestre.id) C["groupes"] = etud["groupes"] n = 0 for partition_id in etud["partitions"]: @@ -125,7 +112,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): Le contenu du dictionnaire dépend des options (rangs, ...) et de la version choisie (short, long, selectedevals). - Cette fonction est utilisée pour les bulletins HTML et PDF, mais pas ceux en XML. + Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...) + en HTML et PDF, mais pas ceux en XML. """ from app.scodoc import sco_abs @@ -190,7 +178,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): ) I["etud_etat"] = nt.get_etud_etat(etudid) I["filigranne"] = sco_bulletins_pdf.get_filigranne( - I["etud_etat"], prefs, decision_dem=I["decision_sem"] + I["etud_etat"], prefs, decision_sem=I["decision_sem"] ) I["demission"] = "" if I["etud_etat"] == scu.DEMISSION: @@ -384,7 +372,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid)) # - C = make_context_dict(I["sem"], I["etud"]) + C = make_context_dict(formsemestre, I["etud"]) C.update(I) # # log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo @@ -842,7 +830,7 @@ def formsemestre_bulletinetud( H = [ _formsemestre_bulletinetud_header_html( - etud, etudid, sem, formsemestre_id, format, version + etud, etudid, formsemestre, format, version ), bulletin, ] @@ -1063,8 +1051,7 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): def _formsemestre_bulletinetud_header_html( etud, etudid, - sem, - formsemestre_id=None, + formsemestre: FormSemestre, format=None, version=None, ): @@ -1078,33 +1065,27 @@ def _formsemestre_bulletinetud_header_html( ], cssstyles=["css/radar_bulletin.css"], ), - """' % fontorange) H.append( - '' - % (modimpl["moduleimpl_id"], mod_descr, mod.code) + f"""""" ) H.append( '' diff --git a/app/scodoc/sco_up_to_date.py b/app/scodoc/sco_up_to_date.py index 152b05691..6404ef0ed 100644 --- a/app/scodoc/sco_up_to_date.py +++ b/app/scodoc/sco_up_to_date.py @@ -33,6 +33,8 @@ import json import requests import time from flask import current_app + +from app import log import app.scodoc.sco_utils as scu from sco_version import SCOVERSION, SCONAME @@ -43,7 +45,7 @@ def is_up_to_date() -> str: """ diag = "" try: - response = requests.get(scu.SCO_UP2DATE) + response = requests.get(scu.SCO_UP2DATE + "/" + SCOVERSION) except requests.exceptions.ConnectionError: current_app.logger.debug("is_up_to_date: %s", diag) return f"""
Attention: installation de {SCONAME} non fonctionnelle.
@@ -60,7 +62,6 @@ def is_up_to_date() -> str: voir la documentation. """ - if response.status_code != 200: current_app.logger.debug( f"is_up_to_date: invalid response code ({response.status_code})" @@ -75,12 +76,16 @@ def is_up_to_date() -> str: return f"""
Attention: réponse invalide de {scu.SCO_WEBSITE}
(erreur json).
""" - # nb: si de nouveaux paquets sont publiés chaque jour, le décalage ne sera jamais signalé. - # mais en régime "normal", on aura une alerte après 24h sans mise à jour. - days_since_last_package = (time.time() - infos["publication_time"]) / (24 * 60 * 60) - if (infos["version"] != SCOVERSION) and (days_since_last_package > 1.0): + if infos["status"] != "ok": + # problème coté serveur, ignore discrètement + log(f"is_up_to_date: server {infos['status']}") + return "" + if (SCOVERSION != infos["last_version"]) and ( + (time.time() - infos["last_version_date"]) > (24 * 60 * 60) + ): + # nouvelle version publiée depuis plus de 24h ! return f"""
Attention: {SCONAME} version ({SCOVERSION}) non à jour - ({infos["version"]} disponible).
+ ({infos["last_version"]} disponible).
Contacter votre administrateur système (documentation).
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 03e02cbf6..6df7ed6eb 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -361,7 +361,7 @@ SCO_DEV_MAIL = "emmanuel.viennet@gmail.com" # SVP ne pas changer # Adresse pour l'envoi des dumps (pour assistance technnique): # ne pas changer (ou vous perdez le support) SCO_DUMP_UP_URL = "https://scodoc.org/scodoc-installmgr/upload-dump" -SCO_UP2DATE = "https://scodoc.org/scodoc-installmgr/last_stable_version" +SCO_UP2DATE = "https://scodoc.org/scodoc-installmgr/check_version" CSV_FIELDSEP = ";" CSV_LINESEP = "\n" CSV_MIMETYPE = "text/comma-separated-values" diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index d8a7249c4..6c0b06b8f 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2884,7 +2884,7 @@ div.othersemlist input { div#update_warning { - /* display: none; */ + display: none; border: 1px solid red; background-color: rgb(250,220,220); margin: 3ex; diff --git a/app/static/js/scodoc.js b/app/static/js/scodoc.js index b1c15017e..7cb5bb626 100644 --- a/app/static/js/scodoc.js +++ b/app/static/js/scodoc.js @@ -58,7 +58,12 @@ $(function () { if (update_div) { fetch('install_info').then( response => response.text() - ).then(text => update_div.innerHTML = text); + ).then(text => { + update_div.innerHTML = text; + if (text) { + update_div.style.display = "block"; + } + }); } }); diff --git a/app/templates/sco_page.html b/app/templates/sco_page.html index e1aa9e3c5..f1590302a 100644 --- a/app/templates/sco_page.html +++ b/app/templates/sco_page.html @@ -9,6 +9,7 @@ +{# #} {% endblock %} From 9032b1fa67ff555a34a4116e6dbd6de997102b78 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 21 Mar 2022 11:48:34 +0100 Subject: [PATCH 76/81] Exception speciale pour envoyer bug report --- app/__init__.py | 26 ++++++++++++++++++++- app/scodoc/sco_bulletins_standard.py | 7 +++++- app/scodoc/sco_dump_db.py | 35 +++++----------------------- app/scodoc/sco_exceptions.py | 5 +++- app/views/notes.py | 1 + app/views/scolar.py | 24 +++++++++++++++++-- sco_version.py | 2 +- 7 files changed, 65 insertions(+), 35 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 7521520e0..76760bd79 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -10,6 +10,7 @@ import traceback import logging from logging.handlers import SMTPHandler, WatchedFileHandler +from threading import Thread from flask import current_app, g, request from flask import Flask @@ -27,6 +28,7 @@ import sqlalchemy from app.scodoc.sco_exceptions import ( AccessDenied, + ScoBugCatcher, ScoGenError, ScoValueError, APIInvalidParams, @@ -77,6 +79,28 @@ def internal_server_error(exc): ) +def handle_sco_bug(exc): + """Un bug, en général rare, sur lequel les dev cherchent des + informations pour le corriger. + """ + Thread( + target=_async_dump, args=(current_app._get_current_object(), request.url) + ).start() + + return internal_server_error(exc) + + +def _async_dump(app, request_url: str): + from app.scodoc.sco_dump_db import sco_dump_and_send_db + + with app.app_context(): + ndb.open_db_connection() + try: + sco_dump_and_send_db("ScoBugCatcher", request_url=request_url) + except ScoValueError: + pass + + def handle_invalid_usage(error): response = jsonify(error.to_dict()) response.status_code = error.status_code @@ -196,7 +220,7 @@ def create_app(config_class=DevConfig): app.register_error_handler(ScoGenError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error) - + app.register_error_handler(ScoBugCatcher, handle_sco_bug) app.register_error_handler(AccessDenied, handle_access_denied) app.register_error_handler(500, internal_server_error) app.register_error_handler(503, postgresql_server_error) diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index f1b2b7cc7..e42a38847 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -49,6 +49,7 @@ Balises img: actuellement interdites. from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table from reportlab.lib.units import cm, mm from reportlab.lib.colors import Color, blue +from app.scodoc.sco_exceptions import ScoBugCatcher import app.scodoc.sco_utils as scu from app.scodoc.sco_pdf import SU @@ -416,7 +417,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): # Chaque UE: for ue in I["ues"]: ue_type = None - coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else "" + try: + coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else "" + except TypeError as exc: + raise ScoBugCatcher(f"ue={ue!r}") from exc + ue_descr = ue["ue_descr_txt"] rowstyle = "" plusminus = minuslink # diff --git a/app/scodoc/sco_dump_db.py b/app/scodoc/sco_dump_db.py index 190cec961..fd0b15c0d 100644 --- a/app/scodoc/sco_dump_db.py +++ b/app/scodoc/sco_dump_db.py @@ -51,14 +51,12 @@ import fcntl import subprocess import requests -from flask import flash, request +from flask import g, request from flask_login import current_user import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log -from app.scodoc import html_sco_header -from app.scodoc import sco_preferences from app.scodoc import sco_users import sco_version from app.scodoc.sco_exceptions import ScoValueError @@ -68,8 +66,7 @@ SCO_DUMP_LOCK = "/tmp/scodump.lock" def sco_dump_and_send_db(message: str = "", request_url: str = ""): """Dump base de données et l'envoie anonymisée pour debug""" - H = [html_sco_header.sco_header(page_title="Assistance technique")] - # get currect (dept) DB name: + # get current (dept) DB name: cursor = ndb.SimpleQuery("SELECT current_database()", {}) db_name = cursor.fetchone()[0] ano_db_name = "ANO" + db_name @@ -96,27 +93,7 @@ def sco_dump_and_send_db(message: str = "", request_url: str = ""): # Send r = _send_db(ano_db_name, message, request_url) - if ( - r.status_code - == requests.codes.INSUFFICIENT_STORAGE # pylint: disable=no-member - ): - H.append( - """

- Erreur: espace serveur trop plein. - Merci de contacter {0}

""".format( - scu.SCO_DEV_MAIL - ) - ) - elif r.status_code == requests.codes.OK: # pylint: disable=no-member - H.append("""

Opération effectuée.

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

- Erreur: code {0} {1} - Merci de contacter {2}

""".format( - r.status_code, r.reason, scu.SCO_DEV_MAIL - ) - ) + code = r.status_code finally: # Drop anonymized database @@ -125,8 +102,8 @@ def sco_dump_and_send_db(message: str = "", request_url: str = ""): fcntl.flock(x, fcntl.LOCK_UN) log("sco_dump_and_send_db: done.") - flash("Données envoyées au serveur d'assistance") - return "\n".join(H) + html_sco_header.sco_footer() + + return code def _duplicate_db(db_name, ano_db_name): @@ -195,7 +172,7 @@ def _send_db(ano_db_name: str, message: str = "", request_url: str = ""): scu.SCO_DUMP_UP_URL, files=files, data={ - "dept_name": sco_preferences.get_preference("DeptName"), + "dept_name": getattr(g, "scodoc_dept", "-"), "message": message or "", "request_url": request_url or request.url, "serial": _get_scodoc_serial(), diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 394dbf1e5..35d2d9d6e 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -47,9 +47,12 @@ class ScoValueError(ScoException): self.dest_url = dest_url +class ScoBugCatcher(ScoException): + "bug avec enquete en cours" + + class NoteProcessError(ScoValueError): "Valeurs notes invalides" - pass class InvalidEtudId(NoteProcessError): diff --git a/app/views/notes.py b/app/views/notes.py index c8587c4fb..7a9e50673 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -73,6 +73,7 @@ from app.scodoc.scolog import logdb from app.scodoc.sco_exceptions import ( AccessDenied, + ScoBugCatcher, ScoException, ScoValueError, ScoInvalidIdType, diff --git a/app/views/scolar.py b/app/views/scolar.py index 67cb7b7b6..a25994bd9 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -30,7 +30,7 @@ issu de ScoDoc7 / ZScolar.py Emmanuel Viennet, 2021 """ -import os +import requests import time import flask @@ -2198,4 +2198,24 @@ def stat_bac(formsemestre_id): @scodoc7func def sco_dump_and_send_db(message="", request_url=""): "Send anonymized data to supervision" - return sco_dump_db.sco_dump_and_send_db(message, request_url) + + status_code = sco_dump_db.sco_dump_and_send_db(message, request_url) + H = [html_sco_header.sco_header(page_title="Assistance technique")] + if status_code == requests.codes.INSUFFICIENT_STORAGE: # pylint: disable=no-member + H.append( + """

+ Erreur: espace serveur trop plein. + Merci de contacter {0}

""".format( + scu.SCO_DEV_MAIL + ) + ) + elif status_code == requests.codes.OK: # pylint: disable=no-member + H.append("""

Opération effectuée.

""") + else: + H.append( + f"""

+ Erreur: code {status_code} + Merci de contacter {scu.SCO_DEV_MAIL}

""" + ) + flash("Données envoyées au serveur d'assistance") + return "\n".join(H) + html_sco_header.sco_footer() diff --git a/sco_version.py b/sco_version.py index 0cedeb3dc..547202122 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.82" +SCOVERSION = "9.1.83" SCONAME = "ScoDoc" From a902f6c1fe89c3fa036478eed096b9ddb883a2cf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 21 Mar 2022 14:33:38 +0100 Subject: [PATCH 77/81] =?UTF-8?q?Affichage=20cas=20inscription=20non=20enr?= =?UTF-8?q?egistr=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/etudiants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 3e8b91457..65c0701fe 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -289,10 +289,10 @@ class Identite(db.Model): log( f"*** situation inconsistante pour {self} (inscrit mais pas d'event)" ) - date_ins = "???" # ??? + situation += " (inscription non enregistrée)" # ??? else: date_ins = events[0].event_date - situation += date_ins.strftime(" le %d/%m/%Y") + situation += date_ins.strftime(" le %d/%m/%Y") else: situation = f"démission de {inscr.formsemestre.titre_mois()}" # Cherche la date de demission dans scolar_events: From 474f3347554b95e4c2d2d84d67b7024f427e8269 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 21 Mar 2022 19:41:14 +0100 Subject: [PATCH 78/81] Calcul des rangs / modules en formations classiques --- app/comp/res_classic.py | 19 ++++++++++++++++++- app/comp/res_common.py | 3 ++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 91614935d..b8f8ade02 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -15,7 +15,7 @@ from flask import g, url_for from app import db from app import log -from app.comp import moy_mat, moy_mod, moy_ue, inscr_mod +from app.comp import moy_mat, moy_mod, moy_sem, moy_ue, inscr_mod from app.comp.res_common import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig @@ -35,6 +35,7 @@ class ResultatsSemestreClassic(NotesTableCompat): "modimpl_coefs", "modimpl_idx", "sem_matrix", + "mod_rangs", ) def __init__(self, formsemestre): @@ -142,6 +143,22 @@ class ResultatsSemestreClassic(NotesTableCompat): if sco_preferences.get_preference("bul_show_matieres", self.formsemestre.id): self.compute_moyennes_matieres() + def compute_rangs(self): + """Calcul des rangs (classements) dans le semestre (moy. gen.), les UE + et les modules. + """ + # rangs moy gen et UEs sont calculées par la méthode commune à toutes les formations: + super().compute_rangs() + # les rangs des modules n'existent que dans les formations classiques: + self.mod_rangs = {} + for modimpl_result in self.modimpls_results.values(): + # ne prend que les rangs sous forme de chaines: + rangs = moy_sem.comp_ranks_series(modimpl_result.etuds_moy_module)[0] + self.mod_rangs[modimpl_result.moduleimpl_id] = ( + rangs, + modimpl_result.nb_inscrits_module, + ) + def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index fa2b9c30b..977d195d8 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -66,7 +66,7 @@ class ResultatsSemestre(ResultatsCache): """Moyennes de matières, si calculées. { matiere_id : Series, index etudid }""" def __repr__(self): - return f"<{self.__class__.__name__}(id={self.id}, formsemestre='{self.formsemestre}')>" + return f"<{self.__class__.__name__}(formsemestre='{self.formsemestre}')>" def compute(self): "Charge les notes et inscriptions et calcule toutes les moyennes" @@ -369,6 +369,7 @@ class NotesTableCompat(ResultatsSemestre): self.bonus_ues = None # virtuel self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues} self.mod_rangs = None # sera surchargé en Classic, mais pas en APC + """{ modimpl_id : (rangs, effectif) }""" self.moy_min = "NA" self.moy_max = "NA" self.moy_moy = "NA" From 590c52c138995c4a0d2dcde84e3e437610e9aced Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 21 Mar 2022 22:07:34 +0100 Subject: [PATCH 79/81] =?UTF-8?q?Ne=20r=C3=A9initialise=20pas=20syst=C3=A9?= =?UTF-8?q?matiquement=20les=20permissions=20des=20r=C3=B4les=20standards.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 2 +- app/auth/models.py | 24 +++++++++++++++++------- app/auth/routes.py | 10 +++++++++- app/templates/configuration.html | 9 ++++++--- app/views/users.py | 2 +- tests/unit/test_users.py | 2 +- 6 files changed, 35 insertions(+), 14 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 76760bd79..a1862aaa7 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -366,7 +366,7 @@ def user_db_init(): current_app.logger.info("Init User's db") # Create roles: - Role.insert_roles() + Role.reset_standard_roles_permissions() current_app.logger.info("created initial roles") # Ensure that admin exists admin_mail = current_app.config.get("SCODOC_ADMIN_MAIL") diff --git a/app/auth/models.py b/app/auth/models.py index 544afc319..cfab21a9c 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -410,20 +410,30 @@ class Role(db.Model): return self.permissions & perm == perm @staticmethod - def insert_roles(): - """Create default roles""" + def reset_standard_roles_permissions(reset_permissions=True): + """Create default roles if missing, then, if reset_permissions, + reset their permissions to default values. + """ default_role = "Observateur" for role_name, permissions in SCO_ROLES_DEFAULTS.items(): role = Role.query.filter_by(name=role_name).first() if role is None: role = Role(name=role_name) - role.reset_permissions() - for perm in permissions: - role.add_permission(perm) - role.default = role.name == default_role - db.session.add(role) + role.default = role.name == default_role + db.session.add(role) + if reset_permissions: + role.reset_permissions() + for perm in permissions: + role.add_permission(perm) + db.session.add(role) + db.session.commit() + @staticmethod + def ensure_standard_roles(): + """Create default roles if missing""" + Role.reset_standard_roles_permissions(reset_permissions=False) + @staticmethod def get_named_role(name): """Returns existing role with given name, or None.""" diff --git a/app/auth/routes.py b/app/auth/routes.py index df3401515..24daa8ca0 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -19,7 +19,7 @@ from app.auth.forms import ( ResetPasswordForm, DeactivateUserForm, ) -from app.auth.models import Permission +from app.auth.models import Role from app.auth.models import User from app.auth.email import send_password_reset_email from app.decorators import admin_required @@ -121,3 +121,11 @@ def reset_password(token): flash(_("Votre mot de passe a été changé.")) return redirect(url_for("auth.login")) return render_template("auth/reset_password.html", form=form, user=user) + + +@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"]) +@admin_required +def reset_standard_roles_permissions(): + Role.reset_standard_roles_permissions() + flash("rôles standard réinitialisés !") + return redirect(url_for("scodoc.configuration")) diff --git a/app/templates/configuration.html b/app/templates/configuration.html index 823772de5..33912fbf9 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -36,12 +36,15 @@

Gestion des images: logos, signatures, ...

Ces images peuvent être intégrées dans les documents générés par ScoDoc: bulletins, PV, etc.
-

configuration des images et logos +

configuration des images et logos

Exports Apogée

-

configuration des codes de décision

- +

configuration des codes de décision

+ +

Utilisateurs

+

remettre les permissions des + rôles standards à leurs valeurs par défaut (efface les modifications apportées)

diff --git a/app/views/users.py b/app/views/users.py index 06157cbce..10f1124dd 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -153,7 +153,7 @@ def create_user_form(user_name=None, edit=0, all_roles=False): "form. création ou édition utilisateur" if user_name is not None: # scodoc7func converti en int ! user_name = str(user_name) - Role.insert_roles() # assure la mise à jour des rôles en base + Role.ensure_standard_roles() # assure la présence des rôles en base auth_dept = current_user.dept from_mail = current_app.config["SCODOC_MAIL_FROM"] # current_user.email initvalues = {} diff --git a/tests/unit/test_users.py b/tests/unit/test_users.py index 8c429386c..21b13fb42 100644 --- a/tests/unit/test_users.py +++ b/tests/unit/test_users.py @@ -40,7 +40,7 @@ def test_roles_permissions(test_client): role.remove_permission(perm) assert not role.has_permission(perm) # Default roles: - Role.insert_roles() + Role.reset_standard_roles_permissions() # Bien présents ? role_names = [r.name for r in Role.query.filter_by().all()] assert len(role_names) == len(SCO_ROLES_DEFAULTS) From b9a53f9c436946043514d67e65767ab4d2aa57c7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 21 Mar 2022 22:29:55 +0100 Subject: [PATCH 80/81] Fix: fiche etudiant et jury : affichage des UEs en APC --- app/scodoc/sco_formsemestre_validation.py | 14 ++++++++------ app/scodoc/sco_parcours_dut.py | 4 +++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index c83e1cc4d..43f10ad81 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -591,12 +591,14 @@ def formsemestre_recap_parcours_table( etud_ue_status = { ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues } - ues = [ - ue - for ue in ues - if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"]) - or etud_ue_status[ue["ue_id"]]["is_capitalized"] - ] + if not nt.is_apc: + # formations classiques: filtre UE sur inscriptions (et garde UE capitalisées) + ues = [ + ue + for ue in ues + if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"]) + or etud_ue_status[ue["ue_id"]]["is_capitalized"] + ] for ue in ues: H.append('
' % ue["acronyme"]) diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py index fbf190c52..55110e5b0 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_parcours_dut.py @@ -1011,7 +1011,9 @@ def formsemestre_has_decisions(formsemestre_id): def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id): - """Vrai si l'étudiant est inscrit à au moins un module de cette UE dans ce semestre""" + """Vrai si l'étudiant est inscrit à au moins un module de cette UE dans ce semestre. + Ne pas utiliser pour les formations APC ! + """ cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """SELECT mi.* From b10fa09eb74363e1b45b6bc06e167472e8789ed3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 22 Mar 2022 08:58:47 +0100 Subject: [PATCH 81/81] Fix: mod rang si aucune note dans le module --- app/comp/moy_sem.py | 6 +++++- app/comp/res_common.py | 2 +- sco_version.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 23e989d95..61b5fd15c 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -93,8 +93,12 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series): """Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique) en tenant compte des ex-aequos. - Result: Series { etudid : rang:str } où rang est une chaine decrivant le rang. + Result: couple (tuple) + Series { etudid : rang:str } où rang est une chaine decrivant le rang, + Series { etudid : rang:int } le rang comme un nombre """ + if (notes is None) or (len(notes) == 0): + return (pd.Series([], dtype=object), pd.Series([], dtype=int)) notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant rangs_str = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne rangs_int = pd.Series(index=notes.index, dtype=int) # le rang numérique pour tris diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 977d195d8..737347479 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -133,7 +133,7 @@ class ResultatsSemestre(ResultatsCache): - En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre. - - En classique: toutes les UEs des modimpls auxquels l'étufdiant est inscrit sont + - En classique: toutes les UEs des modimpls auxquels l'étudiant est inscrit sont susceptibles d'être validées. Les UE "bonus" (sport) ne sont jamais "validables". diff --git a/sco_version.py b/sco_version.py index 547202122..6769973e2 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.83" +SCOVERSION = "9.1.84" SCONAME = "ScoDoc"
-

%s

- """ - % ( + f""" -
+

""" - % request.base_url, - f"""Bulletin {sem["titremois"]} -
""" - % sem, - """""", - """""" % time.strftime("%d/%m/%Y à %Hh%M"), - """""") - H.append( - '' - % ( - url_for( - "notes.formsemestre_bulletinetud", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, - etudid=etudid, - format="pdf", + +def _formsemestre_bulletinetud_header_html( + etud, + formsemestre: FormSemestre, + format=None, + version=None, +): + H = [ + html_sco_header.sco_header( + page_title=f"Bulletin de {etud.nomprenom}", + javascripts=[ + "js/bulletin.js", + "libjs/d3.v3.min.js", + "js/radar_bulletin.js", + ], + cssstyles=["css/radar_bulletin.css"], + ), + render_template( + "bul_head.html", + etud=etud, + format=format, + formsemestre=formsemestre, + menu_autres_operations=make_menu_autres_operations( + etud=etud, + formsemestre=formsemestre, + endpoint="notes.formsemestre_bulletinetud", version=version, ), - scu.ICON_PDF, - ) - ) - H.append("""
établi le %s (notes sur 20) - """ - % formsemestre_id, - """""" % etudid, - """""" % format, - """ + + +
établi le {time.strftime("%d/%m/%Y à %Hh%M")} (notes sur 20) + + + + """) # Menu endpoint = "notes.formsemestre_bulletinetud" + menu_autres_operations = make_menu_autres_operations( + formsemestre, etud, endpoint, version + ) - menuBul = [ + H.append("""""") + H.append( + '' + % ( + url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + etudid=etud.id, + format="pdf", + version=version, + ), + scu.ICON_PDF, + ) + ) + H.append("""

{etud["nomprenom"]}

+ "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id + )}">{etud.nomprenom}
Bulletin
- +
""") + H.append(menu_autres_operations) + H.append("""
%s
""") + # + H.append( + """
%s + """ + % ( + url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id), + sco_photos.etud_photo_html(etud, title="fiche de " + etud.nomprenom), + ) + ) + H.append( + """
+ """ + ) + + return "".join(H) + + +def make_menu_autres_operations( + formsemestre: FormSemestre, etud: Identite, endpoint: str, version: str +) -> str: + etud_email = etud.get_first_email() or "" + etud_perso = etud.get_first_email("emailperso") or "" + menu_items = [ { "title": "Réglages bulletins", "endpoint": "notes.formsemestre_edit_options", @@ -1124,43 +1159,42 @@ def _formsemestre_bulletinetud_header_html( "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "version": version, "format": "pdf", }, }, { - "title": "Envoi par mail à %s" % etud["email"], + "title": f"Envoi par mail à {etud_email}", "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "version": version, "format": "pdfmail", }, # possible slt si on a un mail... - "enabled": etud["email"] and can_send_bulletin_by_mail(formsemestre.id), + "enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id), }, { - "title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"], + "title": f"Envoi par mail à {etud_perso} (adr. personnelle)", "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "version": version, "format": "pdfmail", "prefer_mail_perso": 1, }, # possible slt si on a un mail... - "enabled": etud["emailperso"] - and can_send_bulletin_by_mail(formsemestre.id), + "enabled": etud_perso and can_send_bulletin_by_mail(formsemestre.id), }, { "title": "Version json", "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "version": version, "format": "json", }, @@ -1170,7 +1204,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "version": version, "format": "xml", }, @@ -1180,7 +1214,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.appreciation_add_form", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": ( formsemestre.can_be_edited_by(current_user) @@ -1192,7 +1226,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.formsemestre_ext_create_form", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": current_user.has_permission(Permission.ScoImplement), }, @@ -1201,7 +1235,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.formsemestre_validate_previous_ue", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), }, @@ -1210,7 +1244,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.external_ue_create_form", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), }, @@ -1219,7 +1253,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.formsemestre_validation_etud_form", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), }, @@ -1228,43 +1262,44 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.formsemestre_pvjury_pdf", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": True, }, ] + return htmlutils.make_menu("Autres opérations", menu_items, alone=True) - H.append("""
""") - H.append(htmlutils.make_menu("Autres opérations", menuBul, alone=True)) - H.append("""
%s
""") - # - H.append( - """

%s - """ - % ( - url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), - sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]), - ) - ) - H.append( - """
- """ - ) - - return "".join(H) + scu=scu, + time=time, + version=version, + ), + ] + return "\n".join(H) diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index ceeb0aac2..7f6a53c60 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -117,7 +117,7 @@ class BulletinGenerator: def get_filename(self): """Build a filename to be proposed to the web client""" sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"]) - return scu.bul_filename(sem, self.infos["etud"], "pdf") + return scu.bul_filename_old(sem, self.infos["etud"], "pdf") def generate(self, format="", stand_alone=True): """Return bulletin in specified format""" diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index ee57b60e7..ee0ddae27 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -138,7 +138,7 @@ def formsemestre_bulletinetud_published_dict( if not published: return d # stop ! - etat_inscription = etud.etat_inscription(formsemestre.id) + etat_inscription = etud.inscription_etat(formsemestre.id) if etat_inscription != scu.INSCRIT: d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True)) return d diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index d019d2df2..48c6b82bf 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -33,8 +33,7 @@ import os import time from operator import itemgetter -from flask import url_for, g, request -from flask_mail import Message +from flask import url_for, g from app import email from app import log @@ -46,7 +45,6 @@ from app.scodoc.sco_exceptions import ScoGenError, ScoValueError from app.scodoc import safehtml from app.scodoc import sco_preferences from app.scodoc.scolog import logdb -from app.scodoc.TrivialFormulator import TrivialFormulator def format_etud_ident(etud): @@ -860,7 +858,7 @@ def list_scolog(etudid): return cursor.dictfetchall() -def fill_etuds_info(etuds, add_admission=True): +def fill_etuds_info(etuds: list[dict], add_admission=True): """etuds est une liste d'etudiants (mappings) Pour chaque etudiant, ajoute ou formatte les champs -> informations pour fiche etudiant ou listes diverses @@ -977,7 +975,10 @@ def etud_inscriptions_infos(etudid: int, ne="") -> dict: def descr_situation_etud(etudid: int, ne="") -> str: - """chaîne décrivant la situation actuelle de l'étudiant""" + """Chaîne décrivant la situation actuelle de l'étudiant + XXX Obsolete, utiliser Identite.descr_situation_etud() dans + les nouveaux codes + """ from app.scodoc import sco_formsemestre cnx = ndb.GetDBConnexion() diff --git a/app/scodoc/sco_photos.py b/app/scodoc/sco_photos.py index 80f3553e5..cf66a8526 100644 --- a/app/scodoc/sco_photos.py +++ b/app/scodoc/sco_photos.py @@ -351,7 +351,8 @@ def copy_portal_photo_to_fs(etud): """Copy the photo from portal (distant website) to local fs. Returns rel. path or None if copy failed, with a diagnostic message """ - sco_etud.format_etud_ident(etud) + if "nomprenom" not in etud: + sco_etud.format_etud_ident(etud) url = photo_portal_url(etud) if not url: return None, "%(nomprenom)s: pas de code NIP" % etud diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 196c2c210..b30c493ca 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -608,7 +608,7 @@ def is_valid_filename(filename): return VALID_EXP.match(filename) -def bul_filename(sem, etud, format): +def bul_filename_old(sem: dict, etud: dict, format): """Build a filename for this bulletin""" dt = time.strftime("%Y-%m-%d") filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{format}" @@ -616,6 +616,14 @@ def bul_filename(sem, etud, format): return filename +def bul_filename(formsemestre, etud, format): + """Build a filename for this bulletin""" + dt = time.strftime("%Y-%m-%d") + filename = f"bul-{formsemestre.sem.titre_num}-{dt}-{etud.nom}.{format}" + filename = make_filename(filename) + return filename + + def flash_errors(form): """Flashes form errors (version sommaire)""" for field, errors in form.errors.items(): diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index 076fef1af..c7c649e51 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -41,7 +41,7 @@ class releveBUT extends HTMLElement { } set showData(data) { - this.showInformations(data); + // this.showInformations(data); this.showSemestre(data); this.showSynthese(data); this.showEvaluations(data); @@ -68,13 +68,7 @@ class releveBUT extends HTMLElement {
- - - -
- Photo de l'étudiant -
-
+ diff --git a/app/templates/bul_head.html b/app/templates/bul_head.html new file mode 100644 index 000000000..5211fcd4f --- /dev/null +++ b/app/templates/bul_head.html @@ -0,0 +1,58 @@ +{# -*- mode: jinja-html -*- #} +{# L'en-tête des bulletins HTML #} +{# was _formsemestre_bulletinetud_header_html #} + + + + + + +
+

{{etud.nomprenom}}

+
+ Bulletin {{formsemestre.titre_mois()}} +
+ + + + + + + +
établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20) + + + + + + +
{{menu_autres_operations|safe}}
+
{{scu.ICON_PDF|safe}} +
+
+
{{etud.photo_html(title="fiche de " + etud["nom"])|safe}} +
diff --git a/app/templates/but/bulletin.html b/app/templates/but/bulletin.html index ff0682fce..02a09e848 100644 --- a/app/templates/but/bulletin.html +++ b/app/templates/but/bulletin.html @@ -6,6 +6,8 @@ {% endblock %} {% block app_content %} +

Totoro

+ From 2220b617b883319725b5e269a1c20d6d068898a5 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 7 Mar 2022 21:49:11 +0100 Subject: [PATCH 07/81] =?UTF-8?q?WIP:=20int=C3=A9gration=20bulletins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 27 +++++++++--- app/models/etudiants.py | 1 + app/models/formsemestre.py | 14 +++++- app/scodoc/sco_bulletins.py | 8 ++-- app/scodoc/sco_bulletins_generator.py | 6 ++- app/scodoc/sco_formsemestre_status.py | 36 +++++----------- app/scodoc/sco_photos.py | 2 +- app/scodoc/sco_utils.py | 2 +- app/static/css/releve-but.css | 37 ++++++++++++---- app/static/css/scodoc.css | 4 +- app/static/js/releve-but.js | 15 ++++--- app/templates/bul_head.html | 4 ++ app/templates/but/bulletin.html | 2 +- app/templates/formsemestre_page_title.html | 50 ++++++++++++++++++++++ app/views/__init__.py | 37 +++++++++------- app/views/notes.py | 25 +++++++++-- 16 files changed, 193 insertions(+), 77 deletions(-) create mode 100644 app/templates/formsemestre_page_title.html diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 4379fdac5..f64769fc9 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -111,9 +111,10 @@ class BulletinBUT: d["modules"] = self.etud_mods_results(etud, modimpls_spo) return d - def etud_mods_results(self, etud, modimpls) -> dict: + def etud_mods_results(self, etud, modimpls, version="long") -> dict: """dict synthèse résultats des modules indiqués, - avec évaluations de chacun.""" + avec évaluations de chacun (sauf si version == "short") + """ res = self.res d = {} # etud_idx = self.etud_index[etud.id] @@ -154,12 +155,14 @@ class BulletinBUT: "evaluations": [ self.etud_eval_results(etud, e) for e in modimpl.evaluations - if e.visibulletin + if (e.visibulletin or version == "long") and ( modimpl_results.evaluations_etat[e.id].is_complete or self.prefs["bul_show_all_evals"] ) - ], + ] + if version != "short" + else [], } return d @@ -217,9 +220,17 @@ class BulletinBUT: return f"Bonus de {fmt_note(bonus_vect.iloc[0])}" def bulletin_etud( - self, etud: Identite, formsemestre: FormSemestre, force_publishing=False + self, + etud: Identite, + formsemestre: FormSemestre, + force_publishing=False, + version="long", ) -> dict: """Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML. + - version: + "long", "selectedevals": toutes les infos (notes des évaluations) + "short" : ne descend pas plus bas que les modules. + - Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai (bulletins non publiés). """ @@ -282,8 +293,10 @@ class BulletinBUT: ) d.update( { - "ressources": self.etud_mods_results(etud, res.ressources), - "saes": self.etud_mods_results(etud, res.saes), + "ressources": self.etud_mods_results( + etud, res.ressources, version=version + ), + "saes": self.etud_mods_results(etud, res.saes, version=version), "ues": { ue.acronyme: self.etud_ue_results(etud, ue) for ue in res.ues diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 953fb280a..060debc3b 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -160,6 +160,7 @@ class Identite(db.Model): "etudid": self.id, "nom": self.nom_disp(), "prenom": self.prenom, + "nomprenom": self.nomprenom, } if include_urls: d["fiche_url"] = url_for( diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index ce162d249..d92c375fb 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -22,6 +22,7 @@ from app.scodoc import sco_codes_parcours from app.scodoc import sco_preferences from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_permissions import Permission +from app.scodoc.sco_utils import MONTH_NAMES_ABBREV class FormSemestre(db.Model): @@ -162,8 +163,8 @@ class FormSemestre(db.Model): d["periode"] = 2 # typiquement, début en février: S2, S4... d["titre_num"] = self.titre_num() d["titreannee"] = self.titre_annee() - d["mois_debut"] = f"{self.date_debut.month} {self.date_debut.year}" - d["mois_fin"] = f"{self.date_fin.month} {self.date_fin.year}" + d["mois_debut"] = self.mois_debut() + d["mois_fin"] = self.mois_fin() d["titremois"] = "%s %s (%s - %s)" % ( d["titre_num"], self.modalite or "", @@ -293,6 +294,7 @@ class FormSemestre(db.Model): """chaîne "J. Dupond, X. Martin" ou "Jacques Dupond, Xavier Martin" """ + # was "nomcomplet" if not self.responsables: return "" if abbrev_prenom: @@ -304,6 +306,14 @@ class FormSemestre(db.Model): "2021 - 2022" return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month) + def mois_debut(self) -> str: + "Oct 2021" + return f"{MONTH_NAMES_ABBREV[self.date_debut.month - 1]} {self.date_debut.year}" + + def mois_fin(self) -> str: + "Jul 2022" + return f"{MONTH_NAMES_ABBREV[self.date_fin.month - 1]} {self.date_debut.year}" + def session_id(self) -> str: """identifiant externe de semestre de formation Exemple: RT-DUT-FI-S1-ANNEE diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 046c81468..b21404276 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -793,13 +793,14 @@ def etud_descr_situation_semestre( def formsemestre_bulletinetud( etudid=None, formsemestre_id=None, - format="html", + format=None, version="long", xml_with_decisions=False, force_publishing=False, # force publication meme si semestre non publie sur "portail" prefer_mail_perso=False, ): "page bulletin de notes" + format = format or "html" etud: Identite = Identite.query.get_or_404(etudid) formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) if not formsemestre: @@ -879,7 +880,7 @@ def do_formsemestre_bulletinetud( formsemestre: FormSemestre, etudid: int, version="long", # short, long, selectedevals - format="html", + format=None, nohtml=False, xml_with_decisions=False, # force décisions dans XML force_publishing=False, # force publication meme si semestre non publié sur "portail" @@ -890,6 +891,7 @@ def do_formsemestre_bulletinetud( où bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json) et filigranne est un message à placer en "filigranne" (eg "Provisoire"). """ + format = format or "html" if format == "xml": bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud( formsemestre.id, @@ -1258,7 +1260,7 @@ def make_menu_autres_operations( "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), }, { - "title": "Editer PV jury", + "title": "Éditer PV jury", "endpoint": "notes.formsemestre_pvjury_pdf", "args": { "formsemestre_id": formsemestre.id, diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index 7f6a53c60..2aeb792fb 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -297,7 +297,11 @@ def register_bulletin_class(klass): def bulletin_class_descriptions(): - return [x.description for x in BULLETIN_CLASSES.values()] + return [ + BULLETIN_CLASSES[class_name].description + for class_name in BULLETIN_CLASSES + if BULLETIN_CLASSES[class_name].list_in_menu + ] def bulletin_class_names() -> list[str]: diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 2080e9572..11e665d92 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -31,7 +31,7 @@ from flask import current_app from flask import g from flask import request -from flask import url_for +from flask import render_template, url_for from flask_login import current_user from app import log @@ -411,7 +411,7 @@ def formsemestre_status_menubar(sem): "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), }, { - "title": "Editer les PV et archiver les résultats", + "title": "Éditer les PV et archiver les résultats", "endpoint": "notes.formsemestre_archive", "args": {"formsemestre_id": formsemestre_id}, "enabled": sco_permissions_check.can_edit_pv(formsemestre_id), @@ -445,6 +445,7 @@ def retreive_formsemestre_from_request() -> int: """Cherche si on a de quoi déduire le semestre affiché à partir des arguments de la requête: formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id + Returns None si pas défini. """ if request.method == "GET": args = request.args @@ -505,34 +506,17 @@ def formsemestre_page_title(): return "" try: formsemestre_id = int(formsemestre_id) - sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy() + formsemestre = FormSemestre.query.get(formsemestre_id) except: log("can't find formsemestre_id %s" % formsemestre_id) return "" - fill_formsemestre(sem) - - h = f"""
- - {formsemestre_status_menubar(sem)} -
- """ + h = render_template( + "formsemestre_page_title.html", + formsemestre=formsemestre, + scu=scu, + sem_menu_bar=formsemestre_status_menubar(formsemestre.to_dict()), + ) return h diff --git a/app/scodoc/sco_photos.py b/app/scodoc/sco_photos.py index cf66a8526..0dfeaafe3 100644 --- a/app/scodoc/sco_photos.py +++ b/app/scodoc/sco_photos.py @@ -175,7 +175,7 @@ def etud_photo_is_local(etud: dict, size="small"): return photo_pathname(etud["photo_filename"], size=size) -def etud_photo_html(etud=None, etudid=None, title=None, size="small"): +def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"): """HTML img tag for the photo, either in small size (h90) or original size (size=="orig") """ diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index b30c493ca..ba7fd504a 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -619,7 +619,7 @@ def bul_filename_old(sem: dict, etud: dict, format): def bul_filename(formsemestre, etud, format): """Build a filename for this bulletin""" dt = time.strftime("%Y-%m-%d") - filename = f"bul-{formsemestre.sem.titre_num}-{dt}-{etud.nom}.{format}" + filename = f"bul-{formsemestre.titre_num()}-{dt}-{etud.nom}.{format}" filename = make_filename(filename) return filename diff --git a/app/static/css/releve-but.css b/app/static/css/releve-but.css index 02cceb94c..d9756ace4 100644 --- a/app/static/css/releve-but.css +++ b/app/static/css/releve-but.css @@ -14,16 +14,25 @@ } main{ --couleurPrincipale: rgb(240,250,255); - --couleurFondTitresUE: rgb(206,255,235); - --couleurFondTitresRes: rgb(125, 170, 255); - --couleurFondTitresSAE: rgb(211, 255, 255); + --couleurFondTitresUE: #b6ebff; + --couleurFondTitresRes: #f8c844; + --couleurFondTitresSAE: #c6ffab; --couleurSecondaire: #fec; - --couleurIntense: #c09; - --couleurSurlignage: rgba(232, 255, 132, 0.47); + --couleurIntense: rgb(4, 16, 159);; + --couleurSurlignage: rgba(255, 253, 110, 0.49); max-width: 1000px; margin: auto; display: none; } +.releve a, .releve a:visited { + color: navy; + text-decoration: none; +} +.releve a:hover { + color: red; + text-decoration: underline; +} + .ready .wait{display: none;} .ready main{display: block;} h2{ @@ -152,12 +161,14 @@ section>div:nth-child(1){ column-gap: 4px; flex: none; } -.infoSemestre>div:nth-child(1){ - margin-right: auto; -} + .infoSemestre>div>div:nth-child(even){ text-align: right; } +.photo { + border: none; + margin-left: auto; +} .rang{ font-weight: bold; } @@ -213,7 +224,6 @@ section>div:nth-child(1){ scroll-margin-top: 60px; } .module, .ue { - background: var(--couleurSecondaire); color: #000; padding: 4px 32px; border-radius: 4px; @@ -225,6 +235,15 @@ section>div:nth-child(1){ cursor: pointer; position: relative; } +.ue { + background: var(--couleurFondTitresRes); +} +.module { + background: var(--couleurFondTitresRes); +} +.module h3 { + background: var(--couleurFondTitresRes); +} .module::before, .ue::before { content:url("data:image/svg+xml;utf8,"); width: 26px; diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index a9390db24..31603a34b 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1963,7 +1963,9 @@ table.notes_recapcomplet a:hover { div.notes_bulletin { margin-right: 5px; } - +div.bulletin_menubar { + margin-right: 2em; +} table.notes_bulletin { border-collapse: collapse; border: 2px solid rgb(100,100,240); diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index c7c649e51..97f97e29d 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -79,8 +79,8 @@ class releveBUT extends HTMLElement {
-

Semestre

-
+

+
@@ -97,7 +97,7 @@ class releveBUT extends HTMLElement {
-

Synthèse

+

Unités d'enseignement

La moyenne des ressources dans une UE dépend des poids donnés aux évaluations.
@@ -126,7 +126,7 @@ class releveBUT extends HTMLElement {
-

SAÉ

+

Situations d'apprentissage et d'évaluation (SAÉ)

Liste @@ -192,7 +192,8 @@ class releveBUT extends HTMLElement { /* Information sur le semestre */ /*******************************/ showSemestre(data) { - this.shadow.querySelector("h2").innerHTML += data.semestre.numero; + + this.shadow.querySelector("#identite_etudiant").innerHTML = ` ${data.etudiant.nomprenom} `; this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription); let output = `
@@ -206,7 +207,9 @@ class releveBUT extends HTMLElement {
Absences
N.J. ${data.semestre.absences?.injustifie ?? "-"}
Total ${data.semestre.absences?.total ?? "-"}
-
`; +
+ photo de l'étudiant + `; /*${data.semestre.groupes.map(groupe => { return `
diff --git a/app/templates/bul_head.html b/app/templates/bul_head.html index 5211fcd4f..12df3c4f8 100644 --- a/app/templates/bul_head.html +++ b/app/templates/bul_head.html @@ -5,10 +5,12 @@ +{% if not is_apc %} +{% endif %}
+{% if not is_apc %}

{{etud.nomprenom}}

+{% endif %}
Bulletin
{{etud.photo_html(title="fiche de " + etud["nom"])|safe}}
diff --git a/app/templates/but/bulletin.html b/app/templates/but/bulletin.html index 02a09e848..c3e8c834e 100644 --- a/app/templates/but/bulletin.html +++ b/app/templates/but/bulletin.html @@ -6,8 +6,8 @@ {% endblock %} {% block app_content %} -

Totoro

+{% include 'bul_head.html' %} diff --git a/app/templates/formsemestre_page_title.html b/app/templates/formsemestre_page_title.html new file mode 100644 index 000000000..70fef579d --- /dev/null +++ b/app/templates/formsemestre_page_title.html @@ -0,0 +1,50 @@ +{# -*- mode: jinja-html -*- #} +{# Element HTML decrivant un semestre (barre de menu et infos) #} +{# was formsemestre_page_title #} + + \ No newline at end of file diff --git a/app/views/__init__.py b/app/views/__init__.py index 3d8af0777..f3c8e13d4 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -50,27 +50,29 @@ def close_dept_db_connection(arg): class ScoData: """Classe utilisée pour passer des valeurs aux vues (templates)""" - def __init__(self): + def __init__(self, etud=None, formsemestre=None): # Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête) self.Permission = Permission self.scu = scu self.SCOVERSION = sco_version.SCOVERSION # -- Informations étudiant courant, si sélectionné: - etudid = g.get("etudid", None) - if not etudid: - if request.method == "GET": - etudid = request.args.get("etudid", None) - elif request.method == "POST": - etudid = request.form.get("etudid", None) - - if etudid: + if etud is None: + etudid = g.get("etudid", None) + if etudid is None: + if request.method == "GET": + etudid = request.args.get("etudid", None) + elif request.method == "POST": + etudid = request.form.get("etudid", None) + if etudid is not None: + etud = Identite.query.get_or_404(etudid) + self.etud = etud + if etud is not None: # Infos sur l'étudiant courant - self.etud = Identite.query.get_or_404(etudid) ins = self.etud.inscription_courante() if ins: self.etud_cur_sem = ins.formsemestre self.nbabs, self.nbabsjust = sco_abs.get_abs_count_in_interval( - etudid, + etud.id, self.etud_cur_sem.date_debut.isoformat(), self.etud_cur_sem.date_fin.isoformat(), ) @@ -80,17 +82,22 @@ class ScoData: else: self.etud = None # --- Informations sur semestre courant, si sélectionné - formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request() - if formsemestre_id is None: + if formsemestre is None: + formsemestre_id = ( + sco_formsemestre_status.retreive_formsemestre_from_request() + ) + if formsemestre_id is not None: + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + if formsemestre is None: self.sem = None self.sem_menu_bar = None else: - self.sem = FormSemestre.query.get_or_404(formsemestre_id) + self.sem = formsemestre self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar( self.sem.to_dict() ) # --- Préférences - self.prefs = sco_preferences.SemPreferences(formsemestre_id) + self.prefs = sco_preferences.SemPreferences(formsemestre.id) from app.views import scodoc, notes, scolar, absences, users, pn_modules, refcomp diff --git a/app/views/notes.py b/app/views/notes.py index 6cbf0be43..5ca463b4c 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -32,6 +32,7 @@ Emmanuel Viennet, 2021 """ from operator import itemgetter +import time from xml.etree import ElementTree import flask @@ -276,7 +277,7 @@ sco_publish( def formsemestre_bulletinetud( etudid=None, formsemestre_id=None, - format="html", + format=None, version="long", xml_with_decisions=False, force_publishing=False, @@ -284,6 +285,7 @@ def formsemestre_bulletinetud( code_nip=None, code_ine=None, ): + format = format or "html" if not formsemestre_id: flask.abort(404, "argument manquant: formsemestre_id") if not isinstance(formsemestre_id, int): @@ -311,12 +313,16 @@ def formsemestre_bulletinetud( if format == "json": r = bulletin_but.BulletinBUT(formsemestre) return jsonify( - r.bulletin_etud(etud, formsemestre, force_publishing=force_publishing) + r.bulletin_etud( + etud, + formsemestre, + force_publishing=force_publishing, + version=version, + ) ) elif format == "html": return render_template( "but/bulletin.html", - title=f"Bul. {etud.nom} - BUT", bul_url=url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, @@ -324,8 +330,19 @@ def formsemestre_bulletinetud( etudid=etudid, format="json", force_publishing=1, # pour ScoDoc lui même + version=version, ), - sco=ScoData(), + etud=etud, + formsemestre=formsemestre, + is_apc=formsemestre.formation.is_apc(), + menu_autres_operations=sco_bulletins.make_menu_autres_operations( + formsemestre, etud, "notes.formsemestre_bulletinetud", version + ), + sco=ScoData(etud=etud), + scu=scu, + time=time, + title=f"Bul. {etud.nom} - BUT", + version=version, ) if not (etudid or code_nip or code_ine): From a09418329faf670fc29f1c6873496ab92682cc69 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 7 Mar 2022 23:43:48 +0100 Subject: [PATCH 08/81] =?UTF-8?q?Int=C3=A9gration=20bulletins=20html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_bulletins.py | 47 ++++----------------- app/static/css/scodoc.css | 19 +++++---- app/templates/bul_foot.html | 34 +++++++++++++++ app/templates/bul_head.html | 73 +++++++++++++++------------------ app/templates/but/bulletin.html | 3 ++ app/views/notes.py | 2 + 6 files changed, 93 insertions(+), 85 deletions(-) create mode 100644 app/templates/bul_foot.html diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index b21404276..65dcb3a91 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -822,47 +822,16 @@ def formsemestre_bulletinetud( H = [ _formsemestre_bulletinetud_header_html(etud, formsemestre, format, version), bulletin, + render_template( + "bul_foot.html", + etud=etud, + formsemestre=formsemestre, + inscription_courante=etud.inscription_courante(), + inscription_str=etud.inscription_descr()["inscription_str"], + ), + html_sco_header.sco_footer(), ] - H.append("""

Situation actuelle: """) - inscription_courante = etud.inscription_courante() - if inscription_courante: - H.append( - f"""""" - ) - inscription_descr = etud.inscription_descr() - H.append(inscription_descr["inscription_str"]) - if inscription_courante: - H.append("""""") - H.append("""

""") - if formsemestre.modalite == "EXT": - H.append( - f"""

- Éditer les validations d'UE dans ce semestre extérieur -

""" - ) - # Place du diagramme radar - H.append( - """
- - -
""" - % (etudid, formsemestre_id) - ) - H.append('
') - - # --- Pied de page - H.append(html_sco_header.sco_footer()) - return "".join(H) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 31603a34b..b5f2b325b 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1963,7 +1963,18 @@ table.notes_recapcomplet a:hover { div.notes_bulletin { margin-right: 5px; } -div.bulletin_menubar { +div.bull_head { + display: grid; + justify-content: space-between; + grid-template-columns: auto auto; +} +div.bull_photo { + display: inline-block; + margin-right: 10px; +} +span.bulletin_menubar_but { + display: inline-block; + margin-left: 2em; margin-right: 2em; } table.notes_bulletin { @@ -2105,12 +2116,6 @@ a.bull_link:hover { text-decoration: underline; } -table.bull_head { - width: 100%; -} -td.bull_photo { - text-align: right; -} div.bulletin_menubar { padding-left: 25px; diff --git a/app/templates/bul_foot.html b/app/templates/bul_foot.html new file mode 100644 index 000000000..873b43c76 --- /dev/null +++ b/app/templates/bul_foot.html @@ -0,0 +1,34 @@ +{# -*- mode: jinja-html -*- #} +{# Pied des bulletins HTML #} + +

Situation actuelle: +{% if inscription_courante %} +{{inscription_str}} +{% else %} + {{inscription_str}} +{% endif %} +

+ +{% if formsemestre.modalite == "EXT" %} +

+ Éditer les validations d'UE dans ce semestre extérieur +

+{% endif %} + +{# Place du diagramme radar #} +
+ + +
+
+ + diff --git a/app/templates/bul_head.html b/app/templates/bul_head.html index 12df3c4f8..d706ef9ae 100644 --- a/app/templates/bul_head.html +++ b/app/templates/bul_head.html @@ -2,9 +2,8 @@ {# L'en-tête des bulletins HTML #} {# was _formsemestre_bulletinetud_header_html #} - - - + {% if not is_apc %} - + {% endif %} - -
+
+
{% if not is_apc %}

{{etud.nomprenom}}

{% endif %}
- Bulletin + + + Bulletin + {{formsemestre.titre_mois()}} -
- - - - - - - -
établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20) - - - - - - -
{{menu_autres_operations|safe}}
-
{{formsemestre.titre_mois() + }} + +
+ établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20) + + + + + {{menu_autres_operations|safe}} + {{scu.ICON_PDF|safe}} -
+ +
-
{{etud.photo_html(title="fiche de " + etud["nom"])|safe}} -
+
diff --git a/app/templates/but/bulletin.html b/app/templates/but/bulletin.html index c3e8c834e..d394f255b 100644 --- a/app/templates/but/bulletin.html +++ b/app/templates/but/bulletin.html @@ -11,6 +11,9 @@ + +{% include 'bul_foot.html' %} + ') + # H.append( + # '' + # ) # JS additionels for js in javascripts: H.append("""\n""" % js) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 11e665d92..ae9ce543d 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1170,8 +1170,10 @@ def formsemestre_tableau_modules( H.append('
%s{mod.code}%s %s