Compare commits

...

151 Commits

Author SHA1 Message Date
Emmanuel Viennet 18b1f00586 version 9.6.964 2024-04-24 20:58:02 +02:00
Iziram 6b985620e9 Assiduité : signal_assiduites_diff : plage depuis args url closes #741 2024-04-24 20:55:21 +02:00
Iziram 4d234ba353 Assiduité : désactiver saisie présence closes #793 2024-04-24 20:55:21 +02:00
Iziram 5d45fcf656 Assiduité : signal_assiduites_group : bug fix suppr assi autre 2024-04-24 20:55:21 +02:00
Iziram 0a5919b788 Assiduité : pseudo-fix (catch + http error) #872 2024-04-24 20:55:21 +02:00
Iziram 09f4525e66 Assiduité : maj couleurs minitimeline + legende 2024-04-24 20:55:21 +02:00
Emmanuel Viennet 0bc57807de release build script using gitea again 2024-04-24 20:37:03 +02:00
Emmanuel Viennet 87aaf12d27 Protect against Reflected XSS on home page (and other exception-handling pages) 2024-04-23 18:28:00 +02:00
Emmanuel Viennet c8ab9b9b6c Invalidation cache lors d'une erreur sur association UE/Niveau. Peut-être cause de #874. 2024-04-15 18:06:26 +02:00
Emmanuel Viennet ad7b48e110 Calendrier évaluations: fix #875 2024-04-15 17:53:02 +02:00
Emmanuel Viennet f2ce16f161 Archive PV: gzip large files 2024-04-15 03:21:32 +02:00
Emmanuel Viennet 1ddf9b6ab8 Fix: création utilisateur si un seul département 2024-04-12 15:50:53 +02:00
Emmanuel Viennet 0a2e39cae1 Ajoute aide sur édition parcours UEs 2024-04-12 01:10:42 +02:00
Emmanuel Viennet a194b4b6e0 Edition parcours UE: si tous cochés, tronc commun 2024-04-12 01:05:02 +02:00
Emmanuel Viennet cbe85dfb7d anonymize_users: ignore admin 2024-04-12 01:04:27 +02:00
Emmanuel Viennet beba69bfe4 Améliore/met à jour tests unitaires API 2024-04-11 06:00:00 +02:00
Emmanuel Viennet 41fec29452 Bulletin BUT: ne mentionne pas les évaluations rattrapage/session2 sans notes. (c'est déjà le cas en classic) 2024-04-11 01:45:25 +02:00
Emmanuel Viennet 9bd05ea241 Modify SCO_URL in all js: no trailing slash. 2024-04-11 01:44:17 +02:00
Emmanuel Viennet 58b831513d Améliore traitement des erreurs lors de la génération des PDF 2024-04-10 15:29:30 +02:00
Emmanuel Viennet b861aba6a3 Tableaux génériques: possibilité de déclarer un colonne seulement pour excel. Assiduité: ajout etudid et NIP a visu_assi_group: closes #873. 2024-04-10 15:09:32 +02:00
Emmanuel Viennet c2443c361f Améliore page activation module entreprises. Implements #634 2024-04-09 00:36:46 +02:00
Emmanuel Viennet ab4731bd43 Suppression des anciennes fonctions ScoDoc7 donnant les URLs de base. 2024-04-08 18:57:00 +02:00
Emmanuel Viennet c17bc8b61b Fix: liste semestres avec code 2024-04-08 16:26:38 +02:00
Emmanuel Viennet e44a5ee55d Corrige templates formsemestre 2024-04-07 19:52:22 +02:00
Emmanuel Viennet a747ed22e2 Ajoute équivalences pour ref. comp. QLIO 2024-04-07 19:51:34 +02:00
Emmanuel Viennet 5d0a932634 Bulletins BUT: utilisation de l'abbréviation du titre module si présente. 2024-04-06 12:33:07 +02:00
Emmanuel Viennet 2b150cf521 Modif config Jina2. Refonte ScoData, fournit par défaut à tous les templates. 2024-04-06 12:16:53 +02:00
Emmanuel Viennet 5a5ddcacd7 Associer une formation BUT à un nouveau référentiel 'équivalent'. 2024-04-05 23:41:34 +02:00
Emmanuel Viennet 3f6e65b9da Elimine @cached_property sur Identite, pourrait provoquer incohérences temporaires en multithread 2024-04-05 11:00:01 +02:00
Emmanuel Viennet 5eba6170a5 Fix: typo bloquant affichage formations avec UEs sans semestre_idx 2024-04-05 10:11:34 +02:00
Emmanuel Viennet bd9bf87112 Enrichissement du tableau des formations (coche 'détails') 2024-04-05 00:23:29 +02:00
Emmanuel Viennet a0e2af481f Fonction expérimentale pour changer le ref. de compétences d'une formation 2024-04-05 00:22:14 +02:00
Emmanuel Viennet 42e8f97441 Fix: missing exception 2024-04-04 11:23:26 +02:00
Emmanuel Viennet 8ec0171ca0 Script préparation démos: renommage de tous les étudiants 2024-04-03 19:02:40 +02:00
Emmanuel Viennet 6dfab2d843 Fix typo affichage heures 2024-04-03 18:47:44 +02:00
Emmanuel Viennet 523ec59833 Harmonisation formats affichage dates et heures 2024-04-02 23:37:23 +02:00
Emmanuel Viennet bde6325391 Enrichi tableau jury BUT PV 2024-04-02 17:11:07 +02:00
Emmanuel Viennet 0577347622 Tableau décision jury BUT excel: améliore colonne ECTS 2024-04-02 16:53:04 +02:00
Emmanuel Viennet 28d46e413d Filtrage par groupes dans els pages statistiques: fix #791 2024-03-31 23:04:54 +02:00
Emmanuel Viennet 126ea0741a Edition UE: cosmetic + arg check + invalidation cache desassoc_ue_niveau 2024-03-31 10:21:44 +02:00
Emmanuel Viennet a5b5f49f76 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96 2024-03-29 16:39:36 +01:00
Iziram b7ab10bf4e Assiduité : docs : erratum samples 2024-03-29 16:38:14 +01:00
Emmanuel Viennet 3e0b19c4a8 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96 2024-03-29 16:37:19 +01:00
Emmanuel Viennet 1dd5187fae lien vers doc évaluations 2024-03-29 16:36:05 +01:00
Iziram 9a3a7d33b2 Assiduité : Docs : ajout samples 2024-03-29 16:16:13 +01:00
Iziram a7569fe4f5 Assiduité : signal_assiduites_diff : fix visibilité tableau 2024-03-29 16:15:19 +01:00
Iziram 79e973f06d Assiduité : XXX todo #831 (non fini) 2024-03-29 15:36:35 +01:00
Emmanuel Viennet b6940e4882 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96 2024-03-27 17:13:36 +01:00
Emmanuel Viennet 1f24095c57 Ajout timestamp supplémentaire dans log mail 2024-03-27 17:13:05 +01:00
Emmanuel Viennet 0ed2455028 ne présente plus le lien 'ajouter semestre' si on n'a pas le droit 2024-03-27 17:12:28 +01:00
Emmanuel Viennet b841b2f708 Remplace préférence dépt. bul_display_publication par paramètre global: passerelle_disabled + cosmetic 2024-03-27 16:27:45 +01:00
Iziram 0fa1478138 Assiduité : recup_assiduites_plage: ajout justificatifs 2024-03-27 15:01:58 +01:00
Iziram 85ad7b5f29 Assiduité : suppr pref limite_annee + closes #766 2024-03-27 11:51:50 +01:00
Emmanuel Viennet 6bfd461bf2 Fix: jury BUT cas particulier sans ue 2024-03-27 09:22:34 +01:00
Emmanuel Viennet e1f1a95a14 merge 2024-03-26 14:43:11 +01:00
Emmanuel Viennet 70e3006981 merge 2024-03-26 14:37:02 +01:00
Emmanuel Viennet bae46c2794 Page accueil département: refonte liste semestres en cours 2024-03-26 14:17:42 +01:00
Iziram b1055a4ebe Assiduité : signal_assiduites_group : fix bug photo etud 2024-03-26 09:02:55 +01:00
Iziram b2ef6a4c53 Assiduité : liste_assiduites : formatage des dates 2024-03-25 16:19:05 +01:00
Iziram a7c7bd655d Assiduité : ajout_justif_etud : dates avec heures 2024-03-25 16:15:01 +01:00
Iziram 1309043a98 Assiduités : assiduites_bubble : ajout d'un bouton 📝 pour éditer l'assiduité visée 2024-03-25 15:19:59 +01:00
Iziram a75b41ca5f Assiduité : signal_assiduites_diff : vérification date 2024-03-25 15:12:08 +01:00
Emmanuel Viennet 8df25ca02f Ajout infos semestres dans bulletin classique JSON. Close #583 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 61f9dddeb6 Modif. clé trie étudiants et utilisation dans éditeur partition. 2024-03-25 14:41:20 +01:00
Emmanuel Viennet a1f5340935 Débouchés: tags. Implements #396 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 68128c27d5 Conversion date naissance étudiant. complète #593. 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 8ecaa2bed0 Conversion dates édition évaluations et formsemestres. Fix #593. 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 7c61dd8d63 Cosmetic + reorganisation css edit formation 2024-03-25 14:41:20 +01:00
Emmanuel Viennet f493ba344f Jury BUT: améliore présentation et information sur les UEs capitalisées. Closes #670 2024-03-25 14:41:20 +01:00
Emmanuel Viennet f5079d9aef Jury BUT: affiche la liste des modules avec note en ATTente 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 55add2ffb3 cosmetic: eye, table semestres 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 5865b67652 Adapte ref. pour test_api_formsemestre.py sans nom_short 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 3c8b088d5e Jury BUT auto: avertissement si semestres pairs non bloqués 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 2da359ae41 Fix export excel table jury. Closes #868 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 09ec53f573 Ajout infos semestres dans bulletin classique JSON. Close #583 2024-03-24 15:47:42 +01:00
Emmanuel Viennet 3787e0145a Modif. clé trie étudiants et utilisation dans éditeur partition. 2024-03-24 14:34:55 +01:00
Emmanuel Viennet edf989ee04 Débouchés: tags. Implements #396 2024-03-24 11:23:40 +01:00
Emmanuel Viennet 203f3a5342 Conversion date naissance étudiant. complète #593. 2024-03-24 10:34:02 +01:00
Emmanuel Viennet 161f8476ca Conversion dates édition évaluations et formsemestres. Fix #593. 2024-03-24 09:17:01 +01:00
Emmanuel Viennet d419d75515 Cosmetic + reorganisation css edit formation 2024-03-24 08:27:09 +01:00
Emmanuel Viennet f23630d7fd Jury BUT: améliore présentation et information sur les UEs capitalisées. Closes #670 2024-03-24 07:39:47 +01:00
Emmanuel Viennet fa0417f0b1 Jury BUT: affiche la liste des modules avec note en ATTente 2024-03-23 13:23:26 +01:00
Emmanuel Viennet 12256dc3d4 cosmetic: eye, table semestres 2024-03-23 10:17:49 +01:00
Emmanuel Viennet 46529917ea Adapte ref. pour test_api_formsemestre.py sans nom_short 2024-03-22 22:05:24 +01:00
Emmanuel Viennet 2367984848 Jury BUT auto: avertissement si semestres pairs non bloqués 2024-03-22 21:56:52 +01:00
Emmanuel Viennet 46c86d2928 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into iziram-main96 2024-03-22 17:45:51 +01:00
Emmanuel Viennet 715e4f94ee Fix export excel table jury. Closes #868 2024-03-22 17:39:48 +01:00
Iziram b2e6ef63b9 Assiduité : traitement des justificatifs closes #818 2024-03-22 15:44:47 +01:00
Iziram 30560e5860 Assiduité : etudid dans tableau excel 2024-03-22 15:44:36 +01:00
Iziram 0fbcfb1124 Assiduites : edit_justificatif_etud - retour à la page précédente (back_url) closes #864 2024-03-22 15:44:27 +01:00
Iziram 2daae1c9c5 Assiduité : bilan_dept : inversion ordre tableau/téléchargement 2024-03-22 15:44:15 +01:00
Emmanuel Viennet 635269ff36 Modifie FormSemestre.etudids_actifs: retire @cached_property. Tests OK. 2024-03-22 11:49:51 +01:00
Emmanuel Viennet 4aa30a40bd Fix: front&back saisie note sur DEM 2024-03-21 16:42:28 +01:00
Emmanuel Viennet 03c03f3725 Fix form recherche par étape 2024-03-21 15:54:56 +01:00
Emmanuel Viennet 29eb8c297b Améliore page accueil dept.: formation, cosmétique, export excel 2024-03-21 13:21:25 +01:00
Emmanuel Viennet 38032a8c09 Ré-écriture de la page d'accueil de département. Template. 2024-03-21 12:06:34 +01:00
Emmanuel Viennet 2f2d98954c Maquette: introduit scobox, reprend certaines pages. WIP 2024-03-20 18:13:19 +01:00
Emmanuel Viennet 2e5d94f048 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into iziram-main96 2024-03-19 20:59:28 +01:00
Emmanuel Viennet 1b1b8ebdc4 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96 2024-03-19 20:59:13 +01:00
Emmanuel Viennet 9c6db169f3 Restreint accès aux bulletins PDF si formsemestre.bul_hide_xml (sémantique changée) + WIP tests unitaires API 2024-03-19 18:22:02 +01:00
Iziram 8ded16b94f Assiduité : liste_assi : colonne code et titre module closes #865 2024-03-19 16:30:13 +01:00
Iziram 5d10ee467e Assiduité : téléchargement des assiduités 2024-03-19 16:30:08 +01:00
Emmanuel Viennet 763f60fb3d Fix: /etud_info_html si pas de données admission 2024-03-19 09:34:03 +01:00
Iziram 7af0dd1e1e Assiduite : signal_assiduites_diff tableau transposer + modifs mineurs 2024-03-18 17:41:17 +01:00
Emmanuel Viennet dece9a82d1 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into iziram-main96 2024-03-18 14:24:27 +01:00
Emmanuel Viennet 0262b6e2ac Affichage nb évaluations en attente su rtableau de bord. Ne consière plus les évaluations bloquées comme en attente. 2024-03-18 14:20:34 +01:00
Emmanuel Viennet f8f47e05ff Fix: formsemestre_table_estim_cost: error handling 2024-03-17 13:28:49 +01:00
Iziram b74d525c28 Assiduité: signal_assiduites_diff OK 2024-03-15 16:08:41 +01:00
Iziram c617ee321a Assiduité : signal_assiduites_diff suppr titres 2024-03-14 15:39:42 +01:00
Iziram 56ec4ba43d Assiduité : page signal_assiduites_diff WIP 2024-03-13 16:35:56 +01:00
Iziram d14f7e21b7 Assiduité : calendrier utilisation couleur générale (assiduites.css) 2024-03-11 11:39:36 +01:00
Iziram c3cb1da561 Assiduité : refonte signal_assiduites_group 2024-03-11 11:39:06 +01:00
Iziram cce60d432d Assiduité : timeline ajout timepicker 2024-03-11 11:37:58 +01:00
Iziram 4386994f7d Assiduité : bilan_etud suppr bouton suppression + avertissement tableau 2024-03-11 11:37:21 +01:00
Iziram fddfddfa7b Assiduité : minitimeline utilisation couleur assiduite + assiduite_bubble 2024-03-11 11:36:24 +01:00
Iziram 39dca32d2e Assiduité : date_utils suppression scodoc-datetime + ajout time conflit 2024-03-11 11:35:43 +01:00
Iziram e2b9cd3ded Assiduité : suppression assiduite js non utilisé 2024-03-11 11:35:09 +01:00
Iziram be227f4a2f Assiduité : Prompt : blocage scroll + fermeture on success 2024-03-11 11:33:05 +01:00
Emmanuel Viennet 959a98d0a2 Fix bug: get_assiduites_count / feuille_preparation_jury 2024-03-10 04:44:42 +01:00
Emmanuel Viennet 35a038fd3a code fmt 2024-03-03 23:27:29 +01:00
Emmanuel Viennet b46556c189 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc 2024-03-03 23:26:59 +01:00
Iziram 71f90f5261 Assiduité : annulation / suppression fichier justif 2024-03-01 16:35:39 +01:00
Iziram 1b037d6c7c Assiduité : fix format nbabs sidebar 2024-03-01 16:04:16 +01:00
Emmanuel Viennet 60a97b7baf Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc 2024-03-01 15:55:03 +01:00
Iziram 0332553587 Assiduité : correction bug cache 2024-03-01 12:56:00 +01:00
Iziram 958cf435c8 Assiduité : ajout tests unitaire cache + cas justificatifs 2024-03-01 12:56:00 +01:00
Iziram c69e9c34a0 Assiduité : fix format date 'absences du' 2024-03-01 12:56:00 +01:00
Iziram 17f8771b0b Assiduité : fix bug tableau (actualisation sur les lignes) 2024-03-01 12:56:00 +01:00
Iziram 7eb41fb2eb Assiduité : ajout test api manquant closes #689 2024-03-01 12:56:00 +01:00
Iziram a79ca4a17d Assiduité : suppression ancien tableaux (inutilisé) 2024-03-01 12:56:00 +01:00
Emmanuel Viennet 411ef8ae0d vocabulaire: portail > passerelle 2024-03-01 12:56:00 +01:00
Emmanuel Viennet 169bf17fdd Ajout colonne référentiel à la table des formations 2024-03-01 12:56:00 +01:00
Emmanuel Viennet 75d4c110a8 Améliore anonymisation (users) + lien contact + cosmetic 2024-03-01 12:56:00 +01:00
Emmanuel Viennet 9003a2ca87 vocabulaire: portail > passerelle 2024-03-01 12:03:19 +01:00
Emmanuel Viennet 55ecaa45a9 Ajout colonne référentiel à la table des formations 2024-03-01 12:03:00 +01:00
Emmanuel Viennet ab39454a0d Améliore anonymisation (users) + lien contact + cosmetic 2024-03-01 11:12:36 +01:00
Iziram 5158bd0c8f Assiduité : optimisation justification assiduités 2024-02-29 14:20:39 +01:00
Iziram 21b2e0f582 Assiduité : fix bug module selector signal_assiduites_group 2024-02-29 08:47:03 +01:00
Emmanuel Viennet e56cbfc5a2 Précision nb abs sur table recap 2024-02-28 23:22:15 +01:00
Emmanuel Viennet 9cdab8d1ed Merge branch 'pe-but-v4' of https://scodoc.org/git/cleo/ScoDoc-PE 2024-02-28 12:12:40 +01:00
Cléo Baras 7cdba43e86 Déploie l'option min/max/moy aux tableurs intermédiaires 2024-02-28 11:59:54 +01:00
Iziram 079348bb87 Assiduité : ajout modif justi dans journal etud closes #814 2024-02-28 11:35:16 +01:00
Iziram c882e0d6a0 Assiduité : couleur minitimeline calendrier fixes #817 2024-02-28 11:09:01 +01:00
Cléo Baras 9c7576154c Merge branch 'scodoc-master' into pe-but-v4 2024-02-28 11:02:31 +01:00
Cléo Baras ce0d5ec9fd Corrige prise en compte des inscriptions aux UEs pour les moyennes par tag 2024-02-28 11:00:32 +01:00
Iziram 3d6be2f200 Assiduité : fix bug assi jour complet + affichage calendrier 2024-02-28 10:50:15 +01:00
Cléo Baras e675064cae Corrige bug ressemtagbut sans notes 2024-02-28 10:36:27 +01:00
Iziram 185e061f01 Assiduité : msg erreur ajout_assiduite_etud 2024-02-28 10:23:47 +01:00
Iziram e4c889ec8a Assiduité : calendrier scroll horizontal 2024-02-28 09:26:15 +01:00
Emmanuel Viennet 7ef45e0bac Fix calendrier 2024-02-27 23:17:27 +01:00
Iziram f242fee5ff Assiduité : calendrier fix str_to_time 2024-02-27 23:14:12 +01:00
208 changed files with 8986 additions and 9515 deletions

View File

@ -315,12 +315,6 @@ def create_app(config_class=DevConfig):
app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
# Add some globals
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
@ -338,8 +332,15 @@ def create_app(config_class=DevConfig):
from app.api import api_bp
from app.api import api_web_bp
# Jinja2 configuration
# Enable autoescaping of all templates, including .j2
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
# https://scodoc.fr/ScoDoc
app.register_blueprint(scodoc_bp)

View File

@ -3,14 +3,15 @@
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 API : Assiduités
"""
"""ScoDoc 9 API : Assiduités"""
from datetime import datetime
from flask import g, request
from flask_json import as_json
from flask_login import current_user, login_required
from flask_sqlalchemy.query import Query
from sqlalchemy.orm.exc import ObjectDeletedError
from app import db, log, set_sco_dept
import app.scodoc.sco_assiduites as scass
@ -858,7 +859,10 @@ def assiduite_edit(assiduite_id: int):
msg=f"assiduite: modif {assiduite_unique}",
)
db.session.commit()
scass.simple_invalidate_cache(assiduite_unique.to_dict())
try:
scass.simple_invalidate_cache(assiduite_unique.to_dict())
except ObjectDeletedError:
return json_error(404, "Assiduité supprimée / inexistante")
return {"OK": True}

View File

@ -414,9 +414,16 @@ def bulletin(
if version == "pdf":
version = "long"
pdf = True
if version not in scu.BULLETINS_VERSIONS_BUT:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if version not in (
scu.BULLETINS_VERSIONS_BUT
if formsemestre.formation.is_apc()
else scu.BULLETINS_VERSIONS
):
return json_error(404, "version invalide")
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
if formsemestre.bul_hide_xml and pdf:
return json_error(403, "bulletin non disponible")
# note: la version json est réduite si bul_hide_xml
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
return json_error(404, "formsemestre inexistant")

View File

@ -52,7 +52,8 @@ def formations():
@as_json
def formations_ids():
"""
Retourne la liste de toutes les id de formations (tous départements)
Retourne la liste de toutes les id de formations
(tous départements, ou du département indiqué dans la route)
Exemple de résultat : [ 17, 99, 32 ]
"""
@ -328,6 +329,8 @@ def desassoc_ue_niveau(ue_id: int):
ue.niveau_competence = None
db.session.add(ue)
db.session.commit()
# Invalidation du cache
ue.formation.invalidate_cached_sems()
log(f"desassoc_ue_niveau: {ue}")
if g.scodoc_dept:
# "usage web"

View File

@ -12,7 +12,7 @@ from operator import attrgetter, itemgetter
from flask import g, make_response, request
from flask_json import as_json
from flask_login import current_user, login_required
import sqlalchemy as sa
import app
from app import db
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
@ -38,7 +38,7 @@ from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import ModuleType
import app.scodoc.sco_utils as scu
from app.tables.recap import TableRecap
from app.tables.recap import TableRecap, RowRecap
@bp.route("/formsemestre/<int:formsemestre_id>")
@ -171,6 +171,44 @@ def formsemestres_query():
]
@bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
@scodoc
@permission_required(Permission.EditFormSemestre)
@as_json
def formsemestre_edit(formsemestre_id: int):
"""Modifie les champs d'un formsemestre."""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
args = request.get_json(force=True) # may raise 400 Bad Request
editable_keys = {
"semestre_id",
"titre",
"date_debut",
"date_fin",
"edt_id",
"etat",
"modalite",
"gestion_compensation",
"bul_hide_xml",
"block_moyennes",
"block_moyenne_generale",
"mode_calcul_moyennes",
"gestion_semestrielle",
"bul_bgcolor",
"resp_can_edit",
"resp_can_change_ens",
"ens_can_edit_eval",
"elt_sem_apo",
"elt_annee_apo",
}
formsemestre.from_dict({k: v for (k, v) in args.items() if k in editable_keys})
try:
db.session.commit()
except sa.exc.StatementError as exc:
return json_error(404, f"invalid argument(s): {exc.args[0]}")
return formsemestre.to_dict_api()
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@ -468,13 +506,13 @@ def etat_evals(formsemestre_id: int):
date_mediane = notes_sorted[len(notes_sorted) // 2].date
eval_dict["saisie_notes"] = {
"datetime_debut": date_debut.isoformat()
if date_debut is not None
else None,
"datetime_debut": (
date_debut.isoformat() if date_debut is not None else None
),
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
"datetime_mediane": date_mediane.isoformat()
if date_mediane is not None
else None,
"datetime_mediane": (
date_mediane.isoformat() if date_mediane is not None else None
),
}
list_eval.append(eval_dict)
@ -505,16 +543,30 @@ def formsemestre_resultat(formsemestre_id: int):
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
table = TableRecap(
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
)
# Supprime les champs inutiles (mise en forme)
rows = table.to_list()
# Ajoute le groupe de chaque partition:
# Ajoute le groupe de chaque partition,
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
for row in rows:
row["partitions"] = etud_groups.get(row["etudid"], {})
class RowRecapAPI(RowRecap):
"""Pour table avec partitions et sort_key"""
def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms"""
super().add_etud_cols()
self.add_cell("partitions", "partitions", etud_groups.get(self.etud.id, {}))
self.add_cell("sort_key", "sort_key", self.etud.sort_key)
table = TableRecap(
res,
convert_values=convert_values,
include_evaluations=False,
mode_jury=False,
row_class=RowRecapAPI,
)
rows = table.to_list()
# for row in rows:
# row["partitions"] = etud_groups.get(row["etudid"], {})
return rows

View File

@ -20,14 +20,8 @@ from app.api import api_bp as bp
from app.api import api_web_bp
from app.api import get_model_api_object, tools
from app.decorators import permission_required, scodoc
from app.models import (
Identite,
Justificatif,
Departement,
FormSemestre,
)
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
from app.models.assiduites import (
compute_assiduites_justified,
get_formsemestre_from_data,
)
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
@ -315,7 +309,6 @@ def justif_create(etudid: int = None, nip=None, ine=None):
errors: list[dict] = []
success: list[dict] = []
justifs: list[Justificatif] = []
# énumération des justificatifs
for i, data in enumerate(create_list):
@ -327,11 +320,9 @@ def justif_create(etudid: int = None, nip=None, ine=None):
errors.append({"indice": i, "message": obj})
else:
success.append({"indice": i, "message": obj})
justifs.append(justi)
justi.justifier_assiduites()
scass.simple_invalidate_cache(data, etud.id)
# Actualisation des assiduités justifiées en fonction de tous les nouveaux justificatifs
compute_assiduites_justified(etud.etudid, justifs)
return {"errors": errors, "success": success}
@ -500,9 +491,16 @@ def justif_edit(justif_id: int):
return json_error(404, err)
# Mise à jour du justificatif
justificatif_unique.dejustifier_assiduites()
db.session.add(justificatif_unique)
db.session.commit()
Scolog.logdb(
method="edit_justificatif",
etudid=justificatif_unique.etudiant.id,
msg=f"justificatif modif: {justificatif_unique}",
)
# Génération du dictionnaire de retour
# La couverture correspond
# - aux assiduités précédemment justifiées par le justificatif
@ -510,11 +508,7 @@ def justif_edit(justif_id: int):
retour = {
"couverture": {
"avant": avant_ids,
"apres": compute_assiduites_justified(
justificatif_unique.etudid,
[justificatif_unique],
True,
),
"apres": justificatif_unique.justifier_assiduites(),
}
}
# Invalide le cache
@ -591,14 +585,10 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
# On invalide le cache
scass.simple_invalidate_cache(justificatif_unique.to_dict())
# On actualise les assiduités justifiées de l'étudiant concerné
justificatif_unique.dejustifier_assiduites()
# On supprime le justificatif
db.session.delete(justificatif_unique)
# On actualise les assiduités justifiées de l'étudiant concerné
compute_assiduites_justified(
justificatif_unique.etudid,
Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(),
True,
)
return (200, "OK")
@ -699,7 +689,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
@as_json
@permission_required(Permission.AbsChange)
def justif_remove(justif_id: int = None):
# XXX TODO pas de test unitaire
"""
Supression d'un fichier ou d'une archive
{

View File

@ -603,8 +603,19 @@ class Role(db.Model):
"""Create default roles if missing, then, if reset_permissions,
reset their permissions to default values.
"""
Role.reset_roles_permissions(
SCO_ROLES_DEFAULTS, reset_permissions=reset_permissions
)
@staticmethod
def reset_roles_permissions(roles_perms: dict[str, tuple], reset_permissions=True):
"""Ajoute les permissions aux roles
roles_perms : { "role_name" : (permission, ...) }
reset_permissions : si vrai efface permissions déja existantes
Si le role n'existe pas, il est (re) créé.
"""
default_role = "Observateur"
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
for role_name, permissions in roles_perms.items():
role = Role.query.filter_by(name=role_name).first()
if role is None:
role = Role(name=role_name)

View File

@ -21,7 +21,7 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
return ""
ref_comp = ue.formation.referentiel_competence
if ref_comp is None:
return f"""<div class="ue_advanced">
return f"""<div class="scobox ue_advanced">
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
@ -31,19 +31,28 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
H = [
"""
<div class="ue_advanced">
<h3>Parcours du BUT</h3>
<div class="scobox ue_advanced">
<div class="scobox-title">Parcours du BUT</div>
"""
]
# Choix des parcours
ue_pids = [p.id for p in ue.parcours]
H.append("""<form id="choix_parcours">""")
H.append(
"""
<div class="help">
Cocher tous les parcours dans lesquels cette UE est utilisée,
même si vous n'offrez pas ce parcours dans votre département.
Sans quoi, les UEs de Tronc Commun ne seront pas reconnues.
Ne cocher aucun parcours est équivalent à tous les cocher.
</div>
<form id="choix_parcours" style="margin-top: 12px;">
"""
)
ects_differents = {
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
} != {None}
for parcour in ref_comp.parcours:
ects_parcour = ue.get_ects(parcour)
ects_parcour_txt = (
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
)

View File

@ -9,12 +9,14 @@
import collections
import datetime
import pandas as pd
import numpy as np
from flask import g, has_request_context, url_for
from app import db
from app.comp.moy_mod import ModuleImplResults
from app.comp.res_but import ResultatsSemestreBUT
from app.models import Evaluation, FormSemestre, Identite
from app.models import Evaluation, FormSemestre, Identite, ModuleImpl
from app.models.groups import GroupDescr
from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu
@ -229,7 +231,7 @@ class BulletinBUT:
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
d[modimpl.module.code] = {
"id": modimpl.id,
"titre": modimpl.module.titre,
"titre": modimpl.module.titre_str(),
"code_apogee": modimpl.module.code_apogee,
"url": (
url_for(
@ -249,59 +251,88 @@ class BulletinBUT:
# "moy": fmt_note(moyennes_etuds.mean()),
},
"evaluations": (
[
self.etud_eval_results(etud, e)
for e in modimpl.evaluations
if (e.visibulletin or version == "long")
and (e.id in modimpl_results.evaluations_etat)
and (
modimpl_results.evaluations_etat[e.id].is_complete
or self.prefs["bul_show_all_evals"]
)
]
self.etud_list_modimpl_evaluations(
etud, modimpl, modimpl_results, version
)
if version != "short"
else []
),
}
return d
def etud_eval_results(self, etud, e: Evaluation) -> dict:
def etud_list_modimpl_evaluations(
self,
etud: Identite,
modimpl: ModuleImpl,
modimpl_results: ModuleImplResults,
version: str,
) -> list[dict]:
"""Liste des résultats aux évaluations de ce modimpl à montrer pour cet étudiant"""
evaluation: Evaluation
eval_results = []
for evaluation in modimpl.evaluations:
if (
(evaluation.visibulletin or version == "long")
and (evaluation.id in modimpl_results.evaluations_etat)
and (
modimpl_results.evaluations_etat[evaluation.id].is_complete
or self.prefs["bul_show_all_evals"]
)
):
eval_notes = self.res.modimpls_results[modimpl.id].evals_notes[
evaluation.id
]
if (evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE) or (
not np.isnan(eval_notes[etud.id])
):
eval_results.append(
self.etud_eval_results(etud, evaluation, eval_notes)
)
return eval_results
def etud_eval_results(
self, etud: Identite, evaluation: Evaluation, eval_notes: pd.DataFrame
) -> dict:
"dict resultats d'un étudiant à une évaluation"
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
try:
etud_ues_ids = self.res.etud_ues_ids(etud.id)
poids = {
ue.acronyme: modimpls_evals_poids[ue.id][e.id]
ue.acronyme: modimpls_evals_poids[ue.id][evaluation.id]
for ue in self.res.ues
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
}
except KeyError:
poids = collections.defaultdict(lambda: 0.0)
d = {
"id": e.id,
"id": evaluation.id,
"coef": (
fmt_note(e.coefficient)
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
fmt_note(evaluation.coefficient)
if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
else None
),
"date_debut": e.date_debut.isoformat() if e.date_debut else None,
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
"description": e.description,
"evaluation_type": e.evaluation_type,
"date_debut": (
evaluation.date_debut.isoformat() if evaluation.date_debut else None
),
"date_fin": (
evaluation.date_fin.isoformat() if evaluation.date_fin else None
),
"description": evaluation.description,
"evaluation_type": evaluation.evaluation_type,
"note": (
{
"value": fmt_note(
eval_notes[etud.id],
note_max=e.note_max,
note_max=evaluation.note_max,
),
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
"min": fmt_note(notes_ok.min(), note_max=evaluation.note_max),
"max": fmt_note(notes_ok.max(), note_max=evaluation.note_max),
"moy": fmt_note(notes_ok.mean(), note_max=evaluation.note_max),
}
if not e.is_blocked()
if not evaluation.is_blocked()
else {}
),
"poids": poids,
@ -309,17 +340,25 @@ class BulletinBUT:
url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=e.id,
evaluation_id=evaluation.id,
)
if has_request_context()
else "na"
),
# deprecated (supprimer avant #sco9.7)
"date": e.date_debut.isoformat() if e.date_debut else None,
"heure_debut": (
e.date_debut.time().isoformat("minutes") if e.date_debut else None
"date": (
evaluation.date_debut.isoformat() if evaluation.date_debut else None
),
"heure_debut": (
evaluation.date_debut.time().isoformat("minutes")
if evaluation.date_debut
else None
),
"heure_fin": (
evaluation.date_fin.time().isoformat("minutes")
if evaluation.date_fin
else None
),
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
}
return d
@ -359,7 +398,7 @@ class BulletinBUT:
"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).
(bulletins non publiés sur la passerelle).
"""
if version not in scu.BULLETINS_VERSIONS_BUT:
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
@ -393,7 +432,7 @@ class BulletinBUT:
else:
etud_ues_ids = res.etud_ues_ids(etud.id)
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
nbabsnj, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True
)
@ -408,7 +447,7 @@ class BulletinBUT:
}
if self.prefs["bul_show_abs"]:
semestre_infos["absences"] = {
"injustifie": nbabs - nbabsjust,
"injustifie": nbabsnj,
"total": nbabs,
"metrique": {
"H.": "Heure(s)",
@ -525,7 +564,7 @@ class BulletinBUT:
d["demission"] = ""
# --- Absences
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
_, d["nbabsjust"], d["nbabs"] = self.res.formsemestre.get_abs_count(etud.id)
# --- Decision Jury
infos, _ = sco_bulletins.etud_descr_situation_semestre(

View File

@ -31,6 +31,7 @@ from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc.sco_logos import Logo
from app.scodoc.sco_pdf import PDFLOCK, SU
from app.scodoc.sco_preferences import SemPreferences
from app.scodoc import sco_utils as scu
def make_bulletin_but_court_pdf(
@ -343,9 +344,11 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
for mod in self.bul[mod_type]:
row = [mod, bul[mod_type][mod]["titre"]]
row += [
bul["ues"][ue][mod_type][mod]["moyenne"]
if mod in bul["ues"][ue][mod_type]
else ""
(
bul["ues"][ue][mod_type][mod]["moyenne"]
if mod in bul["ues"][ue][mod_type]
else ""
)
for ue in self.ues_acronyms
]
rows.append(row)
@ -523,7 +526,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
if self.bul["semestre"].get("decision_annee", None):
txt += f"""
Décision saisie le {
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y")
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime(scu.DATE_FMT)
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
<br/>

View File

@ -269,7 +269,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
date_capitalisation = ue.get("date_capitalisation")
if date_capitalisation:
fields_bmr.append(
f"""Capitalisée le {date_capitalisation.strftime("%d/%m/%Y")}"""
f"""Capitalisée le {date_capitalisation.strftime(scu.DATE_FMT)}"""
)
t = {
"titre": " - ".join(fields_bmr),

View File

@ -241,7 +241,7 @@ def bulletin_but_xml_compat(
# --- Absences
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
_, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------

92
app/but/change_refcomp.py Normal file
View File

@ -0,0 +1,92 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Code expérimental: si deux référentiel sont presques identiques
(mêmes compétences, niveaux, parcours)
essaie de changer une formation de référentiel.
"""
from app import clear_scodoc_cache, db
from app.models import (
ApcParcours,
ApcReferentielCompetences,
ApcValidationRCUE,
Formation,
FormSemestreInscription,
UniteEns,
)
from app.scodoc.sco_exceptions import ScoValueError
def formation_change_referentiel(
formation: Formation, new_ref: ApcReferentielCompetences
):
"""Try to change ref."""
if not formation.referentiel_competence:
raise ScoValueError("formation non associée à un référentiel")
if not isinstance(new_ref, ApcReferentielCompetences):
raise ScoValueError("nouveau référentiel invalide")
r = formation.referentiel_competence.map_to_other_referentiel(new_ref)
if isinstance(r, str):
raise ScoValueError(f"référentiels incompatibles: {r}")
parcours_map, competences_map, niveaux_map = r
formation.referentiel_competence = new_ref
db.session.add(formation)
# UEs - Niveaux et UEs - parcours
for ue in formation.ues:
if ue.niveau_competence:
ue.niveau_competence_id = niveaux_map[ue.niveau_competence_id]
db.session.add(ue)
if ue.parcours:
new_list = [ApcParcours.query.get(parcours_map[p.id]) for p in ue.parcours]
ue.parcours.clear()
ue.parcours.extend(new_list)
db.session.add(ue)
# Modules / parcours et app_critiques
for module in formation.modules:
if module.parcours:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in module.parcours
]
module.parcours.clear()
module.parcours.extend(new_list)
db.session.add(module)
if module.app_critiques: # efface les apprentissages critiques
module.app_critiques.clear()
db.session.add(module)
# ApcValidationRCUE
for valid_rcue in ApcValidationRCUE.query.join(
UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id
).filter_by(formation_id=formation.id):
if valid_rcue.parcour:
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
db.session.add(valid_rcue)
for valid_rcue in ApcValidationRCUE.query.join(
UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id
).filter_by(formation_id=formation.id):
if valid_rcue.parcour:
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
db.session.add(valid_rcue)
# FormSemestre / parcours_formsemestre
for formsemestre in formation.formsemestres:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in formsemestre.parcours
]
formsemestre.parcours.clear()
formsemestre.parcours.extend(new_list)
db.session.add(formsemestre)
# FormSemestreInscription.parcour_id
for inscr in FormSemestreInscription.query.filter_by(
formsemestre_id=formsemestre.id
).filter(FormSemestreInscription.parcour_id != None):
if inscr.parcour_id is not None:
inscr.parcour_id = parcours_map[inscr.parcour_id]
#
db.session.commit()
clear_scodoc_cache()

View File

@ -542,9 +542,9 @@ def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int)
for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items():
H.append(
f"""<li>Parcours {parcour_code} : {
len(niveaux)} niveaux sans UEs
<span>
{ ', '.join( f'{niveau.competence.titre} {niveau.ordre}'
len(niveaux)} niveaux sans UEs&nbsp;:
<span class="niveau-nom"><span>
{ '</span>, <span>'.join( f'{niveau.competence.titre} {niveau.ordre}'
for niveau in niveaux
)
}
@ -563,7 +563,8 @@ def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int)
if nb_niveaux_tc != nb_ues_tc:
H.append(
f"""<li>{nb_niveaux_tc} niveaux de compétences de tronc commun,
mais {nb_ues_tc} UEs de tronc commun !</li>"""
mais {nb_ues_tc} UEs de tronc commun ! (c'est normal si
vous avez des UEs différenciées par parcours)</li>"""
)
if H:

View File

@ -10,9 +10,11 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField
from wtforms.validators import DataRequired
class FormationRefCompForm(FlaskForm):
"Choix d'un référentiel"
referentiel_competence = SelectField(
"Choisir parmi les référentiels déjà chargés :"
)
@ -21,6 +23,7 @@ class FormationRefCompForm(FlaskForm):
class RefCompLoadForm(FlaskForm):
"Upload d'un référentiel"
referentiel_standard = SelectField(
"Choisir un référentiel de compétences officiel BUT"
)
@ -47,3 +50,12 @@ class RefCompLoadForm(FlaskForm):
)
return False
return True
class FormationChangeRefCompForm(FlaskForm):
"choix d'un nouveau ref. comp. pour une formation"
object_select = SelectField(
"Choisir le nouveau référentiel", validators=[DataRequired()]
)
submit = SubmitField("Changer le référentiel de la formation")
cancel = SubmitField("Annuler")

View File

@ -23,9 +23,12 @@ from app.models.but_refcomp import (
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
def orebut_import_refcomp(
xml_data: str, dept_id: int, orig_filename=None
) -> ApcReferentielCompetences:
"""Importation XML Orébut
peut lever TypeError ou ScoFormatError
L'objet créé est ajouté et commité.
Résultat: instance de ApcReferentielCompetences
"""
# Vérifie que le même fichier n'a pas déjà été chargé:
@ -41,7 +44,7 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
try:
root = ElementTree.XML(xml_data)
except ElementTree.ParseError as exc:
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}")
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}") from exc
if root.tag != "referentiel_competence":
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
@ -60,7 +63,8 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
db.session.rollback()
raise ScoValueError(
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({
competence.attrib["id"]})
"""
) from exc
ref.competences.append(c)

View File

@ -77,7 +77,7 @@ from app.models.but_refcomp import (
ApcNiveau,
ApcParcours,
)
from app.models import Evaluation, Scolog, ScolarAutorisationInscription
from app.models import Evaluation, ModuleImpl, Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
ApcValidationAnnee,
ApcValidationRCUE,
@ -413,12 +413,12 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Si validée par niveau supérieur:
if self.code_valide == sco_codes.ADSUP:
self.codes.insert(0, sco_codes.ADSUP)
self.explanation = f"<div>{explanation}</div>"
self.explanation = f'<div class="deca-expl">{explanation}</div>'
messages = self.descr_pb_coherence()
if messages:
self.explanation += (
'<div class="warning">'
+ '</div><div class="warning">'.join(messages)
'<div class="warning warning-info">'
+ '</div><div class="warning warning-info">'.join(messages)
+ "</div>"
)
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
@ -796,16 +796,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_pair is not None:
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
def has_notes_en_attente(self) -> bool:
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
res = (
def _get_current_res(self) -> ResultatsSemestreBUT:
"Les res. du semestre d'origine du deca"
return (
self.res_pair
if self.formsemestre_pair
and (self.formsemestre.id == self.formsemestre_pair.id)
else self.res_impair
)
def has_notes_en_attente(self) -> bool:
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
res = self._get_current_res()
return res and self.etud.id in res.get_etudids_attente()
def get_modimpls_attente(self) -> list[ModuleImpl]:
"Liste des ModuleImpl dans lesquels l'étudiant à au moins une note en ATTente"
res = self._get_current_res()
modimpls_results = [
modimpl_result
for modimpl_result in res.modimpls_results.values()
if self.etud.id in modimpl_result.etudids_attente
]
modimpls = [
db.session.get(ModuleImpl, mr.moduleimpl_id) for mr in modimpls_results
]
return sorted(modimpls, key=lambda mi: (mi.module.numero, mi.module.code))
def record_all(self, only_validantes: bool = False) -> bool:
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
et sont donc en mode "automatique".
@ -997,19 +1014,23 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if dec_ue.code_valide not in CODES_UE_VALIDES:
if (
dec_ue.ue_status
and dec_ue.ue_status["was_capitalized"]
and dec_ue.ue_status["is_capitalized"]
):
messages.append(
f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
)
else:
messages.append(
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est (probablement une validation antérieure)"
)
else:
messages.append(
f"L'UE {ue.acronyme} n'a pas décision (???)"
)
# Voyons si on est dispensé de cette ue ?
res = self.res_impair if ue.semestre_idx % 2 else self.res_pair
if res and (self.etud.id, ue.id) in res.dispense_ues:
messages.append(f"Pas (ré)inscrit à l'UE {ue.acronyme}")
return messages
def valide_diplome(self) -> bool:
@ -1514,7 +1535,7 @@ class DecisionsProposeesUE(DecisionsProposees):
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}>"""
} codes={self.codes} explanation="{self.explanation}">"""
def compute_codes(self):
"""Calcul des .codes attribuables et de l'explanation associée"""

View File

@ -55,11 +55,21 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
else:
line_sep = "\n"
rows, titles = pvjury_table_but(formsemestre, line_sep=line_sep)
if fmt.startswith("xls"):
titles.update(
{
"etudid": "etudid",
"code_nip": "nip",
"code_ine": "ine",
"ects_but": "Total ECTS BUT",
"civilite": "Civ.",
"nom": "Nom",
"prenom": "Prénom",
}
)
# Style excel... passages à la ligne sur \n
xls_style_base = sco_excel.excel_make_style()
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
tab = GenTable(
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
caption=title,
@ -116,7 +126,7 @@ def pvjury_table_but(
"pas de référentiel de compétences associé à la formation de ce semestre !"
)
titles = {
"nom": "Code" if anonymous else "Nom",
"nom_pv": "Code" if anonymous else "Nom",
"cursus": "Cursus",
"ects": "ECTS",
"ues": "UE validées",
@ -144,33 +154,47 @@ def pvjury_table_but(
except ScoValueError:
deca = None
ects_but_valides = but_ects_valides(etud, referentiel_competence_id)
row = {
"nom": etud.code_ine or etud.code_nip or etud.id
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
else etud.etat_civil_pv(
line_sep=line_sep, with_paragraph=with_paragraph_nom
"nom_pv": (
etud.code_ine or etud.code_nip or etud.id
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
else etud.etat_civil_pv(
line_sep=line_sep, with_paragraph=with_paragraph_nom
)
),
"_nom_order": etud.sort_key,
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
"_nom_target": url_for(
"_nom_pv_order": etud.sort_key,
"_nom_pv_target_attrs": f'class="etudinfo" id="{etud.id}"',
"_nom_pv_td_attrs": f'id="{etud.id}" class="etudinfo"',
"_nom_pv_target": url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
),
"cursus": _descr_cursus_but(etud),
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}""",
"_ects_xls": deca.ects_annee(),
"ects_but": ects_but_valides,
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
if deca
else "-",
"niveaux": (
deca.descr_niveaux_validation(line_sep=line_sep) if deca else "-"
),
"decision_but": deca.code_valide if deca else "",
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
if deca
else "",
"devenir": (
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
if deca
else ""
),
# pour exports excel seulement:
"civilite": etud.civilite_etat_civil_str,
"nom": etud.nom,
"prenom": etud.prenom_etat_civil or etud.prenom or "",
"etudid": etud.id,
"code_nip": etud.code_nip,
"code_ine": etud.code_ine,
}
if deca.valide_diplome() or not only_diplome:
rows.append(row)
rows.sort(key=lambda x: x["_nom_order"])
rows.sort(key=lambda x: x["_nom_pv_order"])
return rows, titles

View File

@ -16,8 +16,8 @@ from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_validation_auto_but(
formsemestre: FormSemestre, only_adm: bool = True
) -> int:
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False
) -> tuple[int, list[jury_but.DecisionsProposeesAnnee]]:
"""Calcul automatique des décisions de jury sur une "année" BUT.
- N'enregistre jamais de décisions de l'année scolaire précédente, même
@ -27,16 +27,22 @@ def formsemestre_validation_auto_but(
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
Returns:
- En mode normal, (nombre d'étudiants pour lesquels on a enregistré au moins un code, []])
- En mode dry_run, (0, list[DecisionsProposeesAnnee])
"""
if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT")
nb_etud_modif = 0
decas = []
with sco_cache.DeferredSemCacheManager():
for etudid in formsemestre.etuds_inscriptions:
etud = Identite.get_etud(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
nb_etud_modif += deca.record_all(only_validantes=only_adm)
if not dry_run:
nb_etud_modif += deca.record_all(only_validantes=only_adm)
else:
decas.append(deca)
db.session.commit()
ScolarNews.add(
@ -49,4 +55,4 @@ def formsemestre_validation_auto_but(
formsemestre_id=formsemestre.id,
),
)
return nb_etud_modif
return nb_etud_modif, decas

View File

@ -21,8 +21,6 @@ from app.but.jury_but import (
DecisionsProposeesRCUE,
DecisionsProposeesUE,
)
from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
from app.models import (
ApcNiveau,
FormSemestre,
@ -33,11 +31,8 @@ from app.models import (
ScolarFormSemestreValidation,
ScolarNews,
)
from app.models.config import ScoDocSiteConfig
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
@ -109,23 +104,32 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
</div>"""
)
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
# Les UEs à afficher,
# qui
ues_ro = [
# Les UEs à afficher : on regarde si read only et si dispense (non ré-inscription à l'UE)
# tuples (UniteEns, read_only, dispense)
ues_ro_dispense = [
(
ue_impair,
rcue.ue_cur_impair is None,
deca.res_impair
and ue_impair
and (deca.etud.id, ue_impair.id) in deca.res_impair.dispense_ues,
),
(
ue_pair,
rcue.ue_cur_pair is None,
deca.res_pair
and ue_pair
and (deca.etud.id, ue_pair.id) in deca.res_pair.dispense_ues,
),
]
# Ordonne selon les dates des 2 semestres considérés:
if reverse_semestre:
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
ues_ro_dispense[0], ues_ro_dispense[1] = (
ues_ro_dispense[1],
ues_ro_dispense[0],
)
# Colonnes d'UE:
for ue, ue_read_only in ues_ro:
for ue, ue_read_only, ue_dispense in ues_ro_dispense:
if ue:
H.append(
_gen_but_niveau_ue(
@ -134,6 +138,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
disabled=read_only or ue_read_only,
annee_prec=ue_read_only,
niveau_id=ue.niveau_competence.id,
ue_dispense=ue_dispense,
)
)
else:
@ -188,21 +193,30 @@ def _gen_but_niveau_ue(
disabled: bool = False,
annee_prec: bool = False,
niveau_id: int = None,
ue_dispense: bool = False,
) -> str:
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
moy_ue_str = f"""<span class="ue_cap">{
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
scoplement = f"""<div class="scoplement">
<div>
<b>UE {ue.acronyme} capitalisée </b>
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
</span>
</div>
<div>UE en cours
if ue_dispense:
etat_en_cours = """Non (ré)inscrit à cette UE"""
else:
etat_en_cours = f"""UE en cours
{ "sans notes" if np.isnan(dec_ue.moy_ue)
else
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
}
"""
scoplement = f"""<div class="scoplement">
<div>
<b>UE {ue.acronyme} capitalisée </b>
<span>le {dec_ue.ue_status["event_date"].strftime(scu.DATE_FMT)}
</span>
</div>
<div>
{ etat_en_cours }
</div>
</div>
"""
@ -214,7 +228,7 @@ def _gen_but_niveau_ue(
<div>
<b>UE {ue.acronyme} antérieure </b>
<span>validée {dec_ue.validation.code}
le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
le {dec_ue.validation.event_date.strftime(scu.DATE_FMT)}
</span>
</div>
<div>Non reprise dans l'année en cours</div>
@ -232,9 +246,7 @@ def _gen_but_niveau_ue(
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
if dec_ue.code_valide:
date_str = (
f"""enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
à {dec_ue.validation.event_date.strftime("%Hh%M")}
"""
f"""enregistré le {dec_ue.validation.event_date.strftime(scu.DATEATIME_FMT)}"""
if dec_ue.validation and dec_ue.validation.event_date
else ""
)
@ -244,7 +256,13 @@ def _gen_but_niveau_ue(
</div>
"""
else:
scoplement = ""
if dec_ue.ue_status and dec_ue.ue_status["was_capitalized"]:
scoplement = """<div class="scoplement">
UE déjà capitalisée avec résultat moins favorable.
</div>
"""
else:
scoplement = ""
ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
if dec_ue.code_valide is not None and dec_ue.codes:

View File

@ -75,7 +75,7 @@ class RegroupementCoherentUE:
else None
)
# Autres validations pour l'UE paire
# Autres validations pour les UEs paire/impaire
self.validation_ue_best_pair = best_autre_ue_validation(
etud.id,
niveau.id,
@ -101,14 +101,24 @@ class RegroupementCoherentUE:
"résultats formsemestre de l'UE si elle est courante, None sinon"
self.ue_status_impair = None
if self.ue_cur_impair:
# UE courante
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
self.ue_1 = self.ue_cur_impair
self.res_impair = res_impair
self.ue_status_impair = ue_status
elif self.validation_ue_best_impair:
# UE capitalisée
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
self.ue_1 = self.validation_ue_best_impair.ue
if (
res_impair
and self.validation_ue_best_impair
and self.validation_ue_best_impair.ue
):
self.ue_status_impair = res_impair.get_etud_ue_status(
etud.id, self.validation_ue_best_impair.ue.id
)
else:
self.moy_ue_1, self.ue_1 = None, None
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0

View File

@ -23,6 +23,7 @@ from app.models import (
)
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_utils as scu
class ValidationsSemestre(ResultatsCache):
@ -84,7 +85,7 @@ class ValidationsSemestre(ResultatsCache):
"code": decision.code,
"assidu": decision.assidu,
"compense_formsemestre_id": decision.compense_formsemestre_id,
"event_date": decision.event_date.strftime("%d/%m/%Y"),
"event_date": decision.event_date.strftime(scu.DATE_FMT),
}
self.decisions_jury = decisions_jury
@ -107,7 +108,7 @@ class ValidationsSemestre(ResultatsCache):
decisions_jury_ues[decision.etudid][decision.ue.id] = {
"code": decision.code,
"ects": ects, # 0. si UE non validée
"event_date": decision.event_date.strftime("%d/%m/%Y"),
"event_date": decision.event_date.strftime(scu.DATE_FMT),
}
self.decisions_jury_ues = decisions_jury_ues

View File

@ -72,7 +72,15 @@ class ModuleImplResults:
les caches sont gérés par ResultatsSemestre.
"""
def __init__(self, moduleimpl: ModuleImpl):
def __init__(
self, moduleimpl: ModuleImpl, etudids: list[int], etudids_actifs: set[int]
):
"""
Args:
- etudids : liste des etudids, qui donne l'index du dataframe
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
"""
self.moduleimpl_id = moduleimpl.id
self.module_id = moduleimpl.module.id
self.etudids = None
@ -105,14 +113,21 @@ class ModuleImplResults:
"""
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.load_notes(etudids, etudids_actifs)
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"""
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
def load_notes(self): # ré-écriture de df_load_modimpl_notes
def load_notes(
self, etudids: list[int], etudids_actifs: set[int]
): # ré-écriture de df_load_modimpl_notes
"""Charge toutes les notes de toutes les évaluations du module.
Args:
- etudids : liste des etudids, qui donne l'index du dataframe
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
Dataframe evals_notes
colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int)
@ -135,12 +150,12 @@ class ModuleImplResults:
qui ont des notes ATT.
"""
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
self.etudids = self._etudids()
self.etudids = etudids
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
moduleimpl.formsemestre.etudids_actifs
etudids_actifs
)
self.nb_inscrits_module = len(inscrits_module)
@ -148,6 +163,7 @@ class ModuleImplResults:
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = []
self.evaluations_completes_dict = {}
self.etudids_attente = set() # empty
for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi
@ -155,13 +171,13 @@ class ModuleImplResults:
# ou évaluation déclarée "à prise en compte immédiate"
# ou rattrapage, 2eme session, bonus
# ET pas bloquée par date (is_blocked)
is_blocked = evaluation.is_blocked()
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = (
(evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
or (evaluation.publish_incomplete)
or (not etudids_sans_note)
) and not evaluation.is_blocked()
) and not is_blocked
self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
@ -178,16 +194,21 @@ class ModuleImplResults:
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
nb_notes = eval_notes_inscr.notna().sum()
# Etudiants avec notes en attente:
# = ceux avec note ATT
eval_etudids_attente = set(
eval_notes_inscr.iloc[
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
].index
)
if evaluation.publish_incomplete:
# et en "immédiat", tous ceux sans note
eval_etudids_attente |= etudids_sans_note
if is_blocked:
eval_etudids_attente = set()
else:
# Etudiants avec notes en attente:
# = ceux avec note ATT
eval_etudids_attente = set(
eval_notes_inscr.iloc[
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
].index
)
if evaluation.publish_incomplete:
# et en "immédiat", tous ceux sans note
eval_etudids_attente |= etudids_sans_note
# Synthèse pour état du module:
self.etudids_attente |= eval_etudids_attente
self.evaluations_etat[evaluation.id] = EvaluationEtat(
@ -229,17 +250,6 @@ class ModuleImplResults:
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
return eval_df
def _etudids(self):
"""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 db.session.get(
ModuleImpl, self.moduleimpl_id
).formsemestre.inscriptions
]
def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
"""Coefficients des évaluations.
Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.

View File

@ -99,9 +99,11 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
default_poids = {
mod.id: 1.0
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
else 0.0
mod.id: (
1.0
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
else 0.0
)
for mod in modules
}
@ -148,10 +150,12 @@ def df_load_modimpl_coefs(
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
default_poids = {
modimpl.id: 1.0
if (modimpl.module.module_type == ModuleType.STANDARD)
and (modimpl.module.ue.type == UE_SPORT)
else 0.0
modimpl.id: (
1.0
if (modimpl.module.module_type == ModuleType.STANDARD)
and (modimpl.module.ue.type == UE_SPORT)
else 0.0
)
for modimpl in formsemestre.modimpls_sorted
}
@ -200,8 +204,9 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
modimpls_results = {}
modimpls_evals_poids = {}
modimpls_notes = []
etudids, etudids_actifs = formsemestre.etudids_actifs()
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
modimpls_results[modimpl.id] = mod_results

View File

@ -256,8 +256,9 @@ def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]
"""
modimpls_results = {}
modimpls_notes = []
etudids, etudids_actifs = formsemestre.etudids_actifs()
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsClassic(modimpl)
mod_results = moy_mod.ModuleImplResultsClassic(modimpl, etudids, etudids_actifs)
etuds_moy_module = mod_results.compute_module_moy()
modimpls_results[modimpl.id] = mod_results
modimpls_notes.append(etuds_moy_module)

View File

@ -209,6 +209,7 @@ class ResultatsSemestre(ResultatsCache):
"evalcomplete" : bool,
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
"nb_notes" : int, # nb notes d'étudiants inscrits
"nb_attente" : int, # nb de notes en ATTente (même si bloquée)
},
"evaluation_id" : int,
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
@ -236,6 +237,7 @@ class ResultatsSemestre(ResultatsCache):
"etat": {
"blocked": evaluation.is_blocked(),
"evalcomplete": etat.is_complete,
"nb_attente": etat.nb_attente,
"nb_notes": etat.nb_notes,
"last_modif": last_modif,
},
@ -436,7 +438,7 @@ class ResultatsSemestre(ResultatsCache):
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict | None:
"""L'état de l'UE pour cet étudiant.
Result: dict, ou None si l'UE n'est pas dans ce semestre.
Result: dict, ou None si l'UE n'existe pas ou n'est pas dans ce semestre.
{
"is_capitalized": # vrai si la version capitalisée est celle prise en compte (meilleure)
"was_capitalized":# si elle a été capitalisée (meilleure ou pas)
@ -454,6 +456,8 @@ class ResultatsSemestre(ResultatsCache):
}
"""
ue: UniteEns = db.session.get(UniteEns, ue_id)
if not ue:
return None
ue_dict = ue.to_dict()
if ue.type == UE_SPORT:

View File

@ -5,6 +5,7 @@
# See LICENSE
##############################################################################
import datetime
from threading import Thread
from flask import current_app, g
@ -83,9 +84,12 @@ Adresses d'origine:
\n\n"""
+ msg.body
)
now = datetime.datetime.now()
formatted_time = now.strftime("%Y-%m-%d %H:%M:%S") + ".{:03d}".format(
now.microsecond // 1000
)
current_app.logger.info(
f"""email sent to{
f"""[{formatted_time}] email sent to{
' (mode test)' if email_test_mode_address else ''
}: {msg.recipients}
from sender {msg.sender}

View File

@ -59,3 +59,4 @@ def check_taxe_now(taxes):
from app.entreprises import routes
from app.entreprises.activate import activate_module

View File

@ -0,0 +1,31 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Activation du module entreprises
L'affichage du module est contrôlé par la config ScoDocConfig.enable_entreprises
Au moment de l'activation, il est en général utile de proposer de configurer les
permissions de rôles standards: AdminEntreprise UtilisateurEntreprise ObservateurEntreprise
Voir associations dans sco_roles_default
"""
from app.auth.models import Role
from app.models import ScoDocSiteConfig
from app.scodoc.sco_roles_default import SCO_ROLES_ENTREPRISES_DEFAULT
def activate_module(
enable: bool = True, set_default_roles_permission: bool = False
) -> bool:
"""Active le module et en option donne les permissions aux rôles standards.
True si l'état d'activation a changé.
"""
change = ScoDocSiteConfig.enable_entreprises(enable)
if enable and set_default_roles_permission:
Role.reset_roles_permissions(SCO_ROLES_ENTREPRISES_DEFAULT)
return change

View File

@ -0,0 +1,17 @@
"""
Formulaire activation module entreprises
"""
from flask_wtf import FlaskForm
from wtforms.fields.simple import BooleanField, SubmitField
from app.models import ScoDocSiteConfig
class ActivateEntreprisesForm(FlaskForm):
"Formulaire activation module entreprises"
set_default_roles_permission = BooleanField(
"(re)mettre les rôles 'Entreprise' à leurs valeurs par défaut"
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -48,13 +48,15 @@ class BonusConfigurationForm(FlaskForm):
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
],
)
submit_bonus = SubmitField("Valider")
submit_bonus = SubmitField("Enregistrer ce bonus")
cancel_bonus = SubmitField("Annuler", render_kw={"formnovalidate": True})
class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration avancée"
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
disable_passerelle = BooleanField( # disable car par défaut activée
"""cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation."""
)
month_debut_annee_scolaire = SelectField(
label="Mois de début des années scolaires",
description="""Date pivot. En France métropolitaine, août.
@ -83,7 +85,7 @@ class ScoDocConfigurationForm(FlaskForm):
disable_bul_pdf = BooleanField(
"interdire les exports des bulletins en PDF (déconseillé)"
)
submit_scodoc = SubmitField("Valider")
submit_scodoc = SubmitField("Enregistrer ces paramètres")
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
@ -98,6 +100,7 @@ def configuration():
form_scodoc = ScoDocConfigurationForm(
data={
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
"disable_passerelle": ScoDocSiteConfig.is_passerelle_disabled(),
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
@ -123,12 +126,12 @@ def configuration():
flash("Fonction bonus inchangée.")
return redirect(url_for("scodoc.index"))
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
if ScoDocSiteConfig.enable_entreprises(
enabled=form_scodoc.data["enable_entreprises"]
if ScoDocSiteConfig.disable_passerelle(
disabled=form_scodoc.data["disable_passerelle"]
):
flash(
"Module entreprise "
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
"Fonction passerelle "
+ ("cachées" if form_scodoc.data["disable_passerelle"] else "montrées")
)
if ScoDocSiteConfig.set_month_debut_annee_scolaire(
int(form_scodoc.data["month_debut_annee_scolaire"])
@ -171,6 +174,7 @@ def configuration():
return render_template(
"configuration.j2",
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
form_bonus=form_bonus,
form_scodoc=form_scodoc,
scu=scu,

View File

@ -1,6 +1,6 @@
# -*- coding: UTF-8 -*
"""Gestion de l'assiduité (assiduités + justificatifs)
"""
"""Gestion de l'assiduité (assiduités + justificatifs)"""
from datetime import datetime
from flask_login import current_user
@ -21,6 +21,7 @@ from app.scodoc import sco_abs_notification
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
from app.scodoc.sco_utils import (
EtatAssiduite,
EtatJustificatif,
@ -113,9 +114,9 @@ class Assiduite(ScoDocModel):
"entry_date": self.entry_date,
"user_id": None if user is None else user.id, # l'uid
"user_name": None if user is None else user.user_name, # le login
"user_nom_complet": None
if user is None
else user.get_nomcomplet(), # "Marie Dupont"
"user_nom_complet": (
None if user is None else user.get_nomcomplet()
), # "Marie Dupont"
"est_just": self.est_just,
"external_data": self.external_data,
}
@ -336,13 +337,19 @@ class Assiduite(ScoDocModel):
"""
return get_formsemestre_from_data(self.to_dict())
def get_module(self, traduire: bool = False) -> int | str:
"TODO documenter"
def get_module(self, traduire: bool = False) -> Module | str:
"""
Retourne le module associé à l'assiduité
Si traduire est vrai, retourne le titre du module précédé du code
Sinon rentourne l'objet Module ou None
"""
if self.moduleimpl_id is not None:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
mod: Module = Module.query.get(modimpl.module_id)
if traduire:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
mod: Module = Module.query.get(modimpl.module_id)
return f"{mod.code} {mod.titre}"
return mod
elif self.external_data is not None and "module" in self.external_data:
return (
@ -358,7 +365,7 @@ class Assiduite(ScoDocModel):
retourne le texte "saisie le <date> par <User>"
"""
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
date: str = self.entry_date.strftime(scu.DATEATIME_FMT)
utilisateur: str = ""
if self.user is not None:
self.user: User
@ -574,11 +581,7 @@ class Justificatif(ScoDocModel):
db.session.delete(self)
db.session.commit()
# On actualise les assiduités justifiées de l'étudiant concerné
compute_assiduites_justified(
self.etudid,
Justificatif.query.filter_by(etudid=self.etudid).all(),
True,
)
self.dejustifier_assiduites()
def get_fichiers(self) -> tuple[list[str], int]:
"""Renvoie la liste des noms de fichiers justicatifs
@ -600,6 +603,82 @@ class Justificatif(ScoDocModel):
accessible_filenames.append(filename[0])
return accessible_filenames, len(filenames)
def justifier_assiduites(
self,
) -> list[int]:
"""Justifie les assiduités sur la période de validité du justificatif"""
log(f"justifier_assiduites: {self}")
assiduites_justifiees: list[int] = []
if self.etat != EtatJustificatif.VALIDE:
return []
# On récupère les assiduités de l'étudiant sur la période donnée
assiduites: Query = self.etudiant.assiduites.filter(
Assiduite.date_debut >= self.date_debut,
Assiduite.date_fin <= self.date_fin,
Assiduite.etat != EtatAssiduite.PRESENT,
)
# Pour chaque assiduité, on la justifie
for assi in assiduites:
assi.est_just = True
assiduites_justifiees.append(assi.assiduite_id)
db.session.add(assi)
db.session.commit()
return assiduites_justifiees
def dejustifier_assiduites(self) -> list[int]:
"""
Déjustifie les assiduités sur la période du justificatif
"""
assiduites_dejustifiees: list[int] = []
# On récupère les assiduités de l'étudiant sur la période donnée
assiduites: Query = self.etudiant.assiduites.filter(
Assiduite.date_debut >= self.date_debut,
Assiduite.date_fin <= self.date_fin,
Assiduite.etat != EtatAssiduite.PRESENT,
)
assi: Assiduite
for assi in assiduites:
# On récupère les justificatifs qui justifient l'assiduité `assi`
assi_justifs: list[int] = get_justifs_from_date(
self.etudiant.etudid,
assi.date_debut,
assi.date_fin,
long=False,
valid=True,
)
# Si il n'y a pas d'autre justificatif valide, on déjustifie l'assiduité
if len(assi_justifs) == 0 or (
len(assi_justifs) == 1 and assi_justifs[0] == self.justif_id
):
assi.est_just = False
assiduites_dejustifiees.append(assi.assiduite_id)
db.session.add(assi)
db.session.commit()
return assiduites_dejustifiees
def get_assiduites(self) -> Query:
"""
get_assiduites Récupère les assiduités qui sont concernées par le justificatif
(Concernée Justifiée, mais qui sont sur la même période)
Ne prends pas en compte les Présences
Returns:
Query: Les assiduités concernées
"""
assiduites_query = Assiduite.query.filter(
Assiduite.etudid == self.etudid,
Assiduite.date_debut >= self.date_debut,
Assiduite.date_fin <= self.date_fin,
Assiduite.etat != EtatAssiduite.PRESENT,
)
return assiduites_query
def is_period_conflicting(
date_debut: datetime,
@ -623,72 +702,6 @@ def is_period_conflicting(
return count > 0
def compute_assiduites_justified(
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
) -> list[int]:
"""
Args:
etudid (int): l'identifiant de l'étudiant
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False.
Returns:
list[int]: la liste des assiduités qui ont été justifiées.
"""
# TODO à optimiser (car très long avec 40000 assiduités)
# On devrait :
# - récupérer uniquement les assiduités qui sont sur la période des justificatifs donnés
# - Pour chaque assiduité trouvée, il faut récupérer les justificatifs qui la justifie
# - Si au moins un justificatif valide couvre la période de l'assiduité alors on la justifie
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
if justificatifs is None:
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
etudid=etudid
).all()
# On ne prend que les justificatifs valides
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
# On récupère les assiduités de l'étudiant
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
assiduites_justifiees: list[int] = []
for assi in assiduites:
# On ne justifie pas les Présences
if assi.etat == EtatAssiduite.PRESENT:
continue
# On récupère les justificatifs qui justifient l'assiduité `assi`
assi_justificatifs = Justificatif.query.filter(
Justificatif.etudid == assi.etudid,
Justificatif.date_debut <= assi.date_debut,
Justificatif.date_fin >= assi.date_fin,
Justificatif.etat == EtatJustificatif.VALIDE,
).all()
# Si au moins un justificatif possède une période qui couvre l'assiduité
if any(
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
for j in justificatifs + assi_justificatifs
):
# On justifie l'assiduité
# On ajoute l'id de l'assiduité à la liste des assiduités justifiées
assi.est_just = True
assiduites_justifiees.append(assi.assiduite_id)
db.session.add(assi)
elif reset:
# Si le paramètre reset est Vrai alors les assiduités non justifiées
# sont remise en "non justifiée"
assi.est_just = False
db.session.add(assi)
# On valide la session
db.session.commit()
# On renvoie la liste des assiduite_id des assiduités justifiées
return assiduites_justifiees
def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
"""
get_assiduites_justif Récupération des justificatifs d'une assiduité

View File

@ -8,16 +8,19 @@
from datetime import datetime
import functools
from operator import attrgetter
import yaml
from flask import g
from flask_sqlalchemy.query import Query
from sqlalchemy.orm import class_mapper
import sqlalchemy
from app import db
from app import db, log
from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
REFCOMP_EQUIVALENCE_FILENAME = "ressources/referentiels/equivalences.yaml"
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
@ -104,6 +107,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
def get_title(self) -> str:
"Titre affichable"
# utilise type_titre (B.U.T.), spécialité, version
return f"{self.type_titre} {self.specialite} {self.get_version()}"
def get_version(self) -> str:
"La version, normalement sous forme de date iso yyy-mm-dd"
if not self.version_orebut:
@ -124,9 +132,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
"type_departement": self.type_departement,
"type_titre": self.type_titre,
"version_orebut": self.version_orebut,
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else "",
"scodoc_date_loaded": (
self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else ""
),
"scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
@ -234,6 +244,92 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return parcours_info
def equivalents(self) -> set["ApcReferentielCompetences"]:
"""Ensemble des référentiels du même département
qui peuvent être considérés comme "équivalents", au sens
une formation de ce référentiel pourrait changer vers un équivalent,
en ignorant les apprentissages critiques.
Pour cela, il faut avoir le même type, etc et les mêmes compétences,
niveaux et parcours (voir map_to_other_referentiel).
"""
candidats = ApcReferentielCompetences.query.filter_by(
dept_id=self.dept_id
).filter(ApcReferentielCompetences.id != self.id)
return {
referentiel
for referentiel in candidats
if not isinstance(self.map_to_other_referentiel(referentiel), str)
}
def map_to_other_referentiel(
self, other: "ApcReferentielCompetences"
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
"""Build mapping between this referentiel and ref2.
If successful, returns 3 dicts mapping self ids to other ids.
Else return a string, error message.
"""
if self.type_structure != other.type_structure:
return "type_structure mismatch"
if self.type_departement != other.type_departement:
return "type_departement mismatch"
# Table d'équivalences entre refs:
equiv = self._load_config_equivalences()
# mêmes parcours ?
eq_parcours = equiv.get("parcours", {})
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
parcours_by_code_2 = {
eq_parcours.get(p.code, p.code): p for p in other.parcours
}
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
return "parcours mismatch"
parcours_map = {
parcours_by_code_1[eq_parcours.get(code, code)]
.id: parcours_by_code_2[eq_parcours.get(code, code)]
.id
for code in parcours_by_code_1
}
# mêmes compétences ?
competence_by_code_1 = {c.titre: c for c in self.competences}
competence_by_code_2 = {c.titre: c for c in other.competences}
if competence_by_code_1.keys() != competence_by_code_2.keys():
return "competences mismatch"
competences_map = {
competence_by_code_1[titre].id: competence_by_code_2[titre].id
for titre in competence_by_code_1
}
# mêmes niveaux (dans chaque compétence) ?
niveaux_map = {}
for titre in competence_by_code_1:
c1 = competence_by_code_1[titre]
c2 = competence_by_code_2[titre]
niveau_by_attr_1 = {(n.annee, n.ordre, n.libelle): n for n in c1.niveaux}
niveau_by_attr_2 = {(n.annee, n.ordre, n.libelle): n for n in c2.niveaux}
if niveau_by_attr_1.keys() != niveau_by_attr_2.keys():
return f"niveaux mismatch in comp. '{titre}'"
niveaux_map.update(
{
niveau_by_attr_1[a].id: niveau_by_attr_2[a].id
for a in niveau_by_attr_1
}
)
return parcours_map, competences_map, niveaux_map
def _load_config_equivalences(self) -> dict:
"""Load config file ressources/referentiels/equivalences.yaml
used to define equivalences between distinct referentiels
"""
try:
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
doc = yaml.safe_load(f.read())
except FileNotFoundError:
log(f"_load_config_equivalences: {REFCOMP_EQUIVALENCE_FILENAME} not found")
return {}
except yaml.parser.ParserError as exc:
raise ScoValueError(
f"erreur dans le fichier {REFCOMP_EQUIVALENCE_FILENAME}"
) from exc
return doc.get(self.specialite, {})
class ApcCompetence(db.Model, XMLModel):
"Compétence"
@ -374,9 +470,11 @@ class ApcNiveau(db.Model, XMLModel):
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
if with_app_critiques
else {},
"app_critiques": (
{x.code: x.to_dict() for x in self.app_critiques}
if with_app_critiques
else {}
),
}
def to_dict_bul(self):
@ -464,9 +562,9 @@ class ApcNiveau(db.Model, XMLModel):
return []
if competence is None:
parcour_niveaux: list[
ApcParcoursNiveauCompetence
] = annee_parcour.niveaux_competences
parcour_niveaux: list[ApcParcoursNiveauCompetence] = (
annee_parcour.niveaux_competences
)
niveaux: list[ApcNiveau] = [
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
for pn in parcour_niveaux

View File

@ -10,6 +10,7 @@ from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
class ApcValidationRCUE(db.Model):
@ -63,14 +64,13 @@ class ApcValidationRCUE(db.Model):
def __str__(self):
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
self.code} enregistrée le {self.date.strftime(scu.DATE_FMT)}"""
def html(self) -> str:
"description en HTML"
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
<b>{self.code}</b>
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
à {self.date.strftime("%Hh%M")}</em>"""
<em>enregistrée le {self.date.strftime(scu.DATEATIME_FMT)}</em>"""
def annee(self) -> str:
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
@ -164,7 +164,7 @@ class ApcValidationAnnee(db.Model):
def html(self) -> str:
"Affichage html"
date_str = (
f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
if self.date
else "(sans date)"
)

View File

@ -92,6 +92,7 @@ class ScoDocSiteConfig(db.Model):
"INSTITUTION_CITY": str,
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
"enable_entreprises": bool,
"disable_passerelle": bool, # remplace pref. bul_display_publication
"month_debut_annee_scolaire": int,
"month_debut_periode2": int,
"disable_bul_pdf": bool,
@ -244,6 +245,12 @@ class ScoDocSiteConfig(db.Model):
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
return cfg is not None and cfg.value
@classmethod
def is_passerelle_disabled(cls):
"""True si on doit cacher les fonctions passerelle ("oeil")."""
cfg = ScoDocSiteConfig.query.filter_by(name="disable_passerelle").first()
return cfg is not None and cfg.value
@classmethod
def is_user_require_email_institutionnel_enabled(cls) -> bool:
"""True si impose saisie email_institutionnel"""
@ -263,6 +270,11 @@ class ScoDocSiteConfig(db.Model):
"""Active (ou déactive) le module entreprises. True si changement."""
return cls.set("enable_entreprises", "on" if enabled else "")
@classmethod
def disable_passerelle(cls, disabled: bool = True) -> bool:
"""Désactive (ou active) les fonctions liées à la présence d'une passerelle. True si changement."""
return cls.set("disable_passerelle", "on" if disabled else "")
@classmethod
def disable_bul_pdf(cls, enabled=True) -> bool:
"""Interdit (ou autorise) les exports PDF. True si changement."""

View File

@ -297,7 +297,7 @@ class Identite(models.ScoDocModel):
else:
return self.nom
@cached_property
@property
def nomprenom(self, reverse=False) -> str:
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
@ -334,16 +334,14 @@ class Identite(models.ScoDocModel):
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
@cached_property
def sort_key(self) -> tuple:
def sort_key(self) -> str:
"clé pour tris par ordre alphabétique"
# Note: scodoc7 utilisait sco_etud.etud_sort_key, à mettre à jour
# si on modifie cette méthode.
return (
scu.sanitize_string(
self.nom_usuel or self.nom or "", remove_spaces=False
).lower(),
scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(),
)
return scu.sanitize_string(
(self.nom_usuel or self.nom or "") + ";" + (self.prenom or ""),
remove_spaces=False,
).lower()
def get_first_email(self, field="email") -> str:
"Le mail associé à la première adresse de l'étudiant, ou None"
@ -483,7 +481,9 @@ class Identite(models.ScoDocModel):
"code_ine": self.code_ine or "",
"code_nip": self.code_nip or "",
"date_naissance": (
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""
self.date_naissance.strftime(scu.DATE_FMT)
if self.date_naissance
else ""
),
"dept_acronym": self.departement.acronym,
"dept_id": self.dept_id,
@ -542,8 +542,6 @@ class Identite(models.ScoDocModel):
def inscriptions(self) -> list["FormSemestreInscription"]:
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
from app.models.formsemestre import FormSemestre, FormSemestreInscription
return (
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
.filter(
@ -569,8 +567,6 @@ class Identite(models.ScoDocModel):
(il est rare qu'il y en ai plus d'une, mais c'est possible).
Triées par date de début de semestre décroissante (le plus récent en premier).
"""
from app.models.formsemestre import FormSemestre, FormSemestreInscription
return (
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
.filter(
@ -739,7 +735,7 @@ class Identite(models.ScoDocModel):
"""
if with_paragraph:
return f"""{self.etat_civil}{line_sep}{self.code_nip or ""}{line_sep}{self.e} le {
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{
self.date_naissance.strftime(scu.DATE_FMT) if self.date_naissance else ""}{
line_sep}à {self.lieu_naissance or ""}"""
return self.etat_civil
@ -1099,6 +1095,5 @@ class EtudAnnotation(db.Model):
return e
from app.models.formsemestre import FormSemestre
from app.models.modules import Module
from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription

View File

@ -207,7 +207,9 @@ class Evaluation(models.ScoDocModel):
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
# Deprecated
e_dict["jour"] = self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
e_dict["jour"] = (
self.date_debut.strftime(scu.DATE_FMT) if self.date_debut else ""
)
return evaluation_enrich_dict(self, e_dict)
@ -315,10 +317,10 @@ class Evaluation(models.ScoDocModel):
def descr_heure(self) -> str:
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
return f"""à {self.date_debut.strftime("%Hh%M")}"""
return f"""à {self.date_debut.strftime(scu.TIME_FMT)}"""
elif self.date_debut and self.date_fin:
return f"""de {self.date_debut.strftime("%Hh%M")
} à {self.date_fin.strftime("%Hh%M")}"""
return f"""de {self.date_debut.strftime(scu.TIME_FMT)
} à {self.date_fin.strftime(scu.TIME_FMT)}"""
else:
return ""
@ -345,7 +347,7 @@ class Evaluation(models.ScoDocModel):
def _h(dt: datetime.datetime) -> str:
if dt.minute:
return dt.strftime("%Hh%M")
return dt.strftime(scu.TIME_FMT)
return f"{dt.hour}h"
if self.date_fin is None:
@ -539,8 +541,8 @@ class EvaluationUEPoids(db.Model):
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
"""add or convert some fields in an evaluation dict"""
# For ScoDoc7 compat
e_dict["heure_debut"] = e.date_debut.strftime("%Hh%M") if e.date_debut else ""
e_dict["heure_fin"] = e.date_fin.strftime("%Hh%M") if e.date_fin else ""
e_dict["heure_debut"] = e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
e_dict["heure_fin"] = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
# Calcule durée en minutes
e_dict["descrheure"] = e.descr_heure()
@ -614,7 +616,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
):
raise ScoValueError(
f"""La date de début de l'évaluation ({
data["date_debut"].strftime("%d/%m/%Y")
data["date_debut"].strftime(scu.DATE_FMT)
}) n'est pas dans le semestre !""",
dest_url="javascript:history.back();",
)
@ -629,7 +631,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
):
raise ScoValueError(
f"""La date de fin de l'évaluation ({
data["date_fin"].strftime("%d/%m/%Y")
data["date_fin"].strftime(scu.DATE_FMT)
}) n'est pas dans le semestre !""",
dest_url="javascript:history.back();",
)

View File

@ -232,7 +232,9 @@ class ScolarNews(db.Model):
)
# Transforme les URL en URL absolues
base = scu.ScoURL()
base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
@ -249,11 +251,12 @@ class ScolarNews(db.Model):
news_list = cls.last_news(n=n)
if not news_list:
return ""
dept_news_url = url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
H = [
f"""<div class="news"><span class="newstitle"><a href="{
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
f"""<div class="scobox news"><div class="scobox-title"><a href="{
dept_news_url
}">Dernières opérations</a>
</span><ul class="newslist">"""
</div><ul class="newslist">"""
]
for news in news_list:
@ -261,16 +264,22 @@ class ScolarNews(db.Model):
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
class="newstext">{news}</span></li>"""
)
H.append(
f"""<li class="newslist">
<span class="newstext"><a href="{dept_news_url}" class="stdlink">...</a>
</span>
</li>"""
)
H.append("</ul>")
H.append("</ul></div>")
# Informations générales
H.append(
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
f"""<div>
Pour en savoir plus sur ScoDoc voir
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">scodoc.org</a>
</div>
"""
)
H.append("</div>")
return "\n".join(H)

View File

@ -1,5 +1,7 @@
"""ScoDoc 9 models : Formations
"""
from flask import abort, g
from flask_sqlalchemy.query import Query
import app
@ -64,6 +66,21 @@ class Formation(db.Model):
"titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
@classmethod
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
"""Formation ou 404, cherche uniquement dans le département spécifié
ou le courant (g.scodoc_dept)"""
if not isinstance(formation_id, int):
try:
formation_id = int(formation_id)
except (TypeError, ValueError):
abort(404, "formation_id invalide")
if g.scodoc_dept:
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
if dept_id is not None:
return cls.query.filter_by(id=formation_id, dept_id=dept_id).first_or_404()
return cls.query.filter_by(id=formation_id).first_or_404()
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
"""As a dict.
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.

View File

@ -25,6 +25,7 @@ from sqlalchemy import func
import app.scodoc.sco_utils as scu
from app import db, log
from app.auth.models import User
from app import models
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
from app.models.but_refcomp import (
ApcParcours,
@ -54,7 +55,7 @@ from app.scodoc.sco_vdi import ApoEtapeVDI
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
class FormSemestre(db.Model):
class FormSemestre(models.ScoDocModel):
"""Mise en oeuvre d'un semestre de formation"""
__tablename__ = "notes_formsemestre"
@ -84,7 +85,7 @@ class FormSemestre(db.Model):
bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
"ne publie pas le bulletin XML ou JSON"
"ne publie pas le bulletin sur l'API"
block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
@ -191,7 +192,8 @@ class FormSemestre(db.Model):
def get_formsemestre(
cls, formsemestre_id: int | str, dept_id: int = None
) -> "FormSemestre":
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
"""FormSemestre ou 404, cherche uniquement dans le département spécifié
ou le courant (g.scodoc_dept)"""
if not isinstance(formsemestre_id, int):
try:
formsemestre_id = int(formsemestre_id)
@ -206,7 +208,7 @@ class FormSemestre(db.Model):
return cls.query.filter_by(id=formsemestre_id).first_or_404()
def sort_key(self) -> tuple:
"""clé pour tris par ordre alphabétique
"""clé pour tris par ordre de date_debut, le plus ancien en tête
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
return (self.date_debut, self.semestre_id)
@ -222,12 +224,12 @@ class FormSemestre(db.Model):
d["formsemestre_id"] = self.id
d["titre_num"] = self.titre_num()
if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
d["date_debut"] = self.date_debut.strftime(scu.DATE_FMT)
d["date_debut_iso"] = self.date_debut.isoformat()
else:
d["date_debut"] = d["date_debut_iso"] = ""
if self.date_fin:
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
d["date_fin"] = self.date_fin.strftime(scu.DATE_FMT)
d["date_fin_iso"] = self.date_fin.isoformat()
else:
d["date_fin"] = d["date_fin_iso"] = ""
@ -245,19 +247,20 @@ class FormSemestre(db.Model):
def to_dict_api(self):
"""
Un dict avec les informations sur le semestre destiné à l'api
Un dict avec les informations sur le semestre destinées à l'api
"""
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
d.pop("groups_auto_assignment_data", None)
d["annee_scolaire"] = self.annee_scolaire()
d["bul_hide_xml"] = self.bul_hide_xml
if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
d["date_debut"] = self.date_debut.strftime(scu.DATE_FMT)
d["date_debut_iso"] = self.date_debut.isoformat()
else:
d["date_debut"] = d["date_debut_iso"] = ""
if self.date_fin:
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
d["date_fin"] = self.date_fin.strftime(scu.DATE_FMT)
d["date_fin_iso"] = self.date_fin.isoformat()
else:
d["date_fin"] = d["date_fin_iso"] = ""
@ -873,9 +876,9 @@ class FormSemestre(db.Model):
descr_sem += " " + self.modalite
return descr_sem
def get_abs_count(self, etudid):
def get_abs_count(self, etudid) -> tuple[int, int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs, nb abs justifiées)
tuple (nb abs non just, nb abs justifiées, nb abs total)
Utilise un cache.
"""
from app.scodoc import sco_assiduites
@ -933,12 +936,16 @@ class FormSemestre(db.Model):
partitions += [p for p in self.partitions if p.partition_name is None]
return partitions
@cached_property
def etudids_actifs(self) -> set:
"Set des etudids inscrits non démissionnaires et non défaillants"
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
def etudids_actifs(self) -> tuple[list[int], set[int]]:
"""Liste les etudids inscrits (incluant DEM et DEF),
qui ser al'index des dataframes de notes
et donne l'ensemble des inscrits non DEM ni DEF.
"""
return [inscr.etudid for inscr in self.inscriptions], {
ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT
}
@cached_property
@property
def etuds_inscriptions(self) -> dict:
"""Map { etudid : inscription } (incluant DEM et DEF)"""
return {ins.etud.id: ins for ins in self.inscriptions}

View File

@ -409,6 +409,14 @@ class UniteEns(models.ScoDocModel):
Renvoie (True, "") si ok, sinon (False, error_message)
"""
msg = ""
# Safety check
if self.formation.referentiel_competence is None:
return False, "pas de référentiel de compétence"
# Si tous les parcours, aucun (tronc commun)
if {p.id for p in parcours} == {
p.id for p in self.formation.referentiel_competence.parcours
}:
parcours = []
# Le niveau est-il dans tous ces parcours ? Sinon, l'enlève
prev_niveau = self.niveau_competence
if (
@ -424,6 +432,7 @@ class UniteEns(models.ScoDocModel):
self.niveau_competence, parcours
)
if not ok:
self.formation.invalidate_cached_sems()
self.niveau_competence = prev_niveau # restore
return False, error_message

View File

@ -72,7 +72,7 @@ class ScolarFormSemestreValidation(db.Model):
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
} ({self.ue_id}): {self.code}"""
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
self.event_date.strftime("%d/%m/%Y")}"""
self.event_date.strftime(scu.DATE_FMT)}"""
def delete(self):
"Efface cette validation"
@ -113,14 +113,14 @@ class ScolarFormSemestreValidation(db.Model):
if self.ue.parcours else ""}
{("émise par " + link)}
: <b>{self.code}</b>{moyenne}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
le {self.event_date.strftime(scu.DATEATIME_FMT)}
"""
else:
return f"""Validation du semestre S{
self.formsemestre.semestre_id if self.formsemestre else "?"}
{self.formsemestre.html_link_status() if self.formsemestre else ""}
: <b>{self.code}</b>
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
le {self.event_date.strftime(scu.DATEATIME_FMT)}
"""
def ects(self) -> float:
@ -175,8 +175,8 @@ class ScolarAutorisationInscription(db.Model):
)
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
{link}
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
"""
le {self.date.strftime(scu.DATEATIME_FMT)}
"""
@classmethod
def autorise_etud(

View File

@ -155,7 +155,9 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
info_tag = tags_dict["personnalises"][tag]
# Les moyennes générales par UEs
moy_ues_tag = self.compute_moy_ues_tag(info_tag=info_tag, pole=None)
moy_ues_tag = self.compute_moy_ues_tag(
self.ues_inscr_parcours_df, info_tag=info_tag, pole=None
)
# Mémorise les moyennes
self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
tag,
@ -176,7 +178,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
# Ajoute la moyenne générale par ressources
if "moyennes_ue_res_sae" in options and options["moyennes_ue_res_sae"]:
moy_res_gen = self.compute_moy_ues_tag(
info_tag=None, pole=ModuleType.RESSOURCE
self.ues_inscr_parcours_df, info_tag=None, pole=ModuleType.RESSOURCE
)
self.moyennes_tags["ressources"] = pe_moytag.MoyennesTag(
"ressources",
@ -187,7 +189,9 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
# Ajoute la moyenne générale par saes
if "moyennes_ue_res_sae" in options and options["moyennes_ue_res_sae"]:
moy_saes_gen = self.compute_moy_ues_tag(info_tag=None, pole=ModuleType.SAE)
moy_saes_gen = self.compute_moy_ues_tag(
self.ues_inscr_parcours_df, info_tag=None, pole=ModuleType.SAE
)
self.moyennes_tags["saes"] = pe_moytag.MoyennesTag(
"saes",
pe_moytag.CODE_MOY_UE,
@ -264,16 +268,21 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
return capitalisations
def compute_moy_ues_tag(
self, info_tag: dict[int, dict] = None, pole=None
self,
ues_inscr_parcours_df: pd.DataFrame,
info_tag: dict[int, dict] = None,
pole=None,
) -> pd.DataFrame:
"""Calcule la moyenne par UE des étudiants pour un tag donné,
en ayant connaissance des informations sur le tag.
en ayant connaissance des informations sur le tag et des inscriptions des étudiants aux différentes UEs.
info_tag détermine les modules pris en compte :
* si non `None`, seuls les modules rattachés au tag sont pris en compte
* si `None`, tous les modules (quelque soit leur rattachement au tag) sont pris
en compte (sert au calcul de la moyenne générale par ressource ou SAE)
ues_inscr_parcours_df détermine les UEs pour lesquels le calcul d'une moyenne à un sens.
`pole` détermine les modules pris en compte :
* si `pole` vaut `ModuleType.RESSOURCE`, seules les ressources sont prises
@ -282,10 +291,11 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
* si `pole` vaut `None` (ou toute autre valeur),
tous les modules sont pris en compte (moyenne d'UEs)
Les informations sur le tag sont un dictionnaire listant les modimpl_id rattachés au tag,
et pour chacun leur éventuel coefficient de **repondération**.
Args:
ues_inscr_parcours_df: L'inscription aux UEs
Returns:
Le dataframe des moyennes du tag par UE
"""
@ -332,6 +342,9 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
colonnes = [ue.id for ue in self.ues_standards]
moyennes_ues_tag = moyennes_ues_tag[colonnes]
# Applique le masque d'inscription aux UE pour ne conserver que les UE dans lequel l'étudiant est inscrit
moyennes_ues_tag = moyennes_ues_tag[colonnes] * ues_inscr_parcours_df[colonnes]
# Transforme les UEs en acronyme
acronymes = [self.ues_to_acronymes[ue.id] for ue in self.ues_standards]
moyennes_ues_tag.columns = acronymes

View File

@ -106,9 +106,6 @@ class TableTag(object):
Returns:
Le dataframe complet de synthèse
"""
if not self.is_significatif():
return None
# Les tags visés
tags_tries = self.get_all_significant_tags()
if not tags_cibles:
@ -123,6 +120,9 @@ class TableTag(object):
else:
df = pd.DataFrame(index=self.etudids)
if not self.is_significatif():
return df
# Ajout des données par tags
for tag in tags_cibles:
if tag in self.moyennes_tags:

View File

@ -90,7 +90,18 @@ class JuryPE(object):
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
"""
def __init__(self, diplome: int, formsemestre_id_base, options=None):
def __init__(
self,
diplome: int,
formsemestre_id_base,
options={
"moyennes_tags": True,
"moyennes_ue_res_sae": True,
"moyennes_ues_rcues": True,
"min_max_moy": False,
"publipostage": False,
},
):
pe_affichage.pe_start_log()
self.diplome = diplome
"L'année du diplome"
@ -151,7 +162,11 @@ class JuryPE(object):
self._gen_xls_synthese_jury_par_tag(zipfile)
self._gen_html_synthese_par_etudiant(zipfile)
except Exception as e:
raise e
if pe_affichage.PE_DEBUG == True:
raise e
else:
pe_affichage.pe_print(str(e))
# raise e
# et le log
self._add_log_to_zip(zipfile)
@ -222,14 +237,15 @@ class JuryPE(object):
) as writer:
onglets = []
for res_sem_tag in self.ressembuttags.values():
onglet = res_sem_tag.get_repr(verbose=True)
onglet = onglet.replace("Semestre ", "S")
onglets += ["📊" + onglet]
df = res_sem_tag.to_df()
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
# écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True)
if res_sem_tag.is_significatif():
onglet = res_sem_tag.get_repr(verbose=True)
onglet = onglet.replace("Semestre ", "S")
onglets += ["📊" + onglet]
df = res_sem_tag.to_df(options=self.options)
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
# écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True)
pe_affichage.pe_print(
f"--> Export excel de {', '.join(onglets)}", info=True
)
@ -316,7 +332,7 @@ class JuryPE(object):
if sxtag.is_significatif():
onglet = sxtag.get_repr(verbose=False)
onglets += ["📊" + onglet]
df = sxtag.to_df()
df = sxtag.to_df(options=self.options)
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
@ -416,7 +432,7 @@ class JuryPE(object):
onglet = rcs_tag.get_repr(verbose=False)
onglets += ["📊" + onglet]
df = rcs_tag.to_df()
df = rcs_tag.to_df(options=self.options)
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
onglets += ["📊" + onglet]
@ -508,7 +524,7 @@ class JuryPE(object):
if interclass.is_significatif():
onglet = interclass.get_repr()
onglets += ["📊" + onglet]
df = interclass.to_df(cohorte="Promo")
df = interclass.to_df(cohorte="Promo", options=self.options)
# Conversion colonnes en multiindex
df = convert_colonnes_to_multiindex(df)
onglets += [onglet]

View File

@ -48,6 +48,7 @@ from typing import Any
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
from openpyxl.utils import get_column_letter
import reportlab
from reportlab.platypus import Paragraph, Spacer
from reportlab.platypus import Table, KeepInFrame
from reportlab.lib.colors import Color
@ -263,16 +264,16 @@ class GenTable:
colspan_count -= 1
# if colspan_count > 0:
# continue # skip cells after a span
if pdf_mode:
content = row.get(f"_{cid}_pdf", False) or row.get(cid, "")
elif xls_mode:
content = row.get(f"_{cid}_xls", False) or row.get(cid, "")
if pdf_mode and f"_{cid}_pdf" in row:
content = row[f"_{cid}_pdf"]
elif xls_mode and f"_{cid}_xls" in row:
content = row[f"_{cid}_xls"]
else:
content = row.get(cid, "")
# Convert None to empty string ""
content = "" if content is None else content
colspan = row.get("_%s_colspan" % cid, 0)
colspan = row.get(f"_{cid}_colspan", 0)
if colspan > 1:
pdf_style_list.append(
(
@ -676,6 +677,7 @@ class GenTable:
fmt="html",
page_title="",
filename=None,
cssstyles=[],
javascripts=[],
with_html_headers=True,
publish=True,
@ -696,6 +698,7 @@ class GenTable:
H.append(
self.html_header
or html_sco_header.sco_header(
cssstyles=cssstyles,
page_title=page_title,
javascripts=javascripts,
init_qtip=init_qtip,
@ -721,7 +724,7 @@ class GenTable:
)
else:
return pdf_doc
elif fmt == "xls" or fmt == "xlsx": # dans les 2 cas retourne du xlsx
elif fmt in ("xls", "xlsx"): # dans les 2 cas retourne du xlsx
xls = self.excel()
if publish:
return scu.send_file(
@ -730,8 +733,7 @@ class GenTable:
suffix=scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
else:
return xls
return xls
elif fmt == "text":
return self.text()
elif fmt == "csv":
@ -811,7 +813,10 @@ if __name__ == "__main__":
document,
)
)
document.build(objects)
try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = doc.getvalue()
with open("/tmp/gen_table.pdf", "wb") as f:
f.write(data)

View File

@ -30,7 +30,7 @@
import html
from flask import g, render_template
from flask import g, render_template, url_for
from flask import request
from flask_login import current_user
@ -163,7 +163,7 @@ def sco_header(
params = {
"page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar,
"ScoURL": scu.ScoURL(),
"ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
"encoding": scu.SCO_ENCODING,
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
"authuser": current_user.user_name,
@ -179,6 +179,7 @@ def sco_header(
H = [
"""<!DOCTYPE html><html lang="fr">
<!-- ScoDoc legacy -->
<head>
<meta charset="utf-8"/>
<title>%(page_title)s</title>
@ -219,7 +220,7 @@ def sco_header(
<script>
window.onload=function(){{enableTooltips("gtrcontent")}};
const SCO_URL="{scu.ScoURL()}";
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
const SCO_TIMEZONE="{scu.TIME_ZONE}";
</script>"""
)
@ -303,13 +304,16 @@ def sco_header(
# div pour affichage messages temporaires
H.append('<div id="sco_msg" class="head_message"></div>')
#
H.append('<div class="sco-app-content">')
return "".join(H)
def sco_footer():
"""Main HTMl pages footer"""
return (
"""</div><!-- /gtrcontent -->""" + scu.CUSTOM_HTML_FOOTER + """</body></html>"""
"""</div></div><!-- /gtrcontent -->"""
+ scu.CUSTOM_HTML_FOOTER
+ """</body></html>"""
)

View File

@ -102,25 +102,33 @@ def sidebar_common():
<a href="{home_link}" class="sidebar">Accueil</a> <br>
<div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}">{current_user.user_name}</a>
<br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
</div>
{sidebar_dept()}
<h2 class="insidebar">Scolarité</h2>
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br>
<a href="{
url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Semestres</a> <br>
<a href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Formations</a> <br>
"""
]
if current_user.has_permission(Permission.AbsChange):
H.append(
f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduité</a> <br> """
f""" <a href="{
url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Assiduité</a> <br> """
)
if current_user.has_permission(
Permission.UsersAdmin
) or current_user.has_permission(Permission.UsersView):
H.append(
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>"""
f"""<a href="{
url_for("users.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Utilisateurs</a> <br>"""
)
if current_user.has_permission(Permission.EditPreferences):
@ -141,7 +149,9 @@ def sidebar(etudid: int = None):
params = {}
H = [
f"""<div class="sidebar">
f"""
<!-- sidebar py -->
<div class="sidebar">
{ sidebar_common() }
<div class="box-chercheetud">Chercher étudiant:<br>
<form method="get" id="form-chercheetud"
@ -175,18 +185,17 @@ def sidebar(etudid: int = None):
inscription = etud.inscription_courante()
if inscription:
formsemestre = inscription.formsemestre
nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count(
nbabsnj, nbabsjust, _ = sco_assiduites.formsemestre_get_assiduites_count(
etudid, formsemestre
)
nbabsnj = nbabs - nbabsjust
H.append(
f"""<span title="absences du {
formsemestre.date_debut.strftime("%d/%m/%Y")
formsemestre.date_debut.strftime(scu.DATE_FMT)
} au {
formsemestre.date_fin.strftime("%d/%m/%Y")
formsemestre.date_fin.strftime(scu.DATE_FMT)
}">({
sco_preferences.get_preference("assi_metrique", None)})
<br>{nbabsjust:1.0f} J., {nbabsnj:1.0f} N.J.</span>"""
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
)
H.append("<ul>")
if current_user.has_permission(Permission.AbsChange):

View File

@ -12,6 +12,7 @@ import psycopg2.extras
from app import log
from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
from app.scodoc import sco_utils as scu
quote_html = html.escape
@ -460,7 +461,8 @@ def dictfilter(d, fields, filter_nulls=True):
# --- Misc Tools
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None: # XXX deprecated
# XXX deprecated, voir convert_fr_date
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None:
"""Convert date string from french format (or ISO) to ISO.
If null_is_empty (default false), returns "" if no input.
"""
@ -474,7 +476,7 @@ def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None: # XXX deprecated
if not isinstance(dmy, str):
raise ScoValueError(f'Date (j/m/a) invalide: "{dmy}"')
try:
dt = datetime.datetime.strptime(dmy, "%d/%m/%Y")
dt = datetime.datetime.strptime(dmy, scu.DATE_FMT)
except ValueError:
try:
dt = datetime.datetime.fromisoformat(dmy)

View File

@ -34,6 +34,7 @@ from app.models.absences import BilletAbsence
from app.models.etudiants import Identite
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
def query_billets_etud(etudid: int = None, etat: bool = None) -> Query:
@ -89,12 +90,12 @@ def table_billets(
m = " matin"
else:
m = " après-midi"
billet_dict["abs_begin_str"] = billet.abs_begin.strftime("%d/%m/%Y") + m
billet_dict["abs_begin_str"] = billet.abs_begin.strftime(scu.DATE_FMT) + m
if billet.abs_end.hour < 12:
m = " matin"
else:
m = " après-midi"
billet_dict["abs_end_str"] = billet.abs_end.strftime("%d/%m/%Y") + m
billet_dict["abs_end_str"] = billet.abs_end.strftime(scu.DATE_FMT) + m
if billet.etat == 0:
if billet.justified:
billet_dict["etat_str"] = "à traiter"

View File

@ -67,7 +67,7 @@ def abs_notify(etudid: int, date: str | datetime.datetime):
if not formsemestre:
return # non inscrit a la date, pas de notification
nbabs, nbabsjust = sco_assiduites.get_assiduites_count_in_interval(
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count_in_interval(
etudid,
metrique=scu.translate_assiduites_metric(
sco_preferences.get_preference(

View File

@ -515,11 +515,13 @@ class ApoEtud(dict):
# ne trouve pas de semestre impair
self.validation_annee_but = None
return
self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
).first()
self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
).first()
)
self.is_nar = (
self.validation_annee_but and self.validation_annee_but.code == NAR
)
@ -1003,7 +1005,7 @@ def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
def nar_etuds_table(apo_data, nar_etuds):
"""Liste les NAR -> excel table"""
code_etape = apo_data.etape_apogee
today = datetime.datetime.today().strftime("%d/%m/%y")
today = datetime.datetime.today().strftime(scu.DATE_FMT)
rows = []
nar_etuds.sort(key=lambda k: k["nom"])
for e in nar_etuds:

View File

@ -49,11 +49,13 @@
"""
import datetime
import glob
import gzip
import mimetypes
import os
import re
import shutil
import time
import zlib
import chardet
@ -62,7 +64,7 @@ from flask import g
import app.scodoc.sco_utils as scu
from config import Config
from app import log
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_exceptions import ScoException, ScoValueError
class BaseArchiver:
@ -241,11 +243,13 @@ class BaseArchiver:
filename: str,
data: str | bytes,
dept_id: int = None,
compress=False,
):
"""Store data in archive, under given filename.
Filename may be modified (sanitized): return used filename
The file is created or replaced.
data may be str or bytes
If compress, data is gziped and filename suffix ".gz" added.
"""
if isinstance(data, str):
data = data.encode(scu.SCO_ENCODING)
@ -255,8 +259,14 @@ class BaseArchiver:
try:
scu.GSL.acquire()
fname = os.path.join(archive_id, filename)
with open(fname, "wb") as f:
f.write(data)
if compress:
if not fname.endswith(".gz"):
fname += ".gz"
with gzip.open(fname, "wb") as f:
f.write(data)
else:
with open(fname, "wb") as f:
f.write(data)
except FileNotFoundError as exc:
raise ScoValueError(
f"Erreur stockage archive (dossier inexistant, chemin {fname})"
@ -274,8 +284,17 @@ class BaseArchiver:
fname = os.path.join(archive_id, filename)
log(f"reading archive file {fname}")
try:
with open(fname, "rb") as f:
data = f.read()
if fname.endswith(".gz"):
try:
with gzip.open(fname) as f:
data = f.read()
except (OSError, EOFError, zlib.error) as exc:
raise ScoValueError(
f"Erreur lecture archive ({fname} invalide)"
) from exc
else:
with open(fname, "rb") as f:
data = f.read()
except FileNotFoundError as exc:
raise ScoValueError(
f"Erreur lecture archive (inexistant, chemin {fname})"
@ -288,6 +307,8 @@ class BaseArchiver:
"""
archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
data = self.get(archive_id, filename)
if filename.endswith(".gz"):
filename = filename[:-3]
mime = mimetypes.guess_type(filename)[0]
if mime is None:
mime = "application/octet-stream"

View File

@ -68,7 +68,7 @@ PV_ARCHIVER = SemsArchiver()
def do_formsemestre_archive(
formsemestre_id,
formsemestre: FormSemestre,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
description="",
date_jury="",
@ -92,19 +92,18 @@ def do_formsemestre_archive(
raise ScoValueError(
"do_formsemestre_archive: version de bulletin demandée invalide"
)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem_archive_id = formsemestre_id
sem_archive_id = formsemestre.id
archive_id = PV_ARCHIVER.create_obj_archive(
sem_archive_id, description, formsemestre.dept_id
)
date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
date = PV_ARCHIVER.get_archive_date(archive_id).strftime(scu.DATEATIME_FMT)
if not group_ids:
# tous les inscrits du semestre
group_ids = [sco_groups.get_default_group(formsemestre_id)]
group_ids = [sco_groups.get_default_group(formsemestre.id)]
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id
group_ids, formsemestre_id=formsemestre.id
)
groups_filename = "-" + groups_infos.groups_filename
etudids = [m["etudid"] for m in groups_infos.members]
@ -142,19 +141,23 @@ def do_formsemestre_archive(
)
# Bulletins en JSON
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
data = gen_formsemestre_recapcomplet_json(formsemestre.id, xml_with_decisions=True)
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
if data:
PV_ARCHIVER.store(
archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id
archive_id,
"Bulletins.json",
data_js,
dept_id=formsemestre.dept_id,
compress=True,
)
# Décisions de jury, en XLS
if formsemestre.formation.is_apc():
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
response = jury_but_pv.pvjury_page_but(formsemestre.id, fmt="xls")
data = response.get_data()
else: # formations classiques
data = sco_pv_forms.formsemestre_pvjury(
formsemestre_id, fmt="xls", publish=False
formsemestre.id, fmt="xls", publish=False
)
if data:
PV_ARCHIVER.store(
@ -165,7 +168,7 @@ def do_formsemestre_archive(
)
# Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=bul_version
formsemestre.id, version=bul_version
)
if data:
PV_ARCHIVER.store(
@ -173,10 +176,11 @@ def do_formsemestre_archive(
"Bulletins.pdf",
data,
dept_id=formsemestre.dept_id,
compress=True,
)
# Lettres individuelles (PDF):
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id,
formsemestre.id,
etudids=etudids,
date_jury=date_jury,
date_commission=date_commission,
@ -217,7 +221,7 @@ def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
"""Make and store new archive for this formsemestre.
(all students or only selected groups)
"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_edit_pv():
raise ScoPermissionDenied(
dest_url=url_for(
@ -320,7 +324,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
else:
tf[2]["anonymous"] = False
do_formsemestre_archive(
formsemestre_id,
formsemestre,
group_ids=group_ids,
description=tf[2]["description"],
date_jury=tf[2]["date_jury"],
@ -352,7 +356,7 @@ def formsemestre_list_archives(formsemestre_id):
"""Page listing archives"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre_id
L = []
archives_descr = []
for archive_id in PV_ARCHIVER.list_obj_archives(
sem_archive_id, dept_id=formsemestre.dept_id
):
@ -366,28 +370,30 @@ def formsemestre_list_archives(formsemestre_id):
archive_id, dept_id=formsemestre.dept_id
),
}
L.append(a)
archives_descr.append(a)
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
if not L:
if not archives_descr:
H.append("<p>aucune archive enregistrée</p>")
else:
H.append("<ul>")
for a in L:
for a in archives_descr:
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
H.append(
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
% (
a["date"].strftime("%d/%m/%Y %H:%M"),
a["description"],
formsemestre_id,
archive_name,
)
f"""<li>{a["date"].strftime("%d/%m/%Y %H:%M")} : <em>{a["description"]}</em>
(<a href="{ url_for( "notes.formsemestre_delete_archive", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, archive_name=archive_name
)}">supprimer</a>)
<ul>"""
)
for filename in a["content"]:
H.append(
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
% (formsemestre_id, archive_name, filename, filename)
f"""<li><a href="{
url_for( "notes.formsemestre_get_archived_file", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
archive_name=archive_name,
filename=filename
)}">{filename[:-3] if filename.endswith(".gz") else filename}</a></li>"""
)
if not a["content"]:
H.append("<li><em>aucun fichier !</em></li>")
@ -399,7 +405,7 @@ def formsemestre_list_archives(formsemestre_id):
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
"""Send file to client."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre.id
return PV_ARCHIVER.get_archived_file(
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id

View File

@ -17,7 +17,7 @@ from app.models import (
ModuleImplInscription,
ScoDocSiteConfig,
)
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
from app.models.assiduites import Assiduite, Justificatif
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_preferences
from app.scodoc import sco_cache
@ -372,12 +372,38 @@ def str_to_time(time_str: str) -> time:
def get_assiduites_stats(
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
) -> dict[str, int | float]:
"""Compte les assiduités en fonction des filtres"""
# XXX TODO-assiduite : documenter !!!
# Que sont les filtres ? Quelles valeurs ?
# documenter permet de faire moins de bug: qualité du code non satisfaisante.
#
# + on se perd entre les clés en majuscules et en minuscules. Pourquoi
"""
Calcule les statistiques sur les assiduités
(nombre de jours, demi-journées et heures passées,
non justifiées, justifiées et total)
Les filtres :
- etat : filtre les assiduités par leur état
valeur : (absent, present, retard)
- date_debut/date_fin : prend les assiduités qui se trouvent entre les dates
valeur : datetime.datetime
- moduleimpl_id : filtre les assiduités en fonction du moduleimpl_id
valeur : int | None
- formsemestre : prend les assiduités du formsemestre donné
valeur : FormSemestre
- formsemestre_modimpls : prend les assiduités avec un moduleimpl du formsemestre
valeur : FormSemestre
- est_just : filtre les assiduités en fonction de si elles sont justifiées ou non
valeur : bool
- user_id : filtre les assiduités en fonction de l'utilisateur qui les a créées
valeur : int
- split : effectue un comptage par état d'assiduité
valeur : str (du moment que la clé est présente dans filtered)
Les métriques :
- journee : comptage en nombre de journée
- demi : comptage en nombre de demi journée
- heure : comptage en heure
- compte : nombre d'objets
- all : renvoi toute les métriques
"""
if filtered is not None:
deb, fin = None, None
@ -414,34 +440,71 @@ def get_assiduites_stats(
calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites(assiduites)
# S'il n'y a pas de filtre ou que le filtre split n'est pas dans les filtres
if filtered is None or "split" not in filtered:
# On récupère le comptage total
# only_total permet de ne récupérer que le total
count: dict = calculator.to_dict(only_total=True)
# On ne garde que les métriques demandées
for key, val in count.items():
if key in metrics:
output[key] = val
# On renvoie le total si on a rien demandé (ou que metrics == ["all"])
return output if output else count
# Récupération des états
etats: list[str] = (
filtered["etat"].split(",") if "etat" in filtered else scu.EtatAssiduite.all()
)
# être sur que les états sont corrects
etats = [etat for etat in etats if etat.upper() in scu.EtatAssiduite.all()]
# Préparation du dictionnaire de retour avec les valeurs du calcul
count: dict = calculator.to_dict(only_total=False)
# Récupération des états depuis la saisie utilisateur
etats: list[str] = (
filtered["etat"].split(",") if "etat" in filtered else scu.EtatAssiduite.all()
)
for etat in etats:
# TODO-assiduite: on se perd entre les lower et upper.
# Pourquoi EtatAssiduite est en majuscules si tout le reste est en minuscules ?
etat = etat.lower()
# On vérifie que l'état est bien un état d'assiduité
# sinon on passe à l'état suivant
if not scu.EtatAssiduite.contains(etat):
continue
# On récupère le comptage pour chaque état
if etat != "present":
output[etat] = count[etat]
output[etat]["justifie"] = count[etat + "_just"]
output[etat]["non_justifie"] = count[etat + "_non_just"]
else:
output[etat] = count[etat]
output["total"] = count["total"]
# le dictionnaire devrait ressembler à :
# {
# "absent": {
# "journee": 1,
# "demi": 2,
# "heure": 3,
# "compte": 4,
# "justifie": {
# "journee": 1,
# "demi": 2,
# "heure": 3,
# "compte": 4
# },
# "non_justifie": {
# "journee": 1,
# "demi": 2,
# "heure": 3,
# "compte": 4
# }
# },
# ...
# "total": {
# "journee": 1,
# "demi": 2,
# "heure": 3,
# "compte": 4
# }
# }
return output
@ -661,7 +724,7 @@ def create_absence_billet(
db.session.add(justi)
db.session.commit()
compute_assiduites_justified(etud.id, [justi])
justi.justifier_assiduites()
calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites([assiduite_unique])
@ -669,9 +732,9 @@ def create_absence_billet(
# Gestion du cache
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
Utilise un cache.
"""
metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
@ -685,19 +748,19 @@ def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
def formsemestre_get_assiduites_count(
etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
) -> tuple[int, int]:
) -> tuple[int, int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
Utilise un cache.
"""
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
return get_assiduites_count_in_interval(
etudid,
date_debut=scu.localize_datetime(
datetime.combine(formsemestre.date_debut, time(8, 0))
datetime.combine(formsemestre.date_debut, time(0, 0))
),
date_fin=scu.localize_datetime(
datetime.combine(formsemestre.date_fin, time(18, 0))
datetime.combine(formsemestre.date_fin, time(23, 0))
),
metrique=scu.translate_assiduites_metric(metrique),
moduleimpl_id=moduleimpl_id,
@ -712,14 +775,14 @@ def get_assiduites_count_in_interval(
date_debut: datetime = None,
date_fin: datetime = None,
moduleimpl_id: int = None,
):
) -> tuple[int, int, int]:
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
On peut spécifier les dates comme datetime ou iso.
Utilise un cache.
"""
date_debut_iso = date_debut_iso or date_debut.isoformat()
date_fin_iso = date_fin_iso or date_fin.isoformat()
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
r = sco_cache.AbsSemEtudCache.get(key)
@ -744,9 +807,10 @@ def get_assiduites_count_in_interval(
if not ans:
log("warning: get_assiduites_count failed to cache")
nb_abs: dict = r["absent"][metrique]
nb_abs_just: dict = r["absent_just"][metrique]
return (nb_abs, nb_abs_just)
nb_abs: int = r["absent"][metrique]
nb_abs_nj: int = r["absent_non_just"][metrique]
nb_abs_just: int = r["absent_just"][metrique]
return (nb_abs_nj, nb_abs_just, nb_abs)
def invalidate_assiduites_count(etudid: int, sem: dict):

View File

@ -126,7 +126,7 @@ def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict:
# ajoute date courante
t = time.localtime()
C["date_dmy"] = time.strftime("%d/%m/%Y", t)
C["date_dmy"] = time.strftime(scu.DATE_FMT, t)
C["date_iso"] = time.strftime("%Y-%m-%d", t)
return C
@ -196,7 +196,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
# --- Absences
I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
_, I["nbabsjust"], I["nbabs"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
# --- Decision Jury
infos, dpv = etud_descr_situation_semestre(
@ -446,7 +446,8 @@ def _ue_mod_bulletin(
):
"""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).
Result: liste de modules de l'UE avec les infos dans chacun (seulement
ceux l'étudiant est inscrit).
"""
bul_show_mod_rangs = sco_preferences.get_preference(
"bul_show_mod_rangs", formsemestre_id
@ -471,7 +472,7 @@ def _ue_mod_bulletin(
) # peut etre 'NI'
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
if bul_show_abs_modules:
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
mod_abs = [nbabs, nbabsjust]
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
else:

View File

@ -61,7 +61,7 @@ from flask_login import current_user
from app.models import FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError
from app.scodoc.sco_exceptions import NoteProcessError, ScoPDFFormatError
from app import log
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
@ -228,7 +228,15 @@ class BulletinGenerator:
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
)
document.build(story)
try:
document.build(story)
except (
ValueError,
KeyError,
reportlab.platypus.doctemplate.LayoutError,
) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue()
return data

View File

@ -89,7 +89,7 @@ def formsemestre_bulletinetud_published_dict(
version="long",
) -> dict:
"""Dictionnaire representant les informations _publiees_ du bulletin de notes
Utilisé pour JSON, devrait l'être aussi pour XML. (todo)
Utilisé pour JSON des formations classiques (mais pas pour le XML, qui est deprecated).
version:
short (sans les évaluations)
@ -114,10 +114,8 @@ def formsemestre_bulletinetud_published_dict(
if etudid not in nt.identdict:
abort(404, "etudiant non inscrit dans ce semestre")
d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing:
published = True
else:
published = False
published = (not formsemestre.bul_hide_xml) or force_publishing
if xml_nodate:
docdate = ""
else:
@ -171,6 +169,21 @@ def formsemestre_bulletinetud_published_dict(
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
# Il serait préférable de factoriser et d'avoir la même section
# "semestre" que celle des bulletins BUT.
etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True
)
d["semestre"] = {
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
"date_debut": formsemestre.date_debut.isoformat(),
"date_fin": formsemestre.date_fin.isoformat(),
"annee_universitaire": formsemestre.annee_scolaire_str(),
"numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
"groupes": [group.to_dict() for group in etud_groups],
}
ues_stat = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls_dict()
nbetuds = len(nt.etud_moy_gen_ranks)
@ -296,7 +309,7 @@ def formsemestre_bulletinetud_published_dict(
# --- Absences
if prefs["bul_show_abs"]:
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
# --- Décision Jury

View File

@ -352,7 +352,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
H.append(
f"""<p>
<span class="bull_appreciations_date">{
appreciation.date.strftime("%d/%m/%y") if appreciation.date else ""
appreciation.date.strftime(scu.DATE_FMT) if appreciation.date else ""
}</span>
{appreciation.comment_safe()}
<span class="bull_appreciations_link">{mlink}</span>

View File

@ -182,7 +182,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
H.append(
f"""<p>
<span class="bull_appreciations_date">{
appreciation.date.strftime("%d/%m/%Y")
appreciation.date.strftime(scu.DATE_FMT)
if appreciation.date else ""}</span>
{appreciation.comment_safe()}
<span class="bull_appreciations_link">{mlink}</span>

View File

@ -260,7 +260,7 @@ def make_xml_formsemestre_bulletinetud(
numero=str(mod["numero"]),
titre=quote_xml_attr(mod["titre"]),
abbrev=quote_xml_attr(mod["abbrev"]),
code_apogee=quote_xml_attr(mod["code_apogee"])
code_apogee=quote_xml_attr(mod["code_apogee"]),
# ects=ects ects des modules maintenant inutilisés
)
x_ue.append(x_mod)
@ -347,7 +347,7 @@ def make_xml_formsemestre_bulletinetud(
# --- Absences
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
# --- Decision Jury
if (

View File

@ -55,7 +55,6 @@ from flask import g
import app
from app import db, log
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException
@ -174,17 +173,15 @@ class EvaluationCache(ScoDocCache):
@classmethod
def invalidate_all_sems(cls):
"delete all evaluations in current dept from cache"
from app.models.evaluations import Evaluation
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
evaluation_ids = [
x[0]
for x in ndb.SimpleQuery(
"""SELECT e.id
FROM notes_evaluation e, notes_moduleimpl mi, notes_formsemestre s
WHERE s.dept_id=%(dept_id)s
AND s.id = mi.formsemestre_id
AND mi.id = e.moduleimpl_id;
""",
{"dept_id": g.scodoc_dept_id},
)
e.id
for e in Evaluation.query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
]
cls.delete_many(evaluation_ids)

View File

@ -29,7 +29,6 @@
"""
import calendar
import datetime
import html
import time
@ -231,41 +230,41 @@ def next_iso_day(date):
def YearTable(
year,
events=[],
events_by_day: dict[str, list[dict]],
firstmonth=9,
lastmonth=7,
halfday=0,
dayattributes="",
pad_width=8,
):
# Code simplifié en 2024: utilisé seulement pour calendrier évaluations
"""Generate a calendar table
events = list of tuples (date, text, color, href [,halfday])
where date is a string in ISO format (yyyy-mm-dd)
halfday is boolean (true: morning, false: afternoon)
text = text to put in calendar (must be short, 1-5 cars) (optional)
if halfday, generate 2 cells per day (morning, afternoon)
"""
T = [
'<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">'
"""<table id="maincalendar" class="maincalendar"
border="3" cellpadding="1" cellspacing="1" frame="box">"""
]
T.append("<tr>")
month = firstmonth
while 1:
while True:
T.append('<td valign="top">')
T.append(MonthTableHead(month))
T.append(_month_table_head(month))
T.append(
MonthTableBody(
_month_table_body(
month,
year,
events,
halfday,
events_by_day,
dayattributes,
is_work_saturday(),
pad_width=pad_width,
)
)
T.append(MonthTableTail())
T.append("</td>")
T.append(
"""
</table>
</td>"""
)
if month == lastmonth:
break
month = month + 1
@ -323,29 +322,32 @@ WEEKDAYCOLOR = GRAY1
WEEKENDCOLOR = GREEN3
def MonthTableHead(month):
def _month_table_head(month):
color = WHITE
return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
<tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % (
color,
MONTHNAMES_ABREV[month - 1],
)
return f"""<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
<tr bgcolor="{color}">
<td class="calcol" colspan="2" align="center">{MONTHNAMES_ABREV[month - 1]}</td>
</tr>\n"""
def MonthTableTail():
return "</table>\n"
def MonthTableBody(
month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8
):
def _month_table_body(
month,
year,
events_by_day: dict[str, list[dict]],
trattributes="",
work_saturday=False,
) -> str:
"""
events : [event]
event = [ yyyy-mm-dd, legend, href, color, descr ] XXX
"""
firstday, nbdays = calendar.monthrange(year, month)
localtime = time.localtime()
current_weeknum = time.strftime("%U", localtime)
current_year = localtime[0]
T = []
rows = []
# cherche date du lundi de la 1ere semaine de ce mois
monday = ddmmyyyy("1/%d/%d" % (month, year))
monday = ddmmyyyy(f"1/{month}/{year}")
while monday.weekday != 0:
monday = monday.prev()
@ -354,158 +356,51 @@ def MonthTableBody(
else:
weekend = ("S", "D")
if not halfday:
for d in range(1, nbdays + 1):
weeknum = time.strftime(
"%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y")
)
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
if day in weekend:
bgcolor = WEEKENDCOLOR
weekclass = "wkend"
attrs = ""
else:
bgcolor = WEEKDAYCOLOR
weekclass = "wk" + str(monday).replace("/", "_")
attrs = trattributes
color = None
legend = ""
href = ""
descr = ""
# event this day ?
# each event is a tuple (date, text, color, href)
# where date is a string in ISO format (yyyy-mm-dd)
for ev in events:
ev_year = int(ev[0][:4])
ev_month = int(ev[0][5:7])
ev_day = int(ev[0][8:10])
if year == ev_year and month == ev_month and ev_day == d:
if ev[1]:
legend = ev[1]
if ev[2]:
color = ev[2]
if ev[3]:
href = ev[3]
if len(ev) > 4 and ev[4]:
descr = ev[4]
#
cc = []
if color is not None:
cc.append('<td bgcolor="%s" class="calcell">' % color)
else:
cc.append('<td class="calcell">')
for d in range(1, nbdays + 1):
weeknum = time.strftime(
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
)
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
if day in weekend:
bgcolor = WEEKENDCOLOR
weekclass = "wkend"
attrs = ""
else:
bgcolor = WEEKDAYCOLOR
weekclass = "wk" + str(monday).replace("/", "_")
attrs = trattributes
# events this day ?
events = events_by_day.get(f"{year}-{month:02}-{d:02}", [])
color = None
ev_txts = []
for ev in events:
color = ev.get("color")
href = ev.get("href", "")
description = ev.get("description", "")
if href:
href = 'href="%s"' % href
if descr:
descr = 'title="%s"' % html.escape(descr, quote=True)
if href or descr:
cc.append("<a %s %s>" % (href, descr))
if legend or d == 1:
if pad_width is not None:
n = pad_width - len(legend) # pad to 8 cars
if n > 0:
legend = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
href = f'href="{href}"'
if description:
description = f"""title="{html.escape(description, quote=True)}" """
if href or description:
ev_txts.append(f"""<a {href} {description}>{ev.get("title", "")}</a>""")
else:
legend = "&nbsp;" # empty cell
cc.append(legend)
if href or descr:
cc.append("</a>")
cc.append("</td>")
cell = "".join(cc)
if day == "D":
monday = monday.next_day(7)
if (
weeknum == current_weeknum
and current_year == year
and weekclass != "wkend"
):
weekclass += " currentweek"
T.append(
'<tr bgcolor="%s" class="%s" %s><td class="calday">%d%s</td>%s</tr>'
% (bgcolor, weekclass, attrs, d, day, cell)
)
else:
# Calendar with 2 cells / day
for d in range(1, nbdays + 1):
weeknum = time.strftime(
"%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y")
)
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
if day in weekend:
bgcolor = WEEKENDCOLOR
weekclass = "wkend"
attrs = ""
else:
bgcolor = WEEKDAYCOLOR
weekclass = "wk" + str(monday).replace("/", "_")
attrs = trattributes
if (
weeknum == current_weeknum
and current_year == year
and weekclass != "wkend"
):
weeknum += " currentweek"
ev_txts.append(ev.get("title", "&nbsp;"))
#
cc = []
if color is not None:
cc.append(f'<td bgcolor="{color}" class="calcell">')
else:
cc.append('<td class="calcell">')
if day == "D":
monday = monday.next_day(7)
T.append(
'<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>'
% (bgcolor, weekclass, attrs, d, day)
)
cc = []
for morning in (True, False):
color = None
legend = ""
href = ""
descr = ""
for ev in events:
ev_year = int(ev[0][:4])
ev_month = int(ev[0][5:7])
ev_day = int(ev[0][8:10])
if ev[4] is not None:
ev_half = int(ev[4])
else:
ev_half = 0
if (
year == ev_year
and month == ev_month
and ev_day == d
and morning == ev_half
):
if ev[1]:
legend = ev[1]
if ev[2]:
color = ev[2]
if ev[3]:
href = ev[3]
if len(ev) > 5 and ev[5]:
descr = ev[5]
#
if color is not None:
cc.append('<td bgcolor="%s" class="calcell">' % (color))
else:
cc.append('<td class="calcell">')
if href:
href = 'href="%s"' % href
if descr:
descr = 'title="%s"' % html.escape(descr, quote=True)
if href or descr:
cc.append("<a %s %s>" % (href, descr))
if legend or d == 1:
n = 3 - len(legend) # pad to 3 cars
if n > 0:
legend = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else:
legend = "&nbsp;&nbsp;&nbsp;" # empty cell
cc.append(legend)
if href or descr:
cc.append("</a>")
cc.append("</td>\n")
T.append("".join(cc) + "</tr>")
return "\n".join(T)
cc.append(f"{', '.join(ev_txts)}</td>")
cells = "".join(cc)
if day == "D":
monday = monday.next_day(7)
if weeknum == current_weeknum and current_year == year and weekclass != "wkend":
weekclass += " currentweek"
rows.append(
f"""<tr bgcolor="{bgcolor}" class="{weekclass}" {attrs}>
<td class="calday">{d}{day}</td>{cells}</tr>"""
)
return "\n".join(rows)

View File

@ -30,17 +30,18 @@
(coût théorique en heures équivalent TD)
"""
from flask import request
from flask import request, Response
from app.models import FormSemestre
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
import sco_version
def formsemestre_table_estim_cost(
formsemestre_id,
formsemestre: FormSemestre,
n_group_td=1,
n_group_tp=1,
coef_tp=1,
@ -55,8 +56,6 @@ def formsemestre_table_estim_cost(
peut conduire à une sur-estimation du coût s'il y a des modules optionnels
(dans ce cas, retoucher le tableau excel exporté).
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
rows = []
for modimpl in formsemestre.modimpls:
rows.append(
@ -76,14 +75,14 @@ def formsemestre_table_estim_cost(
+ coef_cours * row["heures_cours"]
+ coef_tp * row["heures_tp"]
)
sum_cours = sum([t["heures_cours"] for t in rows])
sum_td = sum([t["heures_td"] for t in rows])
sum_tp = sum([t["heures_tp"] for t in rows])
sum_cours = sum(t["heures_cours"] for t in rows)
sum_td = sum(t["heures_td"] for t in rows)
sum_tp = sum(t["heures_tp"] for t in rows)
sum_heqtd = sum_td + coef_cours * sum_cours + coef_tp * sum_tp
assert abs(sum([t["HeqTD"] for t in rows]) - sum_heqtd) < 0.01, "%s != %s" % (
sum([t["HeqTD"] for t in rows]),
sum_heqtd,
)
# assert abs(sum(t["HeqTD"] for t in rows) - sum_heqtd) < 0.01, "%s != %s" % (
# sum(t["HeqTD"] for t in rows),
# sum_heqtd,
# )
rows.append(
{
@ -117,7 +116,7 @@ def formsemestre_table_estim_cost(
),
rows=rows,
html_sortable=True,
preferences=sco_preferences.SemPreferences(formsemestre_id),
preferences=sco_preferences.SemPreferences(formsemestre.id),
html_class="table_leftalign table_listegroupe",
xls_before_table=[
[formsemestre.titre_annee()],
@ -146,47 +145,45 @@ def formsemestre_table_estim_cost(
return tab
# view
def formsemestre_estim_cost(
formsemestre_id,
n_group_td=1,
n_group_tp=1,
coef_tp=1,
coef_cours=1.5,
formsemestre_id: int,
n_group_td: int | str = 1,
n_group_tp: int | str = 1,
coef_tp: float | str = 1.0,
coef_cours: float | str = 1.5,
fmt="html",
):
) -> str | Response:
"""Page (formulaire) estimation coûts"""
try:
n_group_td = int(n_group_td)
n_group_tp = int(n_group_tp)
coef_tp = float(coef_tp)
coef_cours = float(coef_cours)
except ValueError as exc:
raise ScoValueError("paramètre invalide: utiliser des nombres") from exc
n_group_td = int(n_group_td)
n_group_tp = int(n_group_tp)
coef_tp = float(coef_tp)
coef_cours = float(coef_cours)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
tab = formsemestre_table_estim_cost(
formsemestre_id,
formsemestre,
n_group_td=n_group_td,
n_group_tp=n_group_tp,
coef_tp=coef_tp,
coef_cours=coef_cours,
)
h = """
<form name="f" method="get" action="%s">
<input type="hidden" name="formsemestre_id" value="%s"></input>
Nombre de groupes de TD: <input type="text" name="n_group_td" value="%s" onchange="document.f.submit()"/><br>
Nombre de groupes de TP: <input type="text" name="n_group_tp" value="%s" onchange="document.f.submit()"/>
&nbsp;Coefficient heures TP: <input type="text" name="coef_tp" value="%s" onchange="document.f.submit()"/>
tab.html_before_table = f"""
<form name="f" method="get" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}"></input>
Nombre de groupes de TD: <input type="text" name="n_group_td" value="{n_group_td}" onchange="document.f.submit()"/><br>
Nombre de groupes de TP: <input type="text" name="n_group_tp" value="{n_group_tp}" onchange="document.f.submit()"/>
&nbsp;Coefficient heures TP: <input type="text" name="coef_tp" value="{coef_tp}" onchange="document.f.submit()"/>
<br>
</form>
""" % (
request.base_url,
formsemestre_id,
n_group_td,
n_group_tp,
coef_tp,
)
tab.html_before_table = h
"""
tab.base_url = "%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s" % (
request.base_url,
formsemestre_id,
formsemestre.id,
n_group_td,
n_group_tp,
coef_tp,

View File

@ -71,12 +71,10 @@ def report_debouche_date(start_year=None, fmt="html"):
etudids = get_etudids_with_debouche(start_year)
tab = table_debouche_etudids(etudids, keep_numeric=keep_numeric)
tab.filename = scu.make_filename("debouche_scodoc_%s" % start_year)
tab.origin = (
"Généré par %s le " % sco_version.SCONAME + scu.timedate_human_repr() + ""
)
tab.caption = "Récapitulatif débouchés à partir du 1/1/%s." % start_year
tab.base_url = "%s?start_year=%s" % (request.base_url, start_year)
tab.filename = scu.make_filename(f"debouche_scodoc_{start_year}")
tab.origin = f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}"
tab.caption = f"Récapitulatif débouchés à partir du 1/1/{start_year}."
tab.base_url = f"{request.base_url}?start_year={start_year}"
return tab.make_page(
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
init_qtip=True,
@ -118,7 +116,16 @@ def get_etudids_with_debouche(start_year):
def table_debouche_etudids(etudids, keep_numeric=True):
"""Rapport pour ces étudiants"""
L = []
rows = []
# Recherche les débouchés:
itemsuivi_etuds = {etudid: itemsuivi_list_etud(etudid) for etudid in etudids}
all_tags = set()
for debouche in itemsuivi_etuds.values():
if debouche:
for it in debouche:
all_tags.update(tag.strip() for tag in it["tags"].split(","))
all_tags = tuple(sorted(all_tags))
for etudid in etudids:
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
# retrouve le "dernier" semestre (au sens de la date de fin)
@ -152,25 +159,33 @@ def table_debouche_etudids(etudids, keep_numeric=True):
"sem_ident": "%s %s"
% (last_sem["date_debut_iso"], last_sem["titre"]), # utile pour tris
}
# recherche des débouchés
debouche = itemsuivi_list_etud(etudid) # liste de plusieurs items
debouche = itemsuivi_etuds[etudid] # liste de plusieurs items
if debouche:
row["debouche"] = "<br>".join(
[
str(it["item_date"])
+ " : "
+ it["situation"]
+ " <i>"
+ it["tags"]
+ "</i>"
for it in debouche
]
) #
if keep_numeric: # pour excel:
row["debouche"] = "\n".join(
f"""{it["item_date"]}: {it["situation"]}""" for it in debouche
)
else:
row["debouche"] = "<br>".join(
[
str(it["item_date"])
+ " : "
+ it["situation"]
+ " <i>"
+ it["tags"]
+ "</i>"
for it in debouche
]
)
for it in debouche:
for tag in it["tags"].split(","):
tag = tag.strip()
row[f"tag_{tag}"] = tag
else:
row["debouche"] = "non renseigné"
L.append(row)
L.sort(key=lambda x: x["sem_ident"])
rows.append(row)
rows.sort(key=lambda x: x["sem_ident"])
titles = {
"civilite": "",
@ -184,21 +199,25 @@ def table_debouche_etudids(etudids, keep_numeric=True):
"effectif": "Eff.",
"debouche": "Débouché",
}
columns_ids = [
"semestre",
"semestre_id",
"periode",
"civilite",
"nom",
"prenom",
"moy",
"rang",
"effectif",
"debouche",
]
for tag in all_tags:
titles[f"tag_{tag}"] = tag
columns_ids.append(f"tag_{tag}")
tab = GenTable(
columns_ids=(
"semestre",
"semestre_id",
"periode",
"civilite",
"nom",
"prenom",
"moy",
"rang",
"effectif",
"debouche",
),
columns_ids=columns_ids,
titles=titles,
rows=L,
rows=rows,
# html_col_width='4em',
html_sortable=True,
html_class="table_leftalign table_listegroupe",

View File

@ -3,7 +3,7 @@
##############################################################################
#
# Gestion scolarite IUT
# ScoDoc
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
@ -28,252 +28,171 @@
"""Page accueil département (liste des semestres, etc)
"""
from flask import g
from flask import url_for
from sqlalchemy import desc
from flask import g, url_for, render_template
from flask_login import current_user
from flask_sqlalchemy.query import Query
import app
from app import log
from app.models import ScolarNews
from app.models import FormSemestre, ScolarNews, ScoDocSiteConfig
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
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_preferences
from app.scodoc import sco_users
from app.views import ScoData
def index_html(showcodes=0, showsemtable=0):
def index_html(showcodes=0, showsemtable=0, export_table_formsemestres=False):
"Page accueil département (liste des semestres)"
showcodes = int(showcodes)
showsemtable = int(showsemtable)
H = []
showsemtable = int(showsemtable) or export_table_formsemestres
# News:
H.append(ScolarNews.scolar_news_summary_html())
# Avertissement de mise à jour:
H.append("""<div id="update_warning"></div>""")
# Liste de toutes les sessions:
sems = sco_formsemestre.do_formsemestre_list()
cursems = [] # semestres "courants"
othersems = [] # autres (verrouillés)
# icon image:
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
emptygroupicon = scu.icontag(
"emptygroupicon_img", title="Pas d'inscrits", border="0"
# Liste tous les formsemestres du dept, le plus récent d'abord
current_formsemestres = (
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id, etat=True)
.filter(FormSemestre.modalite != "EXT")
.order_by(desc(FormSemestre.date_debut))
)
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
# Sélection sur l'etat du semestre
for sem in sems:
if sem["etat"] and sem["modalite"] != "EXT":
sem["lockimg"] = ""
cursems.append(sem)
else:
sem["lockimg"] = lockicon
othersems.append(sem)
# Responsable de formation:
sco_formsemestre.sem_set_responsable_name(sem)
if showcodes:
sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
else:
sem["tmpcode"] = ""
# Nombre d'inscrits:
args = {"formsemestre_id": sem["formsemestre_id"]}
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args)
nb = len(ins) # nb etudiants
sem["nb_inscrits"] = nb
if nb > 0:
sem["groupicon"] = groupicon
else:
sem["groupicon"] = emptygroupicon
# S'il n'y a pas d'utilisateurs dans la base, affiche message
if not sco_users.get_users_count(dept=g.scodoc_dept):
H.append(
"""<h2>Aucun utilisateur défini !</h2><p>Pour définir des utilisateurs
<a href="Users">passez par la page Utilisateurs</a>.
<br>
Définissez au moins un utilisateur avec le rôle AdminXXX
(le responsable du département XXX).
</p>
"""
locked_formsemestres = (
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id, etat=False)
.filter(FormSemestre.modalite != "EXT")
.order_by(desc(FormSemestre.date_debut))
)
formsemestres = (
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
.filter(FormSemestre.modalite != "EXT")
.order_by(desc(FormSemestre.date_debut))
)
if showsemtable: # table de tous les formsemestres
table = _sem_table_gt(
formsemestres,
showcodes=showcodes,
fmt="xlsx" if export_table_formsemestres else "html",
)
# Liste des formsemestres "courants"
if cursems:
H.append('<h2 class="listesems">Sessions en cours</h2>')
H.append(_sem_table(cursems))
if export_table_formsemestres:
return table # cas spécial: on renvoie juste cette table
html_table_formsemestres = table.html()
else:
# aucun semestre courant: affiche aide
H.append(
"""<h2 class="listesems">Aucune session en cours !</h2>
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
</p><p>
, en bas de page, suivez le lien
"<em>Mettre en place un nouveau semestre de formation...</em>"
</p>"""
)
html_table_formsemestres = None
if showsemtable:
H.append(
f"""<hr>
<h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
"""
)
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>")
if not showsemtable:
H.append(
f"""<hr>
<p><a class="stdlink" href="{url_for('scolar.index_html',
scodoc_dept=g.scodoc_dept, showsemtable=1)
}">Voir table des semestres (dont {len(othersems)}
verrouillé{'s' if len(othersems) else ''})</a>
</p>"""
)
H.append(
f"""<p>
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
Chercher étape courante:
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form>
</p>"""
current_formsemestres_by_modalite, modalites = (
sco_modalites.group_formsemestres_by_modalite(current_formsemestres)
)
#
H.append(
"""<hr>
<h3>Gestion des étudiants</h3>
<ul>
"""
)
if current_user.has_permission(Permission.EtudInscrit):
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.etudident_create_form", scodoc_dept=g.scodoc_dept)
}">créer <em>un</em> nouvel étudiant</a>
</li>
<li><a class="stdlink" href="{
url_for("scolar.form_students_import_excel", scodoc_dept=g.scodoc_dept)
}">importer de nouveaux étudiants</a>
(<em>ne pas utiliser</em> sauf cas particulier&nbsp;: utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)
</li>
"""
)
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.export_etudiants_courants", scodoc_dept=g.scodoc_dept)
}">exporter tableau des étudiants des semestres en cours</a>
</li>
"""
)
if current_user.has_permission(
Permission.EtudInscrit
) and sco_preferences.get_preference("portal_url"):
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.formsemestre_import_etud_admission",
scodoc_dept=g.scodoc_dept, tous_courants=1)
}">resynchroniser les données étudiants des semestres en cours depuis le portail</a>
</li>
"""
)
H.append("</ul>")
#
if current_user.has_permission(Permission.EditApogee):
H.append(
f"""<hr>
<h3>Exports Apogée</h3>
<ul>
<li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
}">Années scolaires / exports Apogée</a></li>
</ul>
"""
)
#
H.append(
"""<hr>
<h3>Assistance</h3>
<ul>
<li><a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a></li>
</ul>
"""
)
#
return (
html_sco_header.sco_header(
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
)
+ "\n".join(H)
+ html_sco_header.sco_footer()
passerelle_disabled = ScoDocSiteConfig.is_passerelle_disabled()
return render_template(
"scolar/index.j2",
current_user=current_user,
current_formsemestres=current_formsemestres,
current_formsemestres_by_modalite=current_formsemestres_by_modalite,
dept_name=sco_preferences.get_preference("DeptName"),
emptygroupicon=scu.icontag(
"emptygroupicon_img", title="Pas d'inscrits", border="0"
),
formsemestres=formsemestres,
groupicon=scu.icontag("groupicon_img", title="Inscrits", border="0"),
html_table_formsemestres=html_table_formsemestres,
icon_hidden="" if passerelle_disabled else scu.ICON_HIDDEN,
icon_published="" if passerelle_disabled else scu.ICON_PUBLISHED,
locked_formsemestres=locked_formsemestres,
modalites=modalites,
nb_locked=locked_formsemestres.count(),
nb_user_accounts=sco_users.get_users_count(dept=g.scodoc_dept),
page_title=f"ScoDoc {g.scodoc_dept}",
Permission=Permission,
scolar_news_summary=ScolarNews.scolar_news_summary_html(),
showcodes=showcodes,
showsemtable=showsemtable,
sco=ScoData(),
)
def _sem_table(sems):
"""Affiche liste des semestres, utilisée pour semestres en cours"""
tmpl = """<tr class="%(trclass)s">%(tmpcode)s
<td class="semicon">%(lockimg)s <a href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
<td class="datesem">%(mois_debut)s <a title="%(session_id)s">-</a> %(mois_fin)s</td>
<td><a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>
</td>
</tr>
"""
# Liste des semestres, groupés par modalités
sems_by_mod, modalites = sco_modalites.group_sems_by_modalite(sems)
H = ['<table class="listesems">']
for modalite in modalites:
if len(modalites) > 1:
H.append('<tr><th colspan="3">%s</th></tr>' % modalite["titre"])
if sems_by_mod[modalite["modalite"]]:
cur_idx = sems_by_mod[modalite["modalite"]][0]["semestre_id"]
for sem in sems_by_mod[modalite["modalite"]]:
if cur_idx != sem["semestre_id"]:
sem["trclass"] = "firstsem" # separe les groupes de semestres
cur_idx = sem["semestre_id"]
else:
sem["trclass"] = ""
sem["notes_url"] = scu.NotesURL()
H.append(tmpl % sem)
H.append("</table>")
return "\n".join(H)
def _convert_formsemestres_to_dicts(
formsemestres: Query, showcodes: bool, fmt: str = "html"
) -> list[dict]:
""" """
if fmt == "html":
# icon images:
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
emptygroupicon = scu.icontag(
"emptygroupicon_img", title="Pas d'inscrits", border="0"
)
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
else:
groupicon = "X"
emptygroupicon = ""
lockicon = "X"
# génère liste de dict
sems = []
formsemestre: FormSemestre
for formsemestre in formsemestres:
nb_inscrits = len(formsemestre.inscriptions)
formation = formsemestre.formation
sem = {
"anneescolaire": formsemestre.annee_scolaire(),
"anneescolaire_str": formsemestre.annee_scolaire_str(),
"bul_hide_xml": formsemestre.bul_hide_xml,
"dateord": formsemestre.date_debut,
"elt_annee_apo": formsemestre.elt_annee_apo,
"elt_sem_apo": formsemestre.elt_sem_apo,
"etapes_apo_str": formsemestre.etapes_apo_str(),
"formation": f"{formation.acronyme} v{formation.version}",
"_formation_target": url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=formation.id,
semestre_idx=formsemestre.semestre_id,
),
"formsemestre_id": formsemestre.id,
"groupicon": groupicon if nb_inscrits > 0 else emptygroupicon,
"lockimg": "" if formsemestre.etat else lockicon,
"modalite": formsemestre.modalite,
"mois_debut": formsemestre.mois_debut(),
"mois_fin": formsemestre.mois_fin(),
"nb_inscrits": nb_inscrits,
"responsable_name": formsemestre.responsables_str(),
"semestre_id": formsemestre.semestre_id,
"session_id": formsemestre.session_id(),
"titre_num": formsemestre.titre_num(),
"tmpcode": (f"<td><tt>{formsemestre.id}</tt></td>" if showcodes else ""),
}
sems.append(sem)
return sems
def _sem_table_gt(sems, showcodes=False):
"""Nouvelle version de la table des semestres
def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable:
"""Table des semestres
Utilise une datatables.
"""
_style_sems(sems)
columns_ids = (
"lockimg",
sems = _style_sems(
_convert_formsemestres_to_dicts(formsemestres, showcodes, fmt=fmt), fmt=fmt
)
sems.sort(
key=lambda s: (
-s["anneescolaire"],
s["semestre_id"] if s["semestre_id"] > 0 else -s["semestre_id"] * 1000,
s["modalite"],
)
)
columns_ids = ["lockimg"]
if not ScoDocSiteConfig.is_passerelle_disabled():
columns_ids.append("published")
columns_ids += [
"dash_mois_fin",
"semestre_id_n",
"modalite",
#'mois_debut',
"dash_mois_fin",
"titre_resp",
"nb_inscrits",
"formation",
"etapes_apo_str",
"elt_annee_apo",
"elt_sem_apo",
)
]
if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids
columns_ids.insert(0, "formsemestre_id") # prepend
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
if current_user.has_permission(Permission.EditApogee):
@ -282,7 +201,7 @@ def _sem_table_gt(sems, showcodes=False):
titles={
"formsemestre_id": "id",
"semestre_id_n": "S#",
"modalite": "",
"modalite": "" if fmt == "html" else "Modalité",
"mois_debut": "Début",
"dash_mois_fin": "Année",
"titre_resp": "Semestre",
@ -290,6 +209,7 @@ def _sem_table_gt(sems, showcodes=False):
"etapes_apo_str": "Étape Apo.",
"elt_annee_apo": "Elt. année Apo.",
"elt_sem_apo": "Elt. sem. Apo.",
"formation": "Formation",
},
columns_ids=columns_ids,
rows=sems,
@ -309,22 +229,39 @@ def _sem_table_gt(sems, showcodes=False):
return tab
def _style_sems(sems):
def _style_sems(sems: list[dict], fmt="html") -> list[dict]:
"""ajoute quelques attributs de présentation pour la table"""
is_h = fmt == "html"
if is_h:
icon_published = scu.ICON_PUBLISHED
icon_hidden = scu.ICON_HIDDEN
else:
icon_published = "publié"
icon_hidden = "non publié"
for sem in sems:
sem["notes_url"] = scu.NotesURL()
sem["_groupicon_target"] = (
"%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
% sem
status_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=sem["formsemestre_id"],
)
sem["_groupicon_target"] = status_url
sem["_formsemestre_id_class"] = "blacktt"
sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
sem["dash_mois_fin"] = (
(f"""<a title="{sem['session_id']}">{sem['anneescolaire_str']}</a>""")
if is_h
else sem["anneescolaire_str"]
)
sem["_dash_mois_fin_class"] = "datesem"
sem["titre_resp"] = (
"""<a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>"""
% sem
(
f"""<a class="stdlink" href="{status_url}">{sem['titre_num']}</a>
<span class="respsem">({sem['responsable_name']})</span>"""
)
if is_h
else f"""{sem['titre_num']} ({sem["responsable_name"]})"""
)
sem["published"] = icon_hidden if sem["bul_hide_xml"] else icon_published
sem["_css_row_class"] = "css_S%d css_M%s" % (
sem["semestre_id"],
sem["modalite"],
@ -345,6 +282,7 @@ def _style_sems(sems):
sem["_elt_sem_apo_td_attrs"] = (
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
)
return sems
def delete_dept(dept_id: int) -> str:

View File

@ -58,21 +58,20 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
html_sco_header.sco_header(page_title="Suppression d'une formation"),
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
]
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id})
if sems:
formsemestres = formation.formsemestres.all()
if formsemestres:
H.append(
"""<p class="warning">Impossible de supprimer cette formation,
car les sessions suivantes l'utilisent:</p>
<ul>"""
)
for sem in sems:
H.append(
'<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
% sem
)
for formsemestre in formsemestres:
H.append(f"""<li>{formsemestre.html_link_status()}</li>""")
H.append(
'</ul><p><a class="stdlink" href="%s">Revenir</a></p>' % scu.NotesURL()
f"""</ul>
<p><a class="stdlink" href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}">Revenir</a></p>"""
)
else:
if not dialog_confirmed:
@ -85,14 +84,16 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
</p>
""",
OK="Supprimer cette formation",
cancel_url=scu.NotesURL(),
cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
parameters={"formation_id": formation_id},
)
else:
do_formation_delete(formation_id)
H.append(
f"""<p>OK, formation supprimée.</p>
<p><a class="stdlink" href="{scu.NotesURL()}">continuer</a></p>"""
<p><a class="stdlink" href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}">continuer</a></p>"""
)
H.append(html_sco_header.sco_footer())
@ -252,7 +253,7 @@ def formation_edit(formation_id=None, create=False):
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(scu.NotesURL())
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else:
# check unicity : constraint UNIQUE(acronyme,titre,version)
if create:

View File

@ -448,7 +448,7 @@ def module_edit(
(
"titre",
{
"size": 30,
"size": 64,
"explanation": """nom du module. Exemple:
<em>Introduction à la démarche ergonomique</em>""",
},
@ -456,8 +456,8 @@ def module_edit(
(
"abbrev",
{
"size": 20,
"explanation": """nom abrégé (pour bulletins).
"size": 32,
"explanation": """(optionnel) nom abrégé pour bulletins.
Exemple: <em>Intro. à l'ergonomie</em>""",
},
),

View File

@ -298,27 +298,6 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
cursus = formation.get_cursus()
is_apc = cursus.APC_SAE
semestres_indices = list(range(1, cursus.NB_SEM + 1))
H = [
html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"]),
"<h2>" + title,
f" (formation {formation.acronyme}, version {formation.version})</h2>",
"""
<p class="help">Les UE sont des groupes de modules dans une formation donnée,
utilisés pour la validation (on calcule des moyennes par UE et applique des
seuils ("barres")).
</p>
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
Seuls les <em>modules</em> ont des coefficients.
</p>""",
(
f"""
<h4>UE du semestre S{ue.semestre_idx}</h4>
"""
if is_apc and ue
else ""
),
]
ue_types = cursus.ALLOWED_UE_TYPES
ue_types.sort()
@ -489,7 +468,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
if ue and is_apc:
ue_parcours_div = apc_edit_ue.form_ue_choix_parcours(ue)
if ue and ue.modules.count() and ue.semestre_idx is not None:
modules_div = f"""<div id="ue_list_modules">
modules_div = f"""<div class="scobox" id="ue_list_modules">
<div><b>{ue.modules.count()} modules sont rattachés
à cette UE</b> du semestre S{ue.semestre_idx},
elle ne peut donc pas être changée de semestre.</div>
@ -511,18 +490,34 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"""
else:
clone_form = ""
bonus_div = """<div id="bonus_description"></div>"""
ue_div = """<div id="ue_list_code" class="sco_box sco_green_bg"></div>"""
return (
"\n".join(H)
+ tf[1]
+ clone_form
+ ue_parcours_div
+ modules_div
+ bonus_div
+ ue_div
+ html_sco_header.sco_footer()
)
return f"""
{html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"])}
<h2>{title}, (formation {formation.acronyme}, version {formation.version})</h2>
<p class="help">Les UEs sont des groupes de modules dans une formation donnée,
utilisés pour la validation (on calcule des moyennes par UE et applique des
seuils ("barres")).
</p>
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
Seuls les <em>modules</em> ont des coefficients.
</p>
<div class="scobox">
<div class="scobox-title">
Édition de l'UE {('du semestre S'+str(ue.semestre_idx)) if is_apc and ue else ''}
</div>
{tf[1]}
</div>
{clone_form}
{ue_parcours_div}
{modules_div}
<div id="bonus_description"></div>
<div id="ue_list_code" class="sco_box sco_green_bg"></div>
{html_sco_header.sco_footer()}
"""
elif tf[0] == 1:
if create:
if not tf[2]["ue_code"]:
@ -756,7 +751,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
H = [
html_sco_header.sco_header(
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/ue_table.css"],
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ [
"libjs/jinplace-1.2.1.min.js",
@ -842,8 +837,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<a href="{url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
class="stdlink">
{formation.referentiel_competence.type_titre}
{formation.referentiel_competence.specialite_long}
{formation.referentiel_competence.get_title()}
</a>&nbsp;"""
msg_refcomp = "changer"
H.append(f"""<ul><li>{descr_refcomp}""")
@ -1170,14 +1164,17 @@ def _ue_table_ues(
if has_perm_change:
H.append(
f"""<a class="stdlink" href="{
url_for("notes.ue_set_internal", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
url_for("notes.ue_set_internal",
scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
}">transformer en UE ordinaire</a>&nbsp;"""
)
H.append("</span>")
ue_editable = editable and not ue_is_locked(ue["ue_id"])
if ue_editable:
H.append(
'<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % ue
f"""<a class="stdlink" href="{
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
}">modifier</a>"""
)
else:
H.append('<span class="locked">[verrouillé]</span>')

View File

@ -478,11 +478,11 @@ def convert_ics(
"heure_deb": event.decoded("dtstart")
.replace(tzinfo=timezone.utc)
.astimezone(tz=None)
.strftime("%H:%M"),
.strftime(scu.TIME_FMT),
"heure_fin": event.decoded("dtend")
.replace(tzinfo=timezone.utc)
.astimezone(tz=None)
.strftime("%H:%M"),
.strftime(scu.TIME_FMT),
"jour": event.decoded("dtstart").date().isoformat(),
"start": event.decoded("dtstart").isoformat(),
"end": event.decoded("dtend").isoformat(),

View File

@ -452,7 +452,7 @@ def table_apo_csv_list(semset):
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
t["filename"] = apo_data.apo_csv.titles["apoC_Fichier_Exp"]
t["nb_etuds"] = len(apo_data.etuds)
t["date_str"] = t["date"].strftime("%d/%m/%Y à %H:%M")
t["date_str"] = t["date"].strftime(scu.DATEATIME_FMT)
view_link = url_for(
"notes.view_apo_csv",
scodoc_dept=g.scodoc_dept,

View File

@ -122,16 +122,14 @@ def format_pays(s):
return ""
def etud_sort_key(etud: dict) -> tuple:
def etud_sort_key(etud: dict) -> str:
"""Clé de tri pour les étudiants représentés par des dict (anciens codes).
Equivalent moderne: identite.sort_key
"""
return (
scu.sanitize_string(
etud.get("nom_usuel") or etud["nom"] or "", remove_spaces=False
).lower(),
scu.sanitize_string(etud["prenom"] or "", remove_spaces=False).lower(),
)
return scu.sanitize_string(
(etud.get("nom_usuel") or etud["nom"] or "") + ";" + (etud["prenom"] or ""),
remove_spaces=False,
).lower()
_identiteEditor = ndb.EditableTable(

View File

@ -135,7 +135,7 @@ def evaluation_check_absences_html(
f"""<h2 class="eval_check_absences">{
evaluation.description or "évaluation"
} du {
evaluation.date_debut.strftime("%d/%m/%Y") if evaluation.date_debut else ""
evaluation.date_debut.strftime(scu.DATE_FMT) if evaluation.date_debut else ""
} """
]
if (

View File

@ -90,7 +90,7 @@ def evaluation_create_form(
raise ValueError("missing moduleimpl_id parameter")
numeros = [(e.numero or 0) for e in modimpl.evaluations]
initvalues = {
"jour": time.strftime("%d/%m/%Y", time.localtime()),
"jour": time.strftime(scu.DATE_FMT, time.localtime()),
"note_max": 20,
"numero": (max(numeros) + 1) if numeros else 0,
"publish_incomplete": is_malus,
@ -144,7 +144,7 @@ def evaluation_create_form(
if edit:
initvalues["blocked"] = evaluation.is_blocked()
initvalues["blocked_until"] = (
evaluation.blocked_until.strftime("%d/%m/%Y")
evaluation.blocked_until.strftime(scu.DATE_FMT)
if evaluation.blocked_until
and evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
else ""
@ -231,7 +231,13 @@ def evaluation_create_form(
{
"input_type": "boolcheckbox",
"title": "Prise en compte immédiate",
"explanation": "notes utilisées même si incomplètes",
"explanation": """notes utilisées même si incomplètes (dangereux,
à n'utiliser que dans des cas particuliers
<a target="_blank" rel="noopener noreferrer"
href="https://scodoc.org/Evaluation/#pourquoi-eviter-dutiliser-prise-en-compte-immediate"
>voir la documentation</a>
)
""",
},
),
(
@ -374,13 +380,7 @@ def evaluation_create_form(
args = tf[2]
# modifie le codage des dates
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
if args.get("jour"):
try:
date_debut = datetime.datetime.strptime(args["jour"], "%d/%m/%Y")
except ValueError as exc:
raise ScoValueError("Date (j/m/a) invalide") from exc
else:
date_debut = None
date_debut = scu.convert_fr_date(args["jour"]) if args.get("jour") else None
args["date_debut"] = date_debut
args["date_fin"] = date_debut # même jour
args.pop("jour", None)
@ -405,7 +405,7 @@ def evaluation_create_form(
if args.get("blocked_until"):
try:
args["blocked_until"] = datetime.datetime.strptime(
args["blocked_until"], "%d/%m/%Y"
args["blocked_until"], scu.DATE_FMT
)
except ValueError as exc:
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc

View File

@ -126,13 +126,15 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
evaluation_id=evaluation_id,
),
"_titre_target_attrs": 'class="discretelink"',
"date": e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "",
"date": e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "",
"_date_order": e.date_debut.isoformat() if e.date_debut 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"',
"_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),

View File

@ -279,11 +279,18 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
nb_eval_completes (= prises en compte)
nb_evals_en_cours (= avec des notes, mais pas complete)
nb_evals_vides (= sans aucune note)
nb_evals_attente (= avec des notes en ATTente et pas bloquée)
date derniere modif
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
"""
nb_evals_completes, nb_evals_en_cours, nb_evals_vides, nb_evals_blocked = 0, 0, 0, 0
(
nb_evals_completes,
nb_evals_en_cours,
nb_evals_vides,
nb_evals_blocked,
nb_evals_attente,
) = (0, 0, 0, 0, 0)
dates = []
for e in etat_evals:
if e["etat"]["blocked"]:
@ -294,6 +301,8 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
nb_evals_vides += 1
elif not e["etat"]["blocked"]:
nb_evals_en_cours += 1
if e["etat"]["nb_attente"] and not e["etat"]["blocked"]:
nb_evals_attente += 1
last_modif = e["etat"]["last_modif"]
if last_modif is not None:
dates.append(e["etat"]["last_modif"])
@ -303,6 +312,7 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
return {
"nb_evals": len(etat_evals),
"nb_evals_attente": nb_evals_attente,
"nb_evals_blocked": nb_evals_blocked,
"nb_evals_completes": nb_evals_completes,
"nb_evals_en_cours": nb_evals_en_cours,
@ -350,6 +360,7 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
return etat
# View
def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
@ -363,20 +374,17 @@ def formsemestre_evaluations_cal(formsemestre_id):
color_futur = "#70E0FF"
year = formsemestre.annee_scolaire()
events = {} # (day, halfday) : event
events_by_day = collections.defaultdict(list) # date_iso : event
for e in evaluations:
if e.date_debut is None:
continue # éval. sans date
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
if e.date_debut == e.date_fin:
heure_debut_txt, heure_fin_txt = "?", "?"
heure_debut_txt, heure_fin_txt = "", ""
else:
heure_debut_txt = e.date_debut.strftime("%Hh%M") if e.date_debut else "?"
heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?"
description = f"""{
e.moduleimpl.module.titre
}, de {heure_debut_txt} à {heure_fin_txt}"""
heure_debut_txt = (
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
)
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
# Etat (notes completes) de l'évaluation:
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
@ -386,28 +394,27 @@ def formsemestre_evaluations_cal(formsemestre_id):
color = color_incomplete
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
color = color_futur
href = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl_id,
)
day = e.date_debut.date().isoformat() # yyyy-mm-dd
event = events.get(day)
if not event:
events[day] = [day, txt, color, href, description, e.moduleimpl]
else:
if event[-1].id != e.moduleimpl.id:
# plusieurs evals de modules differents a la meme date
event[1] += ", " + txt
event[4] += ", " + description
if color == color_incomplete:
event[2] = color_incomplete
if color == color_futur:
event[2] = color_futur
event = {
"color": color,
"date_iso": day,
"title": e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval.",
"description": f"""{e.description or e.moduleimpl.module.titre_str()}"""
+ (
f""" de {heure_debut_txt} à {heure_fin_txt}"""
if heure_debut_txt
else ""
),
"href": url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl_id,
),
"modimpl": e.moduleimpl,
}
events_by_day[day].append(event)
cal_html = sco_cal.YearTable(
year, events=list(events.values()), halfday=False, pad_width=None
)
cal_html = sco_cal.YearTable(year, events_by_day=events_by_day)
return f"""
{
@ -516,7 +523,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
"date_first_complete": date_first_complete,
"delai_correction": delai_correction,
"jour": (
e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "sans date"
e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "sans date"
),
"_jour_target": url_for(
"notes.evaluation_listenotes",
@ -529,7 +536,9 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl.id,
),
"module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre,
"module_titre": e.moduleimpl.module.abbrev
or e.moduleimpl.module.titre
or "",
"responsable_id": e.moduleimpl.responsable_id,
"responsable_nomplogin": sco_users.user_info(
e.moduleimpl.responsable_id

View File

@ -72,7 +72,7 @@ def xldate_as_datetime(xldate, datemode=0):
Peut lever une ValueError
"""
try:
return datetime.datetime.strptime(xldate, "%d/%m/%Y")
return datetime.datetime.strptime(xldate, scu.DATE_FMT)
except:
return openpyxl.utils.datetime.from_ISO8601(xldate)

View File

@ -103,7 +103,7 @@ class ScoPDFFormatError(ScoValueError):
super().__init__(
f"""Erreur dans un format pdf:
<p>{msg}</p>
<p>Vérifiez les paramètres (polices de caractères, balisage)
<p>Vérifiez les paramètres (polices de caractères, balisage, réglages bulletins...)
dans les paramètres ou préférences.
</p>
""",

View File

@ -489,9 +489,10 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
return formation.id, modules_old2new, ues_old2new
def formation_list_table() -> GenTable:
def formation_list_table(detail: bool) -> GenTable:
"""List formation, grouped by titre and sorted by versions
and listing associated semestres
and listing associated semestres.
If detail, add column with more details.
returns a table
"""
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
@ -507,6 +508,7 @@ def formation_list_table() -> GenTable:
)
editable = current_user.has_permission(Permission.EditFormation)
can_implement = current_user.has_permission(Permission.EditFormSemestre)
# Traduit/ajoute des champs à afficher:
rows = []
@ -527,6 +529,21 @@ def formation_list_table() -> GenTable:
"_titre_id": f"""titre-{acronyme_no_spaces}""",
"version": formation.version or 0,
"commentaire": formation.commentaire or "",
"referentiel": (
f"""{formation.referentiel_competence.specialite} {
formation.referentiel_competence.get_version()}"""
if formation.referentiel_competence
else ""
),
"_referentiel_target": (
url_for(
"notes.refcomp_show",
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id,
)
if formation.referentiel_competence
else ""
),
}
# Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by(
@ -541,20 +558,28 @@ def formation_list_table() -> GenTable:
)}">{s.session_id()}</a>"""
for s in row["formsemestres"]
]
+ [
f"""<a class="stdlink" id="add-semestre-{
formation.acronyme.lower().replace(" ", "-")}"
+ (
[
f"""<a class="stdlink"
href="{ url_for("notes.formsemestre_createwithmodules",
scodoc_dept=g.scodoc_dept, formation_id=formation.id, semestre_id=1
)
}">ajouter</a>
"""
]
)
if row["formsemestres"]:
row["date_fin_dernier_sem"] = (
row["formsemestres"][-1].date_fin.isoformat(),
]
if can_implement
else []
)
)
# Répartition des UEs dans les semestres
# utilise pour voir si la formation couvre tous les semestres
row["semestres_ues"] = ", ".join(
"S" + str(x if (x is not None and x > 0) else "-")
for x in sorted({(ue.semestre_idx or 0) for ue in formation.ues})
)
# Date surtout utilisées pour le tri:
if row["formsemestres"]:
row["date_fin_dernier_sem"] = row["formsemestres"][-1].date_fin.isoformat()
row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year
else:
row["date_fin_dernier_sem"] = ""
@ -603,9 +628,12 @@ def formation_list_table() -> GenTable:
"formation_code",
"version",
"titre",
"referentiel",
"commentaire",
"sems_list_txt",
)
if detail:
columns_ids += ("annee_dernier_sem", "semestres_ues")
titles = {
"buttons": "",
"commentaire": "Commentaire",
@ -615,6 +643,10 @@ def formation_list_table() -> GenTable:
"version": "Version",
"formation_code": "Code",
"sems_list_txt": "Semestres",
"referentiel": "Réf.",
"date_fin_dernier_sem": "Fin dernier sem.",
"annee_dernier_sem": "Année dernier sem.",
"semestres_ues": "Semestres avec UEs",
}
return GenTable(
columns_ids=columns_ids,
@ -627,7 +659,7 @@ def formation_list_table() -> GenTable:
html_class="formation_list_table table_leftalign",
html_with_td_classes=True,
html_sortable=True,
base_url=f"{request.base_url}",
base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),

View File

@ -494,7 +494,7 @@ def table_formsemestres(
):
"""Une table presentant des semestres"""
for sem in sems:
sem_set_responsable_name(sem)
sem_set_responsable_name(sem) # TODO utiliser formsemestre.responsables_str()
sem["_titre_num_target"] = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,

View File

@ -573,7 +573,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"input_type": "checkbox",
"title": "Publication",
"allowed_values": ["X"],
"explanation": "publier le bulletin sur le portail étudiants",
"explanation": "publier le bulletin sur la passerelle étudiants",
"labels": [""],
},
),
@ -812,14 +812,18 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
)
msg = ""
if tf[0] == 1:
# check dates
if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
msg = '<ul class="tf-msg"><li class="tf-msg">Dates de début et fin incompatibles !</li></ul>'
# convert and check dates
tf[2]["date_debut"] = scu.convert_fr_date(tf[2]["date_debut"])
tf[2]["date_fin"] = scu.convert_fr_date(tf[2]["date_fin"])
if tf[2]["date_debut"] > tf[2]["date_fin"]:
msg = """<ul class="tf-msg">
<li class="tf-msg">Dates de début et fin incompatibles !</li>
</ul>"""
if (
sco_preferences.get_preference("always_require_apo_sem_codes")
and not any(
[tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)]
tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)
)
# n'impose pas d'Apo pour les sem. extérieurs
and ((formsemestre is None) or formsemestre.modalite != "EXT")
@ -1427,18 +1431,25 @@ Ceci n'est possible que si :
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
"""Delete a formsemestre (confirmation)"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Confirmation dialog
if not dialog_confirmed:
return scu.confirm_dialog(
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""",
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2>
<p>(opération irréversible)</p>
""",
dest_url="",
cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
parameters={"formsemestre_id": formsemestre_id},
cancel_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
parameters={"formsemestre_id": formsemestre.id},
)
# Bon, s'il le faut...
do_formsemestre_delete(formsemestre_id)
do_formsemestre_delete(formsemestre.id)
flash("Semestre supprimé !")
return flask.redirect(scu.ScoURL())
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
def formsemestre_has_decisions_or_compensations(

View File

@ -521,7 +521,7 @@ def _record_ue_validations_and_coefs(
coef = _convert_field_to_float(coef)
if coef == "" or coef is False:
coef = None
now_dmy = time.strftime("%d/%m/%Y")
now_dmy = time.strftime(scu.DATE_FMT)
log(
f"_record_ue_validations_and_coefs: {formsemestre.id} etudid={etud.id} ue_id={ue.id} moy_ue={note} ue_coef={coef}"
)

View File

@ -106,7 +106,7 @@ def do_formsemestre_inscription_create(args, method=None):
cnx,
args={
"etudid": args["etudid"],
"event_date": time.strftime("%d/%m/%Y"),
"event_date": time.strftime(scu.DATE_FMT),
"formsemestre_id": args["formsemestre_id"],
"event_type": "INSCRIPTION",
},

View File

@ -25,8 +25,7 @@
#
##############################################################################
"""Tableau de bord semestre
"""
"""Tableau de bord semestre"""
import datetime
@ -65,14 +64,11 @@ from app.scodoc import sco_archives_formsemestre
from app.scodoc import sco_bulletins
from app.scodoc import codes_cursus
from app.scodoc import sco_compute_moy
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations
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_preferences
from app.scodoc import sco_users
from app.scodoc.gen_tables import GenTable
@ -147,8 +143,10 @@ def _build_menu_stats(formsemestre: FormSemestre):
]
def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
"""HTML to render menubar"""
if formsemestre is None:
return ""
formsemestre_id = formsemestre.id
if formsemestre.etat:
change_lock_msg = "Verrouiller"
@ -636,7 +634,7 @@ def formsemestre_description_table(
"UE": modimpl.module.ue.acronyme,
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
"Code": modimpl.module.code or "",
"Module": modimpl.module.abbrev or modimpl.module.titre,
"Module": modimpl.module.abbrev or modimpl.module.titre or "",
"_Module_class": "scotext",
"Inscrits": mod_nb_inscrits,
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],
@ -693,7 +691,7 @@ def formsemestre_description_table(
)
e["_date_evaluation_order"] = e["jour"].isoformat()
e["date_evaluation"] = (
e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
e["jour"].strftime(scu.DATE_FMT) if e["jour"] else ""
)
e["UE"] = row["UE"]
e["_UE_td_attrs"] = row["_UE_td_attrs"]
@ -798,7 +796,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
'Tous les étudiants'}
</div>
<div class="sem-groups-partition-titre">{
"Gestion de l'assiduité" if not partition_is_empty else ""
"Assiduité" if not partition_is_empty else ""
}</div>
"""
)
@ -824,14 +822,14 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
</div>
<div class="sem-groups-assi">
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.visu_assi_group",
scodoc_dept=g.scodoc_dept,
date_debut=formsemestre.date_debut.isoformat(),
date_fin=formsemestre.date_fin.isoformat(),
group_ids=group.id,
)}">
<button>Bilan assiduité</button></a>
Bilan</a>
</div>
"""
)
@ -839,42 +837,42 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
H.append(
f"""
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.visu_assiduites_group",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
jour = datetime.date.today().isoformat(),
group_ids=group.id,
)}">
<button>Visualiser</button></a>
Visualiser</a>
</div>
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
jour=datetime.date.today().isoformat(),
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Saisie journalière</button></a>
Saisie journalière</a>
</div>
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Saisie différée</button></a>
Saisie différée</a>
</div>
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Justificatifs en attente</button></a>
Justificatifs en attente</a>
</div>
"""
)
@ -1128,6 +1126,19 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
_make_listes_sem(formsemestre),
"</div>",
]
# --- Lien Traitement Justificatifs:
if current_user.has_permission(Permission.AbsJustifView):
H.append(
f"""<p>
<a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)}">
Traitement des justificatifs d'absence</a>
</p>"""
)
# --- Lien mail enseignants:
adrlist = list(mails_enseignants - {None, ""})
if adrlist:
@ -1312,7 +1323,9 @@ def formsemestre_tableau_modules(
if etat["attente"]:
H.append(
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
title="Il y a des notes en attente"><span class="evals_attente">en attente</span></a></span>"""
title="Il y a des notes en attente"><span class="evals_attente">{
etat["nb_evals_attente"]
} en attente</span></a></span>"""
)
if not mod_is_conforme:
H.append(
@ -1481,7 +1494,12 @@ def formsemestre_note_etuds_sans_notes(
</div>
{message}
<form method="post">
<style>
.sco-std-form select, .sco-std-form input[type="submit"] {{
height: 24px;
}}
</style>
<form class="sco-std-form" method="post">
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}">
<input type="hidden" name="etudid" value="{etudid or ""}">
@ -1492,7 +1510,7 @@ def formsemestre_note_etuds_sans_notes(
<option value="ATT" selected>ATT (en attente)</option>
<option value="EXC">EXC (neutralisée)</option>
</select>
<input type="submit" name="enregistrer">
<input type="submit" value="Enregistrer">
</form>
{html_sco_header.sco_footer()}
"""

View File

@ -57,14 +57,12 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_assiduites
from app.scodoc import codes_cursus
from app.scodoc import sco_cache
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_cursus
from app.scodoc import sco_cursus_dut
from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict
from app.scodoc.sco_permissions import Permission
@ -722,8 +720,8 @@ def formsemestre_recap_parcours_table(
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
)
# Absences (nb d'abs non just. dans ce semestre)
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
# UEs
for ue in ues:
@ -1162,7 +1160,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
"input_type": "date",
"size": 9,
"explanation": "j/m/a",
"default": time.strftime("%d/%m/%Y"),
"default": time.strftime(scu.DATE_FMT),
},
),
(
@ -1210,7 +1208,9 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
<em>dans un semestre hors ScoDoc</em>.</p>
<p class="expl"><b>Les UE validées dans ScoDoc sont
<div class="scobox explanation">
<p><b>Les UE validées dans ScoDoc sont
automatiquement prises en compte</b>.
</p>
<p>Cette page est surtout utile pour les étudiants ayant
@ -1227,11 +1227,12 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
l'attribution des ECTS si le code jury est validant (ADM).
</p>
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
</div>
{_get_etud_ue_cap_html(etud, formsemestre)}
<div class="sco_box">
<div class="sco_box_title">
<div class="scobox">
<div class="scobox-title">
Enregistrer une UE antérieure
</div>
{tf[1]}

View File

@ -453,6 +453,10 @@ class DisplayedGroupsInfos:
for i in to_remove:
del T[i]
def get_etudids(self) -> set[int]:
"Les etudids des groupes choisis"
return {member["etudid"] for member in self.members}
def get_form_elem(self):
"""html hidden input with groups"""
H = []

View File

@ -513,7 +513,7 @@ def _build_page(
<div>{scu.EMO_WARNING}
<em>Seuls les semestres dont la date de fin est proche de la date de début
de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en
de ce semestre ({formsemestre.date_debut.strftime(scu.DATE_FMT)}) sont pris en
compte.</em>
</div>
{etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)}
@ -704,7 +704,7 @@ def etuds_select_boxes(
elink += (
'<span class="finalisationinscription">'
+ " : inscription finalisée le "
+ etud["datefinalisationinscription"].strftime("%d/%m/%Y")
+ etud["datefinalisationinscription"].strftime(scu.DATE_FMT)
+ "</span>"
)

View File

@ -705,7 +705,8 @@ def _add_eval_columns(
nb_att = 0
sum_notes = 0
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
inscrits = evaluation.moduleimpl.formsemestre.etudids_actifs # set d'etudids
# actifs == inscrit au semestre, non DEM ni DEF:
_, etudids_actifs = evaluation.moduleimpl.formsemestre.etudids_actifs()
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
if evaluation.date_debut:
@ -734,7 +735,7 @@ def _add_eval_columns(
nb_att += 1
# calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES
if (
(etudid in inscrits)
(etudid in etudids_actifs)
and val is not None
and val != scu.NOTES_NEUTRALISE
and val != scu.NOTES_ATTENTE
@ -758,7 +759,7 @@ def _add_eval_columns(
comment,
)
else:
if (etudid in inscrits) and evaluation.publish_incomplete:
if (etudid in etudids_actifs) and evaluation.publish_incomplete:
# Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE
val_fmt = "ATT"
@ -826,7 +827,7 @@ def _add_eval_columns(
nb_notes,
evaluation.description,
(
evaluation.date_debut.strftime("%d/%m/%Y")
evaluation.date_debut.strftime(scu.DATE_FMT)
if evaluation.date_debut
else ""
),
@ -875,8 +876,7 @@ def _add_moymod_column(
col_id = "moymod"
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
inscrits = formsemestre.etudids_actifs
_, etudids_actifs = formsemestre.etudids_actifs()
nb_notes = 0
sum_notes = 0
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
@ -885,7 +885,7 @@ def _add_moymod_column(
val = nt.get_etud_mod_moy(moduleimpl_id, etudid) # note sur 20, ou 'NA','NI'
row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric)
row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
if etudid in inscrits and not isinstance(val, str):
if etudid in etudids_actifs and not isinstance(val, str):
notes.append(val)
if not np.isnan(val):
nb_notes = nb_notes + 1
@ -928,7 +928,7 @@ def _add_apc_columns(
# on va y ajouter une clé par UE du semestre
nt: ResultatsSemestreBUT = res_sem.load_formsemestre_results(modimpl.formsemestre)
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
inscrits = modimpl.formsemestre.etudids_actifs
_, etudids_actifs = modimpl.formsemestre.etudids_actifs()
# les UE dans lesquelles ce module a un coef non nul:
ues_with_coef = nt.modimpl_coefs_df[modimpl.id][
nt.modimpl_coefs_df[modimpl.id] > 0
@ -946,7 +946,7 @@ def _add_apc_columns(
if (
isinstance(moy_ue, float)
and not np.isnan(moy_ue)
and row["etudid"] in inscrits
and row["etudid"] in etudids_actifs
):
sum_by_ue[ue.id] += moy_ue
nb_notes_by_ue[ue.id] += 1

View File

@ -34,22 +34,28 @@ from operator import itemgetter
from flask import url_for, g, request
import app
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
from app.scodoc import sco_report
from app.scodoc import sco_etud
import sco_version
from app.scodoc import (
html_sco_header,
sco_formsemestre,
sco_groups_view,
sco_preferences,
sco_report,
sco_etud,
)
from app.models import FormSemestre
from app.scodoc.gen_tables import GenTable
import app.scodoc.sco_utils as scu
import sco_version
def formsemestre_table_etuds_lycees(
formsemestre_id, group_lycees=True, only_primo=False
formsemestre: FormSemestre, groups_infos, group_lycees=True, only_primo=False
):
"""Récupère liste d'etudiants avec etat et decision."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etuds = sco_report.tsp_etud_list(formsemestre_id, only_primo=only_primo)[0]
sem = sco_formsemestre.get_formsemestre(formsemestre.id)
etuds = sco_report.tsp_etud_list(
formsemestre.id, groups_infos=groups_infos, only_primo=only_primo
)[0]
if only_primo:
primostr = "primo-entrants du "
else:
@ -59,7 +65,7 @@ def formsemestre_table_etuds_lycees(
etuds,
group_lycees,
title,
sco_preferences.SemPreferences(formsemestre_id),
sco_preferences.SemPreferences(formsemestre.id),
)
@ -180,13 +186,20 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
def formsemestre_etuds_lycees(
formsemestre_id,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
fmt="html",
only_primo=False,
no_grouping=False,
):
"""Table des lycées d'origine"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
)
tab, etuds_by_lycee = formsemestre_table_etuds_lycees(
formsemestre_id, only_primo=only_primo, group_lycees=not no_grouping
formsemestre, groups_infos, only_primo=only_primo, group_lycees=not no_grouping
)
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
if only_primo:
@ -196,13 +209,19 @@ def formsemestre_etuds_lycees(
t = tab.make_page(fmt=fmt, with_html_headers=False)
if fmt != "html":
return t
F = [sco_report.tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt)]
F = [
sco_report.tsp_form_primo_group(
only_primo, no_grouping, formsemestre_id, fmt, groups_infos=groups_infos
)
]
H = [
html_sco_header.sco_header(
page_title=tab.page_title,
init_google_maps=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/map_lycees.js"],
cssstyles=sco_groups_view.CSSSTYLES,
javascripts=sco_groups_view.JAVASCRIPTS
+ ["js/etud_info.js", "js/map_lycees.js"],
),
"""<h2 class="formsemestre">Lycées d'origine des étudiants</h2>""",
"\n".join(F),

View File

@ -36,37 +36,45 @@ Elle n'est pas utilisée pour les parcours, ni pour rien d'autre
import collections
import app.scodoc.notesdb as ndb
from app import log
from app.models import FormSemestre
def list_formsemestres_modalites(sems):
def list_formsemestres_modalites(formsemestres: list[FormSemestre]) -> list[dict]:
"""Liste ordonnée des modalités présentes dans ces formsemestres"""
modalites = {}
for sem in sems:
if sem["modalite"] not in modalites:
m = do_modalite_list(args={"modalite": sem["modalite"]})[0]
for formsemestre in formsemestres:
if formsemestre.modalite not in modalites:
m = do_modalite_list(args={"modalite": formsemestre.modalite})[0]
modalites[m["modalite"]] = m
modalites = list(modalites.values())
modalites.sort(key=lambda x: x["numero"])
return modalites
def group_sems_by_modalite(sems):
"""Given the list of fromsemestre, group them by modalite,
def group_formsemestres_by_modalite(
formsemestres: list[FormSemestre],
) -> dict[str, list[FormSemestre]]:
"""Given the list of formsemestre, group them by modalite,
sorted in each one by semestre id and date
"""
sems_by_mod = collections.defaultdict(list)
modalites = list_formsemestres_modalites(sems)
for modalite in modalites:
for sem in sems:
if sem["semestre_id"] < 0: # formations en un semestre
sem["sortkey"] = (-100 * sem["semestre_id"], sem["dateord"])
else:
sem["sortkey"] = (sem["semestre_id"], sem["dateord"])
if sem["modalite"] == modalite["modalite"]:
sems_by_mod[modalite["modalite"]].append(sem)
modalites = list_formsemestres_modalites(formsemestres)
sems_by_mod = {
modalite["modalite"]: [
formsemestre
for formsemestre in formsemestres
if formsemestre.modalite == modalite["modalite"]
]
for modalite in modalites
}
# tri dans chaque modalité par indice de semestre et date debut
for modalite in modalites:
sems_by_mod[modalite["modalite"]].sort(key=lambda x: x["sortkey"])
sems_by_mod[modalite["modalite"]].sort(
key=lambda x: (
x.semestre_id if x.semestre_id > 0 else -1000 * x.semestre_id,
x.date_debut,
)
)
return sems_by_mod, modalites

View File

@ -50,8 +50,6 @@ from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
@ -431,7 +429,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
)
if info["ue_status"]["event_date"]:
H.append(
f"""(cap. le {info["ue_status"]["event_date"].strftime("%d/%m/%Y")})"""
f"""(cap. le {info["ue_status"]["event_date"].strftime(scu.DATE_FMT)})"""
)
if is_apc:
is_inscrit_ue = (etud.id, ue.id) not in res.dispense_ues
@ -584,7 +582,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
validation = validations_ue[-1] if validations_ue else None
expl_validation = (
f"""Validée ({validation.code}) le {
validation.event_date.strftime("%d/%m/%Y")}"""
validation.event_date.strftime(scu.DATE_FMT)}"""
if validation
else ""
)

View File

@ -25,10 +25,9 @@
#
##############################################################################
"""Tableau de bord module
"""
"""Tableau de bord module"""
import math
import time
import datetime
from flask import g, url_for
@ -48,13 +47,10 @@ from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cal
from app.scodoc import sco_compute_moy
from app.scodoc import sco_evaluations
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
from app.tables import list_etuds
@ -262,7 +258,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
# 2ieme ligne: Semestre, Coef
H.append("""<tr><td class="fichetitre2">""")
if formsemestre.semestre_id >= 0:
H.append("""Semestre: </td><td>%s""" % formsemestre.semestre_id)
H.append(f"""Semestre: </td><td>{formsemestre.semestre_id}""")
else:
H.append("""</td><td>""")
if sem_locked:
@ -290,7 +286,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
)
if current_user.has_permission(Permission.EtudInscrit):
H.append(
f"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id={modimpl.id}">modifier</a>"""
f"""<a class="stdlink" style="margin-left:2em;"
href="moduleimpl_inscriptions_edit?moduleimpl_id={modimpl.id}">modifier</a>"""
)
H.append("</td></tr>")
# Ligne: règle de calcul
@ -329,8 +326,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
>Saisie Absences journée</a></span>
"""
)
year, week, day = datetime.date.today().isocalendar()
semaine: str = f"{year}-W{week}"
H.append(
f"""
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
@ -338,11 +333,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
"assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
group_ids=group_id,
semaine=semaine,
formsemestre_id=formsemestre.id,
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
)}"
>Saisie Absences hebdo</a></span>
>Saisie Absences Différée</a></span>
"""
)
@ -617,7 +611,7 @@ def _ligne_evaluation(
if evaluation.is_blocked():
etat_txt = f"""évaluation bloquée {
"jusqu'au " + evaluation.blocked_until.strftime("%d/%m/%Y")
"jusqu'au " + evaluation.blocked_until.strftime(scu.DATE_FMT)
if evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
else "" }
"""
@ -660,7 +654,8 @@ def _ligne_evaluation(
<th class="moduleimpl_evaluations">Notes</th>
<th class="moduleimpl_evaluations">Abs</th>
<th class="moduleimpl_evaluations">N</th>
<th class="moduleimpl_evaluations moduleimpl_evaluation_moy" colspan="2"><span>{etat_txt}</span></th>
<th class="moduleimpl_evaluations moduleimpl_evaluation_moy"
colspan="2"><span>{etat_txt}</span></th>
</tr>
<tr class="{tr_class} mievr_in">
<td class="mievr">"""

View File

@ -180,7 +180,7 @@ def fiche_etud(etudid=None):
)
else:
info["etat_civil"] = ""
info["ScoURL"] = scu.ScoURL()
info["ScoURL"] = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
info["authuser"] = current_user
if restrict_etud_data:
info["info_naissance"] = ""
@ -332,28 +332,29 @@ def fiche_etud(etudid=None):
)
# fiche admission
infos_admission = _infos_admission(etud, restrict_etud_data)
has_adm_notes = any(
infos_admission[k] for k in ("math", "physique", "anglais", "francais")
)
has_bac_info = any(
infos_admission[k]
for k in (
"bac_specialite",
"annee_bac",
"rapporteur",
"commentaire",
"classement",
"type_admission",
"rap",
if etud.admission:
infos_admission = _infos_admission(etud, restrict_etud_data)
has_adm_notes = any(
infos_admission[k] for k in ("math", "physique", "anglais", "francais")
)
)
if has_bac_info or has_adm_notes:
adm_tmpl = """<!-- Donnees admission -->
<div class="fichetitre">Informations admission</div>
"""
if has_adm_notes:
adm_tmpl += """
has_bac_info = any(
infos_admission[k]
for k in (
"bac_specialite",
"annee_bac",
"rapporteur",
"commentaire",
"classement",
"type_admission",
"rap",
)
)
if has_bac_info or has_adm_notes:
adm_tmpl = """<!-- Donnees admission -->
<div class="fichetitre">Informations admission</div>
"""
if has_adm_notes:
adm_tmpl += """
<table>
<tr><th>Bac</th><th>Année</th><th>Rg</th>
<th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr>
@ -364,24 +365,26 @@ def fiche_etud(etudid=None):
<td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td>
</tr>
</table>
"""
adm_tmpl += """
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="info_lycee">%(info_lycee)s</div>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += """<div class="vadmission">"""
if infos_admission["type_admission"]:
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
if infos_admission["classement"]:
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += "</div>"
if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>"""
"""
adm_tmpl += """
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="info_lycee">%(info_lycee)s</div>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += """<div class="vadmission">"""
if infos_admission["type_admission"]:
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
if infos_admission["classement"]:
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += "</div>"
if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>"""
else:
adm_tmpl = "" # pas de boite "info admission"
info["adm_data"] = adm_tmpl % infos_admission
else:
adm_tmpl = "" # pas de boite "info admission"
info["adm_data"] = adm_tmpl % infos_admission
info["adm_data"] = ""
# Fichiers archivés:
info["fichiers_archive_htm"] = (
@ -654,7 +657,7 @@ def _format_adresse(adresse: Adresse | None) -> dict:
def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
"""dict with adminission data, restricted or not"""
"""dict with admission data, restricted or not"""
# info sur rapporteur et son commentaire
rap = ""
if not restrict_etud_data:
@ -727,7 +730,7 @@ def get_html_annotations_list(etud: Identite) -> list[str]:
author = User.query.filter_by(user_name=annot.author).first()
html_annotations_list.append(
f"""<tr><td><span class="annodate">Le {
annot.date.strftime("%d/%m/%Y") if annot.date else "?"}
annot.date.strftime(scu.DATE_FMT) if annot.date else "?"}
par {author.get_prenomnom() if author else "?"} :
</span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
"""
@ -799,8 +802,11 @@ def etud_info_html(etudid, with_photo="1", debug=False):
code_cursus, _ = sco_report.get_code_cursus_etud(
etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", "
)
bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite)
bac_abbrev = bac.abbrev()
if etud.admission:
bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite)
bac_abbrev = bac.abbrev()
else:
bac_abbrev = "-"
H = f"""<div class="etud_info_div">
<div class="eid_left">
<div class="eid_nom"><div><a class="stdlink" target="_blank" href="{

View File

@ -458,7 +458,12 @@ def pdf_basic_page(
if title:
head = Paragraph(SU(title), StyleSheet["Heading3"])
objects = [head] + objects
document.build(objects)
try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue()
return data

View File

@ -335,7 +335,7 @@ class PlacementRunner:
def _production_pdf(self):
pdf_title = "<br>".join(self.desceval)
pdf_title += f"""\nDate : {self.evaluation.date_debut.strftime("%d/%m/%Y")
pdf_title += f"""\nDate : {self.evaluation.date_debut.strftime(scu.DATE_FMT)
if self.evaluation.date_debut else '-'
} - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin()
}"""
@ -485,7 +485,7 @@ class PlacementRunner:
worksheet.append_blank_row()
worksheet.append_single_cell_row(desceval, self.styles["titres"])
worksheet.append_single_cell_row(
f"""Date : {self.evaluation.date_debut.strftime("%d/%m/%Y")
f"""Date : {self.evaluation.date_debut.strftime(scu.DATE_FMT)
if self.evaluation.date_debut else '-'
} - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin()
}""",

View File

@ -517,7 +517,7 @@ def _normalize_apo_fields(infolist):
)
infos["datefinalisationinscription_str"] = infos[
"datefinalisationinscription"
].strftime("%d/%m/%Y")
].strftime(scu.DATE_FMT)
else:
infos["datefinalisationinscription"] = None
infos["datefinalisationinscription_str"] = ""

View File

@ -105,7 +105,9 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
rangs.append(["rang_" + code_module, rang_module])
# Absences
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem)
nbabsnj, nbabsjust, _ = sco_assiduites.get_assiduites_count(
etudid, nt.sem
)
# En BUT, prend tout, sinon ne prend que les semestre validés par le jury
if nt.is_apc or (
dec
@ -125,7 +127,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
("date_debut", s["date_debut"]),
("date_fin", s["date_fin"]),
("periode", "%s - %s" % (s["mois_debut"], s["mois_fin"])),
("AbsNonJust", nbabs - nbabsjust),
("AbsNonJust", nbabsnj),
("AbsJust", nbabsjust),
]
# ajout des 2 champs notes des modules et classement dans chaque module

View File

@ -600,17 +600,6 @@ class BasePreferences:
},
),
# Assiduité
(
"assi_limit_annee",
{
"initvalue": 1,
"title": "Ne lister que l'assiduité de l'année",
"explanation": "Limite l'affichage des listes d'assiduité et de justificatifs à l'année en cours",
"input_type": "boolcheckbox",
"labels": ["non", "oui"],
"category": "assi",
},
),
(
"forcer_module",
{
@ -622,16 +611,17 @@ class BasePreferences:
"explanation": "toute saisie d'absence doit indiquer le module concerné",
},
),
# (
# "forcer_present",
# {
# "initvalue": 0,
# "title": "Forcer l'appel des présents",
# "input_type": "boolcheckbox",
# "labels": ["non", "oui"],
# "category": "assi",
# },
# ),
(
"non_present",
{
"initvalue": 0,
"title": "Désactiver la saisie des présences",
"input_type": "boolcheckbox",
"labels": ["non", "oui"],
"category": "assi",
"explanation": "Désactive la saisie et l'affichage des présences",
},
),
(
"periode_defaut",
{
@ -655,18 +645,18 @@ class BasePreferences:
"category": "assi",
},
),
(
"assi_etat_defaut",
{
"explanation": "⚠ non fonctionnel, travaux en cours !",
"initvalue": "aucun",
"input_type": "menu",
"labels": ["aucun", "present", "retard", "absent"],
"allowed_values": ["aucun", "present", "retard", "absent"],
"title": "Définir l'état par défaut",
"category": "assi",
},
),
# (
# "assi_etat_defaut",
# {
# "explanation": "⚠ non fonctionnel, travaux en cours !",
# "initvalue": "aucun",
# "input_type": "menu",
# "labels": ["aucun", "present", "retard", "absent"],
# "allowed_values": ["aucun", "present", "retard", "absent"],
# "title": "Définir l'état par défaut",
# "category": "assi",
# },
# ),
(
"non_travail",
{
@ -1601,18 +1591,6 @@ class BasePreferences:
"labels": ["non", "oui"],
},
),
(
"bul_display_publication",
{
"initvalue": 1,
"title": "Afficher icône indiquant si les bulletins sont publiés",
"explanation": "décocher si vous n'avez pas de portail étudiant publiant les bulletins",
"input_type": "boolcheckbox",
"labels": ["non", "oui"],
"category": "bul",
"only_global": False,
},
),
# champs des bulletins PDF:
(
"bul_pdf_title",
@ -2283,16 +2261,17 @@ class BasePreferences:
before_table="<details><summary>{title}</summary>",
after_table="</details>",
)
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(scu.ScoURL()) # cancel
else:
for pref in self.prefs_definition:
self.prefs[None][pref[0]] = tf[2][pref[0]]
self.save()
flash("Préférences modifiées")
return flask.redirect(scu.ScoURL())
if tf[0] == -1:
return flask.redirect(dest_url) # cancel
#
for pref in self.prefs_definition:
self.prefs[None][pref[0]] = tf[2][pref[0]]
self.save()
flash("Préférences modifiées")
return flask.redirect(dest_url)
def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None):
"""Build list of elements for TrivialFormulator.
@ -2456,10 +2435,12 @@ function set_global_pref(el, pref_name) {
before_table="<details><summary>{title}</summary>",
after_table="</details>",
)
dest_url = (
scu.NotesURL()
+ "/formsemestre_status?formsemestre_id=%s" % self.formsemestre_id
dest_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre_id,
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
@ -2505,7 +2486,9 @@ function set_global_pref(el, pref_name) {
request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)
)
elif destination == "global":
return flask.redirect(scu.ScoURL() + "/edit_preferences")
return flask.redirect(
url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)
)
#

View File

@ -139,9 +139,9 @@ def feuille_preparation_jury(formsemestre_id):
main_partition_id, ""
)
# absences:
e_nbabs, e_nbabsjust = sco_assiduites.get_assiduites_count(etud.id, sem)
nbabs[etud.id] = e_nbabs
nbabsjust[etud.id] = e_nbabs - e_nbabsjust
_, nbabsjust[etud.id], nbabs[etud.id] = sco_assiduites.get_assiduites_count(
etud.id, sem
)
# Codes des UE "semestre précédent":
ue_prev_codes = list(prev_moy_ue.keys())
@ -327,7 +327,7 @@ def feuille_preparation_jury(formsemestre_id):
"Préparé par %s le %s sur %s pour %s"
% (
sco_version.SCONAME,
time.strftime("%d/%m/%Y"),
time.strftime(scu.DATE_FMT),
request.url_root,
current_user,
)

View File

@ -50,7 +50,7 @@ from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_pv_dict
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
from app.scodoc.sco_cursus_dut import SituationEtudCursus
from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres
import sco_version
@ -132,7 +132,11 @@ def pdf_lettres_individuelles(
)
)
document.build(objects)
try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue()
return data
@ -241,13 +245,14 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non
titre_jury_court = "s"
else:
titre_jury_court = ""
params[
"autorisations_txt"
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
etud.e,
titre_jury_court,
titre_jury_court,
decision["autorisations_descr"],
params["autorisations_txt"] = (
"""Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>"""
% (
etud.e,
titre_jury_court,
titre_jury_court,
decision["autorisations_descr"],
)
)
else:
params["autorisations_txt"] = ""

Some files were not shown because too many files have changed in this diff Show More