Compare commits

...

90 Commits

Author SHA1 Message Date
02a5b00ecf Changement réf. comp. équivalent: SD <> STID. 2024-06-02 12:05:01 +02:00
dcdf6a8012 Assiduite: supprime lien saisie différée + lien choix semaine 2024-06-02 10:05:15 +02:00
912a213dcd Rafraichissement image lors changement photo etud. Pres. trombi. Photos pour demos. 2024-06-01 14:28:42 +02:00
3575e89dc0 check invalid etudid 2024-06-01 14:27:02 +02:00
21c0625147 formsemestre_report_counts: ajout champ 'boursier' 2024-05-30 16:05:45 +02:00
e18c1d8fd0 Merge branch 'iziram-sco_gen_cal' 2024-05-30 13:31:07 +02:00
5867d0f430 typo 2024-05-30 13:30:37 +02:00
9897ccc659 Numéros pages sur bulletins BUT. Closes #652 2024-05-30 12:08:41 +02:00
Iziram
7575959bd4 sco_gen_cal : calendrier_choix_date + implementation dans Assiduité closes #914 2024-05-30 10:52:13 +02:00
Iziram
2aafbad9e2 sco_gen_cal : hightlight + week_index + jour date 2024-05-30 09:49:33 +02:00
50f2cd7a0f Assiduité: Liens et message temporaire 2024-05-29 19:09:06 +02:00
fd8fbb9e02 Merge branch 'iziram-saisie_hebdo' 2024-05-29 18:42:06 +02:00
Iziram
ebcef76950 Assiduité : signal_assiduites_hebdo : choix heures init defaut closes #911 2024-05-29 17:30:07 +02:00
Iziram
13349776af Assiduité : signal_assiduites_hebdo : bulle info assi closes #912 2024-05-29 17:25:57 +02:00
Iziram
f275286b71 Assiduité : liens saisie hebdo 2024-05-29 16:29:34 +02:00
Iziram
f4f6c13d79 Assiduité : signal_assiduites_hebdo : v2 sans mobile 2024-05-29 15:59:19 +02:00
e7f23efe65 Affichage poids sur tableau de bord module: normalisation par evaluation_type. Closes #886 2024-05-29 12:12:31 +02:00
e44d3fd5dc Améliore visualisation coefficients sur tableau bord module. Closes #886. 2024-05-29 11:55:28 +02:00
fac36fa11c Merge branch 'master' into saisie_hebdo 2024-05-29 10:56:55 +02:00
9289535359 Ajout Identite.nom_prenom() 2024-05-29 10:48:34 +02:00
Iziram
d73b925006 Assiduité : signal_assiduites_hedbo : v1 OK 2024-05-28 20:07:25 +02:00
6749ca70d6 Fix prise en compte evals session 2 avec poids ne couvrant pas toutes les UEs (#811) 2024-05-28 13:51:27 +02:00
Iziram
dea403b03d Assiduité : signal_assiduites_hebdo : verif heure matin < aprem 2024-05-28 09:51:40 +02:00
Iziram
ab9543c310 [WIP] Assiduité : signal_assiduites_hebdo : choix horaires 2024-05-27 23:26:13 +02:00
Iziram
f94998f66b [WIP] Assiduité : corrections saisie_assiduites_hebdo 2024-05-27 22:33:01 +02:00
Iziram
eb88a8ca83 [WIP] Assiduité : saisie_assiduites_hebdo 2024-05-27 17:59:34 +02:00
7042650fd9 Merge branch 'lyanis-report' 2024-05-26 22:57:47 +02:00
2745ffd687 Bug report: corrections mineures 2024-05-26 22:57:04 +02:00
9a882ea41d Merge branch 'report' of https://scodoc.org/git/lyanis/ScoDoc into lyanis-report 2024-05-26 20:14:58 +02:00
ea6003e812 Modif message page saisie différée pour 9.6.967 2024-05-26 17:03:25 +02:00
5c6935337e Merge branch 'iziram-modif_assi' 2024-05-25 18:13:16 +02:00
60998d2e20 Assiduite: bg bouton delete + dialog confirm 2024-05-25 18:12:44 +02:00
29b877d9ed Script API pour enregistrer tous les résultats. 2024-05-25 13:03:51 +02:00
Iziram
6834c19015 Assiduité : modif assiduites_bubble 2024-05-24 16:51:44 +02:00
Iziram
f47fc4ba46 Assiduité : signal_assiduites_group : modif bouton mettre tout le monde "aucun" 2024-05-24 16:27:17 +02:00
5894c6f952 search_etud_by_name: case insensitive. 2024-05-24 15:37:07 +02:00
af1d1884c7 Template/wtf form pour bug report
Ajout d'un template pour gérer le formulaire et utilisation de WTF form pour la validation des données.
2024-05-24 13:01:56 +02:00
Iziram
881bf82000 data-tooltip + enableToolTip sur la sidebar 2024-05-24 10:37:11 +02:00
Iziram
2ed4516a97 Assiduité : fusion liste_etud bilan_etud 2024-05-24 10:26:47 +02:00
Iziram
75ce1ccd31 Assiduité : signal_assiduite_group : sauvegarde auto timeline 2024-05-24 09:56:05 +02:00
Iziram
f8d5f6ea11 Assiduité : suppression code non utilisé 2024-05-24 09:40:44 +02:00
Iziram
70995fbd7e Assiduité : suppression préférence periode_defaut 2024-05-24 09:36:42 +02:00
dc095765f2 Retrait décorateur inutile
Le décorateur `@scodoc7func` n'est pas utile pour cette vue, il est retiré.
2024-05-23 16:21:12 +02:00
Iziram
1cec3fa703 Assiduité : signal_assiduite_group : bouton jour suivant / précédent 2024-05-23 09:40:44 +02:00
Iziram
032454aefd Assiduité : signal_assiduites_group : bouton pour remonter la page 2024-05-23 09:23:45 +02:00
Iziram
e3344cf424 Assiduité : signal_assiduites_group : bouton matin/aprem 2024-05-23 09:17:56 +02:00
Iziram
d7acff9d35 Assiduité : reorganisation lien assi page sem + bulle avertissement saisie diff 2024-05-23 09:07:50 +02:00
Iziram
decdf59e20 Assiduité : renommage Saisie journalière -> saisie assiduité 2024-05-23 08:59:10 +02:00
Iziram
42fc08a3a3 Assiduité : suppression page visu_assiduites_group (signal_assiduites_group readonly) 2024-05-23 08:56:12 +02:00
Iziram
f3770fb5c7 Assiduité : avertissement fusion saisie jour - saisie diff 2024-05-23 08:52:08 +02:00
63b28a3277 Ajout d'un formulaire de rapport de bug
- Formulaire permettant de saisir un rapport de bug et de l'envoyer sur une nouvelle API scodoc.org
- Modification du lien de la page d'accueil pour pointer vers le formulaire de rapport de bug au lieu de simplement dump
- Après un échange avec l'API scodoc.org (pour l'upload de dump et la création de ticket), on tente de récuperer le champ json "message" pour l'afficher à l'utilisateur
2024-05-23 00:15:32 +02:00
bb23cdcea7 PV jury: restreint cursus à la formation actuelle. Fix #622. 2024-05-22 19:18:57 +02:00
3ca5636454 Filigranne PDF: légère modif position. 2024-05-22 13:00:44 +02:00
42882154d5 JS initialisation datatables + id sur GenTable. Fix #880. 2024-05-22 00:06:30 +02:00
489acb26d2 Texte additionnel sur pieds de pages PDF. Closes #653. 2024-05-21 21:14:50 +02:00
8ee373db7d Warning si evals rattrapage non conformes en BUT.. Closes #811. 2024-05-21 20:43:45 +02:00
8e56dc2418 Formulaire évaluation: interdit de définir des évaluations non normales immédiates 2024-05-21 20:37:40 +02:00
b3331bd886 Adapte test unitaire pour nouveau search_etuds_infos_from_exp. 2024-05-21 20:24:16 +02:00
89afb672af Support pour plusieurs évaluations de rattrapage en classique et BUT. Avance sur #811. 2024-05-21 20:23:10 +02:00
8f25284038 Code formatting 2024-05-20 23:31:03 +02:00
f29002a57d Tableau évaluations: ajout colonne type 2024-05-20 23:29:25 +02:00
69780b3f24 Evaluations de session 2: moyenne sur plusieurs, en prennant en compte les poids en BUT. Modif vérification conformite (bug #811). WIP: reste à vérifier ratrapages. 2024-05-20 23:28:39 +02:00
fbff151be0 recherche étudiant: modernise code 2024-05-20 16:11:44 +02:00
3b436fa0f3 Enhance ScoValueError messages (lié à 87aaf12d27) 2024-05-20 10:46:36 +02:00
8847a1f008 Fix warning set_ue_poids_dict. Add type_abbrev() method. 2024-05-20 10:01:39 +02:00
ac882e9ccd Fix: cache poids evals (invalidation manquante) 2024-05-19 22:57:21 +02:00
000e016985 Enhance critical_error handling. 2024-05-19 22:53:54 +02:00
22d90215a0 Effacer décisions de jury des formations classiques: closes #884 2024-05-19 15:38:30 +02:00
043985bff6 cosmetic: calendrier evaluations 2024-05-17 15:23:29 +02:00
d20ada1797 Merge branch 'gen_cal' of https://scodoc.org/git/iziram/ScoDoc into gen_cal 2024-05-17 12:02:23 +02:00
Iziram
778fecabb6 sco_gen_cal : correction affichage semaine/année courante 2024-05-15 14:16:11 +02:00
Iziram
fa6f83722e sco_gen_cal : ajout style semaine courante 2024-05-15 13:35:44 +02:00
baa0412071 Merge pull request 'Mise à jour du README' (#881) from lyanis/ScoDoc:readme into master
Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/881
2024-05-13 18:23:34 +02:00
d51a47b71a Fix: formulaire creation étudiant (date naissance vide) 2024-05-13 17:31:54 +02:00
f21ef41de6 README: Mise en forme des blocs de code 2024-05-13 14:54:52 +02:00
2d673e7a5d Mise à jour du README 2024-05-13 11:16:10 +02:00
3e43495831 Fix: bulletins pdf, notes évaluations sans notes. 2024-05-07 18:17:13 +02:00
Iziram
a4db8c4ff8 utilisation sco_gen_cal pour calendrier evaluations #875 2024-05-07 16:47:08 +02:00
Iziram
1ac35d04c2 Assiduité : utilisation sco_gen_cal closes #877 2024-05-07 16:45:03 +02:00
Iziram
687ac3cf13 Assiduité : Généralisation du Calendrier WIP 2024-05-06 17:29:21 +02:00
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
0bc57807de release build script using gitea again 2024-04-24 20:37:03 +02:00
87aaf12d27 Protect against Reflected XSS on home page (and other exception-handling pages) 2024-04-23 18:28:00 +02:00
c8ab9b9b6c Invalidation cache lors d'une erreur sur association UE/Niveau. Peut-être cause de #874. 2024-04-15 18:06:26 +02:00
ad7b48e110 Calendrier évaluations: fix #875 2024-04-15 17:53:02 +02:00
118 changed files with 3668 additions and 1678 deletions

View File

@ -2,7 +2,7 @@
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt). (c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11> Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian12>
Documentation utilisateur: <https://scodoc.org> Documentation utilisateur: <https://scodoc.org>
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### Lignes de commandes ### Lignes de commandes
Voir [https://scodoc.org/GuideConfig](le guide de configuration). Voir [le guide de configuration](https://scodoc.org/GuideConfig).
## Organisation des fichiers ## Organisation des fichiers
@ -41,7 +41,7 @@ Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configu
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé. Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
Principaux contenus: Principaux contenus:
```
/opt/scodoc-data /opt/scodoc-data
/opt/scodoc-data/log # Fichiers de log ScoDoc /opt/scodoc-data/log # Fichiers de log ScoDoc
/opt/scodoc-data/config # Fichiers de configuration /opt/scodoc-data/config # Fichiers de configuration
@ -49,37 +49,33 @@ Principaux contenus:
.../config/depts # un fichier par département .../config/depts # un fichier par département
/opt/scodoc-data/photos # Photos des étudiants /opt/scodoc-data/photos # Photos des étudiants
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants /opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
```
## Pour les développeurs ## Pour les développeurs
### Installation du code ### Installation du code
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)). Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian12)).
Puis remplacer `/opt/scodoc` par un clone du git. Puis remplacer `/opt/scodoc` par un clone du git.
```bash
sudo su sudo su
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
apt-get install git # si besoin apt-get install git # si besoin
cd /opt git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
git clone https://scodoc.org/git/viennet/ScoDoc.git
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !) # (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
# Renommer le répertoire: # Donner ce répertoire à l'utilisateur scodoc:
mv ScoDoc scodoc chown -R scodoc:scodoc /opt/scodoc
```
# Et donner ce répertoire à l'utilisateur scodoc:
chown -R scodoc.scodoc /opt/scodoc
Il faut ensuite installer l'environnement et le fichier de configuration: Il faut ensuite installer l'environnement et le fichier de configuration:
```bash
# Le plus simple est de piquer le virtualenv configuré par l'installeur: # Le plus simple est de piquer le virtualenv configuré par l'installeur:
mv /opt/off-scodoc/venv /opt/scodoc mv /opt/off-scodoc/venv /opt/scodoc
```
Et la config: Et la config:
```bash
ln -s /opt/scodoc-data/.env /opt/scodoc ln -s /opt/scodoc-data/.env /opt/scodoc
```
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`. exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
@ -88,11 +84,11 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`. Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
Avant le premier lancement, créer cette base ainsi: Avant le premier lancement, créer cette base ainsi:
```bash
./tools/create_database.sh SCODOC_TEST ./tools/create_database.sh SCODOC_TEST
export FLASK_ENV=test export FLASK_ENV=test
flask db upgrade flask db upgrade
```
Cette commande n'est nécessaire que la première fois (le contenu de la base Cette commande n'est nécessaire que la première fois (le contenu de la base
est effacé au début de chaque test, mais son schéma reste) et aussi si des est effacé au début de chaque test, mais son schéma reste) et aussi si des
migrations (changements de schéma) ont eu lieu dans le code. migrations (changements de schéma) ont eu lieu dans le code.
@ -100,17 +96,17 @@ migrations (changements de schéma) ont eu lieu dans le code.
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
scripts de tests: scripts de tests:
Lancer au préalable: Lancer au préalable:
```bash
flask delete-dept -fy TEST00 && flask create-dept TEST00 flask delete-dept -fy TEST00 && flask create-dept TEST00
```
Puis dérouler les tests unitaires: Puis dérouler les tests unitaires:
```bash
pytest tests/unit pytest tests/unit
```
Ou avec couverture (`pip install pytest-cov`) Ou avec couverture (`pip install pytest-cov`)
```bash
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/* pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
```
#### Utilisation des tests unitaires pour initialiser la base de dev #### Utilisation des tests unitaires pour initialiser la base de dev
On peut aussi utiliser les tests unitaires pour mettre la base de données de On peut aussi utiliser les tests unitaires pour mettre la base de données de
@ -119,34 +115,34 @@ développement dans un état connu, par exemple pour éviter de recréer à la m
Il suffit de positionner une variable d'environnement indiquant la BD utilisée Il suffit de positionner une variable d'environnement indiquant la BD utilisée
par les tests: par les tests:
```bash
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
```
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer (si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
normalement, par exemple: normalement, par exemple:
```bash
pytest tests/unit/test_sco_basic.py pytest tests/unit/test_sco_basic.py
```
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
utilisateur: utilisateur:
```bash
flask user-password admin flask user-password admin
```
**Attention:** les tests unitaires **effacent** complètement le contenu de la **Attention:** les tests unitaires **effacent** complètement le contenu de la
base de données (tous les départements, et les utilisateurs) avant de commencer ! base de données (tous les départements, et les utilisateurs) avant de commencer !
#### Modification du schéma de la base #### Modification du schéma de la base
On utilise SQLAlchemy avec Alembic et Flask-Migrate. On utilise SQLAlchemy avec Alembic et Flask-Migrate.
```bash
flask db migrate -m "message explicatif....." flask db migrate -m "message explicatif....."
flask db upgrade flask db upgrade
```
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`). Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
**Mémo**: séquence re-création d'une base (vérifiez votre `.env` **Mémo**: séquence re-création d'une base (vérifiez votre `.env`
ou variables d'environnement pour interroger la bonne base !). ou variables d'environnement pour interroger la bonne base !).
```bash
dropdb SCODOC_DEV dropdb SCODOC_DEV
tools/create_database.sh SCODOC_DEV # créé base SQL tools/create_database.sh SCODOC_DEV # créé base SQL
flask db upgrade # créé les tables à partir des migrations flask db upgrade # créé les tables à partir des migrations
@ -155,7 +151,7 @@ ou variables d'environnement pour interroger la bonne base !).
# puis imports: # puis imports:
flask import-scodoc7-users flask import-scodoc7-users
flask import-scodoc7-dept STID SCOSTID flask import-scodoc7-dept STID SCOSTID
```
Si la base utilisée pour les dev n'est plus en phase avec les scripts de Si la base utilisée pour les dev n'est plus en phase avec les scripts de
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
positionner à la bonne étape. positionner à la bonne étape.
@ -163,19 +159,19 @@ positionner à la bonne étape.
### Profiling ### Profiling
Sur une machine de DEV, lancer Sur une machine de DEV, lancer
```bash
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
```
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`). le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien: Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
```bash
pip install snakeviz pip install snakeviz
```
puis puis
```bash
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
```
## Paquet Debian 12 ## Paquet Debian 12
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus

View File

@ -637,14 +637,12 @@ def critical_error(msg):
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
log(f"\n*** CRITICAL ERROR: {msg}") log(f"\n*** CRITICAL ERROR: {msg}")
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg) subject = f"CRITICAL ERROR: {msg}".strip()[:68]
send_scodoc_alarm(subject, msg)
clear_scodoc_cache() clear_scodoc_cache()
raise ScoValueError( raise ScoValueError(
f""" f"""
Une erreur est survenue. Une erreur est survenue, veuillez -essayer.
Si le problème persiste, merci de contacter le support ScoDoc via
{scu.SCO_DISCORD_ASSISTANCE}
{msg} {msg}
""" """

View File

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

View File

@ -124,7 +124,9 @@ def _build_bulletin_but_infos(
formsemestre, bulletins_sem.res formsemestre, bulletins_sem.res
) )
if warn_html: if warn_html:
raise ScoValueError("<b>Formation mal configurée pour le BUT</b>" + warn_html) raise ScoValueError(
"<b>Formation mal configurée pour le BUT</b>" + warn_html, safe=True
)
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau( ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
refcomp, etud refcomp, etud
) )

View File

@ -73,6 +73,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
html_class="notes_bulletin", html_class="notes_bulletin",
html_class_ignore_default=True, html_class_ignore_default=True,
html_with_td_classes=True, html_with_td_classes=True,
table_id="bul-table",
) )
table_objects = table.gen(fmt=fmt) table_objects = table.gen(fmt=fmt)
objects += table_objects objects += table_objects
@ -427,12 +428,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
else "*" else "*"
) )
note_value = e["note"].get("value", "")
t = { t = {
"titre": f"{e['description'] or ''}", "titre": f"{e['description'] or ''}",
"moyenne": e["note"]["value"], "moyenne": note_value,
"_moyenne_pdf": Paragraph( "_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
f"""<para align=right>{e["note"]["value"]}</para>"""
),
"coef": coef, "coef": coef,
"_coef_pdf": Paragraph( "_coef_pdf": Paragraph(
f"""<para align=right fontSize={self.small_fontsize}><i>{ f"""<para align=right fontSize={self.small_fontsize}><i>{

View File

@ -27,6 +27,7 @@
"""caches pour tables APC """caches pour tables APC
""" """
from flask import g
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -47,3 +48,27 @@ class EvaluationsPoidsCache(sco_cache.ScoDocCache):
""" """
prefix = "EPC" prefix = "EPC"
@classmethod
def invalidate_all(cls):
"delete all cached evaluations poids (in current dept)"
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
moduleimpl_ids = [
mi.id
for mi in ModuleImpl.query.join(FormSemestre).filter_by(
dept_id=g.scodoc_dept_id
)
]
cls.delete_many(moduleimpl_ids)
@classmethod
def invalidate_sem(cls, formsemestre_id):
"delete cached evaluations poids for this formsemestre from cache"
from app.models.moduleimpls import ModuleImpl
moduleimpl_ids = [
mi.id for mi in ModuleImpl.query.filter_by(formsemestre_id=formsemestre_id)
]
cls.delete_many(moduleimpl_ids)

View File

@ -45,7 +45,6 @@ from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoBugCatcher
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -113,6 +112,8 @@ class ModuleImplResults:
""" """
self.evals_etudids_sans_note = {} self.evals_etudids_sans_note = {}
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions.""" """dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
self.evals_type = {}
"""Type de chaque eval { evaluation.id : evaluation.evaluation_type }"""
self.load_notes(etudids, etudids_actifs) self.load_notes(etudids, etudids_actifs)
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index) 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""" """1 bool par etud, indique si sa moyenne de module vient de la session2"""
@ -164,7 +165,10 @@ class ModuleImplResults:
self.evaluations_completes = [] self.evaluations_completes = []
self.evaluations_completes_dict = {} self.evaluations_completes_dict = {}
self.etudids_attente = set() # empty self.etudids_attente = set() # empty
self.evals_type = {}
evaluation: Evaluation
for evaluation in moduleimpl.evaluations: for evaluation in moduleimpl.evaluations:
self.evals_type[evaluation.id] = evaluation.evaluation_type
eval_df = self._load_evaluation_notes(evaluation) eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi # is_complete ssi
# tous les inscrits (non dem) au module ont une note # tous les inscrits (non dem) au module ont une note
@ -270,6 +274,24 @@ class ModuleImplResults:
* self.evaluations_completes * self.evaluations_completes
).reshape(-1, 1) ).reshape(-1, 1)
def get_evaluations_special_coefs(
self, modimpl: ModuleImpl, evaluation_type=Evaluation.EVALUATION_SESSION2
) -> np.array:
"""Coefficients des évaluations de session 2 ou rattrapage.
Les évals de session 2 et rattrapage sont réputées "complètes": elles sont toujours
prises en compte mais seules les notes numériques et ABS sont utilisées.
Résultat: 2d-array of floats, shape (nb_evals, 1)
"""
return (
np.array(
[
(e.coefficient if e.evaluation_type == evaluation_type else 0.0)
for e in modimpl.evaluations
],
dtype=float,
)
).reshape(-1, 1)
# was _list_notes_evals_titles # was _list_notes_evals_titles
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]: def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
"Liste des évaluations complètes" "Liste des évaluations complètes"
@ -296,32 +318,26 @@ class ModuleImplResults:
for (etudid, x) in self.evals_notes[evaluation_id].items() for (etudid, x) in self.evals_notes[evaluation_id].items()
} }
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None: def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas. """Les évaluations de rattrapage de ce module.
Rattrapage: la moyenne du module est la meilleure note entre moyenne Rattrapage: la moyenne du module est la meilleure note entre moyenne
des autres évals et la note eval rattrapage. des autres évals et la moyenne des notes de rattrapage.
""" """
eval_list = [ return [
e e
for e in moduleimpl.evaluations for e in moduleimpl.evaluations
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
] ]
if eval_list:
return eval_list[0]
return None
def get_evaluation_session2(self, moduleimpl: ModuleImpl) -> Evaluation | None: def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas. """Les évaluations de deuxième session de ce module, ou None s'il n'en a pas.
Session 2: remplace la note de moyenne des autres évals. La moyenne des notes de Session 2 remplace la note de moyenne des autres évals.
""" """
eval_list = [ return [
e e
for e in moduleimpl.evaluations for e in moduleimpl.evaluations
if e.evaluation_type == Evaluation.EVALUATION_SESSION2 if e.evaluation_type == Evaluation.EVALUATION_SESSION2
] ]
if eval_list:
return eval_list[0]
return None
def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]: def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]:
"""Les évaluations bonus de ce module, ou liste vide s'il n'en a pas.""" """Les évaluations bonus de ce module, ou liste vide s'il n'en a pas."""
@ -344,12 +360,13 @@ class ModuleImplResultsAPC(ModuleImplResults):
"Calcul des moyennes de modules à la mode BUT" "Calcul des moyennes de modules à la mode BUT"
def compute_module_moy( def compute_module_moy(
self, self, evals_poids_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
evals_poids_df: pd.DataFrame,
) -> pd.DataFrame: ) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module """Calcule les moyennes des étudiants dans ce module
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs Argument:
evals_poids: DataFrame, colonnes: UEs, lignes: EVALs
modimpl_coefs_df: DataFrame, colonnes: modimpl_id, lignes: ue_id
Résultat: DataFrame, colonnes UE, lignes etud Résultat: DataFrame, colonnes UE, lignes etud
= la note de l'étudiant dans chaque UE pour ce module. = la note de l'étudiant dans chaque UE pour ce module.
@ -370,6 +387,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
return pd.DataFrame(index=[], columns=evals_poids_df.columns) return pd.DataFrame(index=[], columns=evals_poids_df.columns)
if nb_ues == 0: if nb_ues == 0:
return pd.DataFrame(index=self.evals_notes.index, columns=[]) return pd.DataFrame(index=self.evals_notes.index, columns=[])
# coefs des évals complètes normales (pas rattr., session 2 ni bonus):
evals_coefs = self.get_evaluations_coefs(modimpl) evals_coefs = self.get_evaluations_coefs(modimpl)
evals_poids = evals_poids_df.values * evals_coefs evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues) # -> evals_poids shape : (nb_evals, nb_ues)
@ -398,6 +416,47 @@ class ModuleImplResultsAPC(ModuleImplResults):
) / np.sum(evals_poids_etuds, axis=1) ) / np.sum(evals_poids_etuds, axis=1)
# etuds_moy_module shape: nb_etuds x nb_ues # etuds_moy_module shape: nb_etuds x nb_ues
evals_session2 = self.get_evaluations_session2(modimpl)
evals_rat = self.get_evaluations_rattrapage(modimpl)
if evals_session2:
# Session2 : quand elle existe, remplace la note de module
# Calcul moyenne notes session2 et remplace (si la note session 2 existe)
etuds_moy_module_s2 = self._compute_moy_special(
modimpl,
evals_notes_stacked,
evals_poids_df,
Evaluation.EVALUATION_SESSION2,
)
# Vrai si toutes les UEs avec coef non nul ont bien une note de session 2 calculée:
mod_coefs = modimpl_coefs_df[modimpl.id]
etuds_use_session2 = np.all(
np.isfinite(etuds_moy_module_s2[:, mod_coefs != 0]), axis=1
)
etuds_moy_module = np.where(
etuds_use_session2[:, np.newaxis],
etuds_moy_module_s2,
etuds_moy_module,
)
self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index
)
elif evals_rat:
etuds_moy_module_rat = self._compute_moy_special(
modimpl,
evals_notes_stacked,
evals_poids_df,
Evaluation.EVALUATION_RATTRAPAGE,
)
etuds_ue_use_rattrapage = (
etuds_moy_module_rat > etuds_moy_module
) # etud x UE
etuds_moy_module = np.where(
etuds_ue_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
)
self.etuds_use_rattrapage = pd.Series(
np.any(etuds_ue_use_rattrapage, axis=1), index=self.evals_notes.index
)
# Application des évaluations bonus: # Application des évaluations bonus:
etuds_moy_module = self.apply_bonus( etuds_moy_module = self.apply_bonus(
etuds_moy_module, etuds_moy_module,
@ -405,47 +464,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
evals_poids_df, evals_poids_df,
evals_notes_stacked, evals_notes_stacked,
) )
# Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl)
if eval_session2:
notes_session2 = self.evals_notes[eval_session2.id].values
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
etuds_moy_module = np.where(
etuds_use_session2[:, np.newaxis],
np.tile(
(notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis],
nb_ues,
),
etuds_moy_module,
)
self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index
)
else:
# Rattrapage: remplace la note de module ssi elle est supérieure
eval_rat = self.get_evaluation_rattrapage(modimpl)
if eval_rat:
notes_rat = self.evals_notes[eval_rat.id].values
# remplace les notes invalides (ATT, EXC...) par des NaN
notes_rat = np.where(
notes_rat > scu.NOTES_ABSENCE,
notes_rat / (eval_rat.note_max / 20.0),
np.nan,
)
# "Étend" le rattrapage sur les UE: la note de rattrapage est la même
# pour toutes les UE mais ne remplace que là où elle est supérieure
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1)
# prend le max
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
etuds_moy_module = np.where(
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
)
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
)
self.etuds_moy_module = pd.DataFrame( self.etuds_moy_module = pd.DataFrame(
etuds_moy_module, etuds_moy_module,
index=self.evals_notes.index, index=self.evals_notes.index,
@ -453,6 +471,34 @@ class ModuleImplResultsAPC(ModuleImplResults):
) )
return self.etuds_moy_module return self.etuds_moy_module
def _compute_moy_special(
self,
modimpl: ModuleImpl,
evals_notes_stacked: np.array,
evals_poids_df: pd.DataFrame,
evaluation_type: int,
) -> np.array:
"""Calcul moyenne APC sur évals rattrapage ou session2"""
nb_etuds = self.evals_notes.shape[0]
nb_ues = evals_poids_df.shape[1]
evals_coefs_s2 = self.get_evaluations_special_coefs(
modimpl, evaluation_type=evaluation_type
)
evals_poids_s2 = evals_poids_df.values * evals_coefs_s2
poids_stacked_s2 = np.stack(
[evals_poids_s2] * nb_etuds
) # nb_etuds, nb_evals, nb_ues
evals_poids_etuds_s2 = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked_s2,
0,
)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module_s2 = np.sum(
evals_poids_etuds_s2 * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds_s2, axis=1)
return etuds_moy_module_s2
def apply_bonus( def apply_bonus(
self, self,
etuds_moy_module: pd.DataFrame, etuds_moy_module: pd.DataFrame,
@ -525,6 +571,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
return evals_poids, ues return evals_poids, ues
# appelé par ModuleImpl.check_apc_conformity()
def moduleimpl_is_conforme( def moduleimpl_is_conforme(
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
) -> bool: ) -> bool:
@ -546,12 +593,12 @@ def moduleimpl_is_conforme(
if len(modimpl_coefs_df) != nb_ues: if len(modimpl_coefs_df) != nb_ues:
# il arrive (#bug) que le cache ne soit pas à jour... # il arrive (#bug) que le cache ne soit pas à jour...
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent") return app.critical_error("moduleimpl_is_conforme: err 1")
if moduleimpl.id not in modimpl_coefs_df: if moduleimpl.id not in modimpl_coefs_df:
# soupçon de bug cache coef ? # soupçon de bug cache coef ?
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
raise ScoBugCatcher("Erreur 454 - merci de ré-essayer") return app.critical_error("moduleimpl_is_conforme: err 2")
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0 module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids)) return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
@ -593,46 +640,43 @@ class ModuleImplResultsClassic(ModuleImplResults):
evals_coefs_etuds * evals_notes_20, axis=1 evals_coefs_etuds * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds, axis=1) ) / np.sum(evals_coefs_etuds, axis=1)
# Application des évaluations bonus: evals_session2 = self.get_evaluations_session2(modimpl)
etuds_moy_module = self.apply_bonus( evals_rat = self.get_evaluations_rattrapage(modimpl)
etuds_moy_module, if evals_session2:
modimpl,
evals_notes_20,
)
# Session2 : quand elle existe, remplace la note de module # Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl) # Calcule la moyenne des évaluations de session2
if eval_session2: etuds_moy_module_s2 = self._compute_moy_special(
notes_session2 = self.evals_notes[eval_session2.id].values modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN) )
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
etuds_moy_module = np.where( etuds_moy_module = np.where(
etuds_use_session2, etuds_use_session2,
notes_session2 / (eval_session2.note_max / 20.0), etuds_moy_module_s2,
etuds_moy_module, etuds_moy_module,
) )
self.etuds_use_session2 = pd.Series( self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index etuds_use_session2, index=self.evals_notes.index
) )
else: elif evals_rat:
# Rattrapage: remplace la note de module ssi elle est supérieure # Rattrapage: remplace la note de module ssi elle est supérieure
eval_rat = self.get_evaluation_rattrapage(modimpl) # Calcule la moyenne des évaluations de rattrapage
if eval_rat: etuds_moy_module_rat = self._compute_moy_special(
notes_rat = self.evals_notes[eval_rat.id].values modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
# remplace les notes invalides (ATT, EXC...) par des NaN
notes_rat = np.where(
notes_rat > scu.NOTES_ABSENCE,
notes_rat / (eval_rat.note_max / 20.0),
np.nan,
) )
# prend le max etuds_use_rattrapage = etuds_moy_module_rat > etuds_moy_module
etuds_use_rattrapage = notes_rat > etuds_moy_module
etuds_moy_module = np.where( etuds_moy_module = np.where(
etuds_use_rattrapage, notes_rat, etuds_moy_module etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
) )
self.etuds_use_rattrapage = pd.Series( self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index etuds_use_rattrapage, index=self.evals_notes.index
) )
# Application des évaluations bonus:
etuds_moy_module = self.apply_bonus(
etuds_moy_module,
modimpl,
evals_notes_20,
)
self.etuds_moy_module = pd.Series( self.etuds_moy_module = pd.Series(
etuds_moy_module, etuds_moy_module,
index=self.evals_notes.index, index=self.evals_notes.index,
@ -640,6 +684,28 @@ class ModuleImplResultsClassic(ModuleImplResults):
return self.etuds_moy_module return self.etuds_moy_module
def _compute_moy_special(
self, modimpl: ModuleImpl, evals_notes_20: np.array, evaluation_type: int
) -> np.array:
"""Calcul moyenne sur évals rattrapage ou session2"""
# n'utilise que les notes valides et ABS (0).
# Même calcul que pour les évals normales, mais avec seulement les
# coefs des évals de session 2 ou rattrapage:
nb_etuds = self.evals_notes.shape[0]
evals_coefs = self.get_evaluations_special_coefs(
modimpl, evaluation_type=evaluation_type
).reshape(-1)
coefs_stacked = np.stack([evals_coefs] * nb_etuds)
# zéro partout sauf si une note ou ABS:
evals_coefs_etuds = np.where(
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0
)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum(
evals_coefs_etuds * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds, axis=1)
return etuds_moy_module # array 1d (nb_etuds)
def apply_bonus( def apply_bonus(
self, self,
etuds_moy_module: np.ndarray, etuds_moy_module: np.ndarray,

View File

@ -183,7 +183,9 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
return modimpls_notes.swapaxes(0, 1) return modimpls_notes.swapaxes(0, 1)
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: def notes_sem_load_cube(
formsemestre: FormSemestre, modimpl_coefs_df: pd.DataFrame
) -> tuple:
"""Construit le "cube" (tenseur) des notes du semestre. """Construit le "cube" (tenseur) des notes du semestre.
Charge toutes les notes (sql), calcule les moyennes des modules Charge toutes les notes (sql), calcule les moyennes des modules
et assemble le cube. et assemble le cube.
@ -207,8 +209,8 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
etudids, etudids_actifs = formsemestre.etudids_actifs() etudids, etudids_actifs = formsemestre.etudids_actifs()
for modimpl in formsemestre.modimpls_sorted: for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs) mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id) evals_poids = modimpl.get_evaluations_poids()
etuds_moy_module = mod_results.compute_module_moy(evals_poids) etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
modimpls_results[modimpl.id] = mod_results modimpls_results[modimpl.id] = mod_results
modimpls_evals_poids[modimpl.id] = evals_poids modimpls_evals_poids[modimpl.id] = evals_poids
modimpls_notes.append(etuds_moy_module) modimpls_notes.append(etuds_moy_module)

View File

@ -59,16 +59,17 @@ class ResultatsSemestreBUT(NotesTableCompat):
def compute(self): def compute(self):
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen." "Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
)
( (
self.sem_cube, self.sem_cube,
self.modimpls_evals_poids, self.modimpls_evals_poids,
self.modimpls_results, self.modimpls_results,
) = moy_ue.notes_sem_load_cube(self.formsemestre) ) = moy_ue.notes_sem_load_cube(self.formsemestre, self.modimpl_coefs_df)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
)
# l'idx de la colonne du mod modimpl.id est # l'idx de la colonne du mod modimpl.id est
# modimpl_coefs_df.columns.get_loc(modimpl.id) # modimpl_coefs_df.columns.get_loc(modimpl.id)
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)

View File

@ -242,7 +242,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
) )
}">saisir le coefficient de cette UE avant de continuer</a></p> }">saisir le coefficient de cette UE avant de continuer</a></p>
</div> </div>
""" """,
safe=True,
) )

View File

@ -518,7 +518,8 @@ class ResultatsSemestre(ResultatsCache):
Corrigez ou faite corriger le programme Corrigez ou faite corriger le programme
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=ue_capitalized.formation_id)}">via cette page</a>. formation_id=ue_capitalized.formation_id)}">via cette page</a>.
""" """,
safe=True,
) )
else: else:
# Coefs de l'UE capitalisée en formation classique: # Coefs de l'UE capitalisée en formation classique:

View File

@ -9,9 +9,9 @@ import datetime
from threading import Thread from threading import Thread
from flask import current_app, g from flask import current_app, g
from flask_mail import Message from flask_mail import BadHeaderError, Message
from app import mail from app import log, mail
from app.models.departements import Departement from app.models.departements import Departement
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -20,7 +20,15 @@ from app.scodoc import sco_preferences
def send_async_email(app, msg): def send_async_email(app, msg):
"Send an email, async" "Send an email, async"
with app.app_context(): with app.app_context():
try:
mail.send(msg) mail.send(msg)
except BadHeaderError:
log(
f"""send_async_email: BadHeaderError
msg={msg}
"""
)
raise
def send_email( def send_email(

View File

@ -338,9 +338,11 @@ def add_entreprise():
if form.validate_on_submit(): if form.validate_on_submit():
entreprise = Entreprise( entreprise = Entreprise(
nom=form.nom_entreprise.data.strip(), nom=form.nom_entreprise.data.strip(),
siret=form.siret.data.strip() siret=(
form.siret.data.strip()
if form.siret.data.strip() if form.siret.data.strip()
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}", # siret provisoire else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}"
), # siret provisoire
siret_provisoire=False if form.siret.data.strip() else True, siret_provisoire=False if form.siret.data.strip() else True,
association=form.association.data, association=form.association.data,
adresse=form.adresse.data.strip(), adresse=form.adresse.data.strip(),
@ -352,7 +354,7 @@ def add_entreprise():
db.session.add(entreprise) db.session.add(entreprise)
db.session.commit() db.session.commit()
db.session.refresh(entreprise) db.session.refresh(entreprise)
except: except Exception:
db.session.rollback() db.session.rollback()
flash("Une erreur est survenue veuillez réessayer.") flash("Une erreur est survenue veuillez réessayer.")
return render_template( return render_template(
@ -804,9 +806,9 @@ def add_offre(entreprise_id):
missions=form.missions.data.strip(), missions=form.missions.data.strip(),
duree=form.duree.data.strip(), duree=form.duree.data.strip(),
expiration_date=form.expiration_date.data, expiration_date=form.expiration_date.data,
correspondant_id=form.correspondant.data correspondant_id=(
if form.correspondant.data != "" form.correspondant.data if form.correspondant.data != "" else None
else None, ),
) )
db.session.add(offre) db.session.add(offre)
db.session.commit() db.session.commit()
@ -1328,9 +1330,11 @@ def add_contact(entreprise_id):
).first_or_404(description=f"entreprise {entreprise_id} inconnue") ).first_or_404(description=f"entreprise {entreprise_id} inconnue")
form = ContactCreationForm( form = ContactCreationForm(
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}", date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
utilisateur=f"{current_user.nom} {current_user.prenom} ({current_user.user_name})" utilisateur=(
f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
if current_user.nom and current_user.prenom if current_user.nom and current_user.prenom
else "", else ""
),
) )
if request.method == "POST" and form.cancel.data: if request.method == "POST" and form.cancel.data:
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise_id)) return redirect(url_for("entreprises.contacts", entreprise_id=entreprise_id))
@ -1496,9 +1500,9 @@ def add_stage_apprentissage(entreprise_id):
date_debut=form.date_debut.data, date_debut=form.date_debut.data,
date_fin=form.date_fin.data, date_fin=form.date_fin.data,
formation_text=formation.formsemestre.titre if formation else None, formation_text=formation.formsemestre.titre if formation else None,
formation_scodoc=formation.formsemestre.formsemestre_id formation_scodoc=(
if formation formation.formsemestre.formsemestre_id if formation else None
else None, ),
notes=form.notes.data.strip(), notes=form.notes.data.strip(),
) )
db.session.add(stage_apprentissage) db.session.add(stage_apprentissage)
@ -1802,7 +1806,7 @@ def import_donnees():
db.session.add(entreprise) db.session.add(entreprise)
db.session.commit() db.session.commit()
db.session.refresh(entreprise) db.session.refresh(entreprise)
except: except Exception:
db.session.rollback() db.session.rollback()
flash("Une erreur est survenue veuillez réessayer.") flash("Une erreur est survenue veuillez réessayer.")
return render_template( return render_template(

View File

@ -0,0 +1,66 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""
Formulaire création de ticket de bug
"""
from flask_wtf import FlaskForm
from wtforms import SubmitField, validators
from wtforms.fields.simple import StringField, TextAreaField, BooleanField
from app.scodoc import sco_preferences
class CreateBugReport(FlaskForm):
"""Formulaire permettant la création d'un ticket de bug"""
title = StringField(
label="Titre du ticket",
validators=[
validators.DataRequired("titre du ticket requis"),
],
)
message = TextAreaField(
label="Message",
id="ticket_message",
validators=[
validators.DataRequired("message du ticket requis"),
],
)
etab = StringField(label="Etablissement")
include_dump = BooleanField(
"""Inclure une copie anonymisée de la base de données ?
Ces données faciliteront le traitement du problème et resteront strictement confidentielles.
""",
default=False,
)
submit = SubmitField("Envoyer")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
def __init__(self, *args, **kwargs):
super(CreateBugReport, self).__init__(*args, **kwargs)
self.etab.data = sco_preferences.get_preference("InstituteName") or ""

View File

@ -353,12 +353,12 @@ class Assiduite(ScoDocModel):
elif self.external_data is not None and "module" in self.external_data: elif self.external_data is not None and "module" in self.external_data:
return ( return (
"Tout module" "Autre module (pas dans la liste)"
if self.external_data["module"] == "Autre" if self.external_data["module"] == "Autre"
else self.external_data["module"] else self.external_data["module"]
) )
return "Non spécifié" if traduire else None return "Module non spécifié" if traduire else None
def get_saisie(self) -> str: def get_saisie(self) -> str:
""" """

View File

@ -274,6 +274,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return "type_departement mismatch" return "type_departement mismatch"
# Table d'équivalences entre refs: # Table d'équivalences entre refs:
equiv = self._load_config_equivalences() equiv = self._load_config_equivalences()
# Même specialité (ou alias) ?
if self.specialite != other.specialite and other.specialite not in equiv.get(
"alias", []
):
return "specialite mismatch"
# mêmes parcours ? # mêmes parcours ?
eq_parcours = equiv.get("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_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
@ -317,6 +322,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
def _load_config_equivalences(self) -> dict: def _load_config_equivalences(self) -> dict:
"""Load config file ressources/referentiels/equivalences.yaml """Load config file ressources/referentiels/equivalences.yaml
used to define equivalences between distinct referentiels used to define equivalences between distinct referentiels
return a dict, with optional keys:
alias: list of equivalent names for speciality (eg SD == STID)
parcours: dict with equivalent parcours acronyms
""" """
try: try:
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f: with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:

View File

@ -199,6 +199,11 @@ class Identite(models.ScoDocModel):
@classmethod @classmethod
def get_etud(cls, etudid: int) -> "Identite": def get_etud(cls, etudid: int) -> "Identite":
"""Etudiant ou 404, cherche uniquement dans le département courant""" """Etudiant ou 404, cherche uniquement dans le département courant"""
if not isinstance(etudid, int):
try:
etudid = int(etudid)
except (TypeError, ValueError):
abort(404, "etudid invalide")
if g.scodoc_dept: if g.scodoc_dept:
return cls.query.filter_by( return cls.query.filter_by(
id=etudid, dept_id=g.scodoc_dept_id id=etudid, dept_id=g.scodoc_dept_id
@ -299,9 +304,10 @@ class Identite(models.ScoDocModel):
@property @property
def nomprenom(self, reverse=False) -> str: def nomprenom(self, reverse=False) -> str:
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont" """DEPRECATED
Civilité/prénom/nom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité. Si reverse, "Dupont Pierre", sans civilité.
Prend l'identité courant et non celle de l'état civile si elles diffèrent. Prend l'identité courante et non celle de l'état civil si elles diffèrent.
""" """
nom = self.nom_usuel or self.nom nom = self.nom_usuel or self.nom
prenom = self.prenom_str prenom = self.prenom_str
@ -309,6 +315,12 @@ class Identite(models.ScoDocModel):
return f"{nom} {prenom}".strip() return f"{nom} {prenom}".strip()
return f"{self.civilite_str} {prenom} {nom}".strip() return f"{self.civilite_str} {prenom} {nom}".strip()
def nom_prenom(self) -> str:
"""Civilite NOM Prénom
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
"""
return f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()} {self.prenom_str}"
@property @property
def prenom_str(self): def prenom_str(self):
"""Prénom à afficher. Par exemple: "Jean-Christophe" """ """Prénom à afficher. Par exemple: "Jean-Christophe" """
@ -551,7 +563,7 @@ class Identite(models.ScoDocModel):
.all() .all()
) )
def inscription_courante(self): def inscription_courante(self) -> "FormSemestreInscription | None":
"""La première inscription à un formsemestre _actuellement_ en cours. """La première inscription à un formsemestre _actuellement_ en cours.
None s'il n'y en a pas (ou plus, ou pas encore). None s'il n'y en a pas (ou plus, ou pas encore).
""" """

View File

@ -71,6 +71,15 @@ class Evaluation(models.ScoDocModel):
EVALUATION_BONUS, EVALUATION_BONUS,
} }
def type_abbrev(self) -> str:
"Le nom abrégé du type de cette éval."
return {
self.EVALUATION_NORMALE: "std",
self.EVALUATION_RATTRAPAGE: "rattrapage",
self.EVALUATION_SESSION2: "session 2",
self.EVALUATION_BONUS: "bonus",
}.get(self.evaluation_type, "?")
def __repr__(self): def __repr__(self):
return f"""<Evaluation {self.id} { return f"""<Evaluation {self.id} {
self.date_debut.isoformat() if self.date_debut else ''} "{ self.date_debut.isoformat() if self.date_debut else ''} "{
@ -417,12 +426,13 @@ class Evaluation(models.ScoDocModel):
return modified return modified
def set_ue_poids(self, ue, poids: float) -> None: def set_ue_poids(self, ue, poids: float) -> None:
"""Set poids évaluation vers cette UE""" """Set poids évaluation vers cette UE. Commit."""
self.update_ue_poids_dict({ue.id: poids}) self.update_ue_poids_dict({ue.id: poids})
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None: def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
"""set poids vers les UE (remplace existants) """set poids vers les UE (remplace existants)
ue_poids_dict = { ue_id : poids } ue_poids_dict = { ue_id : poids }
Commit session.
""" """
from app.models.ues import UniteEns from app.models.ues import UniteEns
@ -432,9 +442,12 @@ class Evaluation(models.ScoDocModel):
if ue is None: if ue is None:
raise ScoValueError("poids vers une UE inexistante") raise ScoValueError("poids vers une UE inexistante")
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids) ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
L.append(ue_poids)
db.session.add(ue_poids) db.session.add(ue_poids)
L.append(ue_poids)
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
db.session.commit()
self.moduleimpl.invalidate_evaluations_poids() # inval cache self.moduleimpl.invalidate_evaluations_poids() # inval cache
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None: def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:

View File

@ -6,6 +6,7 @@ from flask import abort, g
from flask_login import current_user from flask_login import current_user
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
import app
from app import db from app import db
from app.auth.models import User from app.auth.models import User
from app.comp import df_cache from app.comp import df_cache
@ -78,7 +79,9 @@ class ModuleImpl(ScoDocModel):
] or self.module.get_edt_ids() ] or self.module.get_edt_ids()
def get_evaluations_poids(self) -> pd.DataFrame: def get_evaluations_poids(self) -> pd.DataFrame:
"""Les poids des évaluations vers les UE (accès via cache)""" """Les poids des évaluations vers les UEs (accès via cache redis).
Toutes les évaluations sont considérées (normales, bonus, rattr., etc.)
"""
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id) evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
if evaluations_poids is None: if evaluations_poids is None:
from app.comp import moy_mod from app.comp import moy_mod
@ -108,20 +111,37 @@ class ModuleImpl(ScoDocModel):
"""Invalide poids cachés""" """Invalide poids cachés"""
df_cache.EvaluationsPoidsCache.delete(self.id) df_cache.EvaluationsPoidsCache.delete(self.id)
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool: def check_apc_conformity(
"""true si les poids des évaluations du module permettent de satisfaire self, res: "ResultatsSemestreBUT", evaluation_type=Evaluation.EVALUATION_NORMALE
les coefficients du PN. ) -> bool:
"""true si les poids des évaluations du type indiqué (normales par défaut)
du module permettent de satisfaire les coefficients du PN.
""" """
# appelé par formsemestre_status, liste notes, et moduleimpl_status
if not self.module.formation.get_cursus().APC_SAE or ( if not self.module.formation.get_cursus().APC_SAE or (
self.module.module_type != scu.ModuleType.RESSOURCE self.module.module_type
and self.module.module_type != scu.ModuleType.SAE not in {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
): ):
return True # Non BUT, toujours conforme return True # Non BUT, toujours conforme
from app.comp import moy_mod from app.comp import moy_mod
mod_results = res.modimpls_results.get(self.id)
if mod_results is None:
app.critical_error("check_apc_conformity: err 1")
selected_evaluations_ids = [
eval_id
for eval_id, eval_type in mod_results.evals_type.items()
if eval_type == evaluation_type
]
if not selected_evaluations_ids:
return True # conforme si pas d'évaluations
selected_evaluations_poids = self.get_evaluations_poids().loc[
selected_evaluations_ids
]
return moy_mod.moduleimpl_is_conforme( return moy_mod.moduleimpl_is_conforme(
self, self,
self.get_evaluations_poids(), selected_evaluations_poids,
res.modimpl_coefs_df, res.modimpl_coefs_df,
) )

View File

@ -340,6 +340,21 @@ class Module(models.ScoDocModel):
# Liste seulement les coefs définis: # Liste seulement les coefs définis:
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()] return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
def get_ue_coefs_descr(self) -> str:
"""Description des coefficients vers les UEs (APC)"""
coefs_descr = ", ".join(
[
f"{ue.acronyme}: {co}"
for ue, co in self.ue_coefs_list()
if isinstance(co, float) and co > 0
]
)
if coefs_descr:
descr = "Coefs: " + coefs_descr
else:
descr = "(pas de coefficients) "
return descr
def get_codes_apogee(self) -> set[str]: def get_codes_apogee(self) -> set[str]:
"""Les codes Apogée (codés en base comme "VRT1,VRT2")""" """Les codes Apogée (codés en base comme "VRT1,VRT2")"""
if self.code_apogee: if self.code_apogee:

View File

@ -432,6 +432,7 @@ class UniteEns(models.ScoDocModel):
self.niveau_competence, parcours self.niveau_competence, parcours
) )
if not ok: if not ok:
self.formation.invalidate_cached_sems()
self.niveau_competence = prev_niveau # restore self.niveau_competence = prev_niveau # restore
return False, error_message return False, error_message

View File

@ -176,6 +176,7 @@ class GenTable:
self.xml_link = xml_link self.xml_link = xml_link
# HTML parameters: # HTML parameters:
if not table_id: # random id if not table_id: # random id
log("Warning: GenTable() called without table_id")
self.table_id = "gt_" + str(random.randint(0, 1000000)) self.table_id = "gt_" + str(random.randint(0, 1000000))
else: else:
self.table_id = table_id self.table_id = table_id

View File

@ -25,8 +25,7 @@
# #
############################################################################## ##############################################################################
"""HTML Header/Footer for ScoDoc pages """HTML Header/Footer for ScoDoc pages"""
"""
import html import html
@ -101,7 +100,7 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script> <script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script> <script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script> <script>
window.onload=function(){{enableTooltips("gtrcontent")}}; window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
</script> </script>
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script> <script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
@ -218,7 +217,7 @@ def sco_header(
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script> <script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script> <script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script> <script>
window.onload=function(){{enableTooltips("gtrcontent")}}; window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}"; const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
const SCO_TIMEZONE="{scu.TIME_ZONE}"; const SCO_TIMEZONE="{scu.TIME_ZONE}";

View File

@ -28,6 +28,7 @@
""" """
Génération de la "sidebar" (marge gauche des pages HTML) Génération de la "sidebar" (marge gauche des pages HTML)
""" """
from flask import render_template, url_for from flask import render_template, url_for
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
@ -151,7 +152,7 @@ def sidebar(etudid: int = None):
H = [ H = [
f""" f"""
<!-- sidebar py --> <!-- sidebar py -->
<div class="sidebar"> <div class="sidebar" id="sidebar">
{ sidebar_common() } { sidebar_common() }
<div class="box-chercheetud">Chercher étudiant:<br> <div class="box-chercheetud">Chercher étudiant:<br>
<form method="get" id="form-chercheetud" <form method="get" id="form-chercheetud"
@ -193,7 +194,7 @@ def sidebar(etudid: int = None):
formsemestre.date_debut.strftime(scu.DATE_FMT) formsemestre.date_debut.strftime(scu.DATE_FMT)
} au { } au {
formsemestre.date_fin.strftime(scu.DATE_FMT) formsemestre.date_fin.strftime(scu.DATE_FMT)
}">({ }" data-tooltip>({
sco_preferences.get_preference("assi_metrique", None)}) sco_preferences.get_preference("assi_metrique", None)})
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>""" <br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
) )
@ -227,12 +228,9 @@ def sidebar(etudid: int = None):
<li><a href="{ url_for('assiduites.calendrier_assi_etud', <li><a href="{ url_for('assiduites.calendrier_assi_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid) scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Calendrier</a></li> }">Calendrier</a></li>
<li><a href="{ url_for('assiduites.liste_assiduites_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Liste</a></li>
<li><a href="{ url_for('assiduites.bilan_etud', <li><a href="{ url_for('assiduites.bilan_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid) scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Bilan</a></li> }" title="Les pages bilan et liste ont été fusionnées">Liste/Bilan</a></li>
</ul> </ul>
""" """
) )

View File

@ -157,5 +157,6 @@ def table_billets(
rows=rows, rows=rows,
html_sortable=True, html_sortable=True,
html_class="table_leftalign", html_class="table_leftalign",
table_id="table_billets",
) )
return tab return tab

View File

@ -288,6 +288,7 @@ def apo_table_compare_etud_results(A, B):
html_class="table_leftalign", html_class="table_leftalign",
html_with_td_classes=True, html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="apo_table_compare_etud_results",
) )
return T return T

View File

@ -917,6 +917,7 @@ class ApoData:
columns_ids=columns_ids, columns_ids=columns_ids,
titles=dict(zip(columns_ids, columns_ids)), titles=dict(zip(columns_ids, columns_ids)),
rows=rows, rows=rows,
table_id="build_cr_table",
xls_sheet_name="Decisions ScoDoc", xls_sheet_name="Decisions ScoDoc",
) )
return T return T
@ -969,6 +970,7 @@ class ApoData:
"rcue": "RCUE", "rcue": "RCUE",
}, },
rows=rows, rows=rows,
table_id="adsup_table",
xls_sheet_name="ADSUPs", xls_sheet_name="ADSUPs",
) )
@ -1054,6 +1056,7 @@ def nar_etuds_table(apo_data, nar_etuds):
columns_ids=columns_ids, columns_ids=columns_ids,
titles=dict(zip(columns_ids, columns_ids)), titles=dict(zip(columns_ids, columns_ids)),
rows=rows, rows=rows,
table_id="nar_etuds_table",
xls_sheet_name="NAR ScoDoc", xls_sheet_name="NAR ScoDoc",
) )
return table.excel() return table.excel()

View File

@ -0,0 +1,102 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Rapport de bug ScoDoc
Permet de créer un rapport de bug (ticket) sur la plateforme git scodoc.org.
Le principe est le suivant:
1- Si l'utilisateur le demande, on dump la base de données et on l'envoie
2- ScoDoc envoie une requête POST à scodoc.org pour qu'un ticket git soit créé avec les
informations fournies par l'utilisateur + quelques métadonnées.
"""
from flask import g
from flask_login import current_user
import requests
import app.scodoc.sco_utils as scu
import sco_version
from app import log
from app.scodoc.sco_dump_db import sco_dump_and_send_db
from app.scodoc.sco_exceptions import ScoValueError
def sco_bug_report(
title: str = "", message: str = "", etab: str = "", include_dump: bool = False
) -> requests.Response:
"""Envoi d'un bug report (ticket)"""
dump_id = None
if include_dump:
dump = sco_dump_and_send_db()
try:
dump_id = dump.json()["dump_id"]
except (requests.exceptions.JSONDecodeError, KeyError):
dump_id = "inconnu (erreur)"
log(f"sco_bug_report: {scu.SCO_BUG_REPORT_URL} by {current_user.user_name}")
try:
r = requests.post(
scu.SCO_BUG_REPORT_URL,
json={
"ticket": {
"title": title,
"message": message,
"etab": etab,
"dept": getattr(g, "scodoc_dept", "-"),
},
"user": {
"name": current_user.get_nomcomplet(),
"email": current_user.email,
},
"dump": {
"included": include_dump,
"id": dump_id,
},
"scodoc": {
"version": sco_version.SCOVERSION,
},
},
timeout=scu.SCO_ORG_TIMEOUT,
)
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc:
log("ConnectionError: Impossible de joindre le serveur d'assistance")
raise ScoValueError(
"""
Impossible de joindre le serveur d'assistance (scodoc.org).
Veuillez contacter le service informatique de votre établissement pour
corriger la configuration de ScoDoc. Dans la plupart des cas, il
s'agit d'un proxy mal configuré.
"""
) from exc
return r

View File

@ -226,6 +226,7 @@ class BulletinGenerator:
server_name=self.server_name, server_name=self.server_name,
filigranne=self.filigranne, filigranne=self.filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
with_page_numbers=self.multi_pages,
) )
) )
try: try:

View File

@ -106,6 +106,7 @@ def assemble_bulletins_pdf(
pagesbookmarks=pagesbookmarks, pagesbookmarks=pagesbookmarks,
filigranne=filigranne, filigranne=filigranne,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
with_page_numbers=False, # on ne veut pas de no de pages sur les bulletins imprimés en masse
) )
) )
document.multiBuild(story) document.multiBuild(story)
@ -122,7 +123,8 @@ def replacement_function(match) -> str:
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4)) return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
raise ScoValueError( raise ScoValueError(
'balise "%s": logo "%s" introuvable' 'balise "%s": logo "%s" introuvable'
% (pydoc.html.escape(balise), pydoc.html.escape(name)) % (pydoc.html.escape(balise), pydoc.html.escape(name)),
safe=True,
) )

View File

@ -114,6 +114,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
html_class="notes_bulletin", html_class="notes_bulletin",
html_class_ignore_default=True, html_class_ignore_default=True,
html_with_td_classes=True, html_with_td_classes=True,
table_id="std_bul_table",
) )
return T.gen(fmt=fmt) return T.gen(fmt=fmt)

View File

@ -274,6 +274,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
"""expire cache pour un semestre (ou tous ceux du département si formsemestre_id non spécifié). """expire cache pour un semestre (ou tous ceux du département si formsemestre_id non spécifié).
Si pdfonly, n'expire que les bulletins pdf cachés. Si pdfonly, n'expire que les bulletins pdf cachés.
""" """
from app.comp import df_cache
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cursus from app.scodoc import sco_cursus
@ -315,12 +316,14 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
and fid in g.formsemestre_results_cache and fid in g.formsemestre_results_cache
): ):
del g.formsemestre_results_cache[fid] del g.formsemestre_results_cache[fid]
df_cache.EvaluationsPoidsCache.invalidate_sem(formsemestre_id)
else: else:
# optimization when we invalidate all evaluations: # optimization when we invalidate all evaluations:
EvaluationCache.invalidate_all_sems() EvaluationCache.invalidate_all_sems()
df_cache.EvaluationsPoidsCache.invalidate_all()
if hasattr(g, "formsemestre_results_cache"): if hasattr(g, "formsemestre_results_cache"):
del g.formsemestre_results_cache del g.formsemestre_results_cache
SemInscriptionsCache.delete_many(formsemestre_ids) SemInscriptionsCache.delete_many(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids) ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids) ValidationsSemestreCache.delete_many(formsemestre_ids)

View File

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

View File

@ -141,6 +141,7 @@ def formsemestre_table_estim_cost(
""", """,
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""", origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
filename=f"EstimCout-S{formsemestre.semestre_id}", filename=f"EstimCout-S{formsemestre.semestre_id}",
table_id="formsemestre_table_estim_cost",
) )
return tab return tab

View File

@ -350,11 +350,13 @@ class SituationEtudCursusClassic(SituationEtudCursus):
l'étudiant (quelle que soit la formation), le plus ancien en tête""" l'étudiant (quelle que soit la formation), le plus ancien en tête"""
return self.sems return self.sems
def get_cursus_descr(self, filter_futur=False): def get_cursus_descr(self, filter_futur=False, filter_formation_code=False):
"""Description brève du parcours: "S1, S2, ..." """Description brève du parcours: "S1, S2, ..."
Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant. Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
Si filter_formation_code, restreint aux semestres de même code formation que le courant.
""" """
cur_begin_date = self.sem["dateord"] cur_begin_date = self.sem["dateord"]
cur_formation_code = self.sem["formation_code"]
p = [] p = []
for s in self.sems: for s in self.sems:
if s["ins"]["etat"] == scu.DEMISSION: if s["ins"]["etat"] == scu.DEMISSION:
@ -363,12 +365,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
dem = "" dem = ""
if filter_futur and s["dateord"] > cur_begin_date: if filter_futur and s["dateord"] > cur_begin_date:
continue # skip semestres demarrant apres le courant continue # skip semestres demarrant apres le courant
SA = self.parcours.SESSION_ABBRV # 'S' ou 'A' if filter_formation_code and s["formation_code"] != cur_formation_code:
continue # restreint aux semestres de la formation courante (pour les PV)
session_abbrv = self.parcours.SESSION_ABBRV # 'S' ou 'A'
if s["semestre_id"] < 0: if s["semestre_id"] < 0:
SA = "A" # force, cas des DUT annuels par exemple session_abbrv = "A" # force, cas des DUT annuels par exemple
p.append("%s%d%s" % (SA, -s["semestre_id"], dem)) p.append("%s%d%s" % (session_abbrv, -s["semestre_id"], dem))
else: else:
p.append("%s%d%s" % (SA, s["semestre_id"], dem)) p.append("%s%d%s" % (session_abbrv, s["semestre_id"], dem))
return ", ".join(p) return ", ".join(p)
def get_parcours_decisions(self): def get_parcours_decisions(self):

View File

@ -222,6 +222,7 @@ def table_debouche_etudids(etudids, keep_numeric=True):
html_sortable=True, html_sortable=True,
html_class="table_leftalign table_listegroupe", html_class="table_leftalign table_listegroupe",
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="table_debouche_etudids",
) )
return tab return tab

View File

@ -198,6 +198,18 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
if current_user.has_permission(Permission.EditApogee): if current_user.has_permission(Permission.EditApogee):
html_class += " apo_editable" html_class += " apo_editable"
tab = GenTable( tab = GenTable(
columns_ids=columns_ids,
html_class_ignore_default=True,
html_class=html_class,
html_sortable=True,
html_table_attrs=f"""
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
""",
html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(),
rows=sems,
titles={ titles={
"formsemestre_id": "id", "formsemestre_id": "id",
"semestre_id_n": "S#", "semestre_id_n": "S#",
@ -211,19 +223,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
"elt_sem_apo": "Elt. sem. Apo.", "elt_sem_apo": "Elt. sem. Apo.",
"formation": "Formation", "formation": "Formation",
}, },
columns_ids=columns_ids,
rows=sems,
table_id="semlist", table_id="semlist",
html_class_ignore_default=True,
html_class=html_class,
html_sortable=True,
html_table_attrs=f"""
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
""",
html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(),
) )
return tab return tab

View File

@ -67,7 +67,7 @@ SCO_DUMP_LOCK = "/tmp/scodump.lock"
def sco_dump_and_send_db( def sco_dump_and_send_db(
message: str = "", request_url: str = "", traceback_str_base64: str = "" message: str = "", request_url: str = "", traceback_str_base64: str = ""
): ) -> requests.Response:
"""Dump base de données et l'envoie anonymisée pour debug""" """Dump base de données et l'envoie anonymisée pour debug"""
traceback_str = base64.urlsafe_b64decode(traceback_str_base64).decode( traceback_str = base64.urlsafe_b64decode(traceback_str_base64).decode(
scu.SCO_ENCODING scu.SCO_ENCODING
@ -97,7 +97,6 @@ def sco_dump_and_send_db(
# Send # Send
r = _send_db(ano_db_name, message, request_url, traceback_str=traceback_str) r = _send_db(ano_db_name, message, request_url, traceback_str=traceback_str)
code = r.status_code
finally: finally:
# Drop anonymized database # Drop anonymized database
@ -107,7 +106,7 @@ def sco_dump_and_send_db(
log("sco_dump_and_send_db: done.") log("sco_dump_and_send_db: done.")
return code return r
def _duplicate_db(db_name, ano_db_name): def _duplicate_db(db_name, ano_db_name):
@ -216,11 +215,11 @@ def _drop_ano_db(ano_db_name):
log("_drop_ano_db: no temp db, nothing to drop") log("_drop_ano_db: no temp db, nothing to drop")
return return
cmd = ["dropdb", ano_db_name] cmd = ["dropdb", ano_db_name]
log("sco_dump_and_send_db: {}".format(cmd)) log(f"sco_dump_and_send_db: {cmd}")
try: try:
_ = subprocess.check_output(cmd) _ = subprocess.check_output(cmd)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as exc:
log("sco_dump_and_send_db: exception dropdb {}".format(e)) log(f"sco_dump_and_send_db: exception dropdb {exc}")
raise ScoValueError( raise ScoValueError(
"erreur lors de la suppression de la base {}".format(ano_db_name) f"erreur lors de la suppression de la base {ano_db_name}"
) ) from exc

View File

@ -326,6 +326,7 @@ def do_formation_create(args: dict) -> Formation:
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation.id, formation_id=formation.id,
), ),
safe=True,
) from exc ) from exc
ScolarNews.add( ScolarNews.add(

View File

@ -30,6 +30,7 @@
Lecture et conversion des ics. Lecture et conversion des ics.
""" """
from datetime import timezone from datetime import timezone
import glob import glob
import os import os
@ -229,7 +230,7 @@ def translate_calendar(
heure_deb=event["heure_deb"], heure_deb=event["heure_deb"],
heure_fin=event["heure_fin"], heure_fin=event["heure_fin"],
moduleimpl_id=modimpl.id, moduleimpl_id=modimpl.id,
jour=event["jour"], day=event["jour"],
) )
if modimpl and group if modimpl and group
else None else None

View File

@ -490,6 +490,7 @@ def table_apo_csv_list(semset):
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id), # base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
# caption='Maquettes enregistrées', # caption='Maquettes enregistrées',
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="apo_csv_list",
) )
return tab return tab
@ -582,6 +583,7 @@ def _view_etuds_page(
html_class="table_leftalign", html_class="table_leftalign",
filename="students_apo", filename="students_apo",
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="view_etuds_page",
) )
if fmt != "html": if fmt != "html":
return tab.make_page(fmt=fmt) return tab.make_page(fmt=fmt)
@ -798,6 +800,7 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
filename="students_" + etape_apo, filename="students_" + etape_apo,
caption="Étudiants Apogée en " + etape_apo, caption="Étudiants Apogée en " + etape_apo,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="view_apo_csv",
) )
if fmt != "html": if fmt != "html":

View File

@ -666,7 +666,9 @@ class EtapeBilan:
col_ids, col_ids,
self.titres, self.titres,
html_class="repartition", html_class="repartition",
html_sortable=True,
html_with_td_classes=True, html_with_td_classes=True,
table_id="apo-repartition",
).gen(fmt="html") ).gen(fmt="html")
) )
return "\n".join(H) return "\n".join(H)
@ -762,9 +764,9 @@ class EtapeBilan:
rows, rows,
col_ids, col_ids,
titles, titles,
table_id="detail",
html_class="table_leftalign", html_class="table_leftalign",
html_sortable=True, html_sortable=True,
table_id="apo-detail",
).gen(fmt="html") ).gen(fmt="html")
) )
return "\n".join(H) return "\n".join(H)

View File

@ -367,6 +367,9 @@ def evaluation_create_form(
+ "\n".join(H) + "\n".join(H)
+ "\n" + "\n"
+ tf[1] + tf[1]
+ render_template(
"scodoc/forms/evaluation_edit.j2",
)
+ render_template( + render_template(
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl "scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
) )

View File

@ -70,8 +70,8 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
Colonnes: Colonnes:
- code (UE ou module), - code (UE ou module),
- titre - titre
- type évaluation
- complete - complete
- publiée
- inscrits (non dem. ni def.) - inscrits (non dem. ni def.)
- nb notes manquantes - nb notes manquantes
- nb ATT - nb ATT
@ -81,9 +81,10 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
rows = [] rows = []
titles = { titles = {
"type": "", "type": "",
"code": "Code", "code": "Module",
"titre": "", "titre": "",
"date": "Date", "date": "Date",
"type_evaluation": "Type",
"complete": "Comptée", "complete": "Comptée",
"inscrits": "Inscrits", "inscrits": "Inscrits",
"manquantes": "Manquantes", # notes eval non entrées "manquantes": "Manquantes", # notes eval non entrées
@ -114,7 +115,9 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
rows.append(row) rows.append(row)
line_idx += 1 line_idx += 1
for evaluation_id in modimpl_results.evals_notes: for evaluation_id in modimpl_results.evals_notes:
e = db.session.get(Evaluation, evaluation_id) e: Evaluation = db.session.get(Evaluation, evaluation_id)
if e is None:
continue # ignore errors (rare race conditions?)
eval_etat = modimpl_results.evaluations_etat[evaluation_id] eval_etat = modimpl_results.evaluations_etat[evaluation_id]
row = { row = {
"type": "", "type": "",
@ -128,6 +131,7 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
"_titre_target_attrs": 'class="discretelink"', "_titre_target_attrs": 'class="discretelink"',
"date": e.date_debut.strftime(scu.DATE_FMT) 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 "", "_date_order": e.date_debut.isoformat() if e.date_debut else "",
"type_evaluation": e.type_abbrev(),
"complete": "oui" if eval_etat.is_complete else "non", "complete": "oui" if eval_etat.is_complete else "non",
"_complete_target": "#", "_complete_target": "#",
"_complete_target_attrs": ( "_complete_target_attrs": (

View File

@ -25,8 +25,8 @@
# #
############################################################################## ##############################################################################
"""Evaluations """Evaluations"""
"""
import collections import collections
import datetime import datetime
import operator import operator
@ -50,6 +50,7 @@ from app.scodoc import sco_cal
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_gen_cal
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
@ -360,6 +361,106 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
return etat return etat
class JourEval(sco_gen_cal.Jour):
"""
Représentation d'un jour dans un calendrier d'évaluations
"""
COLOR_INCOMPLETE = "#FF6060"
COLOR_COMPLETE = "#A0FFA0"
COLOR_FUTUR = "#70E0FF"
def __init__(
self,
date: datetime.date,
evaluations: list[Evaluation],
parent: "CalendrierEval",
):
super().__init__(date)
self.evaluations: list[Evaluation] = evaluations
self.evaluations.sort(key=lambda e: e.date_debut)
self.parent: "CalendrierEval" = parent
def get_html(self) -> str:
htmls = []
for e in self.evaluations:
url: str = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl_id,
)
title: str = (
e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
)
htmls.append(
f"""<a
href="{url}"
style="{self._get_eval_style(e)}"
title="{self._get_eval_title(e)}"
class="stdlink"
>{title}</a>"""
)
return ", ".join(htmls)
def _get_eval_style(self, e: Evaluation) -> str:
color: str = ""
# Etat (notes completes) de l'évaluation:
modimpl_result = self.parent.nt.modimpls_results[e.moduleimpl.id]
if modimpl_result.evaluations_etat[e.id].is_complete:
color = JourEval.COLOR_COMPLETE
else:
color = JourEval.COLOR_INCOMPLETE
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
color = JourEval.COLOR_FUTUR
return f"background-color: {color};"
def _get_eval_title(self, e: Evaluation) -> str:
heure_debut_txt, heure_fin_txt = "", ""
if e.date_debut != e.date_fin:
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 ""
title = f"{e.description or e.moduleimpl.module.titre_str()}"
if heure_debut_txt:
title += f" de {heure_debut_txt} à {heure_fin_txt}"
return title
class CalendrierEval(sco_gen_cal.Calendrier):
"""
Représentation des évaluations d'un semestre dans un calendrier
"""
def __init__(self, year: int, evals: list[Evaluation], nt: NotesTableCompat):
# On prend du 01/09 au 31/08
date_debut: datetime.datetime = datetime.datetime(year, 9, 1, 0, 0)
date_fin: datetime.datetime = datetime.datetime(year + 1, 8, 31, 23, 59)
super().__init__(date_debut, date_fin)
# évalutions du semestre
self.evals: dict[datetime.date, list[Evaluation]] = {}
for e in evals:
if e.date_debut is not None:
day = e.date_debut.date()
if day not in self.evals:
self.evals[day] = []
self.evals[day].append(e)
self.nt: NotesTableCompat = nt
def instanciate_jour(self, date: datetime.date) -> JourEval:
return JourEval(date, self.evals.get(date, []), parent=self)
# View
def formsemestre_evaluations_cal(formsemestre_id): def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre""" """Page avec calendrier de toutes les evaluations de ce semestre"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
@ -368,58 +469,9 @@ def formsemestre_evaluations_cal(formsemestre_id):
evaluations = formsemestre.get_evaluations() evaluations = formsemestre.get_evaluations()
nb_evals = len(evaluations) nb_evals = len(evaluations)
color_incomplete = "#FF6060"
color_complete = "#A0FFA0"
color_futur = "#70E0FF"
year = formsemestre.annee_scolaire() year = formsemestre.annee_scolaire()
events = {} # (day, halfday) : event cal = CalendrierEval(year, evaluations, nt)
for e in evaluations: cal_html = cal.get_html()
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 = "?", "?"
else:
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 "?"
description = f"""{
e.moduleimpl.module.titre
}, de {heure_debut_txt} à {heure_fin_txt}"""
# Etat (notes completes) de l'évaluation:
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
if modimpl_result.evaluations_etat[e.id].is_complete:
color = color_complete
else:
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
cal_html = sco_cal.YearTable(
year, events=list(events.values()), halfday=False, pad_width=None
)
return f""" return f"""
{ {
@ -435,15 +487,15 @@ def formsemestre_evaluations_cal(formsemestre_id):
</p> </p>
<ul> <ul>
<li>en <span style= <li>en <span style=
"background-color: {color_incomplete}">rouge</span> "background-color: {JourEval.COLOR_INCOMPLETE}">rouge</span>
les évaluations passées auxquelles il manque des notes les évaluations passées auxquelles il manque des notes
</li> </li>
<li>en <span style= <li>en <span style=
"background-color: {color_complete}">vert</span> "background-color: {JourEval.COLOR_COMPLETE}">vert</span>
les évaluations déjà notées les évaluations déjà notées
</li> </li>
<li>en <span style= <li>en <span style=
"background-color: {color_futur}">bleu</span> "background-color: {JourEval.COLOR_FUTUR}">bleu</span>
les évaluations futures les évaluations futures
</li> </li>
</ul> </ul>
@ -581,6 +633,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id), base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""", origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()), filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
table_id="formsemestre_evaluations_delai_correction",
) )
return tab.make_page(fmt=fmt) return tab.make_page(fmt=fmt)

View File

@ -45,13 +45,17 @@ class ScoInvalidCSRF(ScoException):
class ScoValueError(ScoException): class ScoValueError(ScoException):
"Exception avec page d'erreur utilisateur, et qui stoque dest_url" """Exception avec page d'erreur utilisateur
- dest_url : url aller après la page d'erreur
- safe (default False): si vrai, affiche le message non html quoté.
"""
# mal nommée: super classe de toutes les exceptions avec page # mal nommée: super classe de toutes les exceptions avec page
# d'erreur gentille. # d'erreur gentille.
def __init__(self, msg, dest_url=None): def __init__(self, msg, dest_url=None, safe=False):
super().__init__(msg) super().__init__(msg)
self.dest_url = dest_url self.dest_url = dest_url
self.safe = safe # utilisé par template sco_value_error.j2
class ScoPermissionDenied(ScoValueError): class ScoPermissionDenied(ScoValueError):

View File

@ -106,6 +106,7 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
html_class="table_leftalign", html_class="table_leftalign",
html_sortable=True, html_sortable=True,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="export_result_table",
) )
return tab, semlist return tab, semlist

View File

@ -32,8 +32,7 @@ from flask import url_for, g, request
from flask_login import current_user from flask_login import current_user
import app import app
from app.models import Departement from app.models import Departement, Identite
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
@ -55,7 +54,9 @@ def form_search_etud(
"form recherche par nom" "form recherche par nom"
H = [] H = []
H.append( H.append(
f"""<form action="{ url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept) }" method="POST"> f"""<form action="{
url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept)
}" method="POST">
<b>{title}</b> <b>{title}</b>
<input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value=""> <input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
<input type="submit" value="Chercher"> <input type="submit" value="Chercher">
@ -100,9 +101,9 @@ def form_search_etud(
return "\n".join(H) return "\n".join(H)
def search_etuds_infos_from_exp(expnom: str = "") -> list[dict]: def search_etuds_infos_from_exp(expnom: str = "") -> list[Identite]:
"""Cherche étudiants, expnom peut être, dans cet ordre: """Cherche étudiants, expnom peut être, dans cet ordre:
un etudid (int), un code NIP, ou le début d'un nom. un etudid (int), un code NIP, ou une partie d'un nom (case insensitive).
""" """
if not isinstance(expnom, int) and len(expnom) <= 1: if not isinstance(expnom, int) and len(expnom) <= 1:
return [] # si expnom est trop court, n'affiche rien return [] # si expnom est trop court, n'affiche rien
@ -111,13 +112,22 @@ def search_etuds_infos_from_exp(expnom: str = "") -> list[dict]:
except ValueError: except ValueError:
etudid = None etudid = None
if etudid is not None: if etudid is not None:
etuds = sco_etud.get_etud_info(filled=True, etudid=expnom) etud = Identite.query.filter_by(dept_id=g.scodoc_dept_id, id=etudid).first()
if len(etuds) == 1: if etud:
return etuds return [etud]
expnom_str = str(expnom) expnom_str = str(expnom)
if scu.is_valid_code_nip(expnom_str): if scu.is_valid_code_nip(expnom_str):
return search_etuds_infos(code_nip=expnom_str) etuds = Identite.query.filter_by(
return search_etuds_infos(expnom=expnom_str) dept_id=g.scodoc_dept_id, code_nip=expnom_str
).all()
if etuds:
return etuds
return (
Identite.query.filter_by(dept_id=g.scodoc_dept_id)
.filter(Identite.nom.op("~*")(expnom_str))
.all()
)
def search_etud_in_dept(expnom=""): def search_etud_in_dept(expnom=""):
@ -152,7 +162,7 @@ def search_etud_in_dept(expnom=""):
if len(etuds) == 1: if len(etuds) == 1:
# va directement a la fiche # va directement a la fiche
url_args["etudid"] = etuds[0]["etudid"] url_args["etudid"] = etuds[0].id
return flask.redirect(url_for(endpoint, **url_args)) return flask.redirect(url_for(endpoint, **url_args))
H = [ H = [
@ -179,14 +189,39 @@ def search_etud_in_dept(expnom=""):
) )
if len(etuds) > 0: if len(etuds) > 0:
# Choix dans la liste des résultats: # Choix dans la liste des résultats:
rows = []
e: Identite
for e in etuds: for e in etuds:
url_args["etudid"] = e["etudid"] url_args["etudid"] = e.id
target = url_for(endpoint, **url_args) target = url_for(endpoint, **url_args)
e["_nomprenom_target"] = target cur_inscription = e.inscription_courante()
e["inscription_target"] = target inscription = (
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"]) e.inscription_descr().get("inscription_str", "")
sco_groups.etud_add_group_infos( if cur_inscription
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None else ""
)
groupes = (
", ".join(
gr.group_name
for gr in sco_groups.get_etud_formsemestre_groups(
e, cur_inscription.formsemestre
)
)
if cur_inscription
else ""
)
rows.append(
{
"code_nip": e.code_nip or "",
"etudid": e.id,
"inscription": inscription,
"inscription_target": target,
"groupes": groupes,
"nomprenom": e.nomprenom,
"_nomprenom_target": target,
"_nomprenom_td_attrs": f'id="{e.id}" class="etudinfo"',
}
) )
tab = GenTable( tab = GenTable(
@ -197,10 +232,11 @@ def search_etud_in_dept(expnom=""):
"inscription": "Inscription", "inscription": "Inscription",
"groupes": "Groupes", "groupes": "Groupes",
}, },
rows=etuds, rows=rows,
html_sortable=True, html_sortable=True,
html_class="table_leftalign", html_class="table_leftalign",
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="search_etud_in_dept",
) )
H.append(tab.html()) H.append(tab.html())
if len(etuds) > 20: # si la page est grande if len(etuds) > 20: # si la page est grande
@ -213,15 +249,16 @@ def search_etud_in_dept(expnom=""):
) )
) )
else: else:
H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom) H.append(f'<h2 style="color: red;">Aucun résultat pour "{expnom}".</h2>')
H.append( H.append(
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant. Saisir au moins deux caractères.</p>""" """<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP
de l'étudiant. Saisir au moins deux caractères.</p>"""
) )
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
# Was chercheEtudsInfo() # Was chercheEtudsInfo()
def search_etuds_infos(expnom=None, code_nip=None): def search_etuds_infos(expnom=None, code_nip=None) -> list[dict]:
"""recherche les étudiants correspondants à expnom ou au code_nip """recherche les étudiants correspondants à expnom ou au code_nip
et ramene liste de mappings utilisables en DTML. et ramene liste de mappings utilisables en DTML.
""" """
@ -264,7 +301,7 @@ def search_etud_by_name(term: str) -> list:
FROM identite FROM identite
WHERE WHERE
dept_id = %(dept_id)s dept_id = %(dept_id)s
AND code_nip LIKE %(beginning)s AND code_nip ILIKE %(beginning)s
ORDER BY nom ORDER BY nom
""", """,
{"beginning": term + "%", "dept_id": g.scodoc_dept_id}, {"beginning": term + "%", "dept_id": g.scodoc_dept_id},
@ -283,7 +320,7 @@ def search_etud_by_name(term: str) -> list:
FROM identite FROM identite
WHERE WHERE
dept_id = %(dept_id)s dept_id = %(dept_id)s
AND nom LIKE %(beginning)s AND nom ILIKE %(beginning)s
ORDER BY nom ORDER BY nom
""", """,
{"beginning": term + "%", "dept_id": g.scodoc_dept_id}, {"beginning": term + "%", "dept_id": g.scodoc_dept_id},
@ -348,6 +385,7 @@ def table_etud_in_accessible_depts(expnom=None):
rows=etuds, rows=etuds,
html_sortable=True, html_sortable=True,
html_class="table_leftalign", html_class="table_leftalign",
table_id="etud_in_accessible_depts",
) )
H.append('<div class="table_etud_in_dept">') H.append('<div class="table_etud_in_dept">')
@ -383,13 +421,13 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
""" """
result, _ = search_etud_in_accessible_depts(code_nip=code_nip) result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
T = [] rows = []
for etuds in result: for etuds in result:
if etuds: if etuds:
dept_id = etuds[0]["dept"] dept_id = etuds[0]["dept"]
for e in etuds: for e in etuds:
for sem in e["sems"]: for sem in e["sems"]:
T.append( rows.append(
{ {
"dept": dept_id, "dept": dept_id,
"etudid": e["etudid"], "etudid": e["etudid"],
@ -414,6 +452,6 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
"date_debut_iso", "date_debut_iso",
"date_fin_iso", "date_fin_iso",
) )
tab = GenTable(columns_ids=columns_ids, rows=T) tab = GenTable(columns_ids=columns_ids, rows=rows, table_id="inscr_etud_by_nip")
return tab.make_page(fmt=fmt, with_html_headers=False, publish=True) return tab.make_page(fmt=fmt, with_html_headers=False, publish=True)

View File

@ -649,20 +649,20 @@ def formation_list_table(detail: bool) -> GenTable:
"semestres_ues": "Semestres avec UEs", "semestres_ues": "Semestres avec UEs",
} }
return GenTable( return GenTable(
columns_ids=columns_ids,
rows=rows,
titles=titles,
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
caption=title,
html_caption=title,
table_id="formation_list_table",
html_class="formation_list_table table_leftalign",
html_with_td_classes=True,
html_sortable=True,
base_url=f"{request.base_url}" + ("?detail=on" if detail else ""), base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
caption=title,
columns_ids=columns_ids,
html_caption=title,
html_class="formation_list_table table_leftalign",
html_sortable=True,
html_with_td_classes=True,
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
page_title=title, page_title=title,
pdf_title=title, pdf_title=title,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
rows=rows,
table_id="formation_list_table",
titles=titles,
) )

View File

@ -527,15 +527,16 @@ def table_formsemestres(
preferences = sco_preferences.SemPreferences() preferences = sco_preferences.SemPreferences()
tab = GenTable( tab = GenTable(
columns_ids=columns_ids, columns_ids=columns_ids,
rows=sems,
titles=titles,
html_class="table_leftalign", html_class="table_leftalign",
html_empty_element="<p><em>aucun résultat</em></p>",
html_next_section=html_next_section,
html_sortable=True, html_sortable=True,
html_title=html_title, html_title=html_title,
html_next_section=html_next_section,
html_empty_element="<p><em>aucun résultat</em></p>",
page_title="Semestres", page_title="Semestres",
preferences=preferences, preferences=preferences,
rows=sems,
table_id="table_formsemestres",
titles=titles,
) )
return tab return tab

View File

@ -726,20 +726,21 @@ def formsemestre_description_table(
rows.append(sums) rows.append(sums)
return GenTable( return GenTable(
columns_ids=columns_ids, base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
rows=rows,
titles=titles,
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
caption=title, caption=title,
columns_ids=columns_ids,
html_caption=title, html_caption=title,
html_class="table_leftalign formsemestre_description", html_class="table_leftalign formsemestre_description",
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
page_title=title,
html_title=html_sco_header.html_sem_header( html_title=html_sco_header.html_sem_header(
"Description du semestre", with_page_header=False "Description du semestre", with_page_header=False
), ),
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
page_title=title,
pdf_title=title, pdf_title=title,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
rows=rows,
table_id="formsemestre_description_table",
titles=titles,
) )
@ -821,6 +822,55 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
</div> </div>
</div> </div>
<div class="sem-groups-assi"> <div class="sem-groups-assi">
"""
)
if can_edit_abs:
H.append(
f"""
<div>
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
day=datetime.date.today().isoformat(),
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Saisir l'assiduité</a>
</div>
"""
)
# YYYY-Www (ISO 8601) :
current_week: str = datetime.datetime.now().strftime("%G-W%V")
H.append(
f"""
<div>
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_hebdo",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
week=current_week,
)}">Saisie hebdomadaire</a>
</div>
"""
)
if can_edit_abs:
H.append(
f"""
<div>
<a class="stdlink" href="{
url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Justificatifs en attente</a>
</div>
"""
)
H.append(
f"""
<div> <div>
<a class="stdlink" href="{ <a class="stdlink" href="{
url_for("assiduites.visu_assi_group", url_for("assiduites.visu_assi_group",
@ -833,49 +883,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
</div> </div>
""" """
) )
if can_edit_abs:
H.append(
f"""
<div>
<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,
)}">
Visualiser</a>
</div>
<div>
<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,
)}">
Saisie journalière</a>
</div>
<div>
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Saisie différée</a>
</div>
<div>
<a class="stdlink" href="{
url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
Justificatifs en attente</a>
</div>
"""
)
H.append("</div>") # /sem-groups-assi H.append("</div>") # /sem-groups-assi
if partition_is_empty: if partition_is_empty:
@ -1186,17 +1193,7 @@ def formsemestre_tableau_modules(
mod_descr = "Module " + (mod.titre or "") mod_descr = "Module " + (mod.titre or "")
is_apc = mod.is_apc() # SAE ou ressource is_apc = mod.is_apc() # SAE ou ressource
if is_apc: if is_apc:
coef_descr = ", ".join( mod_descr += " " + mod.get_ue_coefs_descr()
[
f"{ue.acronyme}: {co}"
for ue, co in mod.ue_coefs_list()
if isinstance(co, float) and co > 0
]
)
if coef_descr:
mod_descr += " Coefs: " + coef_descr
else:
mod_descr += " (pas de coefficients) "
else: else:
mod_descr += ", coef. " + str(mod.coefficient) mod_descr += ", coef. " + str(mod.coefficient)
mod_ens = sco_users.user_info(modimpl.responsable_id)["nomcomplet"] mod_ens = sco_users.user_info(modimpl.responsable_id)["nomcomplet"]

214
app/scodoc/sco_gen_cal.py Normal file
View File

@ -0,0 +1,214 @@
"""
Génération d'un calendrier
(Classe abstraite à implémenter dans les classes filles)
"""
import datetime
from flask import render_template
import app.scodoc.sco_utils as scu
from app import g
class Jour:
"""
Représente un jour dans le calendrier
Permet d'obtenir les informations sur le jour
et générer une représentation html
"""
def __init__(self, date: datetime.date):
self.date = date
self.class_list: list[str] = []
if self.is_non_work():
self.class_list.append("non-travail")
if self.is_current_week():
self.class_list.append("sem-courante")
def get_nom(self, short=True):
"""
Renvoie le nom du jour
"M19" ou "Mer 19"
par défaut en version courte
"""
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
return (
f"{str_jour[0] if short or self.is_non_work() else str_jour[:3]+' '}"
+ f"{self.date.day}"
)
def is_non_work(self):
"""
Renvoie True si le jour est un jour non travaillé
(en fonction de la préférence du département)
"""
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
dept_id=g.scodoc_dept_id
)
def is_current_week(self):
"""
Renvoie True si le jour est dans la semaine courante
"""
return self.date.isocalendar()[0:2] == datetime.date.today().isocalendar()[0:2]
def get_date(self, fmt=scu.DATE_FMT) -> str:
"""
Renvoie la date du jour au format fmt ou "dd/mm/yyyy" par défaut
"""
return self.date.strftime(fmt)
def get_html(self):
"""
Renvoie le code html du jour
à surcharger dans les classes filles
l'html final ressemblera à :
<div class="jour {{jour.get_class()}}">
<span class="nom">{{jour.get_nom()}}</span>
<div class="contenu">
{{jour.get_html() | safe}}
</div>
</div>
"""
raise NotImplementedError("Méthode à implémenter dans les classes filles")
def get_class(self):
"""
Renvoie la classe css du jour
utilise self.class_list
-> fait un join de la liste
"""
return " ".join(self.class_list)
class Calendrier:
"""
Représente un calendrier
Permet d'obtenir les informations sur les jours
et générer une représentation html
highlight: str
-> ["jour", "semaine", "mois"]
permet de mettre en valeur lors du passage de la souris
"""
def __init__(
self,
date_debut: datetime.date,
date_fin: datetime.date,
highlight: str = None,
):
self.date_debut = date_debut
self.date_fin = date_fin
self.jours: dict[str, list[Jour]] = {}
self.highlight: str = highlight
def _get_dates_between(self) -> list[datetime.date]:
"""
get_dates_between Renvoie la liste des dates entre date_debut et date_fin
Returns:
list[datetime.date]: liste des dates entre date_debut et date_fin
"""
resultat = []
date_actuelle: datetime.date = self.date_debut
while date_actuelle <= self.date_fin:
if isinstance(date_actuelle, datetime.datetime):
resultat.append(date_actuelle.date())
elif isinstance(date_actuelle, datetime.date):
resultat.append(date_actuelle)
date_actuelle += datetime.timedelta(days=1)
return resultat
def organize_by_month(self):
"""
Organise les jours par mois
Instancie un objet Jour pour chaque jour
met à jour self.jours
"""
organized = {}
for date in self._get_dates_between():
# Récupérer le mois en français
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
# Ajouter le jour à la liste correspondante au mois
if month not in organized:
organized[month] = {} # semaine {22: []}
jour: Jour = self.instanciate_jour(date)
semaine = date.strftime("%G-W%V")
if semaine not in organized[month]:
organized[month][semaine] = []
organized[month][semaine].append(jour)
self.jours = organized
def instanciate_jour(self, date: datetime.date) -> Jour:
"""
Instancie un objet Jour pour chaque jour
A surcharger dans les classes filles si besoin
"""
raise NotImplementedError("Méthode à implémenter dans les classes filles")
def get_html(self):
"""
get_html Renvoie le code html du calendrier
"""
self.organize_by_month()
return render_template(
"calendrier.j2", calendrier=self.jours, highlight=self.highlight
)
class JourChoix(Jour):
"""
Représente un jour dans le calendrier pour choisir une date
"""
def get_html(self):
return ""
class CalendrierChoix(Calendrier):
"""
Représente un calendrier pour choisir une date
"""
def instanciate_jour(self, date: datetime.date) -> Jour:
return JourChoix(date)
def calendrier_choix_date(
date_debut: datetime.date,
date_fin: datetime.date,
url: str,
mode: str = "jour",
titre: str = "Choisir une date",
):
"""
Permet d'afficher un calendrier pour choisir une date et renvoyer sur une url.
mode : str
- "jour" -> ajoutera "&day=yyyy-mm-dd" à l'url (ex: 2024-05-30)
- "semaine" -> ajoutera "&week=yyyy-Www" à l'url (ex : 2024-W22)
titre : str
- texte à afficher au dessus du calendrier
"""
calendrier: CalendrierChoix = CalendrierChoix(date_debut, date_fin, highlight=mode)
return render_template(
"choix_date.j2",
calendrier=calendrier.get_html(),
url=url,
titre=titre,
mode=mode,
)

View File

@ -92,5 +92,6 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
html_sortable=True, html_sortable=True,
html_class="table_leftalign", html_class="table_leftalign",
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
table_id="groups_export_annotations",
) )
return table.make_page(fmt=fmt) return table.make_page(fmt=fmt)

View File

@ -585,8 +585,8 @@ def groups_table(
etud_info["_nom_disp_order"] = etud_sort_key(etud_info) etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
etud_info["_prenom_target"] = fiche_url etud_info["_prenom_target"] = fiche_url
etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % ( etud_info["_nom_disp_td_attrs"] = (
etud_info["etudid"] 'id="%s" class="etudinfo"' % (etud_info["etudid"])
) )
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non" etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
if etud_info["etat"] == "D": if etud_info["etat"] == "D":
@ -661,6 +661,7 @@ def groups_table(
text_fields_separator=prefs["moodle_csv_separator"], text_fields_separator=prefs["moodle_csv_separator"],
text_with_titles=prefs["moodle_csv_with_headerline"], text_with_titles=prefs["moodle_csv_with_headerline"],
preferences=prefs, preferences=prefs,
table_id="groups_table",
) )
# #
if fmt == "html": if fmt == "html":
@ -982,7 +983,7 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
"assiduites.signal_assiduites_group", "assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
group_ids=",".join(map(str,groups_infos.group_ids)), group_ids=",".join(map(str,groups_infos.group_ids)),
jour=datetime.date.today().isoformat(), day=datetime.date.today().isoformat(),
formsemestre_id=groups_infos.formsemestre_id, formsemestre_id=groups_infos.formsemestre_id,
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
) )
@ -997,12 +998,12 @@ def form_choix_saisie_semaine(groups_infos):
return "" return ""
query_args = parse_qs(request.query_string) query_args = parse_qs(request.query_string)
moduleimpl_id = query_args.get("moduleimpl_id", [None])[0] moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
semaine = datetime.date.today().isocalendar().week semaine = datetime.datetime.now().strftime("%G-W%V")
return f""" return f"""
<button onclick="window.location='{url_for( <button onclick="window.location='{url_for(
"assiduites.signal_assiduites_diff", "assiduites.signal_assiduites_hebdo",
group_ids=",".join(map(str,groups_infos.group_ids)), group_ids=",".join(map(str,groups_infos.group_ids)),
semaine=semaine, week=semaine,
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id, formsemestre_id=groups_infos.formsemestre_id,
moduleimpl_id=moduleimpl_id moduleimpl_id=moduleimpl_id
@ -1028,10 +1029,9 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
moodle_sem_name = sem["session_id"] moodle_sem_name = sem["session_id"]
columns_ids = ("email", "semestre_groupe") columns_ids = ("email", "semestre_groupe")
T = [] rows = []
for partition_id in partitions_etud_groups: for partition_id, members in partitions_etud_groups.items():
partition = sco_groups.get_partition(partition_id) partition = sco_groups.get_partition(partition_id)
members = partitions_etud_groups[partition_id]
for etudid in members: for etudid in members:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
group_name = members[etudid]["group_name"] group_name = members[etudid]["group_name"]
@ -1040,16 +1040,17 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
elts.append(partition["partition_name"]) elts.append(partition["partition_name"])
if group_name: if group_name:
elts.append(group_name) elts.append(group_name)
T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)}) rows.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
# Make table # Make table
prefs = sco_preferences.SemPreferences(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id)
tab = GenTable( tab = GenTable(
rows=T,
columns_ids=("email", "semestre_groupe"), columns_ids=("email", "semestre_groupe"),
filename=moodle_sem_name + "-moodle", filename=moodle_sem_name + "-moodle",
titles={x: x for x in columns_ids}, preferences=prefs,
rows=rows,
text_fields_separator=prefs["moodle_csv_separator"], text_fields_separator=prefs["moodle_csv_separator"],
text_with_titles=prefs["moodle_csv_with_headerline"], text_with_titles=prefs["moodle_csv_with_headerline"],
preferences=prefs, table_id="export_groups_as_moodle_csv",
titles={x: x for x in columns_ids},
) )
return tab.make_page(fmt="csv") return tab.make_page(fmt="csv")

View File

@ -834,11 +834,12 @@ def adm_table_description_format():
columns_ids = ("attribute", "type", "writable", "description", "aliases_str") columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
tab = GenTable( tab = GenTable(
titles=titles,
columns_ids=columns_ids, columns_ids=columns_ids,
rows=list(Fmt.values()),
html_sortable=True,
html_class="table_leftalign", html_class="table_leftalign",
html_sortable=True,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
rows=list(Fmt.values()),
table_id="adm_table_description_format",
titles=titles,
) )
return tab return tab

View File

@ -747,10 +747,11 @@ def etuds_select_box_xls(src_cat):
else: else:
e["paiementinscription_str"] = "-" e["paiementinscription_str"] = "-"
tab = GenTable( tab = GenTable(
titles=titles,
columns_ids=columns_ids,
rows=etuds,
caption="%(title)s. %(help)s" % src_cat["infos"], caption="%(title)s. %(help)s" % src_cat["infos"],
columns_ids=columns_ids,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
rows=etuds,
table_id="etuds_select_box_xls",
titles=titles,
) )
return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"]) return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"])

View File

@ -599,20 +599,21 @@ def _make_table_notes(
) )
# display # display
tab = GenTable( tab = GenTable(
titles=titles,
columns_ids=columns_ids,
rows=rows,
html_sortable=True,
base_url=base_url, base_url=base_url,
filename=filename,
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
caption=caption, caption=caption,
html_next_section=html_next_section, columns_ids=columns_ids,
page_title="Notes de " + formsemestre.titre_mois(), filename=filename,
html_title=html_title,
pdf_title=pdf_title,
html_class="notes_evaluation", html_class="notes_evaluation",
html_next_section=html_next_section,
html_sortable=True,
html_title=html_title,
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
page_title="Notes de " + formsemestre.titre_mois(),
pdf_title=pdf_title,
preferences=sco_preferences.SemPreferences(formsemestre.id), preferences=sco_preferences.SemPreferences(formsemestre.id),
rows=rows,
table_id="table-liste-notes",
titles=titles,
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete # html_generate_cells=False # la derniere ligne (moyennes) est incomplete
) )
if fmt == "bordereau": if fmt == "bordereau":

View File

@ -180,6 +180,7 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
html_class="table_leftalign table_listegroupe", html_class="table_leftalign table_listegroupe",
bottom_titles=bottom_titles, bottom_titles=bottom_titles,
preferences=preferences, preferences=preferences,
table_id="table_etuds_lycees",
) )
return tab, etuds_by_lycee return tab, etuds_by_lycee

View File

@ -64,7 +64,7 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
can_edit_notes_ens = modimpl.can_edit_notes(current_user) can_edit_notes_ens = modimpl.can_edit_notes(current_user)
if can_edit_notes and nbnotes != 0: if can_edit_notes and nbnotes != 0:
sup_label = "Supprimer évaluation impossible (il y a des notes)" sup_label = "Suppression évaluation impossible (il y a des notes)"
else: else:
sup_label = "Supprimer évaluation" sup_label = "Supprimer évaluation"
@ -146,29 +146,48 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
return htmlutils.make_menu("actions", menu_eval, alone=True) return htmlutils.make_menu("actions", menu_eval, alone=True)
def _ue_coefs_html(coefs_lst) -> str: def _ue_coefs_html(modimpl: ModuleImpl) -> str:
""" """ """ """
max_coef = max([x[1] for x in coefs_lst]) if coefs_lst else 1.0 coefs_lst = modimpl.module.ue_coefs_list()
H = """ max_coef = max(x[1] for x in coefs_lst) if coefs_lst else 1.0
H = f"""
<div id="modimpl_coefs"> <div id="modimpl_coefs">
<div>Coefficients vers les UE</div> <div>Coefficients vers les UEs
<span><a class="stdlink" href="{
url_for(
"notes.edit_modules_ue_coefs",
scodoc_dept=g.scodoc_dept,
formation_id=modimpl.module.formation.id,
semestre_idx=modimpl.formsemestre.semestre_id,
)
}">détail</a>
</span>
</div>
""" """
if coefs_lst: if coefs_lst:
H += ( H += _html_hinton_map(
f""" colors=(uc[0].color for uc in coefs_lst),
<div class="coefs_histo" style="--max:{max_coef}"> max_val=max_coef,
""" size=36,
+ "\n".join( title=modimpl.module.get_ue_coefs_descr(),
[ values=(uc[1] for uc in coefs_lst),
f"""<div style="--coef:{coef};
{'background-color: ' + ue.color + ';' if ue.color else ''}
"><div>{coef}</div>{ue.acronyme}</div>"""
for ue, coef in coefs_lst
if coef > 0
]
)
+ "</div>"
) )
# (
# f"""
# <div class="coefs_histo" style="--max:{max_coef}">
# """
# + "\n".join(
# [
# f"""<div style="--coef:{coef};
# {'background-color: ' + ue.color + ';' if ue.color else ''}
# "><div>{coef}</div>{ue.acronyme}</div>"""
# for ue, coef in coefs_lst
# if coef > 0
# ]
# )
# + "</div>"
# )
else: else:
H += """<div class="missing_value">non définis</div>""" H += """<div class="missing_value">non définis</div>"""
H += "</div>" H += "</div>"
@ -195,12 +214,21 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
Evaluation.date_debut.desc(), Evaluation.date_debut.desc(),
).all() ).all()
nb_evaluations = len(evaluations) nb_evaluations = len(evaluations)
max_poids = max( # Le poids max pour chaque catégorie d'évaluation
max_poids_by_type: dict[int, float] = {}
for eval_type in (
Evaluation.EVALUATION_NORMALE,
Evaluation.EVALUATION_RATTRAPAGE,
Evaluation.EVALUATION_SESSION2,
Evaluation.EVALUATION_BONUS,
):
max_poids_by_type[eval_type] = max(
[ [
max([p.poids for p in e.ue_poids] or [0]) * (e.coefficient or 0.0) max([p.poids for p in e.ue_poids] or [0]) * (e.coefficient or 0.0)
for e in evaluations for e in evaluations
if e.evaluation_type == eval_type
] ]
or [0] or [0.0]
) )
# #
sem_locked = not formsemestre.etat sem_locked = not formsemestre.etat
@ -265,7 +293,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
H.append(scu.icontag("lock32_img", title="verrouillé")) H.append(scu.icontag("lock32_img", title="verrouillé"))
H.append("""</td><td class="fichetitre2">""") H.append("""</td><td class="fichetitre2">""")
if modimpl.module.is_apc(): if modimpl.module.is_apc():
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list())) H.append(_ue_coefs_html(modimpl))
else: else:
H.append( H.append(
f"""Coef. dans le semestre: { f"""Coef. dans le semestre: {
@ -318,12 +346,28 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
f""" f"""
<span class="moduleimpl_abs_link"><a class="stdlink" href="{ <span class="moduleimpl_abs_link"><a class="stdlink" href="{
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept) url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
}?group_ids={group_id}&jour={ }?group_ids={group_id}&day={
datetime.date.today().isoformat() datetime.date.today().isoformat()
}&formsemestre_id={formsemestre.id} }&formsemestre_id={formsemestre.id}
&moduleimpl_id={moduleimpl_id} &moduleimpl_id={moduleimpl_id}
" "
>Saisie Absences journée</a></span> >Saisie Absences</a></span>
"""
)
current_week: str = datetime.datetime.now().strftime("%G-W%V")
H.append(
f"""
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
url_for("assiduites.signal_assiduites_hebdo",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group_id,
week=current_week,
moduleimpl_id=moduleimpl_id
)
}
"
>Saisie Absences (Hebdo)</a></span>
""" """
) )
H.append( H.append(
@ -335,8 +379,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
group_ids=group_id, group_ids=group_id,
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
)}" )}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisie Absences`"
>Saisie Absences Différée</a></span> >(Saisie Absences Différée)</a></span>
""" """
) )
@ -344,9 +388,34 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
# #
if not modimpl.check_apc_conformity(nt): if not modimpl.check_apc_conformity(nt):
H.append( H.append(
"""<div class="warning conformite">Les poids des évaluations de ce module ne sont """<div class="warning conformite">Les poids des évaluations de ce
pas encore conformes au PN. module ne permettent pas d'évaluer toutes les UEs (compétences)
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE. prévues par les coefficients du programme.
<b>Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.</b>
Vérifiez les poids des évaluations.
</div>"""
)
if not modimpl.check_apc_conformity(
nt, evaluation_type=Evaluation.EVALUATION_SESSION2
):
H.append(
"""<div class="warning conformite">
Il y a des évaluations de <b>deuxième session</b>
mais leurs poids ne permettent pas d'évaluer toutes les UEs (compétences)
prévues par les coefficients du programme.
La deuxième session ne sera donc <b>pas prise en compte</b>.
Vérifiez les poids de ces évaluations.
</div>"""
)
if not modimpl.check_apc_conformity(
nt, evaluation_type=Evaluation.EVALUATION_RATTRAPAGE
):
H.append(
"""<div class="warning conformite">
Il y a des évaluations de <b>rattrapage</b>
mais leurs poids n'évaluent pas toutes les UEs (compétences)
prévues par les coefficients du programme.
Vérifiez les poids de ces évaluations.
</div>""" </div>"""
) )
@ -437,7 +506,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
eval_index=eval_index, eval_index=eval_index,
nb_evals=nb_evaluations, nb_evals=nb_evaluations,
is_apc=nt.is_apc, is_apc=nt.is_apc,
max_poids=max_poids, max_poids=max_poids_by_type.get(evaluation.evaluation_type, 10000.0),
) )
) )
eval_index -= 1 eval_index -= 1
@ -756,27 +825,27 @@ def _ligne_evaluation(
# #
if etat["nb_notes"] == 0: if etat["nb_notes"] == 0:
H.append(f"""<tr class="{tr_class}"><td></td>""") H.append(f"""<tr class="{tr_class}"><td></td>""")
if modimpl.module.is_apc(): # if modimpl.module.is_apc():
H.append( # H.append(
f"""<td colspan="8" class="eval_poids">{ # f"""<td colspan="8" class="eval_poids">{
evaluation.get_ue_poids_str()}</td>""" # evaluation.get_ue_poids_str()}</td>"""
) # )
else: # else:
H.append('<td colspan="8"></td>') # H.append('<td colspan="8"></td>')
H.append("""</tr>""") H.append("""</tr>""")
else: # il y a deja des notes saisies else: # il y a deja des notes saisies
gr_moyennes = etat["gr_moyennes"] gr_moyennes = etat["gr_moyennes"]
first_group = True # first_group = True
for gr_moyenne in gr_moyennes: for gr_moyenne in gr_moyennes:
H.append(f"""<tr class="{tr_class}"><td>&nbsp;</td>""") H.append(f"""<tr class="{tr_class}"><td>&nbsp;</td>""")
if first_group and modimpl.module.is_apc(): # if first_group and modimpl.module.is_apc():
H.append( # H.append(
f"""<td class="eval_poids" colspan="4">{ # f"""<td class="eval_poids" colspan="4">{
evaluation.get_ue_poids_str()}</td>""" # evaluation.get_ue_poids_str()}</td>"""
) # )
else: # else:
H.append("""<td colspan="4"></td>""") H.append("""<td colspan="4"></td>""")
first_group = False # first_group = False
if gr_moyenne["group_name"] is None: if gr_moyenne["group_name"] is None:
name = "Tous" # tous name = "Tous" # tous
else: else:
@ -832,26 +901,47 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
ue_poids = evaluation.get_ue_poids_dict(sort=True) # { ue_id : poids } ue_poids = evaluation.get_ue_poids_dict(sort=True) # { ue_id : poids }
if not ue_poids: if not ue_poids:
return "" return ""
if max_poids < scu.NOTES_PRECISION: values = [poids * (evaluation.coefficient) for poids in ue_poids.values()]
colors = [db.session.get(UniteEns, ue_id).color for ue_id in ue_poids]
return _html_hinton_map(
classes=("evaluation_poids",),
colors=colors,
max_val=max_poids,
title=f"Poids de l'évaluation vers les UEs: {evaluation.get_ue_poids_str()}",
values=values,
)
def _html_hinton_map(
classes=(),
colors=(),
max_val: float | None = None,
size=12,
title: str = "",
values=(),
) -> str:
"""Représente une liste de nombres sous forme de carrés"""
if max_val is None:
max_val = max(values)
if max_val < scu.NOTES_PRECISION:
return "" return ""
H = ( return (
"""<div class="evaluation_poids">""" f"""<div class="hinton_map {" ".join(classes)}"
style="--size:{size}px;"
title="{title}"
data-tooltip>"""
+ "\n".join( + "\n".join(
[ [
f"""<div title="poids vers {ue.acronyme}: {poids:g}"> f"""<div>
<div style="--size:{math.sqrt(poids*(evaluation.coefficient)/max_poids*144)}px; <div style="--boxsize:{size*math.sqrt(value/max_val)}px;
{'background-color: ' + ue.color + ';' if ue.color else ''} {'background-color: ' + color + ';' if color else ''}
"></div> "></div>
</div>""" </div>"""
for ue, poids in ( for value, color in zip(values, colors)
(db.session.get(UniteEns, ue_id), poids)
for ue_id, poids in ue_poids.items()
)
] ]
) )
+ "</div>" + "</div>"
) )
return H
def _html_modimpl_etuds_attente(res: ResultatsSemestre, modimpl: ModuleImpl) -> str: def _html_modimpl_etuds_attente(res: ResultatsSemestre, modimpl: ModuleImpl) -> str:

View File

@ -247,6 +247,7 @@ class ScoDocPageTemplate(PageTemplate):
footer_template=DEFAULT_PDF_FOOTER_TEMPLATE, footer_template=DEFAULT_PDF_FOOTER_TEMPLATE,
filigranne=None, filigranne=None,
preferences=None, # dictionnary with preferences, required preferences=None, # dictionnary with preferences, required
with_page_numbers=False,
): ):
"""Initialise our page template.""" """Initialise our page template."""
# defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf # defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
@ -259,8 +260,9 @@ class ScoDocPageTemplate(PageTemplate):
self.pdfmeta_subject = subject self.pdfmeta_subject = subject
self.server_name = server_name self.server_name = server_name
self.filigranne = filigranne self.filigranne = filigranne
self.page_number = 1
self.footer_template = footer_template self.footer_template = footer_template
self.with_page_numbers = with_page_numbers
self.page_number = 1
if self.preferences: if self.preferences:
self.with_page_background = self.preferences["bul_pdf_with_background"] self.with_page_background = self.preferences["bul_pdf_with_background"]
else: else:
@ -337,6 +339,7 @@ class ScoDocPageTemplate(PageTemplate):
def draw_footer(self, canv, content): def draw_footer(self, canv, content):
"""Print the footer""" """Print the footer"""
# called 1/page
try: try:
canv.setFont( canv.setFont(
self.preferences["SCOLAR_FONT"], self.preferences["SCOLAR_FONT"],
@ -351,8 +354,11 @@ class ScoDocPageTemplate(PageTemplate):
canv.drawString( canv.drawString(
self.preferences["pdf_footer_x"] * mm, self.preferences["pdf_footer_x"] * mm,
self.preferences["pdf_footer_y"] * mm, self.preferences["pdf_footer_y"] * mm,
content, content + " " + (self.preferences["pdf_footer_extra"] or ""),
) )
if self.with_page_numbers:
canv.drawString(190.0 * mm, 6 * mm, f"Page {self.page_number}")
canv.restoreState() canv.restoreState()
def footer_string(self) -> str: def footer_string(self) -> str:
@ -382,18 +388,14 @@ class ScoDocPageTemplate(PageTemplate):
filigranne = self.filigranne.get(doc.page, None) filigranne = self.filigranne.get(doc.page, None)
if filigranne: if filigranne:
canv.saveState() canv.saveState()
canv.translate(9 * cm, 27.6 * cm) canv.translate(10 * cm, 21.0 * cm)
canv.rotate(30) canv.rotate(36)
canv.scale(4.5, 4.5) canv.scale(7, 7)
canv.setFillColorRGB(1.0, 0.65, 0.65, alpha=0.6) canv.setFillColorRGB(1.0, 0.65, 0.65, alpha=0.6)
canv.drawRightString(0, 0, SU(filigranne)) canv.drawCentredString(0, 0, SU(filigranne))
canv.restoreState() canv.restoreState()
doc.filigranne = None doc.filigranne = None
# Increment page number
def afterPage(self):
"""Called after all flowables have been drawn on a page.
Increment pageNum since the page has been completed.
"""
self.page_number += 1 self.page_number += 1

View File

@ -96,13 +96,16 @@ def photo_portal_url(code_nip: str):
return None return None
def get_etud_photo_url(etudid, size="small"): def get_etud_photo_url(etudid, size="small", seed=None):
"L'URL scodoc vers la photo de l'étudiant"
kwargs = {"seed": seed} if seed else {}
return ( return (
url_for( url_for(
"scolar.get_photo_image", "scolar.get_photo_image",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etudid, etudid=etudid,
size=size, size=size,
**kwargs,
) )
if has_request_context() if has_request_context()
else "" else ""
@ -114,9 +117,11 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str:
If ScoDoc doesn't have an image and a portal is configured, link to it. If ScoDoc doesn't have an image and a portal is configured, link to it.
""" """
photo_url = get_etud_photo_url(etud["etudid"], size=size)
if fast: if fast:
return photo_url return get_etud_photo_url(etud["etudid"], size=size)
photo_url = get_etud_photo_url(
etud["etudid"], size=size, seed=hash(etud.get("photo_filename"))
)
path = photo_pathname(etud["photo_filename"], size=size) path = photo_pathname(etud["photo_filename"], size=size)
if not path: if not path:
# Portail ? # Portail ?
@ -374,7 +379,15 @@ def copy_portal_photo_to_fs(etudid: int):
portal_timeout = sco_preferences.get_preference("portal_timeout") portal_timeout = sco_preferences.get_preference("portal_timeout")
error_message = None error_message = None
try: try:
r = requests.get(url, timeout=portal_timeout) r = requests.get(
url,
timeout=portal_timeout,
params={
"nom": etud.nom or "",
"prenom": etud.prenom or "",
"civilite": etud.civilite,
},
)
except requests.ConnectionError: except requests.ConnectionError:
error_message = "ConnectionError" error_message = "ConnectionError"
except requests.Timeout: except requests.Timeout:

View File

@ -378,6 +378,7 @@ class PlacementRunner:
preferences=sco_preferences.SemPreferences( preferences=sco_preferences.SemPreferences(
self.moduleimpl_data["formsemestre_id"] self.moduleimpl_data["formsemestre_id"]
), ),
table_id="placement_pdf",
) )
return tab.make_page(fmt="pdf", with_html_headers=False) return tab.make_page(fmt="pdf", with_html_headers=False)

View File

@ -221,6 +221,7 @@ def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
html_class="table_leftalign table_listegroupe", html_class="table_leftalign table_listegroupe",
pdf_link=False, # pas d'export pdf pdf_link=False, # pas d'export pdf
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
table_id="formsemestre_poursuite_report",
) )
tab.filename = scu.make_filename("poursuite " + sem["titreannee"]) tab.filename = scu.make_filename("poursuite " + sem["titreannee"])

View File

@ -110,6 +110,7 @@ get_base_preferences(formsemestre_id)
Return base preferences for current scodoc_dept (instance BasePreferences) Return base preferences for current scodoc_dept (instance BasePreferences)
""" """
import flask import flask
from flask import current_app, flash, g, request, url_for from flask import current_app, flash, g, request, url_for
@ -611,26 +612,15 @@ class BasePreferences:
"explanation": "toute saisie d'absence doit indiquer le module concerné", "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",
# },
# ),
( (
"periode_defaut", "non_present",
{ {
"initvalue": 2.0, "initvalue": 0,
"size": 10, "title": "Désactiver la saisie des présences",
"title": "Durée par défaut d'un créneau", "input_type": "boolcheckbox",
"type": "float", "labels": ["non", "oui"],
"category": "assi", "category": "assi",
"only_global": True, "explanation": "Désactive la saisie et l'affichage des présences",
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie",
}, },
), ),
( (
@ -644,18 +634,18 @@ class BasePreferences:
"category": "assi", "category": "assi",
}, },
), ),
( # (
"assi_etat_defaut", # "assi_etat_defaut",
{ # {
"explanation": "⚠ non fonctionnel, travaux en cours !", # "explanation": "⚠ non fonctionnel, travaux en cours !",
"initvalue": "aucun", # "initvalue": "aucun",
"input_type": "menu", # "input_type": "menu",
"labels": ["aucun", "present", "retard", "absent"], # "labels": ["aucun", "present", "retard", "absent"],
"allowed_values": ["aucun", "present", "retard", "absent"], # "allowed_values": ["aucun", "present", "retard", "absent"],
"title": "Définir l'état par défaut", # "title": "Définir l'état par défaut",
"category": "assi", # "category": "assi",
}, # },
), # ),
( (
"non_travail", "non_travail",
{ {
@ -962,6 +952,16 @@ class BasePreferences:
"category": "pdf", "category": "pdf",
}, },
), ),
(
"pdf_footer_extra",
{
"initvalue": "",
"title": "Texte à ajouter en pied de page",
"explanation": "sur tous les documents, par exemple vos coordonnées, ...",
"size": 78,
"category": "pdf",
},
),
( (
"pdf_footer_x", "pdf_footer_x",
{ {
@ -2291,9 +2291,7 @@ class BasePreferences:
if "explanation" in descr: if "explanation" in descr:
del descr["explanation"] del descr["explanation"]
if formsemestre_id: if formsemestre_id:
descr[ descr["explanation"] = f"""ou <span class="spanlink"
"explanation"
] = f"""ou <span class="spanlink"
onclick="set_global_pref(this, '{pref_name}');" onclick="set_global_pref(this, '{pref_name}');"
>utiliser paramètre global</span>""" >utiliser paramètre global</span>"""
if formsemestre_id and self.is_global(formsemestre_id, pref_name): if formsemestre_id and self.is_global(formsemestre_id, pref_name):

View File

@ -42,7 +42,6 @@ from app.models import (
but_validations, but_validations,
) )
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_cursus from app.scodoc import sco_cursus
@ -81,6 +80,7 @@ def dict_pvjury(
}, },
'autorisations' : [ { 'semestre_id' : { ... } } ], 'autorisations' : [ { 'semestre_id' : { ... } } ],
'validation_parcours' : True si parcours validé (diplome obtenu) 'validation_parcours' : True si parcours validé (diplome obtenu)
'parcours' : 'S1, S2, S3, S4, A1',
'prev_code' : code (calculé slt si with_prev), 'prev_code' : code (calculé slt si with_prev),
'mention' : mention (en fct moy gen), 'mention' : mention (en fct moy gen),
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées) 'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
@ -107,10 +107,12 @@ def dict_pvjury(
D = {} # même chose que decisions, mais { etudid : dec } D = {} # même chose que decisions, mais { etudid : dec }
for etudid in etudids: for etudid in etudids:
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)
Se = sco_cursus.get_situation_etud_cursus( situation_etud = sco_cursus.get_situation_etud_cursus(
etud.to_dict_scodoc7(), formsemestre_id etud.to_dict_scodoc7(), formsemestre_id
) )
semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal semestre_non_terminal = (
semestre_non_terminal or situation_etud.semestre_non_terminal
)
d = {} d = {}
d["identite"] = nt.identdict[etudid] d["identite"] = nt.identdict[etudid]
d["etat"] = nt.get_etud_etat( d["etat"] = nt.get_etud_etat(
@ -120,9 +122,8 @@ def dict_pvjury(
d["decisions_ue"] = nt.get_etud_decisions_ue(etudid) d["decisions_ue"] = nt.get_etud_decisions_ue(etudid)
if formsemestre.formation.is_apc(): if formsemestre.formation.is_apc():
d.update(but_validations.dict_decision_jury(etud, formsemestre)) d.update(but_validations.dict_decision_jury(etud, formsemestre))
d["last_formsemestre_id"] = Se.get_semestres()[ # id du dernier semestre (chronologiquement) dans lequel il a été inscrit:
-1 d["last_formsemestre_id"] = situation_etud.get_semestres()[-1]
] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit
ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid) ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values()) d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values())
@ -162,10 +163,13 @@ def dict_pvjury(
d["autorisations"] = [a.to_dict() for a in autorisations] d["autorisations"] = [a.to_dict() for a in autorisations]
d["autorisations_descr"] = descr_autorisations(autorisations) d["autorisations_descr"] = descr_autorisations(autorisations)
d["validation_parcours"] = Se.parcours_validated() d["validation_parcours"] = situation_etud.parcours_validated()
d["parcours"] = Se.get_cursus_descr(filter_futur=True) d["parcours"] = situation_etud.get_cursus_descr(filter_futur=True)
d["parcours_in_cur_formation"] = situation_etud.get_cursus_descr(
filter_futur=True, filter_formation_code=True
)
if with_parcours_decisions: if with_parcours_decisions:
d["parcours_decisions"] = Se.get_parcours_decisions() d["parcours_decisions"] = situation_etud.get_parcours_decisions()
# Observations sur les compensations: # Observations sur les compensations:
compensators = sco_cursus_dut.scolar_formsemestre_validation_list( compensators = sco_cursus_dut.scolar_formsemestre_validation_list(
cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid} cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
@ -206,19 +210,19 @@ def dict_pvjury(
if not info: if not info:
continue # should not occur continue # should not occur
etud = info[0] etud = info[0]
if Se.prev and Se.prev_decision: if situation_etud.prev and situation_etud.prev_decision:
d["prev_decision_sem"] = Se.prev_decision d["prev_decision_sem"] = situation_etud.prev_decision
d["prev_code"] = Se.prev_decision["code"] d["prev_code"] = situation_etud.prev_decision["code"]
d["prev_code_descr"] = _descr_decision_sem( d["prev_code_descr"] = _descr_decision_sem(
scu.INSCRIT, Se.prev_decision scu.INSCRIT, situation_etud.prev_decision
) )
d["prev"] = Se.prev d["prev"] = situation_etud.prev
has_prev = True has_prev = True
else: else:
d["prev_decision_sem"] = None d["prev_decision_sem"] = None
d["prev_code"] = "" d["prev_code"] = ""
d["prev_code_descr"] = "" d["prev_code_descr"] = ""
d["Se"] = Se d["Se"] = situation_etud
decisions.append(d) decisions.append(d)
D[etudid] = d D[etudid] = d

View File

@ -149,7 +149,7 @@ def pvjury_table(
etudid=e["identite"]["etudid"], etudid=e["identite"]["etudid"],
), ),
"_nomprenom_td_attrs": f"""id="{e['identite']['etudid']}" class="etudinfo" """, "_nomprenom_td_attrs": f"""id="{e['identite']['etudid']}" class="etudinfo" """,
"parcours": e["parcours"], "parcours": e["parcours_in_cur_formation"],
"decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]), "decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]),
"ue_cap": e["decisions_ue_descr"], "ue_cap": e["decisions_ue_descr"],
"validation_parcours_code": "ADM" if e["validation_parcours"] else "", "validation_parcours_code": "ADM" if e["validation_parcours"] else "",
@ -252,6 +252,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
html_class="table_leftalign", html_class="table_leftalign",
html_sortable=True, html_sortable=True,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
table_id="formsemestre_pvjury",
) )
if fmt != "html": if fmt != "html":
return tab.make_page( return tab.make_page(
@ -312,6 +313,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
html_sortable=True, html_sortable=True,
html_with_td_classes=True, html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
table_id="formsemestre_pvjury_counts",
).html() ).html()
) )
H.append( H.append(

View File

@ -198,9 +198,9 @@ def formsemestre_recapcomplet(
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Calcul automatique des décisions du jury</a> }">Calcul automatique des décisions du jury</a>
</li> </li>
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase', <li><a class="stdlink" href="{url_for('notes.formsemestre_jury_erase',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
}">Effacer <em>toutes</em> les décisions de jury BUT issues de ce semestre</a> }">Effacer <em>toutes</em> les décisions de jury issues de ce semestre</a>
</li> </li>
""" """
) )

View File

@ -236,6 +236,7 @@ def _results_by_category(
html_col_width="4em", html_col_width="4em",
html_sortable=True, html_sortable=True,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
table_id=f"results_by_category-{category_name}",
) )
@ -350,6 +351,7 @@ def formsemestre_report_counts(
"statut", "statut",
"annee_admission", "annee_admission",
"type_admission", "type_admission",
"boursier",
"boursier_prec", "boursier_prec",
] ]
if jury_but_mode: if jury_but_mode:
@ -695,19 +697,18 @@ def table_suivi_cohorte(
if statut: if statut:
dbac += " statut: %s" % statut dbac += " statut: %s" % statut
tab = GenTable( tab = GenTable(
titles=titles, caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
columns_ids=columns_ids, columns_ids=columns_ids,
rows=L, filename=scu.make_filename("cohorte " + sem["titreannee"]),
html_class="table_cohorte",
html_col_width="4em", html_col_width="4em",
html_sortable=True, html_sortable=True,
filename=scu.make_filename("cohorte " + sem["titreannee"]), origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
page_title="Suivi cohorte " + sem["titreannee"], page_title="Suivi cohorte " + sem["titreannee"],
html_class="table_cohorte",
preferences=sco_preferences.SemPreferences(formsemestre.id), preferences=sco_preferences.SemPreferences(formsemestre.id),
rows=L,
table_id="table_suivi_cohorte",
titles=titles,
) )
# Explication: liste des semestres associés à chaque date # Explication: liste des semestres associés à chaque date
if not P: if not P:
@ -1304,6 +1305,7 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
"code_cursus": len(etuds), "code_cursus": len(etuds),
}, },
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
table_id="table_suivi_cursus",
) )
return tab return tab

View File

@ -87,15 +87,16 @@ def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
bacs.append("Total") bacs.append("Total")
tab = GenTable( tab = GenTable(
titles={bac: bac for bac in bacs},
columns_ids=["titre_indicateur"] + bacs,
rows=rows,
html_sortable=False,
preferences=sco_preferences.SemPreferences(formsemestre_id),
filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"),
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
html_caption="Indicateurs BUT annuels.",
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}", base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
columns_ids=["titre_indicateur"] + bacs,
filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"),
html_caption="Indicateurs BUT annuels.",
html_sortable=False,
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
preferences=sco_preferences.SemPreferences(formsemestre_id),
rows=rows,
titles={bac: bac for bac in bacs},
table_id="formsemestre_but_indicateurs",
) )
title = "Indicateurs suivi annuel BUT" title = "Indicateurs suivi annuel BUT"
t = tab.make_page( t = tab.make_page(

View File

@ -378,9 +378,8 @@ class SemSet(dict):
def html_diagnostic(self): def html_diagnostic(self):
"""Affichage de la partie Effectifs et Liste des étudiants """Affichage de la partie Effectifs et Liste des étudiants
(actif seulement si un portail est configuré) XXX pourquoi ?? (actif seulement si un portail est configuré)
""" """
if sco_portal_apogee.has_portal():
return self.bilan.html_diagnostic() return self.bilan.html_diagnostic()
return "" return ""
@ -482,10 +481,9 @@ def semset_page(fmt="html"):
# (remplacé par n liens vers chacun des semestres) # (remplacé par n liens vers chacun des semestres)
# s['_semtitles_str_target'] = s['_export_link_target'] # s['_semtitles_str_target'] = s['_export_link_target']
# Experimental: # Experimental:
s[ s["_title_td_attrs"] = (
"_title_td_attrs" 'class="inplace_edit" data-url="edit_semset_set_title" id="%s"'
] = 'class="inplace_edit" data-url="edit_semset_set_title" id="%s"' % ( % (s["semset_id"])
s["semset_id"]
) )
tab = GenTable( tab = GenTable(
@ -513,6 +511,7 @@ def semset_page(fmt="html"):
html_class="table_leftalign", html_class="table_leftalign",
filename="semsets", filename="semsets",
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="table-semsets",
) )
if fmt != "html": if fmt != "html":
return tab.make_page(fmt=fmt) return tab.make_page(fmt=fmt)

View File

@ -115,7 +115,8 @@ def formsemestre_synchro_etuds(
url_for('notes.formsemestre_editwithmodules', url_for('notes.formsemestre_editwithmodules',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Modifier ce semestre</a>) }">Modifier ce semestre</a>)
""" """,
safe=True,
) )
footer = html_sco_header.sco_footer() footer = html_sco_header.sco_footer()
base_url = url_for( base_url = url_for(

View File

@ -169,6 +169,7 @@ def evaluation_list_operations(evaluation_id):
preferences=sco_preferences.SemPreferences( preferences=sco_preferences.SemPreferences(
evaluation.moduleimpl.formsemestre_id evaluation.moduleimpl.formsemestre_id
), ),
table_id="evaluation_list_operations",
) )
return tab.make_page() return tab.make_page()
@ -241,6 +242,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id), base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "", origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "",
table_id="formsemestre_list_saisies_notes",
) )
return tab.make_page(fmt=fmt) return tab.make_page(fmt=fmt)

View File

@ -239,6 +239,7 @@ def list_users(
base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0), base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0),
pdf_link=False, # table is too wide to fit in a paper page => disable pdf pdf_link=False, # table is too wide to fit in a paper page => disable pdf
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="list-users",
) )
return tab.make_page(fmt=fmt, with_html_headers=False) return tab.make_page(fmt=fmt, with_html_headers=False)

View File

@ -719,6 +719,7 @@ SCO_DEV_MAIL = "emmanuel.viennet@gmail.com" # SVP ne pas changer
# ne pas changer (ou vous perdez le support) # ne pas changer (ou vous perdez le support)
SCO_DUMP_UP_URL = "https://scodoc.org/scodoc-installmgr/upload-dump" SCO_DUMP_UP_URL = "https://scodoc.org/scodoc-installmgr/upload-dump"
SCO_UP2DATE = "https://scodoc.org/scodoc-installmgr/check_version" SCO_UP2DATE = "https://scodoc.org/scodoc-installmgr/check_version"
SCO_BUG_REPORT_URL = "https://scodoc.org/scodoc-installmgr/report"
SCO_ORG_TIMEOUT = 180 # contacts scodoc.org SCO_ORG_TIMEOUT = 180 # contacts scodoc.org
SCO_EXT_TIMEOUT = 180 # appels à des ressources extérieures (siret, ...) SCO_EXT_TIMEOUT = 180 # appels à des ressources extérieures (siret, ...)
SCO_TEST_API_TIMEOUT = 5 # pour tests unitaires API SCO_TEST_API_TIMEOUT = 5 # pour tests unitaires API
@ -843,7 +844,7 @@ FORBIDDEN_CHARS_EXP = re.compile(r"[*\|~\(\)\\]")
ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE) ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE)
def is_valid_code_nip(s: str) -> bool: def is_valid_code_nip(s: str) -> bool | None:
"""True si s peut être un code NIP: au moins 6 chiffres décimaux""" """True si s peut être un code NIP: au moins 6 chiffres décimaux"""
if not s: if not s:
return False return False

View File

@ -302,7 +302,6 @@
.rbtn { .rbtn {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
} }
@ -327,9 +326,12 @@
background-image: url(../icons/absent.svg); background-image: url(../icons/absent.svg);
} }
.rbtn.aucun::before { .rbtn.aucun {
background-image: url(../icons/aucun.svg); background-image: url("../icons/delete.svg");
background-color: var(--color-defaut-dark); background-size: calc(100% - 8px) calc(100% - 8px);
/* Adjust size to create "margin" */
background-position: center;
background-repeat: no-repeat;
} }
.rbtn.retard::before { .rbtn.retard::before {
@ -730,31 +732,11 @@ tr.row-justificatif.non_valide td.assi-type {
background-color: var(--color-defaut) !important; background-color: var(--color-defaut) !important;
} }
.color.est_just.sans_etat::before { .color.invalide {
content: "";
position: absolute;
width: 25%;
height: 100%;
background-color: var(--color-justi) !important;
right: 0;
}
.color.invalide::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background-color: var(--color-justi-invalide) !important; background-color: var(--color-justi-invalide) !important;
} }
.color.attente::before, .color.attente {
.color.modifie::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background: repeating-linear-gradient(to bottom, background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px, var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px, var(--color-justi-attente-stripe) 4px,
@ -762,6 +744,10 @@ tr.row-justificatif.non_valide td.assi-type {
var(--color-justi-attente) 7px) !important; var(--color-justi-attente) 7px) !important;
} }
.color.est_just {
background-color: var(--color-justi) !important;
}
#gtrcontent .pdp { #gtrcontent .pdp {
display: none; display: none;
} }

View File

@ -1,19 +1,21 @@
.day .dayline { .jour .dayline {
position: absolute; position: absolute;
display: none; display: none;
top: 100%; top: 100%;
z-index: 50; z-index: 50;
width: max-content; width: max-content;
height: 75px;
background-color: #dedede; background-color: #dedede;
border-radius: 15px; border-radius: 8px;
padding: 5px; padding: 5px 5px 15px 5px;
transform: translateX(-50%);
border: 2px solid #333;
} }
.day:hover .dayline { .jour:hover .dayline {
display: block; display: block;
} }
.dayline .mini-timeline { .dayline .mini-timeline {
margin-top: 10%; margin-top: 10%;
} }

View File

@ -1085,18 +1085,35 @@ span.spanlink:hover {
} }
.trombi_box { .trombi_box {
display: inline-block;
width: 110px;
vertical-align: top;
margin-left: 5px; margin-left: 5px;
margin-top: 5px; margin-top: 5px;
width: 140px;
/* Constant width for the box */
display: inline-flex;
flex-direction: column;
/* Ensures trombi-photo is above trombi_legend */
align-items: center;
/* Centers content horizontally */
} }
span.trombi_legend { .trombi-photo {
display: inline-block; display: flex;
justify-content: center;
/* Centers image horizontally within the photo container */
margin-bottom: 10px;
/* Adds some space between the photo and the legend */
} }
span.trombi-photo { .trombi-photo img {
width: auto;
/* Maintains aspect ratio */
height: 120px;
/* Sets the height to 90px */
max-width: 100%;
/* Ensures the image doesn't exceed the container's width */
}
/* span.trombi_legend {
display: inline-block; display: inline-block;
} }
@ -1106,7 +1123,9 @@ span.trombi_box a {
span.trombi_box a img { span.trombi_box a img {
display: inline-block; display: inline-block;
} height: 128px;
width: auto;
} */
.trombi_nom { .trombi_nom {
display: block; display: block;
@ -2096,8 +2115,9 @@ div.evaluation_titre {
vertical-align: super; vertical-align: super;
} }
/* visualisation poids évaluations */ /* visualisation poids évaluations */
.evaluation_poids { .hinton_map {
height: 12px; height: 12px;
display: inline-flex; display: inline-flex;
text-align: center; text-align: center;
@ -2105,10 +2125,10 @@ div.evaluation_titre {
margin-left: 4px; margin-left: 4px;
} }
.evaluation_poids>div { .hinton_map>div {
display: inline-flex; display: inline-flex;
height: 12px; height: var(--size);
width: 12px; width: var(--size);
margin-left: 2px; margin-left: 2px;
margin-right: 2px; margin-right: 2px;
border: 1px solid rgb(180, 180, 180); border: 1px solid rgb(180, 180, 180);
@ -2116,9 +2136,9 @@ div.evaluation_titre {
justify-content: center; justify-content: center;
} }
.evaluation_poids>div>div { .hinton_map>div>div {
height: var(--size); height: var(--boxsize);
width: var(--size); width: var(--boxsize);
background: #09c; background: #09c;
} }
@ -4831,7 +4851,9 @@ table.evaluations_recap th.titre {
} }
table.evaluations_recap td.complete, table.evaluations_recap td.complete,
table.evaluations_recap th.complete { table.evaluations_recap th.complete,
table.evaluations_recap td.type_evaluation,
table.evaluations_recap th.type_evaluation {
text-align: center; text-align: center;
} }

View File

@ -0,0 +1 @@
<svg id="Layer_1" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m170.8 14.221a14.21 14.21 0 0 1 14.2-14.207l141.991-.008a14.233 14.233 0 0 1 14.2 14.223v35.117h-170.391zm233.461 477.443a21.75 21.75 0 0 1 -21.856 20.33h-254.451a21.968 21.968 0 0 1 -21.854-20.416l-21.774-318.518h343.174l-23.234 318.6zm56.568-347.452h-409.658v-33a33.035 33.035 0 0 1 33.005-33.012l343.644-.011a33.051 33.051 0 0 1 33 33.02v33zm-270.79 291.851a14.422 14.422 0 1 0 28.844 0v-202.247a14.42 14.42 0 0 0 -28.839-.01v202.257zm102.9 0a14.424 14.424 0 1 0 28.848 0v-202.247a14.422 14.422 0 0 0 -28.843-.01z" fill="#fc3333" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@ -53,7 +53,7 @@ async function async_get(path, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec * @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/ */
async function async_post(path, data, success, errors) { async function async_post(path, data, success, errors) {
console.log("async_post " + path); // console.log("async_post " + path);
let response; let response;
try { try {
response = await fetch(path, { response = await fetch(path, {
@ -67,9 +67,15 @@ async function async_post(path, data, success, errors) {
if (response.ok) { if (response.ok) {
const responseData = await response.json(); const responseData = await response.json();
success(responseData); success(responseData);
} else {
if (response.status == 404) {
response.json().then((data) => {
if (errors) errors(data);
});
} else { } else {
throw new Error("Network response was not ok."); throw new Error("Network response was not ok.");
} }
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (errors) errors(error); if (errors) errors(error);
@ -296,7 +302,13 @@ function creerLigneEtudiant(etud, index) {
// Création des boutons d'assiduités // Création des boutons d'assiduités
if (readOnly) { if (readOnly) {
} else if (currentAssiduite.type != "conflit") { } else if (currentAssiduite.type != "conflit") {
["present", "retard", "absent"].forEach((abs) => { const etats = ["retard", "absent"];
if (!window.nonPresent) {
etats.splice(0, 0, "present");
}
etats.forEach((abs) => {
const btn = document.createElement("input"); const btn = document.createElement("input");
btn.type = "checkbox"; btn.type = "checkbox";
btn.value = abs; btn.value = abs;
@ -395,7 +407,7 @@ async function creerTousLesEtudiants(etuds) {
* @returns {String} * @returns {String}
*/ */
async function getModuleImpl(assiduite) { async function getModuleImpl(assiduite) {
if (assiduite == null) return "Pas de module"; if (assiduite == null) return "Module non spécifié";
const id = assiduite.moduleimpl_id; const id = assiduite.moduleimpl_id;
if (id == null || id == undefined) { if (id == null || id == undefined) {
@ -408,7 +420,7 @@ async function getModuleImpl(assiduite) {
? "Autre module (pas dans la liste)" ? "Autre module (pas dans la liste)"
: assiduite.external_data.module; : assiduite.external_data.module;
} else { } else {
return "Pas de module"; return "Module non spécifié";
} }
} }
@ -425,7 +437,7 @@ async function getModuleImpl(assiduite) {
return res.json(); return res.json();
}) })
.then((data) => { .then((data) => {
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ''}`; moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ""}`;
return moduleimpls[id]; return moduleimpls[id];
}) })
.catch((_) => { .catch((_) => {
@ -531,12 +543,7 @@ async function MiseAJourLigneEtud(etud) {
async function actionAssiduite(etud, etat, type, assiduite = null) { async function actionAssiduite(etud, etat, type, assiduite = null) {
const modimpl_id = $("#moduleimpl_select").val(); const modimpl_id = $("#moduleimpl_select").val();
if ( if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
assiduite &&
assiduite.etat.toLowerCase() === etat &&
assiduite.moduleimpl_id == modimpl_id
)
type = "suppression";
const { deb, fin } = getPeriodAsDate(); const { deb, fin } = getPeriodAsDate();
@ -614,7 +621,10 @@ function erreurModuleImpl(message) {
openAlertModal("Sélection du module", content); openAlertModal("Sélection du module", content);
} }
if (message == "L'étudiant n'est pas inscrit au module") { if (
message == "L'étudiant n'est pas inscrit au module" ||
message == "param 'moduleimpl_id': etud non inscrit"
) {
const HTML = ` const HTML = `
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p> <p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p> <p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
@ -642,6 +652,9 @@ function mettreToutLeMonde(etat, el = null) {
// Suppression des assiduités // Suppression des assiduités
if (etat == "vide") { if (etat == "vide") {
if (!confirm("Effacer tout les évènements correspondant à cette plage ?")) {
return; // annulation
}
const assiduites_id = lignesEtuds const assiduites_id = lignesEtuds
.filter((e) => e.getAttribute("type") == "edition") .filter((e) => e.getAttribute("type") == "edition")
.map((e) => Number(e.getAttribute("assiduite_id"))); .map((e) => Number(e.getAttribute("assiduite_id")));
@ -758,6 +771,7 @@ function envoiToastEtudiant(etat, etud) {
pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5));
} }
// TODO commenter toutes les fonctions js
function envoiToastTous(etat, count) { function envoiToastTous(etat, count) {
const span = document.createElement("span"); const span = document.createElement("span");
let etatAffiche = etat; let etatAffiche = etat;
@ -797,13 +811,16 @@ function estJourTravail(jour, nonWorkdays) {
return !nonWorkdays.includes(d); return !nonWorkdays.includes(d);
} }
function retourJourTravail(date) { function retourJourTravail(date, anti = true) {
const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms
let jour = date; let jour = date;
let compte = 0; let compte = 0;
while (!estJourTravail(jour, nonWorkDays) && compte++ < 7) { while (!estJourTravail(jour, nonWorkDays) && compte++ < 7) {
jour = new Date(jour - jourMiliSecondes); let temps = anti
? jour - jourMiliSecondes
: jour.valueOf() + jourMiliSecondes;
jour = new Date(temps);
} }
return jour; return jour;
} }
@ -813,9 +830,12 @@ function dateCouranteEstTravaillee() {
if (!estJourTravail(date, nonWorkDays)) { if (!estJourTravail(date, nonWorkDays)) {
const nouvelleDate = retourJourTravail(date); const nouvelleDate = retourJourTravail(date);
$("#date").datepicker("setDate", nouvelleDate); $("#date").datepicker("setDate", nouvelleDate);
let msg = "Le jour sélectionné";
if (new Date().format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) {
msg = "Aujourd'hui";
}
const att = document.createTextNode( const att = document.createTextNode(
`Le jour sélectionné (${Date.toFRA( `${msg} (${Date.toFRA(
date.format("YYYY-MM-DD") date.format("YYYY-MM-DD")
)}) n'est pas un jour travaillé.` )}) n'est pas un jour travaillé.`
); );
@ -836,6 +856,17 @@ function dateCouranteEstTravaillee() {
return true; return true;
} }
function jourSuivant(anti = false) {
let date = $("#date").datepicker("getDate");
date = anti ? date.add(-1, "days") : date.add(1, "days");
const nouvelleDate = retourJourTravail(date, anti);
$("#date").datepicker("setDate", nouvelleDate);
creerTousLesEtudiants(etuds);
}
/** /**
* Ajout de la visualisation des assiduités de la mini timeline * Ajout de la visualisation des assiduités de la mini timeline
* @param {HTMLElement} el l'élément survollé * @param {HTMLElement} el l'élément survollé
@ -875,6 +906,11 @@ function setupAssiduiteBubble(el, assiduite) {
actionsDiv.appendChild(infos); actionsDiv.appendChild(infos);
bubble.appendChild(actionsDiv); bubble.appendChild(actionsDiv);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const idDiv = document.createElement("div"); const idDiv = document.createElement("div");
idDiv.className = "assiduite-id"; idDiv.className = "assiduite-id";
getModuleImpl(assiduite).then((modImpl) => { getModuleImpl(assiduite).then((modImpl) => {
@ -882,26 +918,32 @@ function setupAssiduiteBubble(el, assiduite) {
}); });
bubble.appendChild(idDiv); bubble.appendChild(idDiv);
const periodDivDeb = document.createElement("div"); // Affichage des dates
periodDivDeb.className = "assiduite-period"; // si les jours sont les mêmes, on affiche "jour hh:mm - hh:mm"
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`; // sinon on affiche "jour hh:mm - jour hh:mm"
bubble.appendChild(periodDivDeb); const periodDiv = document.createElement("div");
const periodDivFin = document.createElement("div"); periodDiv.className = "assiduite-period";
periodDivFin.className = "assiduite-period"; const dateDeb = new Date(Date.removeUTC(assiduite.date_debut));
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`; const dateFin = new Date(Date.removeUTC(assiduite.date_fin));
bubble.appendChild(periodDivFin); if (dateDeb.isSame(dateFin, "day")) {
const jour = dateDeb.format("DD/MM/YYYY");
const deb = dateDeb.format("HH:mm");
const fin = dateFin.format("HH:mm");
periodDiv.textContent = `${jour} de ${deb} à ${fin}`;
} else {
const jourDeb = dateDeb.format("DD/MM/YYYY");
const jourFin = dateFin.format("DD/MM/YYYY");
periodDiv.textContent = `du ${jourDeb} au ${jourFin}`;
}
const stateDiv = document.createElement("div"); bubble.appendChild(periodDiv);
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const motifDiv = document.createElement("div"); const motifDiv = document.createElement("div");
stateDiv.className = "assiduite-why"; motifDiv.className = "assiduite-why";
const motif = ["", null, undefined].includes(assiduite.desc) const motif = ["", null, undefined].includes(assiduite.desc)
? "Pas de motif" ? "Non spécifié"
: assiduite.desc.capitalize(); : assiduite.desc.capitalize();
stateDiv.textContent = `Motif: ${motif}`; motifDiv.textContent = `Motif: ${motif}`;
bubble.appendChild(motifDiv); bubble.appendChild(motifDiv);
const userIdDiv = document.createElement("div"); const userIdDiv = document.createElement("div");

View File

@ -430,3 +430,23 @@ class Duration {
function hasTimeConflict(period, interval) { function hasTimeConflict(period, interval) {
return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb); return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb);
} }
// Fonction auxiliaire pour obtenir le numéro de semaine ISO d'une date donnée
function getISOWeek(date) {
const target = new Date(date.valueOf());
const dayNr = (date.getUTCDay() + 6) % 7;
target.setUTCDate(target.getUTCDate() - dayNr + 3);
const firstThursday = target.valueOf();
target.setUTCMonth(0, 1);
if (target.getUTCDay() !== 4) {
target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7));
}
return 1 + Math.ceil((firstThursday - target) / 604800000);
}
// Fonction auxiliaire pour obtenir le nombre de semaines ISO dans une année donnée
function getISOWeeksInYear(year) {
const date = new Date(year, 11, 31);
const week = getISOWeek(date);
return week === 1 ? getISOWeek(new Date(year, 11, 24)) : week;
}

View File

@ -155,18 +155,9 @@ function get_query_args() {
// Tables (gen_tables) // Tables (gen_tables)
$(function () { $(function () {
if ($("table.gt_table").length > 0) { if ($("table.gt_table, table.gt_table_searchable").length > 0) {
const url = new URL(document.URL);
const order_info_key = JSON.stringify(["table_order", url.pathname]);
let order_info;
const x = localStorage.getItem(order_info_key);
if (x) {
try {
order_info = JSON.parse(x);
} catch (error) {
console.error(error);
}
}
var table_options = { var table_options = {
paging: false, paging: false,
searching: false, searching: false,
@ -178,20 +169,46 @@ $(function () {
}, },
orderCellsTop: true, // cellules ligne 1 pour tri orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting aaSorting: [], // Prevent initial sorting
order: order_info, order: "",
drawCallback: function (settings) { drawCallback: function (settings) {
// permet de conserver l'ordre de tri des colonnes // permet de conserver l'ordre de tri des colonnes
let table = $("table.gt_table").DataTable(); let currentTable = $(settings.nTable);
let order_info = JSON.stringify(table.order()); let order_info_key = get_table_order_info_key(currentTable.attr("id"));
let dataTableInstance = $(currentTable).DataTable();
let order_info = JSON.stringify(dataTableInstance.order());
localStorage.setItem(order_info_key, order_info); localStorage.setItem(order_info_key, order_info);
}, },
}; };
$("table.gt_table").DataTable(table_options);
$('.gt_table').each(function() {
const x = localStorage.getItem(get_table_order_info_key(this.id));
if (x) {
try {
let order_info = JSON.parse(x);
table_options.order = order_info;
} catch (error) {
console.error(error);
delete table_options.order;
}
} else {
delete table_options.order;
}
$(this).DataTable(table_options);
});
table_options["searching"] = true; table_options["searching"] = true;
$("table.gt_table_searchable").DataTable(table_options); $("table.gt_table_searchable").each(function() {
$(this).DataTable(table_options);
});
} }
}); });
function get_table_order_info_key(table_id) {
const url = new URL(document.URL);
return JSON.stringify(["table_order", table_id, url.pathname]);
}
// Show tags (readonly) // Show tags (readonly)
function readOnlyTags(nodes) { function readOnlyTags(nodes) {
// nodes are textareas, hide them and create a span showing tags // nodes are textareas, hide them and create a span showing tags
@ -230,11 +247,6 @@ class ScoFieldEditor {
return true; // Aucune modification, pas d'enregistrement mais on continue normalement return true; // Aucune modification, pas d'enregistrement mais on continue normalement
} }
obj.classList.add("sco_wait"); obj.classList.add("sco_wait");
// DEBUG
// console.log(`
// data : ${value},
// id: ${obj.dataset.oid}
// `);
$.post( $.post(
this.save_url, this.save_url,

View File

@ -11,8 +11,8 @@ h.id="btc";
h.setAttribute("id","btc"); h.setAttribute("id","btc");
h.style.position="absolute"; h.style.position="absolute";
document.getElementsByTagName("body")[0].appendChild(h); document.getElementsByTagName("body")[0].appendChild(h);
if(id==null) links=document.getElementsByTagName("a"); if(id==null) links=document.querySelectorAll("a, [data-tooltip]"); // was document.getElementsByTagName("a")
else links=document.getElementById(id).getElementsByTagName("a"); else links=document.getElementById(id).querySelectorAll("a, [data-tooltip]");// was document.getElementById(id).getElementsByTagName("a")
for(i=0;i<links.length;i++){ for(i=0;i<links.length;i++){
Prepare(links[i]); Prepare(links[i]);
} }

View File

@ -4,8 +4,8 @@
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""Liste simple d'étudiants """Liste simple d'étudiants"""
"""
import datetime import datetime
from flask import g, url_for from flask import g, url_for
from app import log from app import log
@ -140,6 +140,13 @@ class RowAssi(tb.Row):
) )
stats = self._get_etud_stats(etud) stats = self._get_etud_stats(etud)
for key, value in stats.items(): for key, value in stats.items():
if key == "present" and sco_preferences.get_preference(
"non_present",
dept_id=g.scodoc_dept_id,
formsemestre_id=self.table.formsemestre.id,
):
continue
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats") self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
if key != "present": if key != "present":
self.add_cell( self.add_cell(

View File

@ -12,7 +12,7 @@
<h1>Traitement de l'assiduité</h1> <h1>Traitement de l'assiduité</h1>
<p class="help"> <p class="help">
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par Pour saisir l'assiduité ou consulter les états, passer par
le semestre concerné (saisie par jour ou saisie différée). le semestre concerné (saisie par jour ou saisie différée).
</p> </p>

View File

@ -86,9 +86,6 @@ Bilan assiduité de {{sco.etud.nomprenom}}
<div class="scobox"> <div class="scobox">
<section class="nonvalide"> <section class="nonvalide">
<div class="help">Le tableau n'affiche que les assiduités non justifiées
et les justificatifs soumis / modifiés
</div>
{{tableau | safe }} {{tableau | safe }}
</section> </section>
</div> </div>
@ -99,6 +96,9 @@ Bilan assiduité de {{sco.etud.nomprenom}}
département)</p> département)</p>
<p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates, <p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
appuyer sur le bouton "Actualiser"</p> appuyer sur le bouton "Actualiser"</p>
{% include "assiduites/explication_etats_justifs.j2" %}
</div> </div>
</div> </div>

View File

@ -23,49 +23,8 @@ Calendrier de l'assiduité
for="mode_demi">mode demi journée</label> for="mode_demi">mode demi journée</label>
</div> </div>
<div class="calendrier"> <div class="cal">
{% for mois,jours in calendrier.items() %} {{calendrier|safe}}
<div class="month">
<h3>{{mois}}</h3>
<div class="days {{'demi' if mode_demi else ''}}">
{% for jour in jours %}
{% if jour.is_non_work() %}
<div class="day {{jour.get_class()}}">
<span>{{jour.get_nom()}}</span>
{% else %}
<div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}">
{% endif %}
{% if mode_demi %}
{% if not jour.is_non_work() %}
<span>{{jour.get_nom()}}</span>
<span class="{{jour.get_demi_class(True, show_pres,show_reta)}}"></span>
<span class="{{jour.get_demi_class(False, show_pres,show_reta)}}"></span>
{% endif %}
{% else %}
{% if not jour.is_non_work() %}
<span>{{jour.get_nom(False)}}</span>
{% endif %}
{% endif %}
{% if not jour.is_non_work() and jour.has_assiduites()%}
<div class="dayline">
<div class="dayline-title">
<span>Assiduité du</span>
<br>
<span>{{jour.get_date()}}</span>
{{jour.generate_minitimeline() | safe}}
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="annee"> <div class="annee">
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer <span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
année: </span> année: </span>
@ -77,36 +36,7 @@ Calendrier de l'assiduité
<div class="help"> <div class="help">
<h3>Calendrier</h3> <h3>Calendrier</h3>
<p>Code couleur</p> {% include "assiduites/widgets/legende_couleur.j2" %}
<ul class="couleurs">
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
période
</li>
<li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Quart Bleu" class="est_just demo"></span> &rightarrow; la période est couverte par un
justificatif valide</li>
<li><span title="Justif. non valide" class="invalide demo"></span> &rightarrow; la période est
couverte par un justificatif non valide
</li>
<li><span title="Justif. en attente" class="attente demo"></span> &rightarrow; la période
a un justificatif en attente de validation
</li>
</ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
</div> </div>
<ul class="couleurs print"> <ul class="couleurs print">
<li><span title="Vert" class="present demo"></span> présence <li><span title="Vert" class="present demo"></span> présence
@ -158,57 +88,34 @@ Calendrier de l'assiduité
.calendrier { .calendrier {
display: flex; display: flex;
justify-content: start; justify-content: center;
overflow-x: scroll; overflow-x: scroll;
border: 1px solid #444; border: 1px solid #444;
border-radius: 12px; border-radius: 12px;
margin-bottom: 12px; margin-bottom: 12px;
} }
.month h3 { .assi_case {
text-align: center;
}
.day,
.demi .day.color.nonwork {
text-align: left;
margin: 2px;
cursor: default;
font-size: 13px;
position: relative;
font-weight: normal;
min-width: 6em;
display: flex; display: flex;
justify-content: start; width: 100%;
height: 100%;
} }
.demo.invalide { .assi_case > span {
background-color: var(--color-justi-invalide) !important; flex: 1;
} }
.demo.attente { .assi_case>span:last-of-type {
background: repeating-linear-gradient(to bottom, border-left: #d5d5d5 solid 1px;
var(--color-justi-attente-stripe) 0px, }
var(--color-justi-attente-stripe) 4px, .assi_case>span:first-of-type {
var(--color-justi-attente) 4px, border-right: #d5d5d5 solid 1px;
var(--color-justi-attente) 7px) !important;
} }
.demo.est_just { .dayline{
background-color: var(--color-justi) !important; display: none;
} }
.demi .day.nonwork>span {
flex: none;
border: none;
}
.demi .day {
border-radius: 0;
}
@media print { @media print {
.couleurs.print { .couleurs.print {
@ -338,7 +245,5 @@ Calendrier de l'assiduité
window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`); window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
}) })
}); });
</script> </script>
{% endblock app_content %} {% endblock app_content %}

View File

@ -1,27 +0,0 @@
{% extends "sco_page.j2" %}
{% block title %}
Assiduité de {{etud.nomprenom}}
{% endblock title %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock styles %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% endblock %}
{% block app_content %}
<div class="pageContent">
<h2>Liste de l'assiduité et des justificatifs de {{sco.etud.html_link_fiche()|safe}}</h2>
{{tableau | safe }}
</div>
{% include "assiduites/explication_etats_justifs.j2" %}
{% endblock app_content %}

View File

@ -310,8 +310,13 @@ async function nouvellePeriode(period = null) {
const assi_btns = document.createElement('div'); const assi_btns = document.createElement('div');
assi_btns.classList.add('assi-btns'); assi_btns.classList.add('assi-btns');
const etats = ["retard", "absent"];
["present", "retard", "absent"].forEach((value) => { if(!window.nonPresent){
etats.splice(0,0,"present");
}
etats.forEach((value) => {
const cbox = document.createElement("input"); const cbox = document.createElement("input");
cbox.type = "checkbox"; cbox.type = "checkbox";
cbox.value = value; cbox.value = value;
@ -499,6 +504,8 @@ const moduleimpls = new Map();
const inscriptionsModules = new Map(); const inscriptionsModules = new Map();
const nonWorkDays = [{{ nonworkdays| safe }}]; const nonWorkDays = [{{ nonworkdays| safe }}];
window.nonPresent = {{ 'true' if non_present else 'false' }};
// Vérification du forçage de module // Vérification du forçage de module
window.forceModule = "{{ forcer_module }}" == "True"; window.forceModule = "{{ forcer_module }}" == "True";
if (window.forceModule) { if (window.forceModule) {
@ -518,12 +525,29 @@ if (window.forceModule) {
} }
}); });
} }
const defaultPlage = {{ nouv_plage | safe}} || [];
/** /**
* Fonction exécutée au lancement de la page * Fonction exécutée au lancement de la page
* - On affiche ou non les photos des étudiants * - On affiche ou non les photos des étudiants
* - On vérifie si la date est un jour travaillé * - On vérifie si la date est un jour travaillé
*/ */
async function main() { async function main() {
// On initialise les sélecteurs avec les valeurs par défaut (si elles existent)
if (defaultPlage.every((e) => e)) {
$("#date").datepicker("setDate", defaultPlage[0]);
$("#debut").val(defaultPlage[1]);
$("#fin").val(defaultPlage[2]);
// On ajoute la période si la date est un jour travaillé
if(dateCouranteEstTravaillee()){
await nouvellePeriode();
}
}
const checked = localStorage.getItem("scodoc-etud-pdp") == "true"; const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
afficherPDP(checked); afficherPDP(checked);
$("#date").on("change", async function (d) { $("#date").on("change", async function (d) {
@ -532,7 +556,7 @@ async function main() {
}); });
} }
main(); window.addEventListener("load", main);
</script> </script>
@ -546,6 +570,36 @@ main();
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2> <h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
<div class="ue_warning warning">
Attention, cette page va prochainement être supprimée, car il est plus facile d'utiliser
<ul>
la page
<li><a class="stdlink" href="{{
url_for('assiduites.signal_assiduites_group',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
group_ids=group_ids)
}}">
saisie de l'assiduité</a> pour saisir à une seule date quelconque
</li>
<li>ou <a class="stdlink" href="{{
url_for('assiduites.signal_assiduites_hebdo',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
group_ids=group_ids,
)
}}">saisie hebdomadaire</a> pour saisir sur une semaine.
</li>
</ul>
<p>Ci-dessous le formulaire vous permettant de saisir plusieurs plages à la fois,
qui va bientôt être retiré.
</p>
<p>N'hésitez pas à commenter sur le <a href="{{scu.SCO_DISCORD_ASSISTANCE}}">salon Discord</a>
si vous avez d'autres besoins.
</p>
</div>
<div id="fix"> <div id="fix">
<!-- Nouvelle Plage <!-- Nouvelle Plage
@ -600,7 +654,9 @@ main();
Intialiser les étudiants comme : Intialiser les étudiants comme :
<select name="etatDef" id="etatDef"> <select name="etatDef" id="etatDef">
<option value="">-</option> <option value="">-</option>
{% if not non_present %}
<option value="present">présents</option> <option value="present">présents</option>
{% endif %}
<option value="retard">en retard</option> <option value="retard">en retard</option>
<option value="absent">absents</option> <option value="absent">absents</option>
</select> </select>

View File

@ -31,6 +31,7 @@
const readOnly = {{ readonly }}; const readOnly = {{ readonly }};
window.forceModule = "{{ forcer_module }}" == "True" window.forceModule = "{{ forcer_module }}" == "True"
window.nonPresent = {{ 'true' if non_present else 'false' }};
const etudsDefDem = {{ defdem | safe }} const etudsDefDem = {{ defdem | safe }}
@ -104,6 +105,24 @@
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css"> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css"> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css"> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
<style>
#retour-haut{
position: fixed;
bottom: 10px;
right: 10px;
font-size: 3em;
padding: 5px;
border-radius: 5px;
text-decoration: none;
}
html{
scroll-behavior: smooth !important;
}
</style>
{% endblock styles %} {% endblock styles %}
@ -112,6 +131,10 @@
{{ minitimeline|safe }} {{ minitimeline|safe }}
<section id="content"> <section id="content">
<a id="retour-haut" href="#gtrcontent">
⬆️
</a>
<div class="no-display"> <div class="no-display">
<span class="formsemestre_id">{{formsemestre_id}}</span> <span class="formsemestre_id">{{formsemestre_id}}</span>
<span id="formsemestre_date_debut">{{formsemestre_date_debut}}</span> <span id="formsemestre_date_debut">{{formsemestre_date_debut}}</span>
@ -130,12 +153,22 @@
<div class="infos"> <div class="infos">
<div class="infos-button">Groupes&nbsp;: {{grp|safe}}</div> <div class="infos-button">Groupes&nbsp;: {{grp|safe}}</div>
<div> <div>
<button class="btn_date" onclick="jourSuivant(true)">
&LeftArrowBar;
</button>
<input type="text" name="date" id="date" class="datepicker" value="{{date}}"> <input type="text" name="date" id="date" class="datepicker" value="{{date}}">
</div> </div>
<button class="btn_date" onclick="jourSuivant(false)">
&RightArrowBar;
</button>
</div> </div>
</fieldset> </fieldset>
<div style="display: {{'none' if readonly == 'true' else 'block'}};"> <div style="display: {{'none' if readonly == 'true' else 'block'}};">
{{timeline|safe}} {{timeline|safe}}
<div>
<button onclick="setPeriodValues(t_start, t_mid)">Matin</button>
<button onclick="setPeriodValues(t_mid, t_end)">Après-Midi</button>
</div>
</div> </div>
{% if readonly == "false" %} {% if readonly == "false" %}
@ -159,14 +192,16 @@
<div class="mass-selection"> <div class="mass-selection">
<span>Mettre tout le monde :</span> <span>Mettre tout le monde :</span>
<fieldset class="btns_field mass"> <fieldset class="btns_field mass">
{% if not non_present %}
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present" <input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present"> class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Indique l'état Présent pour tous les étudiants" data-tooltip>
{% endif %}
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard" <input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard"
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard"> class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Indique l'état Retard pour tous les étudiants" data-tooltip>
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent" <input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
class="rbtn absent" onclick="mettreToutLeMonde('absent', this)" title="Absent"> class="rbtn absent" onclick="mettreToutLeMonde('absent', this)" title="Indique l'état Absent pour tous les étudiants" data-tooltip>
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun" <input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer"> class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Retire l'état pour tous les étudiants" data-tooltip>
</fieldset> </fieldset>
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em> <em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
</div> </div>
@ -178,6 +213,11 @@
</p> </p>
</div> </div>
<div class="help">
<h3>Calendrier</h3>
{% include "assiduites/widgets/legende_couleur.j2" %}
</div>
{% include "assiduites/widgets/toast.j2" %} {% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %} {% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %} {% include "assiduites/widgets/prompt.j2" %}

View File

@ -0,0 +1,899 @@
{% extends "sco_page.j2" %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
<style>
.rbtn::before {
--size: 1.5em;
width: var(--size);
height: var(--size);
}
.ui-timepicker-container,
#ui-datepicker-div {
z-index: 5 !important;
}
#new_periode,
#actions {
display: flex;
flex-direction: column;
width: fit-content;
gap: 0.5em;
}
#actions {
flex-direction: row;
align-items: center;
margin: 5px 0;
}
#actions label {
margin: 0;
}
#fix {
display: flex;
flex-direction: row;
gap: 1em;
justify-content: space-between;
width: fit-content;
}
#fix>.box {
border: 1px solid #444;
border-radius: 0.5em;
padding: 1em;
}
.timepicker {
width: 5em;
text-align: center;
}
#moduleimpl_select {
text-align: center;
}
table {
border-collapse: collapse;
width: 100%;
max-width: 1600px;
position: relative;
table-layout: fixed;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
background-color: white;
}
th {
z-index: 1;
}
.premier th {
position: sticky;
top: 0;
background-color: white;
}
.second th {
position: sticky;
top: 38px;
background-color: white;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 1;
}
.rbtn:not(:checked)::before {
opacity: 0.5;
}
.grayed {
filter: brightness(0.5);
}
.conflit {
background-color: var(--color-conflit);
}
.conflit_calendar{
font-size: 1.5em;
cursor: pointer;
}
</style>
<style>
.timePicker-modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.timePicker-modal.show {
display: block;
}
.timePicker-modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 300px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
text-align: center;
}
.timePicker-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.timePicker-close:hover,
.timePicker-close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
.time-picker-container {
margin: 15px 0;
}
#confirmButton {
padding: 10px 20px;
font-size: 16px;
background-color: var(--color-primary);
color: white;
border: none;
cursor: pointer;
}
#confirmButton:hover {
background-color: var(--color-secondary);
}
.etudinfo{
text-align: left;
}
</style>
{% endblock styles %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% include "sco_timepicker.j2" %}
<script>
const readonly = "{{readonly | safe}}" == "True";
const non_present = "{{non_present | safe}}" == "True";
const etuds = [
{% for etud in etudiants %}
{
id: {{etud.etudid}},
nom: "{{etud.nom}}",
prenom: "{{etud.prenom}}"
},
{% endfor %}
]
let days = [
{% for jour in hebdo_jours %}
{
date : new Date(Date.fromFRA("{{jour[1][1]}}")),
visible : "{{not jour[0]}}" == "True",
nom : "{{jour[1][0]}}",
},
{% endfor %}
] // [0]=Lundi ... [6]=Dimanche -> à 00h00
//Une fonction d'action quand un bouton est cliqué
// 3 possibilités :
// - assiduite_id = null -> créer nv assi avec état du bouton
// - assiduite_id non null et bouton coché == etat assi -> suppression de l'assiduité
// - assiduite_id non null et bouton coché != etat assi -> modification de l'assiduité
async function actionButton(btn, same = false) {
let td = btn.parentElement;
let tr = td.parentElement;
let etudid = tr.getAttribute("etudid");
let etud = etuds.find((etud) => etud.id == etudid);
let etat = btn.value;
let assiduite_id = td.getAttribute("assiduite_id");
let dayInfo = [td.getAttribute("day"), td.getAttribute("time")]// [0]=[0..6] [1]=am/pm
let day = days[dayInfo[0]].date;
dayInfo[1] = dayInfo[1] == "am" ? "matin" : "apresmidi";
let deb = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].debut);
let fin = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].fin);
const assi = {
etudid: etudid,
etat: etat,
moduleimpl_id: document.getElementById("moduleimpl_select").value,
date_debut: deb.toFakeIso(),
date_fin: fin.toFakeIso(),
}
let cancelEvent = false;
if (assiduite_id != "") {
if (same) {
// Suppression
await async_post(
`../../api/assiduite/delete`,
[assiduite_id],
(data) => {
if (data.success.length > 0) {
envoiToastEtudiant("remove", etud);
td.setAttribute("assiduite_id", "");
} else {
console.error(data.errors["0"].message);
cancelEvent = true;
erreurModuleImpl(data.errors["0"].message);
}
},
(error) => {
console.error("Erreur lors de la suppression de l'assiduité", error);
cancelEvent = true;
}
);
} else {
// Modification
await async_post(
`../../api/assiduite/${assiduite_id}/edit`,
assi,
(data) => {
envoiToastEtudiant(etat, etud);
},
(error) => {
console.error("Erreur lors de la modification de l'assiduité", error);
cancelEvent = true;
erreurModuleImpl(error.message);
}
);
}
} else {
// Création
await async_post(
`../../api/assiduite/${etud.id}/create`,
[assi],
(data) => {
if (data.success.length > 0) {
envoiToastEtudiant(etat, etud);
//mise à jour de l'assiduité_id dans le td
td.setAttribute("assiduite_id", data.success["0"].message.assiduite_id);
} else {
console.error(data.errors["0"].message);
erreurModuleImpl(data.errors["0"].message);
cancelEvent = true;
}
},
(error) => {
console.error("Erreur lors de la création de l'assiduité", error);
}
);
}
return cancelEvent;
}
async function recupAssiduitesHebdo(callback) {
const etudIds = etuds.map((etud) => etud.id).join(",");
const date_debut = days[0].date.startOf("day").format("YYYY-MM-DDTHH:mm");
const date_fin = days[6].date.endOf("day").format("YYYY-MM-DDTHH:mm");
url =
`../../api/assiduites/group/query?date_debut=${date_debut}` +
`&date_fin=${date_fin}&etudids=${etudIds}&with_justifs`;
await fetch(url)
.then((res) => {
if (!res.ok) {
throw new Error("Network response was not ok");
}
return res.json();
})
.then((data) => {
let assiduites = []
Object.keys(data).forEach((etudid) => {
assiduites.push(...data[etudid]);
});
callback(assiduites);
})
.catch((error) =>
console.error(
"There has been a problem with your fetch operation:",
error
)
);
}
function updateTable(assiduites) {
const img_conflit = `
<a
class="conflit_calendar"
title="Des assiduités existent déjà pour cette période. Cliquez ici pour voir le calendrier de l'assiduité de l'étudiant"
data-tooltip
target="_blank"
>📅</a>`
// Suppression existant
document.querySelectorAll("td.btns").forEach((el) => {
el.remove();
});
for (let i = 0; i < days.length; i++) {
let day = days[i].date;
let morningPeriod = {
deb: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.debut),
fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.fin),
}
let afternoonPeriod = {
deb: (new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.debut)),
fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.fin),
}
const assiduitesByDay = {
matin: assiduites.filter((assi) => {
const period = {
deb: new Date(assi.date_debut),
fin: new Date(assi.date_fin)
}
return hasTimeConflict(period, morningPeriod);
}),
apresmidi: assiduites.filter((assi) => {
const period = {
deb: new Date(assi.date_debut),
fin: new Date(assi.date_fin)
}
return hasTimeConflict(period, afternoonPeriod);
})
};
// Récupération des tr étudiants
let trs = document.querySelectorAll("tr[etudid]");
trs.forEach((tr) => {
let etudid = tr.getAttribute("etudid");
if (!days[i].visible && i >= 5) {
return;
} else if (!days[i].visible) {
tr.insertAdjacentHTML("beforeend", "<td class='grayed btns' colspan='2'></td>");
return;
}
let etudAssiMorning = assiduitesByDay.matin.filter((a) => {
return a.etudid == etudid;
});
let etudAssiAfternoon = assiduitesByDay.apresmidi.filter((a) => {
return a.etudid == etudid;
});
// Créations des boutons
// matin
let tdMatin = document.createElement("td");
tdMatin.classList.add("btns");
tdMatin.setAttribute("day", i);
tdMatin.setAttribute("time", "am");
tr.appendChild(tdMatin);
// après-midi
let tdApresmidi = document.createElement("td");
tdApresmidi.classList.add("btns");
tdApresmidi.setAttribute("day", i);
tdApresmidi.setAttribute("time", "pm");
tr.appendChild(tdApresmidi);
// Peuplement des boutons en fonction des assiduités
let boutons = `
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn retard" value="retard">
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn absent" value="absent">
`
if (!non_present) {
boutons = `<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn present" value="present">`+boutons;
}
// matin
tdMatin.innerHTML = boutons
tdMatin.setAttribute("assiduite_id", "")
if (etudAssiMorning.length != 0) {
let assi = etudAssiMorning[0];
const deb = new Date(assi.date_debut);
const fin = new Date(assi.date_fin);
// si dates == periode -> cocher bouton correspondant
// Sinon supprimer boutons et mettre case "rouge" + tooltip
if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) {
let etat = assi.etat.toLowerCase();
const input = tdMatin.querySelector(`[value="${etat}"]`)
if (input) {
input.checked = true;
}
tdMatin.setAttribute("assiduite_id", assi.assiduite_id);
let saisie = new Date(assi.entry_date).format("DD/MM/Y HH:mm");
saisie = saisie.split(" ").join(" à ");
let text = `noté ${etat} le ${saisie} par ${assi.user_nom_complet}`;
tdMatin.setAttribute("title", text);
tdMatin.setAttribute("data-tooltip", "");
} else {
tdMatin.innerHTML = img_conflit;
tdMatin.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`;
tdMatin.classList.add("conflit");
}
}
// après-midi
tdApresmidi.innerHTML = boutons
tdApresmidi.setAttribute("assiduite_id", "")
if (etudAssiAfternoon.length != 0) {
let assi = etudAssiAfternoon[0];
const deb = new Date(assi.date_debut);
const fin = new Date(assi.date_fin);
// si dates == periode -> cocher bouton correspondant
// Sinon supprimer boutons et mettre case "rouge" + tooltip
if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) {
let etat = assi.etat.toLowerCase();
const input = tdApresmidi.querySelector(`[value="${etat}"]`)
if (input) {
input.checked = true;
}
tdApresmidi.setAttribute("assiduite_id", assi.assiduite_id);
let saisie = new Date(assi.entry_date).format("DD/MM/Y HH:mm");
saisie = saisie.split(" ").join(" à ");
let text = `noté ${etat} le ${saisie} par ${assi.user_nom_complet}`;
tdApresmidi.setAttribute("title", text);
tdApresmidi.setAttribute("data-tooltip", "");
} else {
tdApresmidi.innerHTML = img_conflit;
tdApresmidi.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`;
tdApresmidi.classList.add("conflit");
}
}
});
}
document.querySelectorAll("td .rbtn").forEach((el) => {
el.addEventListener("click", async (e) => {
if (readonly) {
e.preventDefault();
return;
}
let target = e.target;
let parent = target.parentElement;
let isCancelled = await actionButton(target, !target.checked);
if (isCancelled) {
e.preventDefault();
target.checked = !target.checked;
return;
}
let inputs = parent.querySelectorAll(".rbtn");
inputs.forEach((input) => {
if (input != target) {
input.checked = false;
}
});
});
});
enableTooltips("table");
}
// Une fonction pour changer de semaine (précédente ou suivante)
// fait juste un location.href avec les bons paramètres
function changeWeek(prev = false) {
const currentUrl = new URL(window.location.href); // Récupère l'URL actuelle
const params = new URLSearchParams(currentUrl.search); // Récupère les paramètres de l'URL
let currentWeekParam = params.get('week');
// Extraire l'année et le numéro de semaine du paramètre de la semaine actuelle
const [year, week] = currentWeekParam.split('-W').map(Number);
// Calculer la nouvelle semaine et l'année
let newYear = year;
let newWeek = week + (prev ? -1 : 1);
if (newWeek < 1) {
newYear -= 1; // Passer à l'année précédente
newWeek = getISOWeeksInYear(newYear); // Dernière semaine de l'année précédente
} else if (newWeek > getISOWeeksInYear(newYear)) {
newYear += 1; // Passer à l'année suivante
newWeek = 1; // Première semaine de l'année suivante
}
// Formater le nouveau paramètre de semaine
const newWeekParam = `${newYear}-W${String(newWeek).padStart(2, '0')}`;
params.set('week', newWeekParam); // Mettre à jour le paramètre 'week'
currentUrl.search = params.toString(); // Mettre à jour les paramètres de l'URL
window.location.href = currentUrl.toString(); // Rediriger vers la nouvelle URL
}
// Une fonction pour gérer le bouton "tout le monde présent"
// coche tous les boutons de la colonne
function allPresent(day, time) {
// Version naive : coche tous les boutons de la colonne
// TODO - Optimiser avec une seule requête API
let tds = document.querySelectorAll(`td[day="${day}"][time="${time}"]`);
const real_time = time == "am" ? "matin" : "apresmidi";
const assi = {
etat: "present",
moduleimpl_id: document.getElementById("moduleimpl_select").value,
date_debut: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].debut).toFakeIso(),
date_fin: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].fin).toFakeIso(),
}
let toCreate = []; // [{etudid:<int>}]
let toEdit = [];// [{etudid:<int>, assiduite_id:<int>}]
tds.forEach((td) => {
// on ne touche pas aux conflits
if (td.classList.contains("conflit")) {
return;
}
const tr = td.parentElement;
const etudid = Number(tr.getAttribute("etudid"));
const assiduite_id = td.getAttribute("assiduite_id");
if (assiduite_id == "") {
toCreate.push({ etudid: etudid });
} else {
toEdit.push({ etudid: etudid, assiduite_id: Number(assiduite_id) });
}
})
// Création
toCreate = toCreate.map((el) => {
return {
...assi,
etudid: el.etudid,
}
});
// Modification
toEdit = toEdit.map((el) => {
return {
...assi,
etudid: el.etudid,
assiduite_id: el.assiduite_id,
}
});
// Appel API
let counts = {
create: toCreate.length,
edit: toEdit.length
}
const promiseCreate = async_post(
`../../api/assiduites/create`,
toCreate,
async (data) => {
if (data.errors.length > 0) {
console.error(data.errors);
data.errors.forEach((err) => {
let obj = toCreate[err.indice];
let etu = etuds.find((el) => el.id == obj.etudid);
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
const toast = generateToast(text, "var(--color-error)", 10);
pushToast(toast);
});
}
counts.create = data.success.length;
},
(error) => {
console.error("Erreur lors de la création de l'assiduité", error);
}
);
const promiseEdit = async_post(
`../../api/assiduites/edit`,
toEdit,
async (data) => {
if (data.errors.length > 0) {
console.error(data.errors);
data.errors.forEach((err) => {
let obj = toEdit[err.indice];
let etu = etuds.find((el) => el.id == obj.etudid);
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
const toast = generateToast(text, "var(--color-error)");
pushToast(toast);
});
}
counts.edit = data.success.length;
},
(error) => {
console.error("Erreur lors de l'édition de l'assiduité", error);
}
);
// Affiche un loader
afficheLoader();
Promise.all([promiseCreate, promiseEdit]).then(async () => {
retirerLoader();
await recupAssiduitesHebdo(updateTable);
envoiToastTous("present", counts.create + counts.edit);
});
}
</script>
<script>
function updateTemps(temps){
let matin = document.getElementById("text-matin");
let apresmidi = document.getElementById("text-apresmidi");
matin.textContent = `${temps.matin.debut} à ${temps.matin.fin}`;
apresmidi.textContent = `${temps.apresmidi.debut} à ${temps.apresmidi.fin}`;
recupAssiduitesHebdo(updateTable);
}
const temps = {
matin: {
debut: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
fin: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}"
},
apresmidi: {
debut: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}",
fin: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
}
}
document.getElementById("text-matin").addEventListener("click", (e)=>{
e.preventDefault();
openModal(true);
});
document.getElementById("text-apresmidi").addEventListener("click", (e)=>{
e.preventDefault();
openModal(false);
});
updateTemps(temps);
</script>
<script>
function openModal(morning = true){
let text = morning ? "du matin" : "de l'après-midi";
const modal = document.getElementById("timePickerModal");
modal.querySelector("#timePicker-modal-text").textContent = text;
let time1 = $("#time1");
let time2 = $("#time2");
// Réinitialiser les champs
time1.val(morning ? temps.matin.debut : temps.apresmidi.debut);
time2.val(morning ? temps.matin.fin : temps.apresmidi.fin);
// Définir l'action du bouton de confirmation
document.getElementById("confirmButton").onclick = function(){
let debut = time1.val();
let fin = time2.val();
if (debut == "" || fin == ""){
alert("Veuillez remplir les deux champs");
return;
}
if (debut >= fin){
alert("L'heure de début doit être inférieure à l'heure de fin");
return;
}
if (morning){
if (fin > temps.apresmidi.debut){
alert("L'heure de fin du matin doit être inférieure à l'heure de début de l'après-midi");
return;
}
temps.matin.debut = debut;
temps.matin.fin = fin;
} else {
if (debut < temps.matin.fin){
alert("L'heure de début de l'après-midi doit être supérieure à l'heure de fin du matin");
return;
}
temps.apresmidi.debut = debut;
temps.apresmidi.fin = fin;
}
updateTemps(temps);
modal.classList.remove("show");
}
modal.classList.add("show");
}
document.addEventListener("DOMContentLoaded", ()=>{
const modal = document.getElementById("timePickerModal");
modal.querySelector(".timePicker-close").onclick = function() {
modal.classList.remove("show");
}
document.addEventListener('keyup', function(e) {
if (e.key === "Escape" && modal.classList.contains("show")) {
modal.classList.remove("show");
}
});
document.querySelectorAll("th .rbtn").forEach((el)=>{
el.addEventListener("click", (e)=>{
allPresent(...el.id.split("-"));
e.preventDefault();
})
})
})
</script>
{% endblock scripts %}
{% block title %}
{{ title }}
{% endblock title %}
{% block app_content %}
<h2>Signalement hebdomadaire de l'assiduité {{ gr | safe }}</h2>
<br>
<div id="actions" class="flex">
<button onclick="changeWeek(true)">Semaine précédente</button>
<label for="moduleimpl_select">
Module:
{{moduleimpl_select | safe}}
</label>
<button onclick="changeWeek(false)">Semaine suivante</button>
<span><a href="{{url_choix_semaine}}" class="stdlink">autre semaine<a></span>
</div>
<h3 id="tableau-dates">
Le matin <a href="#" id="text-matin" title="Cliquer pour modifier les horaires">9h à 12h</a> et l'après-midi de <a href="#" id="text-apresmidi" title="Cliquer pour modifier les horaires">13h à 17h</a>
</h3>
{% if readonly %}
<h4
title="Vous n'avez pas les permissions nécessaires afin de modifier les assiduités"
data-tooltip
>
Ouvert en mode <span class="rouge">lecture seule</span>.
</h4>
{% endif %}
<table id="table">
<thead>
<tr class="premier">
<th rowspan="2">Étudiants</th>
{% for jour in hebdo_jours %}
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
<th colspan="2" class="{{'grayed' if jour[0] else ''}}" >{{ jour[1][0] }} {{jour[1][1] }}</th>
{% endif %}
{% endfor %}
</tr>
<tr class="second">
{% for jour in hebdo_jours %}
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
<th class="{{'grayed' if jour[0] else ''}}">Matin</th>
<th class="{{'grayed' if jour[0] else ''}}">Après-midi</th>
{% endif %}
{% endfor %}
</tr>
{% if not readonly and not non_present %}
<tr>
{# Ne pas afficher si preference "non presences" / "readonly" #}
<th></th>
{% for jour in hebdo_jours %}
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
<th class="{{'grayed' if jour[0] else ''}}">
<input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-am" class="rbtn present" {{'disabled' if jour[0] else ''}}>
</th>
<th class="{{'grayed' if jour[0] else ''}}">
<input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-pm" class="rbtn present" {{'disabled' if jour[0] else ''}}>
</th>
{% endif %}
{% endfor %}
</tr>
{% endif %}
</thead>
<tbody>
{% for etud in etudiants %}
<tr etudid="{{etud.etudid}}" id="row-{{etud.etudid}}">
<td class="etudinfo" id="etud-{{etud.etudid}}">{{ etud.nom_prenom() }}</td>
{# Sera rempli en JS #}
{# Ne pas afficher bouton présent si pref "non présences" #}
{# <td>
<input type="checkbox" name="" id="" class="rbtn present">
<input type="checkbox" name="" id="" class="rbtn retard">
<input type="checkbox" name="" id="" class="rbtn absent">
</td>
<td>
<input type="checkbox" name="" id="" class="rbtn present">
<input type="checkbox" name="" id="" class="rbtn retard">
<input type="checkbox" name="" id="" class="rbtn absent">
</td> #}
</tr>
{% endfor %}
</tbody>
</table>
<div id="timePickerModal" class="timePicker-modal">
<div class="timePicker-modal-content">
<span class="timePicker-close">&times;</span>
<h2>Choisissez les horaires <span id="timePicker-modal-text"></span></h2>
<div class="time-picker-container">
<label for="time1">Début</label>
<input type="text" id="time1" name="time1" class="timepicker" placeholder="hh:mm">
</div>
<div class="time-picker-container">
<label for="time2">Fin</label>
<input type="text" id="time2" name="time2" class="timepicker" placeholder="hh:mm">
</div>
<span>
<button id="confirmButton">Confirmer</button>
</div>
</div>
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/toast.j2" %}
{% endblock app_content %}

View File

@ -1,8 +1,7 @@
<div class="assiduite-bubble {{etat}}"> <div class="assiduite-bubble {{etat}}">
<div class="assiduite-id">{{moduleimpl}}</div>
<div class="assiduite-period">{{date_debut}}</div>
<div class="assiduite-period">{{date_fin}}</div>
<div class="assiduite-state">État: {{etat}}</div> <div class="assiduite-state">État: {{etat}}</div>
<div class="assiduite-id">{{moduleimpl}}</div>
<div class="assiduite-period">{{date}}</div>
<div class="assiduite-why">Motif: {{motif}}</div> <div class="assiduite-why">Motif: {{motif}}</div>
<div class="assiduite-user_id">{{saisie}}</div> <div class="assiduite-user_id">{{saisie}}</div>
</div> </div>

View File

@ -1,12 +1,28 @@
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la période <p>Code couleur</p>
<ul class="couleurs">
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
période
</li> </li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la période <li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li> </li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la période <li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li> </li>
<li><span title="Hachure Bleue" class="justified demo"></span> &rightarrow; l'assiduité est justifiée par un <li><span title="Quart Bleu" class="est_just demo color"></span> &rightarrow; la période est couverte par un
justificatif valide</li> justificatif valide</li>
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> &rightarrow; l'assiduité est <li><span title="Justif. non valide" class="invalide demo color "></span> &rightarrow; la période est
justifiée par un justificatif non valide / en attente de validation couverte par un justificatif non valide
</li> </li>
<li><span title="Justif. en attente" class="attente demo color"></span> &rightarrow; la période
a un justificatif en attente de validation
</li>
</ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>

View File

@ -74,7 +74,13 @@
setupAssiduiteBubble(block, assiduité); setupAssiduiteBubble(block, assiduité);
} }
// TODO: ajout couleur justificatif // ajout couleur justificatif
const justificatifs = assiduité.justificatifs || [];
const justified = justificatifs.some(
(justificatif) => justificatif.etat === "VALIDE"
)
if(justified) block.classList.add("est_just");
block.classList.add(assiduité.etat.toLowerCase()); block.classList.add(assiduité.etat.toLowerCase());
if(assiduité.etat != "CRENEAU") block.classList.add("color"); if(assiduité.etat != "CRENEAU") block.classList.add("color");

View File

@ -17,12 +17,13 @@
const timelineContainer = document.querySelector(".timeline-container"); const timelineContainer = document.querySelector(".timeline-container");
const periodTimeLine = document.querySelector(".period"); const periodTimeLine = document.querySelector(".period");
const t_start = {{ t_start }}; const t_start = {{ t_start }};
const t_mid = {{ t_mid }};
const t_end = {{ t_end }}; const t_end = {{ t_end }};
const tick_time = 60 / {{ tick_time }}; const tick_time = 60 / {{ tick_time }};
const tick_delay = 1 / tick_time; const tick_delay = 1 / tick_time;
const period_default = {{ periode_defaut }}; const period_default = 2;
let handleMoving = false; let handleMoving = false;
@ -133,6 +134,7 @@
timelineContainer.removeEventListener("mousemove", onMouseMove); timelineContainer.removeEventListener("mousemove", onMouseMove);
handleMoving = false; handleMoving = false;
func_call(); func_call();
savePeriodInLocalStorage();
} }
timelineContainer.addEventListener("mousemove", onMouseMove); timelineContainer.addEventListener("mousemove", onMouseMove);
@ -166,6 +168,7 @@
snapHandlesToQuarters(); snapHandlesToQuarters();
timelineContainer.removeEventListener("mousemove", onMouseMove); timelineContainer.removeEventListener("mousemove", onMouseMove);
func_call(); func_call();
savePeriodInLocalStorage();
} }
timelineContainer.addEventListener("mousemove", onMouseMove); timelineContainer.addEventListener("mousemove", onMouseMove);
timelineContainer.addEventListener("touchmove", onMouseMove); timelineContainer.addEventListener("touchmove", onMouseMove);
@ -264,6 +267,7 @@
snapHandlesToQuarters(); snapHandlesToQuarters();
updatePeriodTimeLabel() updatePeriodTimeLabel()
func_call(); func_call();
savePeriodInLocalStorage();
} }
function snapHandlesToQuarters() { function snapHandlesToQuarters() {
@ -309,9 +313,23 @@
} }
} }
function savePeriodInLocalStorage(){
const dates = getPeriodValues();
localStorage.setItem("sco-timeline-values", JSON.stringify(dates));
}
function loadPeriodFromLocalStorage(){
const dates = JSON.parse(localStorage.getItem("sco-timeline-values"));
if(dates){
setPeriodValues(...dates);
}else{
setPeriodValues(t_start, t_start + period_default);
}
}
createTicks(); createTicks();
setPeriodValues(t_start, t_start + period_default); loadPeriodFromLocalStorage();
{% if heures %} {% if heures %}
let [heure_deb, heure_fin] = [{{ heures | safe }}] let [heure_deb, heure_fin] = [{{ heures | safe }}]

View File

@ -0,0 +1,93 @@
<div class="calendrier">
{% for mois,semaines in calendrier.items() %}
<div class="mois {{'highlight' if highlight=='mois'}}">
<h3>{{mois}}</h3>
{% for semaine in semaines %}
<div class="jours {{'highlight' if highlight=='semaine'}}" week_index="{{semaine}}">
{% for jour in semaines[semaine] %}
<div class="jour {{jour.get_class()}} {{'highlight' if highlight=='jour'}}" date="{{jour.get_date('%Y-%m-%d')}}">
<span class="nom">{{jour.get_nom()}}</span>
<div class="contenu">
{{jour.get_html() | safe}}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<style>
.calendrier {
display: flex;
justify-content: center;
overflow-x: scroll;
border: 1px solid #444;
border-radius: 12px;
margin-bottom: 12px;
}
.mois {
flex: 1;
}
.mois h3 {
text-align: center;
}
.jour {
text-align: left;
margin: 2px;
cursor: default;
font-size: 13px;
position: relative;
font-weight: normal;
min-width: 6em;
display: flex;
justify-content: start;
}
.jour>.contenu {
background-color: white;
width: 100%;
}
.jour.jour.non-travail>.nom,
.jour.jour.non-travail>.contenu {
border: 0;
background-color: #badfff;
}
.jour>.nom {
width: 3em;
min-width: 3em;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.jour>.contenu,
.jour>.nom {
border: 1px solid #d5d5d5;
position: relative;
}
.jour>.contenu a {
padding: 0px 2px;
}
.jour>.nom {
text-align: center;
}
.sem-courante{
--couleur : #ee752c;
border-left: solid 3px var(--couleur);
border-right: solid 3px var(--couleur);
}
.highlight:hover{
border: solid 3px yellow;
}
</style>

View File

@ -0,0 +1,62 @@
{% extends "sco_page.j2" %}
{% block styles %}
{{super()}}
<style>
.highlight {
cursor: pointer !important;
}
.highlight * {
cursor: pointer !important;
}
#gtrcontent h2.titre {
text-align: center;
margin-top: 10px;
}
.content{
width: 90%;
max-width: 1600px;
}
</style>
{% endblock %}
{% block app_content %}
<div class="content">
<h2 class="titre">{{titre}}</h2>
{{calendrier | safe}}
</div>
{% endblock app_content %}
{% block scripts %}
{{ super() }}
<script>
const mode = "{{mode}}";
const url = new URL(window.location.origin + "{{url | safe}}");
document.addEventListener("DOMContentLoaded", ()=>{
const highlight = document.querySelectorAll(".highlight");
highlight.forEach((el)=>{
el.addEventListener("click", (e)=>{
if (mode == "jour"){
const date = el.getAttribute("date");
url.searchParams.set("day", date);
}
if (mode == "semaine"){
const date = el.getAttribute("week_index");
url.searchParams.set("week", date);
}
window.location.href = url;
})
})
})
</script>
{% endblock scripts %}

View File

@ -6,7 +6,7 @@
<h2>Accès non autorisé</h2> <h2>Accès non autorisé</h2>
{{ exc | safe }} {{ exc }}
<p class="footer"> <p class="footer">
{% if g.scodoc_dept %} {% if g.scodoc_dept %}

View File

@ -49,7 +49,7 @@
</li> </li>
{% if formsemestre_origine is not none %} {% if formsemestre_origine is not none %}
<li><a class="stdlink" href="{{ <li><a class="stdlink" href="{{
url_for('notes.formsemestre_jury_but_erase', url_for('notes.formsemestre_jury_erase',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_origine.id, scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_origine.id,
etudid=etud.id, only_one_sem=1) etudid=etud.id, only_one_sem=1)
}}"> }}">

View File

@ -0,0 +1,19 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.j2' %}
{% import 'wtf.j2' as wtf %}
{% block app_content %}
<h2>Assistance technique</h2>
<p class="help">
Ce formulaire permet d'effectuer une demande d'assistance technique.<br>
Son <b>contenu sera accessible publiquement</b> sur scodoc.org, veuillez donc ne pas y inclure d'informations sensibles.<br>
L'adresse email associée à votre compte ScoDoc est automatiquement transmise avec votre demande mais ne sera pas
affichée publiquement.<br>
</p>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock app_content %}

View File

@ -49,7 +49,7 @@
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script> <script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
<script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script> <script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
<script> <script>
window.onload = function () { enableTooltips("gtrcontent") }; window.onload = function () { enableTooltips("gtrcontent"); enableTooltips("sidebar"); };
const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}"; const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}";
</script> </script>

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