Compare commits
218 Commits
Author | SHA1 | Date | |
---|---|---|---|
02a5b00ecf | |||
dcdf6a8012 | |||
912a213dcd | |||
3575e89dc0 | |||
21c0625147 | |||
e18c1d8fd0 | |||
5867d0f430 | |||
9897ccc659 | |||
|
7575959bd4 | ||
|
2aafbad9e2 | ||
50f2cd7a0f | |||
fd8fbb9e02 | |||
|
ebcef76950 | ||
|
13349776af | ||
|
f275286b71 | ||
|
f4f6c13d79 | ||
e7f23efe65 | |||
e44d3fd5dc | |||
fac36fa11c | |||
9289535359 | |||
|
d73b925006 | ||
6749ca70d6 | |||
|
dea403b03d | ||
|
ab9543c310 | ||
|
f94998f66b | ||
|
eb88a8ca83 | ||
7042650fd9 | |||
2745ffd687 | |||
9a882ea41d | |||
ea6003e812 | |||
5c6935337e | |||
60998d2e20 | |||
29b877d9ed | |||
|
6834c19015 | ||
|
f47fc4ba46 | ||
5894c6f952 | |||
af1d1884c7 | |||
|
881bf82000 | ||
|
2ed4516a97 | ||
|
75ce1ccd31 | ||
|
f8d5f6ea11 | ||
|
70995fbd7e | ||
dc095765f2 | |||
|
1cec3fa703 | ||
|
032454aefd | ||
|
e3344cf424 | ||
|
d7acff9d35 | ||
|
decdf59e20 | ||
|
42fc08a3a3 | ||
|
f3770fb5c7 | ||
63b28a3277 | |||
bb23cdcea7 | |||
3ca5636454 | |||
42882154d5 | |||
489acb26d2 | |||
8ee373db7d | |||
8e56dc2418 | |||
b3331bd886 | |||
89afb672af | |||
8f25284038 | |||
f29002a57d | |||
69780b3f24 | |||
fbff151be0 | |||
3b436fa0f3 | |||
8847a1f008 | |||
ac882e9ccd | |||
000e016985 | |||
22d90215a0 | |||
043985bff6 | |||
d20ada1797 | |||
|
778fecabb6 | ||
|
fa6f83722e | ||
baa0412071 | |||
d51a47b71a | |||
f21ef41de6 | |||
2d673e7a5d | |||
3e43495831 | |||
|
a4db8c4ff8 | ||
|
1ac35d04c2 | ||
|
687ac3cf13 | ||
18b1f00586 | |||
|
6b985620e9 | ||
|
4d234ba353 | ||
|
5d45fcf656 | ||
|
0a5919b788 | ||
|
09f4525e66 | ||
0bc57807de | |||
87aaf12d27 | |||
c8ab9b9b6c | |||
ad7b48e110 | |||
f2ce16f161 | |||
1ddf9b6ab8 | |||
0a2e39cae1 | |||
a194b4b6e0 | |||
cbe85dfb7d | |||
beba69bfe4 | |||
41fec29452 | |||
9bd05ea241 | |||
58b831513d | |||
b861aba6a3 | |||
c2443c361f | |||
ab4731bd43 | |||
c17bc8b61b | |||
e44a5ee55d | |||
a747ed22e2 | |||
5d0a932634 | |||
2b150cf521 | |||
5a5ddcacd7 | |||
3f6e65b9da | |||
5eba6170a5 | |||
bd9bf87112 | |||
a0e2af481f | |||
42e8f97441 | |||
8ec0171ca0 | |||
6dfab2d843 | |||
523ec59833 | |||
bde6325391 | |||
0577347622 | |||
28d46e413d | |||
126ea0741a | |||
a5b5f49f76 | |||
|
b7ab10bf4e | ||
3e0b19c4a8 | |||
1dd5187fae | |||
|
9a3a7d33b2 | ||
|
a7569fe4f5 | ||
|
79e973f06d | ||
b6940e4882 | |||
1f24095c57 | |||
0ed2455028 | |||
b841b2f708 | |||
|
0fa1478138 | ||
|
85ad7b5f29 | ||
6bfd461bf2 | |||
e1f1a95a14 | |||
70e3006981 | |||
bae46c2794 | |||
|
b1055a4ebe | ||
|
b2ef6a4c53 | ||
|
a7c7bd655d | ||
|
1309043a98 | ||
|
a75b41ca5f | ||
8df25ca02f | |||
61f9dddeb6 | |||
a1f5340935 | |||
68128c27d5 | |||
8ecaa2bed0 | |||
7c61dd8d63 | |||
f493ba344f | |||
f5079d9aef | |||
55add2ffb3 | |||
5865b67652 | |||
3c8b088d5e | |||
2da359ae41 | |||
09ec53f573 | |||
3787e0145a | |||
edf989ee04 | |||
203f3a5342 | |||
161f8476ca | |||
d419d75515 | |||
f23630d7fd | |||
fa0417f0b1 | |||
12256dc3d4 | |||
46529917ea | |||
2367984848 | |||
46c86d2928 | |||
715e4f94ee | |||
|
b2e6ef63b9 | ||
|
30560e5860 | ||
|
0fbcfb1124 | ||
|
2daae1c9c5 | ||
635269ff36 | |||
4aa30a40bd | |||
03c03f3725 | |||
29eb8c297b | |||
38032a8c09 | |||
2f2d98954c | |||
2e5d94f048 | |||
1b1b8ebdc4 | |||
9c6db169f3 | |||
|
8ded16b94f | ||
|
5d10ee467e | ||
763f60fb3d | |||
|
7af0dd1e1e | ||
dece9a82d1 | |||
0262b6e2ac | |||
f8f47e05ff | |||
|
b74d525c28 | ||
|
c617ee321a | ||
|
56ec4ba43d | ||
|
d14f7e21b7 | ||
|
c3cb1da561 | ||
|
cce60d432d | ||
|
4386994f7d | ||
|
fddfddfa7b | ||
|
39dca32d2e | ||
|
e2b9cd3ded | ||
|
be227f4a2f | ||
959a98d0a2 | |||
35a038fd3a | |||
b46556c189 | |||
|
71f90f5261 | ||
|
1b037d6c7c | ||
60a97b7baf | |||
|
0332553587 | ||
|
958cf435c8 | ||
|
c69e9c34a0 | ||
|
17f8771b0b | ||
|
7eb41fb2eb | ||
|
a79ca4a17d | ||
411ef8ae0d | |||
169bf17fdd | |||
75d4c110a8 | |||
9003a2ca87 | |||
55ecaa45a9 | |||
ab39454a0d | |||
|
5158bd0c8f | ||
|
21b2e0f582 |
148
README.md
148
README.md
|
@ -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,45 +41,41 @@ 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
|
||||||
.../config/logos # Logos de l'établissement
|
.../config/logos # Logos de l'établissement
|
||||||
.../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
|
||||||
|
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||||
|
apt-get install git # si besoin
|
||||||
|
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
|
||||||
|
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||||
|
|
||||||
sudo su
|
# Donner ce répertoire à l'utilisateur scodoc:
|
||||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
chown -R scodoc:scodoc /opt/scodoc
|
||||||
apt-get install git # si besoin
|
```
|
||||||
cd /opt
|
|
||||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
|
||||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
|
||||||
|
|
||||||
# Renommer le répertoire:
|
|
||||||
mv ScoDoc 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,43 +115,43 @@ 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
|
||||||
|
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||||
|
flask db upgrade # créé les tables à partir des migrations
|
||||||
|
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||||
|
|
||||||
dropdb SCODOC_DEV
|
# puis imports:
|
||||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
flask import-scodoc7-users
|
||||||
flask db upgrade # créé les tables à partir des migrations
|
flask import-scodoc7-dept STID SCOSTID
|
||||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
```
|
||||||
|
|
||||||
# puis imports:
|
|
||||||
flask import-scodoc7-users
|
|
||||||
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,23 +159,23 @@ 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
|
||||||
important est `postinst`qui se charge de configurer le système (install ou
|
important est `postinst` qui se charge de configurer le système (install ou
|
||||||
upgrade de scodoc9).
|
upgrade de scodoc9).
|
||||||
|
|
||||||
La préparation d'une release se fait à l'aide du script
|
La préparation d'une release se fait à l'aide du script
|
||||||
|
|
|
@ -315,12 +315,6 @@ def create_app(config_class=DevConfig):
|
||||||
app.register_error_handler(503, postgresql_server_error)
|
app.register_error_handler(503, postgresql_server_error)
|
||||||
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
|
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
|
||||||
|
|
||||||
# Add some globals
|
|
||||||
# previously in Flask-Bootstrap:
|
|
||||||
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
|
|
||||||
field, HiddenField
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.auth import bp as auth_bp
|
from app.auth import bp as auth_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp, url_prefix="/auth")
|
app.register_blueprint(auth_bp, url_prefix="/auth")
|
||||||
|
@ -338,8 +332,15 @@ def create_app(config_class=DevConfig):
|
||||||
from app.api import api_bp
|
from app.api import api_bp
|
||||||
from app.api import api_web_bp
|
from app.api import api_web_bp
|
||||||
|
|
||||||
|
# Jinja2 configuration
|
||||||
# Enable autoescaping of all templates, including .j2
|
# Enable autoescaping of all templates, including .j2
|
||||||
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
|
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
|
||||||
|
app.jinja_env.trim_blocks = True
|
||||||
|
app.jinja_env.lstrip_blocks = True
|
||||||
|
# previously in Flask-Bootstrap:
|
||||||
|
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
|
||||||
|
field, HiddenField
|
||||||
|
)
|
||||||
|
|
||||||
# https://scodoc.fr/ScoDoc
|
# https://scodoc.fr/ScoDoc
|
||||||
app.register_blueprint(scodoc_bp)
|
app.register_blueprint(scodoc_bp)
|
||||||
|
@ -636,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 ré-essayer.
|
||||||
|
|
||||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
|
||||||
{scu.SCO_DISCORD_ASSISTANCE}
|
|
||||||
|
|
||||||
{msg}
|
{msg}
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -414,9 +414,16 @@ def bulletin(
|
||||||
if version == "pdf":
|
if version == "pdf":
|
||||||
version = "long"
|
version = "long"
|
||||||
pdf = True
|
pdf = True
|
||||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
if version not in (
|
||||||
|
scu.BULLETINS_VERSIONS_BUT
|
||||||
|
if formsemestre.formation.is_apc()
|
||||||
|
else scu.BULLETINS_VERSIONS
|
||||||
|
):
|
||||||
return json_error(404, "version invalide")
|
return json_error(404, "version invalide")
|
||||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
if formsemestre.bul_hide_xml and pdf:
|
||||||
|
return json_error(403, "bulletin non disponible")
|
||||||
|
# note: la version json est réduite si bul_hide_xml
|
||||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||||
return json_error(404, "formsemestre inexistant")
|
return json_error(404, "formsemestre inexistant")
|
||||||
|
|
|
@ -52,7 +52,8 @@ def formations():
|
||||||
@as_json
|
@as_json
|
||||||
def formations_ids():
|
def formations_ids():
|
||||||
"""
|
"""
|
||||||
Retourne la liste de toutes les id de formations (tous départements)
|
Retourne la liste de toutes les id de formations
|
||||||
|
(tous départements, ou du département indiqué dans la route)
|
||||||
|
|
||||||
Exemple de résultat : [ 17, 99, 32 ]
|
Exemple de résultat : [ 17, 99, 32 ]
|
||||||
"""
|
"""
|
||||||
|
@ -328,6 +329,8 @@ def desassoc_ue_niveau(ue_id: int):
|
||||||
ue.niveau_competence = None
|
ue.niveau_competence = None
|
||||||
db.session.add(ue)
|
db.session.add(ue)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
# Invalidation du cache
|
||||||
|
ue.formation.invalidate_cached_sems()
|
||||||
log(f"desassoc_ue_niveau: {ue}")
|
log(f"desassoc_ue_niveau: {ue}")
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
# "usage web"
|
# "usage web"
|
||||||
|
|
|
@ -12,7 +12,7 @@ from operator import attrgetter, itemgetter
|
||||||
from flask import g, make_response, request
|
from flask import g, make_response, 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
|
||||||
|
import sqlalchemy as sa
|
||||||
import app
|
import app
|
||||||
from app import db
|
from app import db
|
||||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
|
@ -38,7 +38,7 @@ from app.scodoc import sco_groups
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.tables.recap import TableRecap
|
from app.tables.recap import TableRecap, RowRecap
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>")
|
@bp.route("/formsemestre/<int:formsemestre_id>")
|
||||||
|
@ -171,6 +171,44 @@ def formsemestres_query():
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.EditFormSemestre)
|
||||||
|
@as_json
|
||||||
|
def formsemestre_edit(formsemestre_id: int):
|
||||||
|
"""Modifie les champs d'un formsemestre."""
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
|
editable_keys = {
|
||||||
|
"semestre_id",
|
||||||
|
"titre",
|
||||||
|
"date_debut",
|
||||||
|
"date_fin",
|
||||||
|
"edt_id",
|
||||||
|
"etat",
|
||||||
|
"modalite",
|
||||||
|
"gestion_compensation",
|
||||||
|
"bul_hide_xml",
|
||||||
|
"block_moyennes",
|
||||||
|
"block_moyenne_generale",
|
||||||
|
"mode_calcul_moyennes",
|
||||||
|
"gestion_semestrielle",
|
||||||
|
"bul_bgcolor",
|
||||||
|
"resp_can_edit",
|
||||||
|
"resp_can_change_ens",
|
||||||
|
"ens_can_edit_eval",
|
||||||
|
"elt_sem_apo",
|
||||||
|
"elt_annee_apo",
|
||||||
|
}
|
||||||
|
formsemestre.from_dict({k: v for (k, v) in args.items() if k in editable_keys})
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
except sa.exc.StatementError as exc:
|
||||||
|
return json_error(404, f"invalid argument(s): {exc.args[0]}")
|
||||||
|
return formsemestre.to_dict_api()
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||||
|
@ -468,13 +506,13 @@ def etat_evals(formsemestre_id: int):
|
||||||
date_mediane = notes_sorted[len(notes_sorted) // 2].date
|
date_mediane = notes_sorted[len(notes_sorted) // 2].date
|
||||||
|
|
||||||
eval_dict["saisie_notes"] = {
|
eval_dict["saisie_notes"] = {
|
||||||
"datetime_debut": date_debut.isoformat()
|
"datetime_debut": (
|
||||||
if date_debut is not None
|
date_debut.isoformat() if date_debut is not None else None
|
||||||
else None,
|
),
|
||||||
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
||||||
"datetime_mediane": date_mediane.isoformat()
|
"datetime_mediane": (
|
||||||
if date_mediane is not None
|
date_mediane.isoformat() if date_mediane is not None else None
|
||||||
else None,
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
list_eval.append(eval_dict)
|
list_eval.append(eval_dict)
|
||||||
|
@ -505,16 +543,30 @@ def formsemestre_resultat(formsemestre_id: int):
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
table = TableRecap(
|
# Ajoute le groupe de chaque partition,
|
||||||
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
|
|
||||||
)
|
|
||||||
# Supprime les champs inutiles (mise en forme)
|
|
||||||
rows = table.to_list()
|
|
||||||
# Ajoute le groupe de chaque partition:
|
|
||||||
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
|
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
|
||||||
for row in rows:
|
|
||||||
row["partitions"] = etud_groups.get(row["etudid"], {})
|
|
||||||
|
|
||||||
|
class RowRecapAPI(RowRecap):
|
||||||
|
"""Pour table avec partitions et sort_key"""
|
||||||
|
|
||||||
|
def add_etud_cols(self):
|
||||||
|
"""Ajoute colonnes étudiant: codes, noms"""
|
||||||
|
super().add_etud_cols()
|
||||||
|
self.add_cell("partitions", "partitions", etud_groups.get(self.etud.id, {}))
|
||||||
|
self.add_cell("sort_key", "sort_key", self.etud.sort_key)
|
||||||
|
|
||||||
|
table = TableRecap(
|
||||||
|
res,
|
||||||
|
convert_values=convert_values,
|
||||||
|
include_evaluations=False,
|
||||||
|
mode_jury=False,
|
||||||
|
row_class=RowRecapAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = table.to_list()
|
||||||
|
|
||||||
|
# for row in rows:
|
||||||
|
# row["partitions"] = etud_groups.get(row["etudid"], {})
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ from app.api import get_model_api_object, tools
|
||||||
from app.decorators import permission_required, scodoc
|
from app.decorators import permission_required, scodoc
|
||||||
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
|
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
|
||||||
from app.models.assiduites import (
|
from app.models.assiduites import (
|
||||||
compute_assiduites_justified,
|
|
||||||
get_formsemestre_from_data,
|
get_formsemestre_from_data,
|
||||||
)
|
)
|
||||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||||
|
@ -310,7 +309,6 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
||||||
|
|
||||||
errors: list[dict] = []
|
errors: list[dict] = []
|
||||||
success: list[dict] = []
|
success: list[dict] = []
|
||||||
justifs: list[Justificatif] = []
|
|
||||||
|
|
||||||
# énumération des justificatifs
|
# énumération des justificatifs
|
||||||
for i, data in enumerate(create_list):
|
for i, data in enumerate(create_list):
|
||||||
|
@ -322,11 +320,9 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
||||||
errors.append({"indice": i, "message": obj})
|
errors.append({"indice": i, "message": obj})
|
||||||
else:
|
else:
|
||||||
success.append({"indice": i, "message": obj})
|
success.append({"indice": i, "message": obj})
|
||||||
justifs.append(justi)
|
justi.justifier_assiduites()
|
||||||
scass.simple_invalidate_cache(data, etud.id)
|
scass.simple_invalidate_cache(data, etud.id)
|
||||||
|
|
||||||
# Actualisation des assiduités justifiées en fonction de tous les nouveaux justificatifs
|
|
||||||
compute_assiduites_justified(etud.etudid, justifs)
|
|
||||||
return {"errors": errors, "success": success}
|
return {"errors": errors, "success": success}
|
||||||
|
|
||||||
|
|
||||||
|
@ -495,6 +491,7 @@ def justif_edit(justif_id: int):
|
||||||
return json_error(404, err)
|
return json_error(404, err)
|
||||||
|
|
||||||
# Mise à jour du justificatif
|
# Mise à jour du justificatif
|
||||||
|
justificatif_unique.dejustifier_assiduites()
|
||||||
db.session.add(justificatif_unique)
|
db.session.add(justificatif_unique)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -511,11 +508,7 @@ def justif_edit(justif_id: int):
|
||||||
retour = {
|
retour = {
|
||||||
"couverture": {
|
"couverture": {
|
||||||
"avant": avant_ids,
|
"avant": avant_ids,
|
||||||
"apres": compute_assiduites_justified(
|
"apres": justificatif_unique.justifier_assiduites(),
|
||||||
justificatif_unique.etudid,
|
|
||||||
[justificatif_unique],
|
|
||||||
True,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# Invalide le cache
|
# Invalide le cache
|
||||||
|
@ -592,14 +585,10 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
||||||
|
|
||||||
# On invalide le cache
|
# On invalide le cache
|
||||||
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||||
|
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||||
|
justificatif_unique.dejustifier_assiduites()
|
||||||
# On supprime le justificatif
|
# On supprime le justificatif
|
||||||
db.session.delete(justificatif_unique)
|
db.session.delete(justificatif_unique)
|
||||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
|
||||||
compute_assiduites_justified(
|
|
||||||
justificatif_unique.etudid,
|
|
||||||
Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return (200, "OK")
|
return (200, "OK")
|
||||||
|
|
||||||
|
@ -700,7 +689,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
|
||||||
@as_json
|
@as_json
|
||||||
@permission_required(Permission.AbsChange)
|
@permission_required(Permission.AbsChange)
|
||||||
def justif_remove(justif_id: int = None):
|
def justif_remove(justif_id: int = None):
|
||||||
# XXX TODO pas de test unitaire
|
|
||||||
"""
|
"""
|
||||||
Supression d'un fichier ou d'une archive
|
Supression d'un fichier ou d'une archive
|
||||||
{
|
{
|
||||||
|
|
|
@ -603,8 +603,19 @@ class Role(db.Model):
|
||||||
"""Create default roles if missing, then, if reset_permissions,
|
"""Create default roles if missing, then, if reset_permissions,
|
||||||
reset their permissions to default values.
|
reset their permissions to default values.
|
||||||
"""
|
"""
|
||||||
|
Role.reset_roles_permissions(
|
||||||
|
SCO_ROLES_DEFAULTS, reset_permissions=reset_permissions
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reset_roles_permissions(roles_perms: dict[str, tuple], reset_permissions=True):
|
||||||
|
"""Ajoute les permissions aux roles
|
||||||
|
roles_perms : { "role_name" : (permission, ...) }
|
||||||
|
reset_permissions : si vrai efface permissions déja existantes
|
||||||
|
Si le role n'existe pas, il est (re) créé.
|
||||||
|
"""
|
||||||
default_role = "Observateur"
|
default_role = "Observateur"
|
||||||
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
|
for role_name, permissions in roles_perms.items():
|
||||||
role = Role.query.filter_by(name=role_name).first()
|
role = Role.query.filter_by(name=role_name).first()
|
||||||
if role is None:
|
if role is None:
|
||||||
role = Role(name=role_name)
|
role = Role(name=role_name)
|
||||||
|
|
|
@ -21,7 +21,7 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
||||||
return ""
|
return ""
|
||||||
ref_comp = ue.formation.referentiel_competence
|
ref_comp = ue.formation.referentiel_competence
|
||||||
if ref_comp is None:
|
if ref_comp is None:
|
||||||
return f"""<div class="ue_advanced">
|
return f"""<div class="scobox ue_advanced">
|
||||||
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
|
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
|
||||||
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
|
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
|
||||||
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
|
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
|
||||||
|
@ -31,19 +31,28 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
||||||
|
|
||||||
H = [
|
H = [
|
||||||
"""
|
"""
|
||||||
<div class="ue_advanced">
|
<div class="scobox ue_advanced">
|
||||||
<h3>Parcours du BUT</h3>
|
<div class="scobox-title">Parcours du BUT</div>
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
# Choix des parcours
|
# Choix des parcours
|
||||||
ue_pids = [p.id for p in ue.parcours]
|
ue_pids = [p.id for p in ue.parcours]
|
||||||
H.append("""<form id="choix_parcours">""")
|
H.append(
|
||||||
|
"""
|
||||||
|
<div class="help">
|
||||||
|
Cocher tous les parcours dans lesquels cette UE est utilisée,
|
||||||
|
même si vous n'offrez pas ce parcours dans votre département.
|
||||||
|
Sans quoi, les UEs de Tronc Commun ne seront pas reconnues.
|
||||||
|
Ne cocher aucun parcours est équivalent à tous les cocher.
|
||||||
|
</div>
|
||||||
|
<form id="choix_parcours" style="margin-top: 12px;">
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
ects_differents = {
|
ects_differents = {
|
||||||
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours
|
||||||
} != {None}
|
} != {None}
|
||||||
for parcour in ref_comp.parcours:
|
for parcour in ref_comp.parcours:
|
||||||
ects_parcour = ue.get_ects(parcour)
|
|
||||||
ects_parcour_txt = (
|
ects_parcour_txt = (
|
||||||
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
|
f" ({ue.get_ects(parcour):.3g} ects)" if ects_differents else ""
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,12 +9,14 @@
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from flask import g, has_request_context, url_for
|
from flask import g, has_request_context, url_for
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.comp.moy_mod import ModuleImplResults
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.models import Evaluation, FormSemestre, Identite
|
from app.models import Evaluation, FormSemestre, Identite, ModuleImpl
|
||||||
from app.models.groups import GroupDescr
|
from app.models.groups import GroupDescr
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_bulletins, sco_utils as scu
|
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||||
|
@ -229,7 +231,7 @@ class BulletinBUT:
|
||||||
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
|
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
|
||||||
d[modimpl.module.code] = {
|
d[modimpl.module.code] = {
|
||||||
"id": modimpl.id,
|
"id": modimpl.id,
|
||||||
"titre": modimpl.module.titre,
|
"titre": modimpl.module.titre_str(),
|
||||||
"code_apogee": modimpl.module.code_apogee,
|
"code_apogee": modimpl.module.code_apogee,
|
||||||
"url": (
|
"url": (
|
||||||
url_for(
|
url_for(
|
||||||
|
@ -249,59 +251,88 @@ class BulletinBUT:
|
||||||
# "moy": fmt_note(moyennes_etuds.mean()),
|
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||||
},
|
},
|
||||||
"evaluations": (
|
"evaluations": (
|
||||||
[
|
self.etud_list_modimpl_evaluations(
|
||||||
self.etud_eval_results(etud, e)
|
etud, modimpl, modimpl_results, version
|
||||||
for e in modimpl.evaluations
|
|
||||||
if (e.visibulletin or version == "long")
|
|
||||||
and (e.id in modimpl_results.evaluations_etat)
|
|
||||||
and (
|
|
||||||
modimpl_results.evaluations_etat[e.id].is_complete
|
|
||||||
or self.prefs["bul_show_all_evals"]
|
|
||||||
)
|
)
|
||||||
]
|
|
||||||
if version != "short"
|
if version != "short"
|
||||||
else []
|
else []
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def etud_eval_results(self, etud, e: Evaluation) -> dict:
|
def etud_list_modimpl_evaluations(
|
||||||
|
self,
|
||||||
|
etud: Identite,
|
||||||
|
modimpl: ModuleImpl,
|
||||||
|
modimpl_results: ModuleImplResults,
|
||||||
|
version: str,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Liste des résultats aux évaluations de ce modimpl à montrer pour cet étudiant"""
|
||||||
|
evaluation: Evaluation
|
||||||
|
eval_results = []
|
||||||
|
for evaluation in modimpl.evaluations:
|
||||||
|
if (
|
||||||
|
(evaluation.visibulletin or version == "long")
|
||||||
|
and (evaluation.id in modimpl_results.evaluations_etat)
|
||||||
|
and (
|
||||||
|
modimpl_results.evaluations_etat[evaluation.id].is_complete
|
||||||
|
or self.prefs["bul_show_all_evals"]
|
||||||
|
)
|
||||||
|
):
|
||||||
|
eval_notes = self.res.modimpls_results[modimpl.id].evals_notes[
|
||||||
|
evaluation.id
|
||||||
|
]
|
||||||
|
|
||||||
|
if (evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE) or (
|
||||||
|
not np.isnan(eval_notes[etud.id])
|
||||||
|
):
|
||||||
|
eval_results.append(
|
||||||
|
self.etud_eval_results(etud, evaluation, eval_notes)
|
||||||
|
)
|
||||||
|
return eval_results
|
||||||
|
|
||||||
|
def etud_eval_results(
|
||||||
|
self, etud: Identite, evaluation: Evaluation, eval_notes: pd.DataFrame
|
||||||
|
) -> dict:
|
||||||
"dict resultats d'un étudiant à une évaluation"
|
"dict resultats d'un étudiant à une évaluation"
|
||||||
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
|
||||||
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
|
|
||||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||||
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
|
modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
|
||||||
try:
|
try:
|
||||||
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
||||||
poids = {
|
poids = {
|
||||||
ue.acronyme: modimpls_evals_poids[ue.id][e.id]
|
ue.acronyme: modimpls_evals_poids[ue.id][evaluation.id]
|
||||||
for ue in self.res.ues
|
for ue in self.res.ues
|
||||||
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
||||||
}
|
}
|
||||||
except KeyError:
|
except KeyError:
|
||||||
poids = collections.defaultdict(lambda: 0.0)
|
poids = collections.defaultdict(lambda: 0.0)
|
||||||
d = {
|
d = {
|
||||||
"id": e.id,
|
"id": evaluation.id,
|
||||||
"coef": (
|
"coef": (
|
||||||
fmt_note(e.coefficient)
|
fmt_note(evaluation.coefficient)
|
||||||
if e.evaluation_type == Evaluation.EVALUATION_NORMALE
|
if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"date_debut": e.date_debut.isoformat() if e.date_debut else None,
|
"date_debut": (
|
||||||
"date_fin": e.date_fin.isoformat() if e.date_fin else None,
|
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
||||||
"description": e.description,
|
),
|
||||||
"evaluation_type": e.evaluation_type,
|
"date_fin": (
|
||||||
|
evaluation.date_fin.isoformat() if evaluation.date_fin else None
|
||||||
|
),
|
||||||
|
"description": evaluation.description,
|
||||||
|
"evaluation_type": evaluation.evaluation_type,
|
||||||
"note": (
|
"note": (
|
||||||
{
|
{
|
||||||
"value": fmt_note(
|
"value": fmt_note(
|
||||||
eval_notes[etud.id],
|
eval_notes[etud.id],
|
||||||
note_max=e.note_max,
|
note_max=evaluation.note_max,
|
||||||
),
|
),
|
||||||
"min": fmt_note(notes_ok.min(), note_max=e.note_max),
|
"min": fmt_note(notes_ok.min(), note_max=evaluation.note_max),
|
||||||
"max": fmt_note(notes_ok.max(), note_max=e.note_max),
|
"max": fmt_note(notes_ok.max(), note_max=evaluation.note_max),
|
||||||
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
|
"moy": fmt_note(notes_ok.mean(), note_max=evaluation.note_max),
|
||||||
}
|
}
|
||||||
if not e.is_blocked()
|
if not evaluation.is_blocked()
|
||||||
else {}
|
else {}
|
||||||
),
|
),
|
||||||
"poids": poids,
|
"poids": poids,
|
||||||
|
@ -309,17 +340,25 @@ class BulletinBUT:
|
||||||
url_for(
|
url_for(
|
||||||
"notes.evaluation_listenotes",
|
"notes.evaluation_listenotes",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
evaluation_id=e.id,
|
evaluation_id=evaluation.id,
|
||||||
)
|
)
|
||||||
if has_request_context()
|
if has_request_context()
|
||||||
else "na"
|
else "na"
|
||||||
),
|
),
|
||||||
# deprecated (supprimer avant #sco9.7)
|
# deprecated (supprimer avant #sco9.7)
|
||||||
"date": e.date_debut.isoformat() if e.date_debut else None,
|
"date": (
|
||||||
"heure_debut": (
|
evaluation.date_debut.isoformat() if evaluation.date_debut else None
|
||||||
e.date_debut.time().isoformat("minutes") if e.date_debut else None
|
),
|
||||||
|
"heure_debut": (
|
||||||
|
evaluation.date_debut.time().isoformat("minutes")
|
||||||
|
if evaluation.date_debut
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"heure_fin": (
|
||||||
|
evaluation.date_fin.time().isoformat("minutes")
|
||||||
|
if evaluation.date_fin
|
||||||
|
else None
|
||||||
),
|
),
|
||||||
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
|
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@ -359,7 +398,7 @@ class BulletinBUT:
|
||||||
"short" : ne descend pas plus bas que les modules.
|
"short" : ne descend pas plus bas que les modules.
|
||||||
|
|
||||||
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
||||||
(bulletins non publiés).
|
(bulletins non publiés sur la passerelle).
|
||||||
"""
|
"""
|
||||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||||
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
|
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
|
||||||
|
@ -393,7 +432,7 @@ class BulletinBUT:
|
||||||
else:
|
else:
|
||||||
etud_ues_ids = res.etud_ues_ids(etud.id)
|
etud_ues_ids = res.etud_ues_ids(etud.id)
|
||||||
|
|
||||||
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
nbabsnj, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
|
||||||
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
||||||
etud, formsemestre, only_to_show=True
|
etud, formsemestre, only_to_show=True
|
||||||
)
|
)
|
||||||
|
@ -408,7 +447,7 @@ class BulletinBUT:
|
||||||
}
|
}
|
||||||
if self.prefs["bul_show_abs"]:
|
if self.prefs["bul_show_abs"]:
|
||||||
semestre_infos["absences"] = {
|
semestre_infos["absences"] = {
|
||||||
"injustifie": nbabs - nbabsjust,
|
"injustifie": nbabsnj,
|
||||||
"total": nbabs,
|
"total": nbabs,
|
||||||
"metrique": {
|
"metrique": {
|
||||||
"H.": "Heure(s)",
|
"H.": "Heure(s)",
|
||||||
|
@ -525,7 +564,7 @@ class BulletinBUT:
|
||||||
d["demission"] = ""
|
d["demission"] = ""
|
||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
|
_, d["nbabsjust"], d["nbabs"] = self.res.formsemestre.get_abs_count(etud.id)
|
||||||
|
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
infos, _ = sco_bulletins.etud_descr_situation_semestre(
|
infos, _ = sco_bulletins.etud_descr_situation_semestre(
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,6 +31,7 @@ from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||||
from app.scodoc.sco_logos import Logo
|
from app.scodoc.sco_logos import Logo
|
||||||
from app.scodoc.sco_pdf import PDFLOCK, SU
|
from app.scodoc.sco_pdf import PDFLOCK, SU
|
||||||
from app.scodoc.sco_preferences import SemPreferences
|
from app.scodoc.sco_preferences import SemPreferences
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
def make_bulletin_but_court_pdf(
|
def make_bulletin_but_court_pdf(
|
||||||
|
@ -343,9 +344,11 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||||
for mod in self.bul[mod_type]:
|
for mod in self.bul[mod_type]:
|
||||||
row = [mod, bul[mod_type][mod]["titre"]]
|
row = [mod, bul[mod_type][mod]["titre"]]
|
||||||
row += [
|
row += [
|
||||||
|
(
|
||||||
bul["ues"][ue][mod_type][mod]["moyenne"]
|
bul["ues"][ue][mod_type][mod]["moyenne"]
|
||||||
if mod in bul["ues"][ue][mod_type]
|
if mod in bul["ues"][ue][mod_type]
|
||||||
else ""
|
else ""
|
||||||
|
)
|
||||||
for ue in self.ues_acronyms
|
for ue in self.ues_acronyms
|
||||||
]
|
]
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
@ -523,7 +526,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||||
if self.bul["semestre"].get("decision_annee", None):
|
if self.bul["semestre"].get("decision_annee", None):
|
||||||
txt += f"""
|
txt += f"""
|
||||||
Décision saisie le {
|
Décision saisie le {
|
||||||
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y")
|
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime(scu.DATE_FMT)
|
||||||
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
||||||
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
|
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
@ -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
|
||||||
|
@ -269,7 +270,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
date_capitalisation = ue.get("date_capitalisation")
|
date_capitalisation = ue.get("date_capitalisation")
|
||||||
if date_capitalisation:
|
if date_capitalisation:
|
||||||
fields_bmr.append(
|
fields_bmr.append(
|
||||||
f"""Capitalisée le {date_capitalisation.strftime("%d/%m/%Y")}"""
|
f"""Capitalisée le {date_capitalisation.strftime(scu.DATE_FMT)}"""
|
||||||
)
|
)
|
||||||
t = {
|
t = {
|
||||||
"titre": " - ".join(fields_bmr),
|
"titre": " - ".join(fields_bmr),
|
||||||
|
@ -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>{
|
||||||
|
|
|
@ -241,7 +241,7 @@ def bulletin_but_xml_compat(
|
||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||||
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
_, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
|
||||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||||
|
|
||||||
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
||||||
|
|
92
app/but/change_refcomp.py
Normal file
92
app/but/change_refcomp.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Code expérimental: si deux référentiel sont presques identiques
|
||||||
|
(mêmes compétences, niveaux, parcours)
|
||||||
|
essaie de changer une formation de référentiel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app import clear_scodoc_cache, db
|
||||||
|
|
||||||
|
from app.models import (
|
||||||
|
ApcParcours,
|
||||||
|
ApcReferentielCompetences,
|
||||||
|
ApcValidationRCUE,
|
||||||
|
Formation,
|
||||||
|
FormSemestreInscription,
|
||||||
|
UniteEns,
|
||||||
|
)
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
|
def formation_change_referentiel(
|
||||||
|
formation: Formation, new_ref: ApcReferentielCompetences
|
||||||
|
):
|
||||||
|
"""Try to change ref."""
|
||||||
|
if not formation.referentiel_competence:
|
||||||
|
raise ScoValueError("formation non associée à un référentiel")
|
||||||
|
if not isinstance(new_ref, ApcReferentielCompetences):
|
||||||
|
raise ScoValueError("nouveau référentiel invalide")
|
||||||
|
|
||||||
|
r = formation.referentiel_competence.map_to_other_referentiel(new_ref)
|
||||||
|
if isinstance(r, str):
|
||||||
|
raise ScoValueError(f"référentiels incompatibles: {r}")
|
||||||
|
parcours_map, competences_map, niveaux_map = r
|
||||||
|
|
||||||
|
formation.referentiel_competence = new_ref
|
||||||
|
db.session.add(formation)
|
||||||
|
# UEs - Niveaux et UEs - parcours
|
||||||
|
for ue in formation.ues:
|
||||||
|
if ue.niveau_competence:
|
||||||
|
ue.niveau_competence_id = niveaux_map[ue.niveau_competence_id]
|
||||||
|
db.session.add(ue)
|
||||||
|
if ue.parcours:
|
||||||
|
new_list = [ApcParcours.query.get(parcours_map[p.id]) for p in ue.parcours]
|
||||||
|
ue.parcours.clear()
|
||||||
|
ue.parcours.extend(new_list)
|
||||||
|
db.session.add(ue)
|
||||||
|
# Modules / parcours et app_critiques
|
||||||
|
for module in formation.modules:
|
||||||
|
if module.parcours:
|
||||||
|
new_list = [
|
||||||
|
ApcParcours.query.get(parcours_map[p.id]) for p in module.parcours
|
||||||
|
]
|
||||||
|
module.parcours.clear()
|
||||||
|
module.parcours.extend(new_list)
|
||||||
|
db.session.add(module)
|
||||||
|
if module.app_critiques: # efface les apprentissages critiques
|
||||||
|
module.app_critiques.clear()
|
||||||
|
db.session.add(module)
|
||||||
|
# ApcValidationRCUE
|
||||||
|
for valid_rcue in ApcValidationRCUE.query.join(
|
||||||
|
UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id
|
||||||
|
).filter_by(formation_id=formation.id):
|
||||||
|
if valid_rcue.parcour:
|
||||||
|
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
|
||||||
|
db.session.add(valid_rcue)
|
||||||
|
for valid_rcue in ApcValidationRCUE.query.join(
|
||||||
|
UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id
|
||||||
|
).filter_by(formation_id=formation.id):
|
||||||
|
if valid_rcue.parcour:
|
||||||
|
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
|
||||||
|
db.session.add(valid_rcue)
|
||||||
|
# FormSemestre / parcours_formsemestre
|
||||||
|
for formsemestre in formation.formsemestres:
|
||||||
|
new_list = [
|
||||||
|
ApcParcours.query.get(parcours_map[p.id]) for p in formsemestre.parcours
|
||||||
|
]
|
||||||
|
formsemestre.parcours.clear()
|
||||||
|
formsemestre.parcours.extend(new_list)
|
||||||
|
db.session.add(formsemestre)
|
||||||
|
# FormSemestreInscription.parcour_id
|
||||||
|
for inscr in FormSemestreInscription.query.filter_by(
|
||||||
|
formsemestre_id=formsemestre.id
|
||||||
|
).filter(FormSemestreInscription.parcour_id != None):
|
||||||
|
if inscr.parcour_id is not None:
|
||||||
|
inscr.parcour_id = parcours_map[inscr.parcour_id]
|
||||||
|
#
|
||||||
|
db.session.commit()
|
||||||
|
clear_scodoc_cache()
|
|
@ -542,9 +542,9 @@ def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int)
|
||||||
for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items():
|
for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items():
|
||||||
H.append(
|
H.append(
|
||||||
f"""<li>Parcours {parcour_code} : {
|
f"""<li>Parcours {parcour_code} : {
|
||||||
len(niveaux)} niveaux sans UEs
|
len(niveaux)} niveaux sans UEs :
|
||||||
<span>
|
<span class="niveau-nom"><span>
|
||||||
{ ', '.join( f'{niveau.competence.titre} {niveau.ordre}'
|
{ '</span>, <span>'.join( f'{niveau.competence.titre} {niveau.ordre}'
|
||||||
for niveau in niveaux
|
for niveau in niveaux
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -563,7 +563,8 @@ def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int)
|
||||||
if nb_niveaux_tc != nb_ues_tc:
|
if nb_niveaux_tc != nb_ues_tc:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<li>{nb_niveaux_tc} niveaux de compétences de tronc commun,
|
f"""<li>{nb_niveaux_tc} niveaux de compétences de tronc commun,
|
||||||
mais {nb_ues_tc} UEs de tronc commun !</li>"""
|
mais {nb_ues_tc} UEs de tronc commun ! (c'est normal si
|
||||||
|
vous avez des UEs différenciées par parcours)</li>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
if H:
|
if H:
|
||||||
|
|
|
@ -10,9 +10,11 @@
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileAllowed
|
from flask_wtf.file import FileField, FileAllowed
|
||||||
from wtforms import SelectField, SubmitField
|
from wtforms import SelectField, SubmitField
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
|
|
||||||
class FormationRefCompForm(FlaskForm):
|
class FormationRefCompForm(FlaskForm):
|
||||||
|
"Choix d'un référentiel"
|
||||||
referentiel_competence = SelectField(
|
referentiel_competence = SelectField(
|
||||||
"Choisir parmi les référentiels déjà chargés :"
|
"Choisir parmi les référentiels déjà chargés :"
|
||||||
)
|
)
|
||||||
|
@ -21,6 +23,7 @@ class FormationRefCompForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class RefCompLoadForm(FlaskForm):
|
class RefCompLoadForm(FlaskForm):
|
||||||
|
"Upload d'un référentiel"
|
||||||
referentiel_standard = SelectField(
|
referentiel_standard = SelectField(
|
||||||
"Choisir un référentiel de compétences officiel BUT"
|
"Choisir un référentiel de compétences officiel BUT"
|
||||||
)
|
)
|
||||||
|
@ -47,3 +50,12 @@ class RefCompLoadForm(FlaskForm):
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class FormationChangeRefCompForm(FlaskForm):
|
||||||
|
"choix d'un nouveau ref. comp. pour une formation"
|
||||||
|
object_select = SelectField(
|
||||||
|
"Choisir le nouveau référentiel", validators=[DataRequired()]
|
||||||
|
)
|
||||||
|
submit = SubmitField("Changer le référentiel de la formation")
|
||||||
|
cancel = SubmitField("Annuler")
|
||||||
|
|
|
@ -23,9 +23,12 @@ from app.models.but_refcomp import (
|
||||||
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
|
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
def orebut_import_refcomp(
|
||||||
|
xml_data: str, dept_id: int, orig_filename=None
|
||||||
|
) -> ApcReferentielCompetences:
|
||||||
"""Importation XML Orébut
|
"""Importation XML Orébut
|
||||||
peut lever TypeError ou ScoFormatError
|
peut lever TypeError ou ScoFormatError
|
||||||
|
L'objet créé est ajouté et commité.
|
||||||
Résultat: instance de ApcReferentielCompetences
|
Résultat: instance de ApcReferentielCompetences
|
||||||
"""
|
"""
|
||||||
# Vérifie que le même fichier n'a pas déjà été chargé:
|
# Vérifie que le même fichier n'a pas déjà été chargé:
|
||||||
|
@ -41,7 +44,7 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
||||||
try:
|
try:
|
||||||
root = ElementTree.XML(xml_data)
|
root = ElementTree.XML(xml_data)
|
||||||
except ElementTree.ParseError as exc:
|
except ElementTree.ParseError as exc:
|
||||||
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}")
|
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}") from exc
|
||||||
if root.tag != "referentiel_competence":
|
if root.tag != "referentiel_competence":
|
||||||
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
|
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
|
||||||
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
|
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
|
||||||
|
@ -60,7 +63,8 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
||||||
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
|
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
|
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({
|
||||||
|
competence.attrib["id"]})
|
||||||
"""
|
"""
|
||||||
) from exc
|
) from exc
|
||||||
ref.competences.append(c)
|
ref.competences.append(c)
|
||||||
|
|
|
@ -77,7 +77,7 @@ from app.models.but_refcomp import (
|
||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
)
|
)
|
||||||
from app.models import Evaluation, Scolog, ScolarAutorisationInscription
|
from app.models import Evaluation, ModuleImpl, Scolog, ScolarAutorisationInscription
|
||||||
from app.models.but_validations import (
|
from app.models.but_validations import (
|
||||||
ApcValidationAnnee,
|
ApcValidationAnnee,
|
||||||
ApcValidationRCUE,
|
ApcValidationRCUE,
|
||||||
|
@ -413,12 +413,12 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||||
# Si validée par niveau supérieur:
|
# Si validée par niveau supérieur:
|
||||||
if self.code_valide == sco_codes.ADSUP:
|
if self.code_valide == sco_codes.ADSUP:
|
||||||
self.codes.insert(0, sco_codes.ADSUP)
|
self.codes.insert(0, sco_codes.ADSUP)
|
||||||
self.explanation = f"<div>{explanation}</div>"
|
self.explanation = f'<div class="deca-expl">{explanation}</div>'
|
||||||
messages = self.descr_pb_coherence()
|
messages = self.descr_pb_coherence()
|
||||||
if messages:
|
if messages:
|
||||||
self.explanation += (
|
self.explanation += (
|
||||||
'<div class="warning">'
|
'<div class="warning warning-info">'
|
||||||
+ '</div><div class="warning">'.join(messages)
|
+ '</div><div class="warning warning-info">'.join(messages)
|
||||||
+ "</div>"
|
+ "</div>"
|
||||||
)
|
)
|
||||||
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
|
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
|
||||||
|
@ -796,16 +796,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||||
if self.formsemestre_pair is not None:
|
if self.formsemestre_pair is not None:
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
||||||
|
|
||||||
def has_notes_en_attente(self) -> bool:
|
def _get_current_res(self) -> ResultatsSemestreBUT:
|
||||||
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
|
"Les res. du semestre d'origine du deca"
|
||||||
res = (
|
return (
|
||||||
self.res_pair
|
self.res_pair
|
||||||
if self.formsemestre_pair
|
if self.formsemestre_pair
|
||||||
and (self.formsemestre.id == self.formsemestre_pair.id)
|
and (self.formsemestre.id == self.formsemestre_pair.id)
|
||||||
else self.res_impair
|
else self.res_impair
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def has_notes_en_attente(self) -> bool:
|
||||||
|
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
|
||||||
|
res = self._get_current_res()
|
||||||
return res and self.etud.id in res.get_etudids_attente()
|
return res and self.etud.id in res.get_etudids_attente()
|
||||||
|
|
||||||
|
def get_modimpls_attente(self) -> list[ModuleImpl]:
|
||||||
|
"Liste des ModuleImpl dans lesquels l'étudiant à au moins une note en ATTente"
|
||||||
|
res = self._get_current_res()
|
||||||
|
modimpls_results = [
|
||||||
|
modimpl_result
|
||||||
|
for modimpl_result in res.modimpls_results.values()
|
||||||
|
if self.etud.id in modimpl_result.etudids_attente
|
||||||
|
]
|
||||||
|
modimpls = [
|
||||||
|
db.session.get(ModuleImpl, mr.moduleimpl_id) for mr in modimpls_results
|
||||||
|
]
|
||||||
|
return sorted(modimpls, key=lambda mi: (mi.module.numero, mi.module.code))
|
||||||
|
|
||||||
def record_all(self, only_validantes: bool = False) -> bool:
|
def record_all(self, only_validantes: bool = False) -> bool:
|
||||||
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
|
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
|
||||||
et sont donc en mode "automatique".
|
et sont donc en mode "automatique".
|
||||||
|
@ -997,19 +1014,23 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||||
if dec_ue.code_valide not in CODES_UE_VALIDES:
|
if dec_ue.code_valide not in CODES_UE_VALIDES:
|
||||||
if (
|
if (
|
||||||
dec_ue.ue_status
|
dec_ue.ue_status
|
||||||
and dec_ue.ue_status["was_capitalized"]
|
and dec_ue.ue_status["is_capitalized"]
|
||||||
):
|
):
|
||||||
messages.append(
|
messages.append(
|
||||||
f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
|
f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
messages.append(
|
messages.append(
|
||||||
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
|
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est (probablement une validation antérieure)"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
messages.append(
|
messages.append(
|
||||||
f"L'UE {ue.acronyme} n'a pas décision (???)"
|
f"L'UE {ue.acronyme} n'a pas décision (???)"
|
||||||
)
|
)
|
||||||
|
# Voyons si on est dispensé de cette ue ?
|
||||||
|
res = self.res_impair if ue.semestre_idx % 2 else self.res_pair
|
||||||
|
if res and (self.etud.id, ue.id) in res.dispense_ues:
|
||||||
|
messages.append(f"Pas (ré)inscrit à l'UE {ue.acronyme}")
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
def valide_diplome(self) -> bool:
|
def valide_diplome(self) -> bool:
|
||||||
|
@ -1514,7 +1535,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
|
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
|
||||||
} codes={self.codes} explanation={self.explanation}>"""
|
} codes={self.codes} explanation="{self.explanation}">"""
|
||||||
|
|
||||||
def compute_codes(self):
|
def compute_codes(self):
|
||||||
"""Calcul des .codes attribuables et de l'explanation associée"""
|
"""Calcul des .codes attribuables et de l'explanation associée"""
|
||||||
|
|
|
@ -55,11 +55,21 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
|
||||||
else:
|
else:
|
||||||
line_sep = "\n"
|
line_sep = "\n"
|
||||||
rows, titles = pvjury_table_but(formsemestre, line_sep=line_sep)
|
rows, titles = pvjury_table_but(formsemestre, line_sep=line_sep)
|
||||||
|
if fmt.startswith("xls"):
|
||||||
|
titles.update(
|
||||||
|
{
|
||||||
|
"etudid": "etudid",
|
||||||
|
"code_nip": "nip",
|
||||||
|
"code_ine": "ine",
|
||||||
|
"ects_but": "Total ECTS BUT",
|
||||||
|
"civilite": "Civ.",
|
||||||
|
"nom": "Nom",
|
||||||
|
"prenom": "Prénom",
|
||||||
|
}
|
||||||
|
)
|
||||||
# Style excel... passages à la ligne sur \n
|
# Style excel... passages à la ligne sur \n
|
||||||
xls_style_base = sco_excel.excel_make_style()
|
xls_style_base = sco_excel.excel_make_style()
|
||||||
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
|
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
|
||||||
|
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
|
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
|
||||||
caption=title,
|
caption=title,
|
||||||
|
@ -116,7 +126,7 @@ def pvjury_table_but(
|
||||||
"pas de référentiel de compétences associé à la formation de ce semestre !"
|
"pas de référentiel de compétences associé à la formation de ce semestre !"
|
||||||
)
|
)
|
||||||
titles = {
|
titles = {
|
||||||
"nom": "Code" if anonymous else "Nom",
|
"nom_pv": "Code" if anonymous else "Nom",
|
||||||
"cursus": "Cursus",
|
"cursus": "Cursus",
|
||||||
"ects": "ECTS",
|
"ects": "ECTS",
|
||||||
"ues": "UE validées",
|
"ues": "UE validées",
|
||||||
|
@ -144,33 +154,47 @@ def pvjury_table_but(
|
||||||
except ScoValueError:
|
except ScoValueError:
|
||||||
deca = None
|
deca = None
|
||||||
|
|
||||||
|
ects_but_valides = but_ects_valides(etud, referentiel_competence_id)
|
||||||
row = {
|
row = {
|
||||||
"nom": etud.code_ine or etud.code_nip or etud.id
|
"nom_pv": (
|
||||||
|
etud.code_ine or etud.code_nip or etud.id
|
||||||
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
|
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
|
||||||
else etud.etat_civil_pv(
|
else etud.etat_civil_pv(
|
||||||
line_sep=line_sep, with_paragraph=with_paragraph_nom
|
line_sep=line_sep, with_paragraph=with_paragraph_nom
|
||||||
|
)
|
||||||
),
|
),
|
||||||
"_nom_order": etud.sort_key,
|
"_nom_pv_order": etud.sort_key,
|
||||||
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
"_nom_pv_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||||
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
"_nom_pv_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||||
"_nom_target": url_for(
|
"_nom_pv_target": url_for(
|
||||||
"scolar.fiche_etud",
|
"scolar.fiche_etud",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
),
|
),
|
||||||
"cursus": _descr_cursus_but(etud),
|
"cursus": _descr_cursus_but(etud),
|
||||||
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
|
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}""",
|
||||||
|
"_ects_xls": deca.ects_annee(),
|
||||||
|
"ects_but": ects_but_valides,
|
||||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||||
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
"niveaux": (
|
||||||
if deca
|
deca.descr_niveaux_validation(line_sep=line_sep) if deca else "-"
|
||||||
else "-",
|
),
|
||||||
"decision_but": deca.code_valide if deca else "",
|
"decision_but": deca.code_valide if deca else "",
|
||||||
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
"devenir": (
|
||||||
|
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||||
if deca
|
if deca
|
||||||
else "",
|
else ""
|
||||||
|
),
|
||||||
|
# pour exports excel seulement:
|
||||||
|
"civilite": etud.civilite_etat_civil_str,
|
||||||
|
"nom": etud.nom,
|
||||||
|
"prenom": etud.prenom_etat_civil or etud.prenom or "",
|
||||||
|
"etudid": etud.id,
|
||||||
|
"code_nip": etud.code_nip,
|
||||||
|
"code_ine": etud.code_ine,
|
||||||
}
|
}
|
||||||
if deca.valide_diplome() or not only_diplome:
|
if deca.valide_diplome() or not only_diplome:
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
rows.sort(key=lambda x: x["_nom_order"])
|
rows.sort(key=lambda x: x["_nom_pv_order"])
|
||||||
return rows, titles
|
return rows, titles
|
||||||
|
|
|
@ -16,8 +16,8 @@ from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_validation_auto_but(
|
def formsemestre_validation_auto_but(
|
||||||
formsemestre: FormSemestre, only_adm: bool = True
|
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False
|
||||||
) -> int:
|
) -> tuple[int, list[jury_but.DecisionsProposeesAnnee]]:
|
||||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||||
|
|
||||||
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
||||||
|
@ -27,16 +27,22 @@ def formsemestre_validation_auto_but(
|
||||||
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||||
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||||
|
|
||||||
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
|
Returns:
|
||||||
|
- En mode normal, (nombre d'étudiants pour lesquels on a enregistré au moins un code, []])
|
||||||
|
- En mode dry_run, (0, list[DecisionsProposeesAnnee])
|
||||||
"""
|
"""
|
||||||
if not formsemestre.formation.is_apc():
|
if not formsemestre.formation.is_apc():
|
||||||
raise ScoValueError("fonction réservée aux formations BUT")
|
raise ScoValueError("fonction réservée aux formations BUT")
|
||||||
nb_etud_modif = 0
|
nb_etud_modif = 0
|
||||||
|
decas = []
|
||||||
with sco_cache.DeferredSemCacheManager():
|
with sco_cache.DeferredSemCacheManager():
|
||||||
for etudid in formsemestre.etuds_inscriptions:
|
for etudid in formsemestre.etuds_inscriptions:
|
||||||
etud = Identite.get_etud(etudid)
|
etud = Identite.get_etud(etudid)
|
||||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
|
if not dry_run:
|
||||||
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
||||||
|
else:
|
||||||
|
decas.append(deca)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
|
@ -49,4 +55,4 @@ def formsemestre_validation_auto_but(
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return nb_etud_modif
|
return nb_etud_modif, decas
|
||||||
|
|
|
@ -21,8 +21,6 @@ from app.but.jury_but import (
|
||||||
DecisionsProposeesRCUE,
|
DecisionsProposeesRCUE,
|
||||||
DecisionsProposeesUE,
|
DecisionsProposeesUE,
|
||||||
)
|
)
|
||||||
from app.comp import res_sem
|
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
|
||||||
from app.models import (
|
from app.models import (
|
||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
|
@ -33,11 +31,8 @@ from app.models import (
|
||||||
ScolarFormSemestreValidation,
|
ScolarFormSemestreValidation,
|
||||||
ScolarNews,
|
ScolarNews,
|
||||||
)
|
)
|
||||||
from app.models.config import ScoDocSiteConfig
|
|
||||||
from app.scodoc import html_sco_header
|
|
||||||
from app.scodoc import codes_cursus as sco_codes
|
from app.scodoc import codes_cursus as sco_codes
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,23 +104,32 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||||
</div>"""
|
</div>"""
|
||||||
)
|
)
|
||||||
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
|
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
|
||||||
# Les UEs à afficher,
|
# Les UEs à afficher : on regarde si read only et si dispense (non ré-inscription à l'UE)
|
||||||
# qui
|
# tuples (UniteEns, read_only, dispense)
|
||||||
ues_ro = [
|
ues_ro_dispense = [
|
||||||
(
|
(
|
||||||
ue_impair,
|
ue_impair,
|
||||||
rcue.ue_cur_impair is None,
|
rcue.ue_cur_impair is None,
|
||||||
|
deca.res_impair
|
||||||
|
and ue_impair
|
||||||
|
and (deca.etud.id, ue_impair.id) in deca.res_impair.dispense_ues,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
ue_pair,
|
ue_pair,
|
||||||
rcue.ue_cur_pair is None,
|
rcue.ue_cur_pair is None,
|
||||||
|
deca.res_pair
|
||||||
|
and ue_pair
|
||||||
|
and (deca.etud.id, ue_pair.id) in deca.res_pair.dispense_ues,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
# Ordonne selon les dates des 2 semestres considérés:
|
# Ordonne selon les dates des 2 semestres considérés:
|
||||||
if reverse_semestre:
|
if reverse_semestre:
|
||||||
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
|
ues_ro_dispense[0], ues_ro_dispense[1] = (
|
||||||
|
ues_ro_dispense[1],
|
||||||
|
ues_ro_dispense[0],
|
||||||
|
)
|
||||||
# Colonnes d'UE:
|
# Colonnes d'UE:
|
||||||
for ue, ue_read_only in ues_ro:
|
for ue, ue_read_only, ue_dispense in ues_ro_dispense:
|
||||||
if ue:
|
if ue:
|
||||||
H.append(
|
H.append(
|
||||||
_gen_but_niveau_ue(
|
_gen_but_niveau_ue(
|
||||||
|
@ -134,6 +138,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||||
disabled=read_only or ue_read_only,
|
disabled=read_only or ue_read_only,
|
||||||
annee_prec=ue_read_only,
|
annee_prec=ue_read_only,
|
||||||
niveau_id=ue.niveau_competence.id,
|
niveau_id=ue.niveau_competence.id,
|
||||||
|
ue_dispense=ue_dispense,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -188,21 +193,30 @@ def _gen_but_niveau_ue(
|
||||||
disabled: bool = False,
|
disabled: bool = False,
|
||||||
annee_prec: bool = False,
|
annee_prec: bool = False,
|
||||||
niveau_id: int = None,
|
niveau_id: int = None,
|
||||||
|
ue_dispense: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
|
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
|
||||||
moy_ue_str = f"""<span class="ue_cap">{
|
moy_ue_str = f"""<span class="ue_cap">{
|
||||||
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
|
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
|
||||||
scoplement = f"""<div class="scoplement">
|
|
||||||
<div>
|
if ue_dispense:
|
||||||
<b>UE {ue.acronyme} capitalisée </b>
|
etat_en_cours = """Non (ré)inscrit à cette UE"""
|
||||||
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
|
else:
|
||||||
</span>
|
etat_en_cours = f"""UE en cours
|
||||||
</div>
|
|
||||||
<div>UE en cours
|
|
||||||
{ "sans notes" if np.isnan(dec_ue.moy_ue)
|
{ "sans notes" if np.isnan(dec_ue.moy_ue)
|
||||||
else
|
else
|
||||||
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
|
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
|
||||||
}
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
scoplement = f"""<div class="scoplement">
|
||||||
|
<div>
|
||||||
|
<b>UE {ue.acronyme} capitalisée </b>
|
||||||
|
<span>le {dec_ue.ue_status["event_date"].strftime(scu.DATE_FMT)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{ etat_en_cours }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
@ -214,7 +228,7 @@ def _gen_but_niveau_ue(
|
||||||
<div>
|
<div>
|
||||||
<b>UE {ue.acronyme} antérieure </b>
|
<b>UE {ue.acronyme} antérieure </b>
|
||||||
<span>validée {dec_ue.validation.code}
|
<span>validée {dec_ue.validation.code}
|
||||||
le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
le {dec_ue.validation.event_date.strftime(scu.DATE_FMT)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>Non reprise dans l'année en cours</div>
|
<div>Non reprise dans l'année en cours</div>
|
||||||
|
@ -232,9 +246,7 @@ def _gen_but_niveau_ue(
|
||||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
||||||
if dec_ue.code_valide:
|
if dec_ue.code_valide:
|
||||||
date_str = (
|
date_str = (
|
||||||
f"""enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
f"""enregistré le {dec_ue.validation.event_date.strftime(scu.DATEATIME_FMT)}"""
|
||||||
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
|
||||||
"""
|
|
||||||
if dec_ue.validation and dec_ue.validation.event_date
|
if dec_ue.validation and dec_ue.validation.event_date
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
@ -243,6 +255,12 @@ def _gen_but_niveau_ue(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
else:
|
||||||
|
if dec_ue.ue_status and dec_ue.ue_status["was_capitalized"]:
|
||||||
|
scoplement = """<div class="scoplement">
|
||||||
|
UE déjà capitalisée avec résultat moins favorable.
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
else:
|
else:
|
||||||
scoplement = ""
|
scoplement = ""
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ class RegroupementCoherentUE:
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Autres validations pour l'UE paire
|
# Autres validations pour les UEs paire/impaire
|
||||||
self.validation_ue_best_pair = best_autre_ue_validation(
|
self.validation_ue_best_pair = best_autre_ue_validation(
|
||||||
etud.id,
|
etud.id,
|
||||||
niveau.id,
|
niveau.id,
|
||||||
|
@ -101,14 +101,24 @@ class RegroupementCoherentUE:
|
||||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
||||||
self.ue_status_impair = None
|
self.ue_status_impair = None
|
||||||
if self.ue_cur_impair:
|
if self.ue_cur_impair:
|
||||||
|
# UE courante
|
||||||
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
|
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
|
||||||
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
|
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
|
||||||
self.ue_1 = self.ue_cur_impair
|
self.ue_1 = self.ue_cur_impair
|
||||||
self.res_impair = res_impair
|
self.res_impair = res_impair
|
||||||
self.ue_status_impair = ue_status
|
self.ue_status_impair = ue_status
|
||||||
elif self.validation_ue_best_impair:
|
elif self.validation_ue_best_impair:
|
||||||
|
# UE capitalisée
|
||||||
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
|
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
|
||||||
self.ue_1 = self.validation_ue_best_impair.ue
|
self.ue_1 = self.validation_ue_best_impair.ue
|
||||||
|
if (
|
||||||
|
res_impair
|
||||||
|
and self.validation_ue_best_impair
|
||||||
|
and self.validation_ue_best_impair.ue
|
||||||
|
):
|
||||||
|
self.ue_status_impair = res_impair.get_etud_ue_status(
|
||||||
|
etud.id, self.validation_ue_best_impair.ue.id
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.moy_ue_1, self.ue_1 = None, None
|
self.moy_ue_1, self.ue_1 = None, None
|
||||||
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -23,6 +23,7 @@ from app.models import (
|
||||||
)
|
)
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class ValidationsSemestre(ResultatsCache):
|
class ValidationsSemestre(ResultatsCache):
|
||||||
|
@ -84,7 +85,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||||
"code": decision.code,
|
"code": decision.code,
|
||||||
"assidu": decision.assidu,
|
"assidu": decision.assidu,
|
||||||
"compense_formsemestre_id": decision.compense_formsemestre_id,
|
"compense_formsemestre_id": decision.compense_formsemestre_id,
|
||||||
"event_date": decision.event_date.strftime("%d/%m/%Y"),
|
"event_date": decision.event_date.strftime(scu.DATE_FMT),
|
||||||
}
|
}
|
||||||
self.decisions_jury = decisions_jury
|
self.decisions_jury = decisions_jury
|
||||||
|
|
||||||
|
@ -107,7 +108,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||||
decisions_jury_ues[decision.etudid][decision.ue.id] = {
|
decisions_jury_ues[decision.etudid][decision.ue.id] = {
|
||||||
"code": decision.code,
|
"code": decision.code,
|
||||||
"ects": ects, # 0. si UE non validée
|
"ects": ects, # 0. si UE non validée
|
||||||
"event_date": decision.event_date.strftime("%d/%m/%Y"),
|
"event_date": decision.event_date.strftime(scu.DATE_FMT),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.decisions_jury_ues = decisions_jury_ues
|
self.decisions_jury_ues = decisions_jury_ues
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,7 +71,15 @@ class ModuleImplResults:
|
||||||
les caches sont gérés par ResultatsSemestre.
|
les caches sont gérés par ResultatsSemestre.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, moduleimpl: ModuleImpl):
|
def __init__(
|
||||||
|
self, moduleimpl: ModuleImpl, etudids: list[int], etudids_actifs: set[int]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
- etudids : liste des etudids, qui donne l'index du dataframe
|
||||||
|
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
|
||||||
|
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
|
||||||
|
"""
|
||||||
self.moduleimpl_id = moduleimpl.id
|
self.moduleimpl_id = moduleimpl.id
|
||||||
self.module_id = moduleimpl.module.id
|
self.module_id = moduleimpl.module.id
|
||||||
self.etudids = None
|
self.etudids = None
|
||||||
|
@ -105,14 +112,23 @@ 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.load_notes()
|
self.evals_type = {}
|
||||||
|
"""Type de chaque eval { evaluation.id : evaluation.evaluation_type }"""
|
||||||
|
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"""
|
||||||
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
|
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
|
||||||
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
|
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
|
||||||
|
|
||||||
def load_notes(self): # ré-écriture de df_load_modimpl_notes
|
def load_notes(
|
||||||
|
self, etudids: list[int], etudids_actifs: set[int]
|
||||||
|
): # ré-écriture de df_load_modimpl_notes
|
||||||
"""Charge toutes les notes de toutes les évaluations du module.
|
"""Charge toutes les notes de toutes les évaluations du module.
|
||||||
|
Args:
|
||||||
|
- etudids : liste des etudids, qui donne l'index du dataframe
|
||||||
|
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
|
||||||
|
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
|
||||||
|
|
||||||
Dataframe evals_notes
|
Dataframe evals_notes
|
||||||
colonnes: le nom de la colonne est l'evaluation_id (int)
|
colonnes: le nom de la colonne est l'evaluation_id (int)
|
||||||
index (lignes): etudid (int)
|
index (lignes): etudid (int)
|
||||||
|
@ -135,12 +151,12 @@ class ModuleImplResults:
|
||||||
qui ont des notes ATT.
|
qui ont des notes ATT.
|
||||||
"""
|
"""
|
||||||
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
||||||
self.etudids = self._etudids()
|
self.etudids = etudids
|
||||||
|
|
||||||
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
|
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
|
||||||
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
|
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
|
||||||
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
|
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
|
||||||
moduleimpl.formsemestre.etudids_actifs
|
etudids_actifs
|
||||||
)
|
)
|
||||||
self.nb_inscrits_module = len(inscrits_module)
|
self.nb_inscrits_module = len(inscrits_module)
|
||||||
|
|
||||||
|
@ -148,20 +164,24 @@ class ModuleImplResults:
|
||||||
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
||||||
self.evaluations_completes = []
|
self.evaluations_completes = []
|
||||||
self.evaluations_completes_dict = {}
|
self.evaluations_completes_dict = {}
|
||||||
|
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
|
||||||
# ou évaluation déclarée "à prise en compte immédiate"
|
# ou évaluation déclarée "à prise en compte immédiate"
|
||||||
# ou rattrapage, 2eme session, bonus
|
# ou rattrapage, 2eme session, bonus
|
||||||
# ET pas bloquée par date (is_blocked)
|
# ET pas bloquée par date (is_blocked)
|
||||||
|
is_blocked = evaluation.is_blocked()
|
||||||
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
|
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
|
||||||
is_complete = (
|
is_complete = (
|
||||||
(evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
|
(evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
|
||||||
or (evaluation.publish_incomplete)
|
or (evaluation.publish_incomplete)
|
||||||
or (not etudids_sans_note)
|
or (not etudids_sans_note)
|
||||||
) and not evaluation.is_blocked()
|
) and not is_blocked
|
||||||
self.evaluations_completes.append(is_complete)
|
self.evaluations_completes.append(is_complete)
|
||||||
self.evaluations_completes_dict[evaluation.id] = is_complete
|
self.evaluations_completes_dict[evaluation.id] = is_complete
|
||||||
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
|
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
|
||||||
|
@ -178,6 +198,10 @@ class ModuleImplResults:
|
||||||
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||||
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
|
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
|
||||||
nb_notes = eval_notes_inscr.notna().sum()
|
nb_notes = eval_notes_inscr.notna().sum()
|
||||||
|
|
||||||
|
if is_blocked:
|
||||||
|
eval_etudids_attente = set()
|
||||||
|
else:
|
||||||
# Etudiants avec notes en attente:
|
# Etudiants avec notes en attente:
|
||||||
# = ceux avec note ATT
|
# = ceux avec note ATT
|
||||||
eval_etudids_attente = set(
|
eval_etudids_attente = set(
|
||||||
|
@ -188,6 +212,7 @@ class ModuleImplResults:
|
||||||
if evaluation.publish_incomplete:
|
if evaluation.publish_incomplete:
|
||||||
# et en "immédiat", tous ceux sans note
|
# et en "immédiat", tous ceux sans note
|
||||||
eval_etudids_attente |= etudids_sans_note
|
eval_etudids_attente |= etudids_sans_note
|
||||||
|
|
||||||
# Synthèse pour état du module:
|
# Synthèse pour état du module:
|
||||||
self.etudids_attente |= eval_etudids_attente
|
self.etudids_attente |= eval_etudids_attente
|
||||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||||
|
@ -229,17 +254,6 @@ class ModuleImplResults:
|
||||||
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
|
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
|
||||||
return eval_df
|
return eval_df
|
||||||
|
|
||||||
def _etudids(self):
|
|
||||||
"""L'index du dataframe est la liste de tous les étudiants inscrits au semestre
|
|
||||||
(incluant les DEM et DEF)
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
inscr.etudid
|
|
||||||
for inscr in db.session.get(
|
|
||||||
ModuleImpl, self.moduleimpl_id
|
|
||||||
).formsemestre.inscriptions
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
|
def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
|
||||||
"""Coefficients des évaluations.
|
"""Coefficients des évaluations.
|
||||||
Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.
|
Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.
|
||||||
|
@ -260,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"
|
||||||
|
@ -286,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."""
|
||||||
|
@ -334,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.
|
||||||
|
@ -360,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)
|
||||||
|
@ -388,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,
|
||||||
|
@ -395,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,
|
||||||
|
@ -443,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,
|
||||||
|
@ -515,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:
|
||||||
|
@ -536,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))
|
||||||
|
@ -583,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,
|
||||||
|
@ -630,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,
|
||||||
|
|
|
@ -99,9 +99,11 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
||||||
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
||||||
# sur toutes les UE)
|
# sur toutes les UE)
|
||||||
default_poids = {
|
default_poids = {
|
||||||
mod.id: 1.0
|
mod.id: (
|
||||||
|
1.0
|
||||||
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
|
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
|
||||||
else 0.0
|
else 0.0
|
||||||
|
)
|
||||||
for mod in modules
|
for mod in modules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,10 +150,12 @@ def df_load_modimpl_coefs(
|
||||||
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
||||||
# sur toutes les UE)
|
# sur toutes les UE)
|
||||||
default_poids = {
|
default_poids = {
|
||||||
modimpl.id: 1.0
|
modimpl.id: (
|
||||||
|
1.0
|
||||||
if (modimpl.module.module_type == ModuleType.STANDARD)
|
if (modimpl.module.module_type == ModuleType.STANDARD)
|
||||||
and (modimpl.module.ue.type == UE_SPORT)
|
and (modimpl.module.ue.type == UE_SPORT)
|
||||||
else 0.0
|
else 0.0
|
||||||
|
)
|
||||||
for modimpl in formsemestre.modimpls_sorted
|
for modimpl in formsemestre.modimpls_sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,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.
|
||||||
|
@ -200,10 +206,11 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||||
modimpls_results = {}
|
modimpls_results = {}
|
||||||
modimpls_evals_poids = {}
|
modimpls_evals_poids = {}
|
||||||
modimpls_notes = []
|
modimpls_notes = []
|
||||||
|
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||||
for modimpl in formsemestre.modimpls_sorted:
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
|
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
||||||
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -256,8 +257,9 @@ def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]
|
||||||
"""
|
"""
|
||||||
modimpls_results = {}
|
modimpls_results = {}
|
||||||
modimpls_notes = []
|
modimpls_notes = []
|
||||||
|
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||||
for modimpl in formsemestre.modimpls_sorted:
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
mod_results = moy_mod.ModuleImplResultsClassic(modimpl)
|
mod_results = moy_mod.ModuleImplResultsClassic(modimpl, etudids, etudids_actifs)
|
||||||
etuds_moy_module = mod_results.compute_module_moy()
|
etuds_moy_module = mod_results.compute_module_moy()
|
||||||
modimpls_results[modimpl.id] = mod_results
|
modimpls_results[modimpl.id] = mod_results
|
||||||
modimpls_notes.append(etuds_moy_module)
|
modimpls_notes.append(etuds_moy_module)
|
||||||
|
|
|
@ -209,6 +209,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
"evalcomplete" : bool,
|
"evalcomplete" : bool,
|
||||||
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
|
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
|
||||||
"nb_notes" : int, # nb notes d'étudiants inscrits
|
"nb_notes" : int, # nb notes d'étudiants inscrits
|
||||||
|
"nb_attente" : int, # nb de notes en ATTente (même si bloquée)
|
||||||
},
|
},
|
||||||
"evaluation_id" : int,
|
"evaluation_id" : int,
|
||||||
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
|
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
|
||||||
|
@ -236,6 +237,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
"etat": {
|
"etat": {
|
||||||
"blocked": evaluation.is_blocked(),
|
"blocked": evaluation.is_blocked(),
|
||||||
"evalcomplete": etat.is_complete,
|
"evalcomplete": etat.is_complete,
|
||||||
|
"nb_attente": etat.nb_attente,
|
||||||
"nb_notes": etat.nb_notes,
|
"nb_notes": etat.nb_notes,
|
||||||
"last_modif": last_modif,
|
"last_modif": last_modif,
|
||||||
},
|
},
|
||||||
|
@ -436,7 +438,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
|
|
||||||
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict | None:
|
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict | None:
|
||||||
"""L'état de l'UE pour cet étudiant.
|
"""L'état de l'UE pour cet étudiant.
|
||||||
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
Result: dict, ou None si l'UE n'existe pas ou n'est pas dans ce semestre.
|
||||||
{
|
{
|
||||||
"is_capitalized": # vrai si la version capitalisée est celle prise en compte (meilleure)
|
"is_capitalized": # vrai si la version capitalisée est celle prise en compte (meilleure)
|
||||||
"was_capitalized":# si elle a été capitalisée (meilleure ou pas)
|
"was_capitalized":# si elle a été capitalisée (meilleure ou pas)
|
||||||
|
@ -454,6 +456,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||||
|
if not ue:
|
||||||
|
return None
|
||||||
ue_dict = ue.to_dict()
|
ue_dict = ue.to_dict()
|
||||||
|
|
||||||
if ue.type == UE_SPORT:
|
if ue.type == UE_SPORT:
|
||||||
|
@ -514,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:
|
||||||
|
|
20
app/email.py
20
app/email.py
|
@ -5,12 +5,13 @@
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -19,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(
|
||||||
|
@ -83,9 +92,12 @@ Adresses d'origine:
|
||||||
\n\n"""
|
\n\n"""
|
||||||
+ msg.body
|
+ msg.body
|
||||||
)
|
)
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
formatted_time = now.strftime("%Y-%m-%d %H:%M:%S") + ".{:03d}".format(
|
||||||
|
now.microsecond // 1000
|
||||||
|
)
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"""email sent to{
|
f"""[{formatted_time}] email sent to{
|
||||||
' (mode test)' if email_test_mode_address else ''
|
' (mode test)' if email_test_mode_address else ''
|
||||||
}: {msg.recipients}
|
}: {msg.recipients}
|
||||||
from sender {msg.sender}
|
from sender {msg.sender}
|
||||||
|
|
|
@ -59,3 +59,4 @@ def check_taxe_now(taxes):
|
||||||
|
|
||||||
|
|
||||||
from app.entreprises import routes
|
from app.entreprises import routes
|
||||||
|
from app.entreprises.activate import activate_module
|
||||||
|
|
31
app/entreprises/activate.py
Normal file
31
app/entreprises/activate.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Activation du module entreprises
|
||||||
|
|
||||||
|
L'affichage du module est contrôlé par la config ScoDocConfig.enable_entreprises
|
||||||
|
|
||||||
|
Au moment de l'activation, il est en général utile de proposer de configurer les
|
||||||
|
permissions de rôles standards: AdminEntreprise UtilisateurEntreprise ObservateurEntreprise
|
||||||
|
|
||||||
|
Voir associations dans sco_roles_default
|
||||||
|
|
||||||
|
"""
|
||||||
|
from app.auth.models import Role
|
||||||
|
from app.models import ScoDocSiteConfig
|
||||||
|
from app.scodoc.sco_roles_default import SCO_ROLES_ENTREPRISES_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def activate_module(
|
||||||
|
enable: bool = True, set_default_roles_permission: bool = False
|
||||||
|
) -> bool:
|
||||||
|
"""Active le module et en option donne les permissions aux rôles standards.
|
||||||
|
True si l'état d'activation a changé.
|
||||||
|
"""
|
||||||
|
change = ScoDocSiteConfig.enable_entreprises(enable)
|
||||||
|
if enable and set_default_roles_permission:
|
||||||
|
Role.reset_roles_permissions(SCO_ROLES_ENTREPRISES_DEFAULT)
|
||||||
|
return change
|
|
@ -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(
|
||||||
|
|
17
app/forms/main/activate_entreprises.py
Normal file
17
app/forms/main/activate_entreprises.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
"""
|
||||||
|
Formulaire activation module entreprises
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms.fields.simple import BooleanField, SubmitField
|
||||||
|
|
||||||
|
from app.models import ScoDocSiteConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ActivateEntreprisesForm(FlaskForm):
|
||||||
|
"Formulaire activation module entreprises"
|
||||||
|
set_default_roles_permission = BooleanField(
|
||||||
|
"(re)mettre les rôles 'Entreprise' à leurs valeurs par défaut"
|
||||||
|
)
|
||||||
|
submit = SubmitField("Valider")
|
||||||
|
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -48,13 +48,15 @@ class BonusConfigurationForm(FlaskForm):
|
||||||
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
|
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
submit_bonus = SubmitField("Valider")
|
submit_bonus = SubmitField("Enregistrer ce bonus")
|
||||||
cancel_bonus = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel_bonus = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
|
||||||
class ScoDocConfigurationForm(FlaskForm):
|
class ScoDocConfigurationForm(FlaskForm):
|
||||||
"Panneau de configuration avancée"
|
"Panneau de configuration avancée"
|
||||||
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
disable_passerelle = BooleanField( # disable car par défaut activée
|
||||||
|
"""cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation."""
|
||||||
|
)
|
||||||
month_debut_annee_scolaire = SelectField(
|
month_debut_annee_scolaire = SelectField(
|
||||||
label="Mois de début des années scolaires",
|
label="Mois de début des années scolaires",
|
||||||
description="""Date pivot. En France métropolitaine, août.
|
description="""Date pivot. En France métropolitaine, août.
|
||||||
|
@ -83,7 +85,7 @@ class ScoDocConfigurationForm(FlaskForm):
|
||||||
disable_bul_pdf = BooleanField(
|
disable_bul_pdf = BooleanField(
|
||||||
"interdire les exports des bulletins en PDF (déconseillé)"
|
"interdire les exports des bulletins en PDF (déconseillé)"
|
||||||
)
|
)
|
||||||
submit_scodoc = SubmitField("Valider")
|
submit_scodoc = SubmitField("Enregistrer ces paramètres")
|
||||||
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,6 +100,7 @@ def configuration():
|
||||||
form_scodoc = ScoDocConfigurationForm(
|
form_scodoc = ScoDocConfigurationForm(
|
||||||
data={
|
data={
|
||||||
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
|
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
|
||||||
|
"disable_passerelle": ScoDocSiteConfig.is_passerelle_disabled(),
|
||||||
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||||
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
||||||
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
||||||
|
@ -123,12 +126,12 @@ def configuration():
|
||||||
flash("Fonction bonus inchangée.")
|
flash("Fonction bonus inchangée.")
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
|
||||||
if ScoDocSiteConfig.enable_entreprises(
|
if ScoDocSiteConfig.disable_passerelle(
|
||||||
enabled=form_scodoc.data["enable_entreprises"]
|
disabled=form_scodoc.data["disable_passerelle"]
|
||||||
):
|
):
|
||||||
flash(
|
flash(
|
||||||
"Module entreprise "
|
"Fonction passerelle "
|
||||||
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
+ ("cachées" if form_scodoc.data["disable_passerelle"] else "montrées")
|
||||||
)
|
)
|
||||||
if ScoDocSiteConfig.set_month_debut_annee_scolaire(
|
if ScoDocSiteConfig.set_month_debut_annee_scolaire(
|
||||||
int(form_scodoc.data["month_debut_annee_scolaire"])
|
int(form_scodoc.data["month_debut_annee_scolaire"])
|
||||||
|
@ -171,6 +174,7 @@ def configuration():
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"configuration.j2",
|
"configuration.j2",
|
||||||
|
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
|
||||||
form_bonus=form_bonus,
|
form_bonus=form_bonus,
|
||||||
form_scodoc=form_scodoc,
|
form_scodoc=form_scodoc,
|
||||||
scu=scu,
|
scu=scu,
|
||||||
|
|
66
app/forms/main/create_bug_report.py
Normal file
66
app/forms/main/create_bug_report.py
Normal 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 ""
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
"""Gestion de l'assiduité (assiduités + justificatifs)
|
"""Gestion de l'assiduité (assiduités + justificatifs)"""
|
||||||
"""
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
@ -21,6 +21,7 @@ from app.scodoc import sco_abs_notification
|
||||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_utils import (
|
from app.scodoc.sco_utils import (
|
||||||
EtatAssiduite,
|
EtatAssiduite,
|
||||||
EtatJustificatif,
|
EtatJustificatif,
|
||||||
|
@ -113,9 +114,9 @@ class Assiduite(ScoDocModel):
|
||||||
"entry_date": self.entry_date,
|
"entry_date": self.entry_date,
|
||||||
"user_id": None if user is None else user.id, # l'uid
|
"user_id": None if user is None else user.id, # l'uid
|
||||||
"user_name": None if user is None else user.user_name, # le login
|
"user_name": None if user is None else user.user_name, # le login
|
||||||
"user_nom_complet": None
|
"user_nom_complet": (
|
||||||
if user is None
|
None if user is None else user.get_nomcomplet()
|
||||||
else user.get_nomcomplet(), # "Marie Dupont"
|
), # "Marie Dupont"
|
||||||
"est_just": self.est_just,
|
"est_just": self.est_just,
|
||||||
"external_data": self.external_data,
|
"external_data": self.external_data,
|
||||||
}
|
}
|
||||||
|
@ -336,29 +337,35 @@ class Assiduite(ScoDocModel):
|
||||||
"""
|
"""
|
||||||
return get_formsemestre_from_data(self.to_dict())
|
return get_formsemestre_from_data(self.to_dict())
|
||||||
|
|
||||||
def get_module(self, traduire: bool = False) -> int | str:
|
def get_module(self, traduire: bool = False) -> Module | str:
|
||||||
"TODO documenter"
|
"""
|
||||||
|
Retourne le module associé à l'assiduité
|
||||||
|
Si traduire est vrai, retourne le titre du module précédé du code
|
||||||
|
Sinon rentourne l'objet Module ou None
|
||||||
|
"""
|
||||||
|
|
||||||
if self.moduleimpl_id is not None:
|
if self.moduleimpl_id is not None:
|
||||||
if traduire:
|
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||||
mod: Module = Module.query.get(modimpl.module_id)
|
mod: Module = Module.query.get(modimpl.module_id)
|
||||||
|
if traduire:
|
||||||
return f"{mod.code} {mod.titre}"
|
return f"{mod.code} {mod.titre}"
|
||||||
|
return mod
|
||||||
|
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
retourne le texte "saisie le <date> par <User>"
|
retourne le texte "saisie le <date> par <User>"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
|
date: str = self.entry_date.strftime(scu.DATEATIME_FMT)
|
||||||
utilisateur: str = ""
|
utilisateur: str = ""
|
||||||
if self.user is not None:
|
if self.user is not None:
|
||||||
self.user: User
|
self.user: User
|
||||||
|
@ -574,11 +581,7 @@ class Justificatif(ScoDocModel):
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||||
compute_assiduites_justified(
|
self.dejustifier_assiduites()
|
||||||
self.etudid,
|
|
||||||
Justificatif.query.filter_by(etudid=self.etudid).all(),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_fichiers(self) -> tuple[list[str], int]:
|
def get_fichiers(self) -> tuple[list[str], int]:
|
||||||
"""Renvoie la liste des noms de fichiers justicatifs
|
"""Renvoie la liste des noms de fichiers justicatifs
|
||||||
|
@ -600,6 +603,82 @@ class Justificatif(ScoDocModel):
|
||||||
accessible_filenames.append(filename[0])
|
accessible_filenames.append(filename[0])
|
||||||
return accessible_filenames, len(filenames)
|
return accessible_filenames, len(filenames)
|
||||||
|
|
||||||
|
def justifier_assiduites(
|
||||||
|
self,
|
||||||
|
) -> list[int]:
|
||||||
|
"""Justifie les assiduités sur la période de validité du justificatif"""
|
||||||
|
log(f"justifier_assiduites: {self}")
|
||||||
|
assiduites_justifiees: list[int] = []
|
||||||
|
if self.etat != EtatJustificatif.VALIDE:
|
||||||
|
return []
|
||||||
|
# On récupère les assiduités de l'étudiant sur la période donnée
|
||||||
|
assiduites: Query = self.etudiant.assiduites.filter(
|
||||||
|
Assiduite.date_debut >= self.date_debut,
|
||||||
|
Assiduite.date_fin <= self.date_fin,
|
||||||
|
Assiduite.etat != EtatAssiduite.PRESENT,
|
||||||
|
)
|
||||||
|
# Pour chaque assiduité, on la justifie
|
||||||
|
for assi in assiduites:
|
||||||
|
assi.est_just = True
|
||||||
|
assiduites_justifiees.append(assi.assiduite_id)
|
||||||
|
db.session.add(assi)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return assiduites_justifiees
|
||||||
|
|
||||||
|
def dejustifier_assiduites(self) -> list[int]:
|
||||||
|
"""
|
||||||
|
Déjustifie les assiduités sur la période du justificatif
|
||||||
|
"""
|
||||||
|
assiduites_dejustifiees: list[int] = []
|
||||||
|
|
||||||
|
# On récupère les assiduités de l'étudiant sur la période donnée
|
||||||
|
assiduites: Query = self.etudiant.assiduites.filter(
|
||||||
|
Assiduite.date_debut >= self.date_debut,
|
||||||
|
Assiduite.date_fin <= self.date_fin,
|
||||||
|
Assiduite.etat != EtatAssiduite.PRESENT,
|
||||||
|
)
|
||||||
|
assi: Assiduite
|
||||||
|
for assi in assiduites:
|
||||||
|
# On récupère les justificatifs qui justifient l'assiduité `assi`
|
||||||
|
assi_justifs: list[int] = get_justifs_from_date(
|
||||||
|
self.etudiant.etudid,
|
||||||
|
assi.date_debut,
|
||||||
|
assi.date_fin,
|
||||||
|
long=False,
|
||||||
|
valid=True,
|
||||||
|
)
|
||||||
|
# Si il n'y a pas d'autre justificatif valide, on déjustifie l'assiduité
|
||||||
|
if len(assi_justifs) == 0 or (
|
||||||
|
len(assi_justifs) == 1 and assi_justifs[0] == self.justif_id
|
||||||
|
):
|
||||||
|
assi.est_just = False
|
||||||
|
assiduites_dejustifiees.append(assi.assiduite_id)
|
||||||
|
db.session.add(assi)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return assiduites_dejustifiees
|
||||||
|
|
||||||
|
def get_assiduites(self) -> Query:
|
||||||
|
"""
|
||||||
|
get_assiduites Récupère les assiduités qui sont concernées par le justificatif
|
||||||
|
(Concernée ≠ Justifiée, mais qui sont sur la même période)
|
||||||
|
Ne prends pas en compte les Présences
|
||||||
|
Returns:
|
||||||
|
Query: Les assiduités concernées
|
||||||
|
"""
|
||||||
|
|
||||||
|
assiduites_query = Assiduite.query.filter(
|
||||||
|
Assiduite.etudid == self.etudid,
|
||||||
|
Assiduite.date_debut >= self.date_debut,
|
||||||
|
Assiduite.date_fin <= self.date_fin,
|
||||||
|
Assiduite.etat != EtatAssiduite.PRESENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
return assiduites_query
|
||||||
|
|
||||||
|
|
||||||
def is_period_conflicting(
|
def is_period_conflicting(
|
||||||
date_debut: datetime,
|
date_debut: datetime,
|
||||||
|
@ -623,72 +702,6 @@ def is_period_conflicting(
|
||||||
return count > 0
|
return count > 0
|
||||||
|
|
||||||
|
|
||||||
def compute_assiduites_justified(
|
|
||||||
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
|
|
||||||
) -> list[int]:
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
etudid (int): l'identifiant de l'étudiant
|
|
||||||
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
|
|
||||||
reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[int]: la liste des assiduités qui ont été justifiées.
|
|
||||||
"""
|
|
||||||
# TODO à optimiser (car très long avec 40000 assiduités)
|
|
||||||
# On devrait :
|
|
||||||
# - récupérer uniquement les assiduités qui sont sur la période des justificatifs donnés
|
|
||||||
# - Pour chaque assiduité trouvée, il faut récupérer les justificatifs qui la justifie
|
|
||||||
# - Si au moins un justificatif valide couvre la période de l'assiduité alors on la justifie
|
|
||||||
|
|
||||||
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
|
|
||||||
if justificatifs is None:
|
|
||||||
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
|
|
||||||
etudid=etudid
|
|
||||||
).all()
|
|
||||||
|
|
||||||
# On ne prend que les justificatifs valides
|
|
||||||
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
|
|
||||||
|
|
||||||
# On récupère les assiduités de l'étudiant
|
|
||||||
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
|
||||||
|
|
||||||
assiduites_justifiees: list[int] = []
|
|
||||||
|
|
||||||
for assi in assiduites:
|
|
||||||
# On ne justifie pas les Présences
|
|
||||||
if assi.etat == EtatAssiduite.PRESENT:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# On récupère les justificatifs qui justifient l'assiduité `assi`
|
|
||||||
assi_justificatifs = Justificatif.query.filter(
|
|
||||||
Justificatif.etudid == assi.etudid,
|
|
||||||
Justificatif.date_debut <= assi.date_debut,
|
|
||||||
Justificatif.date_fin >= assi.date_fin,
|
|
||||||
Justificatif.etat == EtatJustificatif.VALIDE,
|
|
||||||
).all()
|
|
||||||
|
|
||||||
# Si au moins un justificatif possède une période qui couvre l'assiduité
|
|
||||||
if any(
|
|
||||||
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
|
|
||||||
for j in justificatifs + assi_justificatifs
|
|
||||||
):
|
|
||||||
# On justifie l'assiduité
|
|
||||||
# On ajoute l'id de l'assiduité à la liste des assiduités justifiées
|
|
||||||
assi.est_just = True
|
|
||||||
assiduites_justifiees.append(assi.assiduite_id)
|
|
||||||
db.session.add(assi)
|
|
||||||
elif reset:
|
|
||||||
# Si le paramètre reset est Vrai alors les assiduités non justifiées
|
|
||||||
# sont remise en "non justifiée"
|
|
||||||
assi.est_just = False
|
|
||||||
db.session.add(assi)
|
|
||||||
# On valide la session
|
|
||||||
db.session.commit()
|
|
||||||
# On renvoie la liste des assiduite_id des assiduités justifiées
|
|
||||||
return assiduites_justifiees
|
|
||||||
|
|
||||||
|
|
||||||
def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
|
def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
|
||||||
"""
|
"""
|
||||||
get_assiduites_justif Récupération des justificatifs d'une assiduité
|
get_assiduites_justif Récupération des justificatifs d'une assiduité
|
||||||
|
|
|
@ -8,16 +8,19 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import functools
|
import functools
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
import yaml
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
from sqlalchemy.orm import class_mapper
|
from sqlalchemy.orm import class_mapper
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from app import db
|
from app import db, log
|
||||||
|
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||||
|
|
||||||
|
REFCOMP_EQUIVALENCE_FILENAME = "ressources/referentiels/equivalences.yaml"
|
||||||
|
|
||||||
|
|
||||||
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
||||||
|
@ -104,6 +107,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||||
|
|
||||||
|
def get_title(self) -> str:
|
||||||
|
"Titre affichable"
|
||||||
|
# utilise type_titre (B.U.T.), spécialité, version
|
||||||
|
return f"{self.type_titre} {self.specialite} {self.get_version()}"
|
||||||
|
|
||||||
def get_version(self) -> str:
|
def get_version(self) -> str:
|
||||||
"La version, normalement sous forme de date iso yyy-mm-dd"
|
"La version, normalement sous forme de date iso yyy-mm-dd"
|
||||||
if not self.version_orebut:
|
if not self.version_orebut:
|
||||||
|
@ -124,9 +132,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||||
"type_departement": self.type_departement,
|
"type_departement": self.type_departement,
|
||||||
"type_titre": self.type_titre,
|
"type_titre": self.type_titre,
|
||||||
"version_orebut": self.version_orebut,
|
"version_orebut": self.version_orebut,
|
||||||
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
|
"scodoc_date_loaded": (
|
||||||
|
self.scodoc_date_loaded.isoformat() + "Z"
|
||||||
if self.scodoc_date_loaded
|
if self.scodoc_date_loaded
|
||||||
else "",
|
else ""
|
||||||
|
),
|
||||||
"scodoc_orig_filename": self.scodoc_orig_filename,
|
"scodoc_orig_filename": self.scodoc_orig_filename,
|
||||||
"competences": {
|
"competences": {
|
||||||
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
||||||
|
@ -234,6 +244,100 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||||
|
|
||||||
return parcours_info
|
return parcours_info
|
||||||
|
|
||||||
|
def equivalents(self) -> set["ApcReferentielCompetences"]:
|
||||||
|
"""Ensemble des référentiels du même département
|
||||||
|
qui peuvent être considérés comme "équivalents", au sens
|
||||||
|
une formation de ce référentiel pourrait changer vers un équivalent,
|
||||||
|
en ignorant les apprentissages critiques.
|
||||||
|
Pour cela, il faut avoir le même type, etc et les mêmes compétences,
|
||||||
|
niveaux et parcours (voir map_to_other_referentiel).
|
||||||
|
"""
|
||||||
|
candidats = ApcReferentielCompetences.query.filter_by(
|
||||||
|
dept_id=self.dept_id
|
||||||
|
).filter(ApcReferentielCompetences.id != self.id)
|
||||||
|
return {
|
||||||
|
referentiel
|
||||||
|
for referentiel in candidats
|
||||||
|
if not isinstance(self.map_to_other_referentiel(referentiel), str)
|
||||||
|
}
|
||||||
|
|
||||||
|
def map_to_other_referentiel(
|
||||||
|
self, other: "ApcReferentielCompetences"
|
||||||
|
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
|
||||||
|
"""Build mapping between this referentiel and ref2.
|
||||||
|
If successful, returns 3 dicts mapping self ids to other ids.
|
||||||
|
Else return a string, error message.
|
||||||
|
"""
|
||||||
|
if self.type_structure != other.type_structure:
|
||||||
|
return "type_structure mismatch"
|
||||||
|
if self.type_departement != other.type_departement:
|
||||||
|
return "type_departement mismatch"
|
||||||
|
# Table d'équivalences entre refs:
|
||||||
|
equiv = self._load_config_equivalences()
|
||||||
|
# Même specialité (ou alias) ?
|
||||||
|
if self.specialite != other.specialite and other.specialite not in equiv.get(
|
||||||
|
"alias", []
|
||||||
|
):
|
||||||
|
return "specialite mismatch"
|
||||||
|
# mêmes parcours ?
|
||||||
|
eq_parcours = equiv.get("parcours", {})
|
||||||
|
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
|
||||||
|
parcours_by_code_2 = {
|
||||||
|
eq_parcours.get(p.code, p.code): p for p in other.parcours
|
||||||
|
}
|
||||||
|
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
|
||||||
|
return "parcours mismatch"
|
||||||
|
parcours_map = {
|
||||||
|
parcours_by_code_1[eq_parcours.get(code, code)]
|
||||||
|
.id: parcours_by_code_2[eq_parcours.get(code, code)]
|
||||||
|
.id
|
||||||
|
for code in parcours_by_code_1
|
||||||
|
}
|
||||||
|
# mêmes compétences ?
|
||||||
|
competence_by_code_1 = {c.titre: c for c in self.competences}
|
||||||
|
competence_by_code_2 = {c.titre: c for c in other.competences}
|
||||||
|
if competence_by_code_1.keys() != competence_by_code_2.keys():
|
||||||
|
return "competences mismatch"
|
||||||
|
competences_map = {
|
||||||
|
competence_by_code_1[titre].id: competence_by_code_2[titre].id
|
||||||
|
for titre in competence_by_code_1
|
||||||
|
}
|
||||||
|
# mêmes niveaux (dans chaque compétence) ?
|
||||||
|
niveaux_map = {}
|
||||||
|
for titre in competence_by_code_1:
|
||||||
|
c1 = competence_by_code_1[titre]
|
||||||
|
c2 = competence_by_code_2[titre]
|
||||||
|
niveau_by_attr_1 = {(n.annee, n.ordre, n.libelle): n for n in c1.niveaux}
|
||||||
|
niveau_by_attr_2 = {(n.annee, n.ordre, n.libelle): n for n in c2.niveaux}
|
||||||
|
if niveau_by_attr_1.keys() != niveau_by_attr_2.keys():
|
||||||
|
return f"niveaux mismatch in comp. '{titre}'"
|
||||||
|
niveaux_map.update(
|
||||||
|
{
|
||||||
|
niveau_by_attr_1[a].id: niveau_by_attr_2[a].id
|
||||||
|
for a in niveau_by_attr_1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return parcours_map, competences_map, niveaux_map
|
||||||
|
|
||||||
|
def _load_config_equivalences(self) -> dict:
|
||||||
|
"""Load config file ressources/referentiels/equivalences.yaml
|
||||||
|
used to define equivalences between distinct referentiels
|
||||||
|
return a dict, with optional keys:
|
||||||
|
alias: list of equivalent names for speciality (eg SD == STID)
|
||||||
|
parcours: dict with equivalent parcours acronyms
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
|
||||||
|
doc = yaml.safe_load(f.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
log(f"_load_config_equivalences: {REFCOMP_EQUIVALENCE_FILENAME} not found")
|
||||||
|
return {}
|
||||||
|
except yaml.parser.ParserError as exc:
|
||||||
|
raise ScoValueError(
|
||||||
|
f"erreur dans le fichier {REFCOMP_EQUIVALENCE_FILENAME}"
|
||||||
|
) from exc
|
||||||
|
return doc.get(self.specialite, {})
|
||||||
|
|
||||||
|
|
||||||
class ApcCompetence(db.Model, XMLModel):
|
class ApcCompetence(db.Model, XMLModel):
|
||||||
"Compétence"
|
"Compétence"
|
||||||
|
@ -374,9 +478,11 @@ class ApcNiveau(db.Model, XMLModel):
|
||||||
"libelle": self.libelle,
|
"libelle": self.libelle,
|
||||||
"annee": self.annee,
|
"annee": self.annee,
|
||||||
"ordre": self.ordre,
|
"ordre": self.ordre,
|
||||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
|
"app_critiques": (
|
||||||
|
{x.code: x.to_dict() for x in self.app_critiques}
|
||||||
if with_app_critiques
|
if with_app_critiques
|
||||||
else {},
|
else {}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_dict_bul(self):
|
def to_dict_bul(self):
|
||||||
|
@ -464,9 +570,9 @@ class ApcNiveau(db.Model, XMLModel):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if competence is None:
|
if competence is None:
|
||||||
parcour_niveaux: list[
|
parcour_niveaux: list[ApcParcoursNiveauCompetence] = (
|
||||||
ApcParcoursNiveauCompetence
|
annee_parcour.niveaux_competences
|
||||||
] = annee_parcour.niveaux_competences
|
)
|
||||||
niveaux: list[ApcNiveau] = [
|
niveaux: list[ApcNiveau] = [
|
||||||
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
|
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
|
||||||
for pn in parcour_niveaux
|
for pn in parcour_niveaux
|
||||||
|
|
|
@ -10,6 +10,7 @@ from app.models.etudiants import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class ApcValidationRCUE(db.Model):
|
class ApcValidationRCUE(db.Model):
|
||||||
|
@ -63,14 +64,13 @@ class ApcValidationRCUE(db.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||||
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
self.code} enregistrée le {self.date.strftime(scu.DATE_FMT)}"""
|
||||||
|
|
||||||
def html(self) -> str:
|
def html(self) -> str:
|
||||||
"description en HTML"
|
"description en HTML"
|
||||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||||
<b>{self.code}</b>
|
<b>{self.code}</b>
|
||||||
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
|
<em>enregistrée le {self.date.strftime(scu.DATEATIME_FMT)}</em>"""
|
||||||
à {self.date.strftime("%Hh%M")}</em>"""
|
|
||||||
|
|
||||||
def annee(self) -> str:
|
def annee(self) -> str:
|
||||||
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
||||||
|
@ -164,7 +164,7 @@ class ApcValidationAnnee(db.Model):
|
||||||
def html(self) -> str:
|
def html(self) -> str:
|
||||||
"Affichage html"
|
"Affichage html"
|
||||||
date_str = (
|
date_str = (
|
||||||
f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
|
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
|
||||||
if self.date
|
if self.date
|
||||||
else "(sans date)"
|
else "(sans date)"
|
||||||
)
|
)
|
||||||
|
|
|
@ -92,6 +92,7 @@ class ScoDocSiteConfig(db.Model):
|
||||||
"INSTITUTION_CITY": str,
|
"INSTITUTION_CITY": str,
|
||||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||||
"enable_entreprises": bool,
|
"enable_entreprises": bool,
|
||||||
|
"disable_passerelle": bool, # remplace pref. bul_display_publication
|
||||||
"month_debut_annee_scolaire": int,
|
"month_debut_annee_scolaire": int,
|
||||||
"month_debut_periode2": int,
|
"month_debut_periode2": int,
|
||||||
"disable_bul_pdf": bool,
|
"disable_bul_pdf": bool,
|
||||||
|
@ -244,6 +245,12 @@ class ScoDocSiteConfig(db.Model):
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||||
return cfg is not None and cfg.value
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_passerelle_disabled(cls):
|
||||||
|
"""True si on doit cacher les fonctions passerelle ("oeil")."""
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name="disable_passerelle").first()
|
||||||
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_user_require_email_institutionnel_enabled(cls) -> bool:
|
def is_user_require_email_institutionnel_enabled(cls) -> bool:
|
||||||
"""True si impose saisie email_institutionnel"""
|
"""True si impose saisie email_institutionnel"""
|
||||||
|
@ -263,6 +270,11 @@ class ScoDocSiteConfig(db.Model):
|
||||||
"""Active (ou déactive) le module entreprises. True si changement."""
|
"""Active (ou déactive) le module entreprises. True si changement."""
|
||||||
return cls.set("enable_entreprises", "on" if enabled else "")
|
return cls.set("enable_entreprises", "on" if enabled else "")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def disable_passerelle(cls, disabled: bool = True) -> bool:
|
||||||
|
"""Désactive (ou active) les fonctions liées à la présence d'une passerelle. True si changement."""
|
||||||
|
return cls.set("disable_passerelle", "on" if disabled else "")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def disable_bul_pdf(cls, enabled=True) -> bool:
|
def disable_bul_pdf(cls, enabled=True) -> bool:
|
||||||
"""Interdit (ou autorise) les exports PDF. True si changement."""
|
"""Interdit (ou autorise) les exports PDF. True si changement."""
|
||||||
|
|
|
@ -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
|
||||||
|
@ -297,11 +302,12 @@ class Identite(models.ScoDocModel):
|
||||||
else:
|
else:
|
||||||
return self.nom
|
return self.nom
|
||||||
|
|
||||||
@cached_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" """
|
||||||
|
@ -334,16 +346,14 @@ class Identite(models.ScoDocModel):
|
||||||
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
|
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def sort_key(self) -> tuple:
|
def sort_key(self) -> str:
|
||||||
"clé pour tris par ordre alphabétique"
|
"clé pour tris par ordre alphabétique"
|
||||||
# Note: scodoc7 utilisait sco_etud.etud_sort_key, à mettre à jour
|
# Note: scodoc7 utilisait sco_etud.etud_sort_key, à mettre à jour
|
||||||
# si on modifie cette méthode.
|
# si on modifie cette méthode.
|
||||||
return (
|
return scu.sanitize_string(
|
||||||
scu.sanitize_string(
|
(self.nom_usuel or self.nom or "") + ";" + (self.prenom or ""),
|
||||||
self.nom_usuel or self.nom or "", remove_spaces=False
|
remove_spaces=False,
|
||||||
).lower(),
|
).lower()
|
||||||
scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_first_email(self, field="email") -> str:
|
def get_first_email(self, field="email") -> str:
|
||||||
"Le mail associé à la première adresse de l'étudiant, ou None"
|
"Le mail associé à la première adresse de l'étudiant, ou None"
|
||||||
|
@ -483,7 +493,9 @@ class Identite(models.ScoDocModel):
|
||||||
"code_ine": self.code_ine or "",
|
"code_ine": self.code_ine or "",
|
||||||
"code_nip": self.code_nip or "",
|
"code_nip": self.code_nip or "",
|
||||||
"date_naissance": (
|
"date_naissance": (
|
||||||
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""
|
self.date_naissance.strftime(scu.DATE_FMT)
|
||||||
|
if self.date_naissance
|
||||||
|
else ""
|
||||||
),
|
),
|
||||||
"dept_acronym": self.departement.acronym,
|
"dept_acronym": self.departement.acronym,
|
||||||
"dept_id": self.dept_id,
|
"dept_id": self.dept_id,
|
||||||
|
@ -542,8 +554,6 @@ class Identite(models.ScoDocModel):
|
||||||
|
|
||||||
def inscriptions(self) -> list["FormSemestreInscription"]:
|
def inscriptions(self) -> list["FormSemestreInscription"]:
|
||||||
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
|
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
|
||||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -553,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).
|
||||||
"""
|
"""
|
||||||
|
@ -569,8 +579,6 @@ class Identite(models.ScoDocModel):
|
||||||
(il est rare qu'il y en ai plus d'une, mais c'est possible).
|
(il est rare qu'il y en ai plus d'une, mais c'est possible).
|
||||||
Triées par date de début de semestre décroissante (le plus récent en premier).
|
Triées par date de début de semestre décroissante (le plus récent en premier).
|
||||||
"""
|
"""
|
||||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -739,7 +747,7 @@ class Identite(models.ScoDocModel):
|
||||||
"""
|
"""
|
||||||
if with_paragraph:
|
if with_paragraph:
|
||||||
return f"""{self.etat_civil}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {
|
return f"""{self.etat_civil}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {
|
||||||
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{
|
self.date_naissance.strftime(scu.DATE_FMT) if self.date_naissance else ""}{
|
||||||
line_sep}à {self.lieu_naissance or ""}"""
|
line_sep}à {self.lieu_naissance or ""}"""
|
||||||
return self.etat_civil
|
return self.etat_civil
|
||||||
|
|
||||||
|
@ -1099,6 +1107,5 @@ class EtudAnnotation(db.Model):
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
from app.models.modules import Module
|
|
||||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||||
|
|
|
@ -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 ''} "{
|
||||||
|
@ -207,7 +216,9 @@ class Evaluation(models.ScoDocModel):
|
||||||
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||||
|
|
||||||
# Deprecated
|
# Deprecated
|
||||||
e_dict["jour"] = self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
|
e_dict["jour"] = (
|
||||||
|
self.date_debut.strftime(scu.DATE_FMT) if self.date_debut else ""
|
||||||
|
)
|
||||||
|
|
||||||
return evaluation_enrich_dict(self, e_dict)
|
return evaluation_enrich_dict(self, e_dict)
|
||||||
|
|
||||||
|
@ -315,10 +326,10 @@ class Evaluation(models.ScoDocModel):
|
||||||
def descr_heure(self) -> str:
|
def descr_heure(self) -> str:
|
||||||
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
|
"Description de la plage horaire pour affichages ('de 13h00 à 14h00')"
|
||||||
if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
|
if self.date_debut and (not self.date_fin or self.date_fin == self.date_debut):
|
||||||
return f"""à {self.date_debut.strftime("%Hh%M")}"""
|
return f"""à {self.date_debut.strftime(scu.TIME_FMT)}"""
|
||||||
elif self.date_debut and self.date_fin:
|
elif self.date_debut and self.date_fin:
|
||||||
return f"""de {self.date_debut.strftime("%Hh%M")
|
return f"""de {self.date_debut.strftime(scu.TIME_FMT)
|
||||||
} à {self.date_fin.strftime("%Hh%M")}"""
|
} à {self.date_fin.strftime(scu.TIME_FMT)}"""
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@ -345,7 +356,7 @@ class Evaluation(models.ScoDocModel):
|
||||||
|
|
||||||
def _h(dt: datetime.datetime) -> str:
|
def _h(dt: datetime.datetime) -> str:
|
||||||
if dt.minute:
|
if dt.minute:
|
||||||
return dt.strftime("%Hh%M")
|
return dt.strftime(scu.TIME_FMT)
|
||||||
return f"{dt.hour}h"
|
return f"{dt.hour}h"
|
||||||
|
|
||||||
if self.date_fin is None:
|
if self.date_fin is None:
|
||||||
|
@ -415,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
|
||||||
|
|
||||||
|
@ -430,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:
|
||||||
|
@ -539,8 +554,8 @@ class EvaluationUEPoids(db.Model):
|
||||||
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
|
||||||
"""add or convert some fields in an evaluation dict"""
|
"""add or convert some fields in an evaluation dict"""
|
||||||
# For ScoDoc7 compat
|
# For ScoDoc7 compat
|
||||||
e_dict["heure_debut"] = e.date_debut.strftime("%Hh%M") if e.date_debut else ""
|
e_dict["heure_debut"] = e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
|
||||||
e_dict["heure_fin"] = e.date_fin.strftime("%Hh%M") if e.date_fin else ""
|
e_dict["heure_fin"] = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
|
||||||
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
|
e_dict["jour_iso"] = e.date_debut.isoformat() if e.date_debut else ""
|
||||||
# Calcule durée en minutes
|
# Calcule durée en minutes
|
||||||
e_dict["descrheure"] = e.descr_heure()
|
e_dict["descrheure"] = e.descr_heure()
|
||||||
|
@ -614,7 +629,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
||||||
):
|
):
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""La date de début de l'évaluation ({
|
f"""La date de début de l'évaluation ({
|
||||||
data["date_debut"].strftime("%d/%m/%Y")
|
data["date_debut"].strftime(scu.DATE_FMT)
|
||||||
}) n'est pas dans le semestre !""",
|
}) n'est pas dans le semestre !""",
|
||||||
dest_url="javascript:history.back();",
|
dest_url="javascript:history.back();",
|
||||||
)
|
)
|
||||||
|
@ -629,7 +644,7 @@ def check_and_convert_evaluation_args(data: dict, moduleimpl: "ModuleImpl"):
|
||||||
):
|
):
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""La date de fin de l'évaluation ({
|
f"""La date de fin de l'évaluation ({
|
||||||
data["date_fin"].strftime("%d/%m/%Y")
|
data["date_fin"].strftime(scu.DATE_FMT)
|
||||||
}) n'est pas dans le semestre !""",
|
}) n'est pas dans le semestre !""",
|
||||||
dest_url="javascript:history.back();",
|
dest_url="javascript:history.back();",
|
||||||
)
|
)
|
||||||
|
|
|
@ -232,7 +232,9 @@ class ScolarNews(db.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Transforme les URL en URL absolues
|
# Transforme les URL en URL absolues
|
||||||
base = scu.ScoURL()
|
base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
|
||||||
|
: -len("/index_html")
|
||||||
|
]
|
||||||
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
||||||
|
|
||||||
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
||||||
|
@ -249,11 +251,12 @@ class ScolarNews(db.Model):
|
||||||
news_list = cls.last_news(n=n)
|
news_list = cls.last_news(n=n)
|
||||||
if not news_list:
|
if not news_list:
|
||||||
return ""
|
return ""
|
||||||
|
dept_news_url = url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
||||||
H = [
|
H = [
|
||||||
f"""<div class="news"><span class="newstitle"><a href="{
|
f"""<div class="scobox news"><div class="scobox-title"><a href="{
|
||||||
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
dept_news_url
|
||||||
}">Dernières opérations</a>
|
}">Dernières opérations</a>
|
||||||
</span><ul class="newslist">"""
|
</div><ul class="newslist">"""
|
||||||
]
|
]
|
||||||
|
|
||||||
for news in news_list:
|
for news in news_list:
|
||||||
|
@ -261,16 +264,22 @@ class ScolarNews(db.Model):
|
||||||
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
|
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
|
||||||
class="newstext">{news}</span></li>"""
|
class="newstext">{news}</span></li>"""
|
||||||
)
|
)
|
||||||
|
H.append(
|
||||||
|
f"""<li class="newslist">
|
||||||
|
<span class="newstext"><a href="{dept_news_url}" class="stdlink">...</a>
|
||||||
|
</span>
|
||||||
|
</li>"""
|
||||||
|
)
|
||||||
|
|
||||||
H.append("</ul>")
|
H.append("</ul></div>")
|
||||||
|
|
||||||
# Informations générales
|
# Informations générales
|
||||||
H.append(
|
H.append(
|
||||||
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
f"""<div>
|
||||||
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
|
Pour en savoir plus sur ScoDoc voir
|
||||||
|
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">scodoc.org</a>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
H.append("</div>")
|
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""ScoDoc 9 models : Formations
|
"""ScoDoc 9 models : Formations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from flask import abort, g
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
import app
|
import app
|
||||||
|
@ -64,6 +66,21 @@ class Formation(db.Model):
|
||||||
"titre complet pour affichage"
|
"titre complet pour affichage"
|
||||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
|
||||||
|
"""Formation ou 404, cherche uniquement dans le département spécifié
|
||||||
|
ou le courant (g.scodoc_dept)"""
|
||||||
|
if not isinstance(formation_id, int):
|
||||||
|
try:
|
||||||
|
formation_id = int(formation_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
abort(404, "formation_id invalide")
|
||||||
|
if g.scodoc_dept:
|
||||||
|
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||||
|
if dept_id is not None:
|
||||||
|
return cls.query.filter_by(id=formation_id, dept_id=dept_id).first_or_404()
|
||||||
|
return cls.query.filter_by(id=formation_id).first_or_404()
|
||||||
|
|
||||||
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
|
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
|
||||||
"""As a dict.
|
"""As a dict.
|
||||||
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
||||||
|
|
|
@ -25,6 +25,7 @@ from sqlalchemy import func
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
|
from app import models
|
||||||
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
|
@ -54,7 +55,7 @@ from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
|
||||||
|
|
||||||
|
|
||||||
class FormSemestre(db.Model):
|
class FormSemestre(models.ScoDocModel):
|
||||||
"""Mise en oeuvre d'un semestre de formation"""
|
"""Mise en oeuvre d'un semestre de formation"""
|
||||||
|
|
||||||
__tablename__ = "notes_formsemestre"
|
__tablename__ = "notes_formsemestre"
|
||||||
|
@ -84,7 +85,7 @@ class FormSemestre(db.Model):
|
||||||
bul_hide_xml = db.Column(
|
bul_hide_xml = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
"ne publie pas le bulletin XML ou JSON"
|
"ne publie pas le bulletin sur l'API"
|
||||||
block_moyennes = db.Column(
|
block_moyennes = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
|
@ -191,7 +192,8 @@ class FormSemestre(db.Model):
|
||||||
def get_formsemestre(
|
def get_formsemestre(
|
||||||
cls, formsemestre_id: int | str, dept_id: int = None
|
cls, formsemestre_id: int | str, dept_id: int = None
|
||||||
) -> "FormSemestre":
|
) -> "FormSemestre":
|
||||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
"""FormSemestre ou 404, cherche uniquement dans le département spécifié
|
||||||
|
ou le courant (g.scodoc_dept)"""
|
||||||
if not isinstance(formsemestre_id, int):
|
if not isinstance(formsemestre_id, int):
|
||||||
try:
|
try:
|
||||||
formsemestre_id = int(formsemestre_id)
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
@ -206,7 +208,7 @@ class FormSemestre(db.Model):
|
||||||
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
||||||
|
|
||||||
def sort_key(self) -> tuple:
|
def sort_key(self) -> tuple:
|
||||||
"""clé pour tris par ordre alphabétique
|
"""clé pour tris par ordre de date_debut, le plus ancien en tête
|
||||||
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
||||||
return (self.date_debut, self.semestre_id)
|
return (self.date_debut, self.semestre_id)
|
||||||
|
|
||||||
|
@ -222,12 +224,12 @@ class FormSemestre(db.Model):
|
||||||
d["formsemestre_id"] = self.id
|
d["formsemestre_id"] = self.id
|
||||||
d["titre_num"] = self.titre_num()
|
d["titre_num"] = self.titre_num()
|
||||||
if self.date_debut:
|
if self.date_debut:
|
||||||
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
|
d["date_debut"] = self.date_debut.strftime(scu.DATE_FMT)
|
||||||
d["date_debut_iso"] = self.date_debut.isoformat()
|
d["date_debut_iso"] = self.date_debut.isoformat()
|
||||||
else:
|
else:
|
||||||
d["date_debut"] = d["date_debut_iso"] = ""
|
d["date_debut"] = d["date_debut_iso"] = ""
|
||||||
if self.date_fin:
|
if self.date_fin:
|
||||||
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
|
d["date_fin"] = self.date_fin.strftime(scu.DATE_FMT)
|
||||||
d["date_fin_iso"] = self.date_fin.isoformat()
|
d["date_fin_iso"] = self.date_fin.isoformat()
|
||||||
else:
|
else:
|
||||||
d["date_fin"] = d["date_fin_iso"] = ""
|
d["date_fin"] = d["date_fin_iso"] = ""
|
||||||
|
@ -245,19 +247,20 @@ class FormSemestre(db.Model):
|
||||||
|
|
||||||
def to_dict_api(self):
|
def to_dict_api(self):
|
||||||
"""
|
"""
|
||||||
Un dict avec les informations sur le semestre destiné à l'api
|
Un dict avec les informations sur le semestre destinées à l'api
|
||||||
"""
|
"""
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
d.pop("groups_auto_assignment_data", None)
|
d.pop("groups_auto_assignment_data", None)
|
||||||
d["annee_scolaire"] = self.annee_scolaire()
|
d["annee_scolaire"] = self.annee_scolaire()
|
||||||
|
d["bul_hide_xml"] = self.bul_hide_xml
|
||||||
if self.date_debut:
|
if self.date_debut:
|
||||||
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
|
d["date_debut"] = self.date_debut.strftime(scu.DATE_FMT)
|
||||||
d["date_debut_iso"] = self.date_debut.isoformat()
|
d["date_debut_iso"] = self.date_debut.isoformat()
|
||||||
else:
|
else:
|
||||||
d["date_debut"] = d["date_debut_iso"] = ""
|
d["date_debut"] = d["date_debut_iso"] = ""
|
||||||
if self.date_fin:
|
if self.date_fin:
|
||||||
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
|
d["date_fin"] = self.date_fin.strftime(scu.DATE_FMT)
|
||||||
d["date_fin_iso"] = self.date_fin.isoformat()
|
d["date_fin_iso"] = self.date_fin.isoformat()
|
||||||
else:
|
else:
|
||||||
d["date_fin"] = d["date_fin_iso"] = ""
|
d["date_fin"] = d["date_fin_iso"] = ""
|
||||||
|
@ -873,9 +876,9 @@ class FormSemestre(db.Model):
|
||||||
descr_sem += " " + self.modalite
|
descr_sem += " " + self.modalite
|
||||||
return descr_sem
|
return descr_sem
|
||||||
|
|
||||||
def get_abs_count(self, etudid):
|
def get_abs_count(self, etudid) -> tuple[int, int, int]:
|
||||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||||
tuple (nb abs, nb abs justifiées)
|
tuple (nb abs non just, nb abs justifiées, nb abs total)
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_assiduites
|
from app.scodoc import sco_assiduites
|
||||||
|
@ -933,12 +936,16 @@ class FormSemestre(db.Model):
|
||||||
partitions += [p for p in self.partitions if p.partition_name is None]
|
partitions += [p for p in self.partitions if p.partition_name is None]
|
||||||
return partitions
|
return partitions
|
||||||
|
|
||||||
@cached_property
|
def etudids_actifs(self) -> tuple[list[int], set[int]]:
|
||||||
def etudids_actifs(self) -> set:
|
"""Liste les etudids inscrits (incluant DEM et DEF),
|
||||||
"Set des etudids inscrits non démissionnaires et non défaillants"
|
qui ser al'index des dataframes de notes
|
||||||
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
|
et donne l'ensemble des inscrits non DEM ni DEF.
|
||||||
|
"""
|
||||||
|
return [inscr.etudid for inscr in self.inscriptions], {
|
||||||
|
ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT
|
||||||
|
}
|
||||||
|
|
||||||
@cached_property
|
@property
|
||||||
def etuds_inscriptions(self) -> dict:
|
def etuds_inscriptions(self) -> dict:
|
||||||
"""Map { etudid : inscription } (incluant DEM et DEF)"""
|
"""Map { etudid : inscription } (incluant DEM et DEF)"""
|
||||||
return {ins.etud.id: ins for ins in self.inscriptions}
|
return {ins.etud.id: ins for ins in self.inscriptions}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -409,6 +409,14 @@ class UniteEns(models.ScoDocModel):
|
||||||
Renvoie (True, "") si ok, sinon (False, error_message)
|
Renvoie (True, "") si ok, sinon (False, error_message)
|
||||||
"""
|
"""
|
||||||
msg = ""
|
msg = ""
|
||||||
|
# Safety check
|
||||||
|
if self.formation.referentiel_competence is None:
|
||||||
|
return False, "pas de référentiel de compétence"
|
||||||
|
# Si tous les parcours, aucun (tronc commun)
|
||||||
|
if {p.id for p in parcours} == {
|
||||||
|
p.id for p in self.formation.referentiel_competence.parcours
|
||||||
|
}:
|
||||||
|
parcours = []
|
||||||
# Le niveau est-il dans tous ces parcours ? Sinon, l'enlève
|
# Le niveau est-il dans tous ces parcours ? Sinon, l'enlève
|
||||||
prev_niveau = self.niveau_competence
|
prev_niveau = self.niveau_competence
|
||||||
if (
|
if (
|
||||||
|
@ -424,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
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ class ScolarFormSemestreValidation(db.Model):
|
||||||
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
|
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
|
||||||
} ({self.ue_id}): {self.code}"""
|
} ({self.ue_id}): {self.code}"""
|
||||||
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
|
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
|
||||||
self.event_date.strftime("%d/%m/%Y")}"""
|
self.event_date.strftime(scu.DATE_FMT)}"""
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"Efface cette validation"
|
"Efface cette validation"
|
||||||
|
@ -113,14 +113,14 @@ class ScolarFormSemestreValidation(db.Model):
|
||||||
if self.ue.parcours else ""}
|
if self.ue.parcours else ""}
|
||||||
{("émise par " + link)}
|
{("émise par " + link)}
|
||||||
: <b>{self.code}</b>{moyenne}
|
: <b>{self.code}</b>{moyenne}
|
||||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
le {self.event_date.strftime(scu.DATEATIME_FMT)}
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
return f"""Validation du semestre S{
|
return f"""Validation du semestre S{
|
||||||
self.formsemestre.semestre_id if self.formsemestre else "?"}
|
self.formsemestre.semestre_id if self.formsemestre else "?"}
|
||||||
{self.formsemestre.html_link_status() if self.formsemestre else ""}
|
{self.formsemestre.html_link_status() if self.formsemestre else ""}
|
||||||
: <b>{self.code}</b>
|
: <b>{self.code}</b>
|
||||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
le {self.event_date.strftime(scu.DATEATIME_FMT)}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def ects(self) -> float:
|
def ects(self) -> float:
|
||||||
|
@ -175,7 +175,7 @@ class ScolarAutorisationInscription(db.Model):
|
||||||
)
|
)
|
||||||
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
|
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
|
||||||
{link}
|
{link}
|
||||||
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
|
le {self.date.strftime(scu.DATEATIME_FMT)}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -48,6 +48,7 @@ from typing import Any
|
||||||
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
|
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
|
||||||
|
|
||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
|
import reportlab
|
||||||
from reportlab.platypus import Paragraph, Spacer
|
from reportlab.platypus import Paragraph, Spacer
|
||||||
from reportlab.platypus import Table, KeepInFrame
|
from reportlab.platypus import Table, KeepInFrame
|
||||||
from reportlab.lib.colors import Color
|
from reportlab.lib.colors import Color
|
||||||
|
@ -175,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
|
||||||
|
@ -263,16 +265,16 @@ class GenTable:
|
||||||
colspan_count -= 1
|
colspan_count -= 1
|
||||||
# if colspan_count > 0:
|
# if colspan_count > 0:
|
||||||
# continue # skip cells after a span
|
# continue # skip cells after a span
|
||||||
if pdf_mode:
|
if pdf_mode and f"_{cid}_pdf" in row:
|
||||||
content = row.get(f"_{cid}_pdf", False) or row.get(cid, "")
|
content = row[f"_{cid}_pdf"]
|
||||||
elif xls_mode:
|
elif xls_mode and f"_{cid}_xls" in row:
|
||||||
content = row.get(f"_{cid}_xls", False) or row.get(cid, "")
|
content = row[f"_{cid}_xls"]
|
||||||
else:
|
else:
|
||||||
content = row.get(cid, "")
|
content = row.get(cid, "")
|
||||||
# Convert None to empty string ""
|
# Convert None to empty string ""
|
||||||
content = "" if content is None else content
|
content = "" if content is None else content
|
||||||
|
|
||||||
colspan = row.get("_%s_colspan" % cid, 0)
|
colspan = row.get(f"_{cid}_colspan", 0)
|
||||||
if colspan > 1:
|
if colspan > 1:
|
||||||
pdf_style_list.append(
|
pdf_style_list.append(
|
||||||
(
|
(
|
||||||
|
@ -676,6 +678,7 @@ class GenTable:
|
||||||
fmt="html",
|
fmt="html",
|
||||||
page_title="",
|
page_title="",
|
||||||
filename=None,
|
filename=None,
|
||||||
|
cssstyles=[],
|
||||||
javascripts=[],
|
javascripts=[],
|
||||||
with_html_headers=True,
|
with_html_headers=True,
|
||||||
publish=True,
|
publish=True,
|
||||||
|
@ -696,6 +699,7 @@ class GenTable:
|
||||||
H.append(
|
H.append(
|
||||||
self.html_header
|
self.html_header
|
||||||
or html_sco_header.sco_header(
|
or html_sco_header.sco_header(
|
||||||
|
cssstyles=cssstyles,
|
||||||
page_title=page_title,
|
page_title=page_title,
|
||||||
javascripts=javascripts,
|
javascripts=javascripts,
|
||||||
init_qtip=init_qtip,
|
init_qtip=init_qtip,
|
||||||
|
@ -721,7 +725,7 @@ class GenTable:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return pdf_doc
|
return pdf_doc
|
||||||
elif fmt == "xls" or fmt == "xlsx": # dans les 2 cas retourne du xlsx
|
elif fmt in ("xls", "xlsx"): # dans les 2 cas retourne du xlsx
|
||||||
xls = self.excel()
|
xls = self.excel()
|
||||||
if publish:
|
if publish:
|
||||||
return scu.send_file(
|
return scu.send_file(
|
||||||
|
@ -730,7 +734,6 @@ class GenTable:
|
||||||
suffix=scu.XLSX_SUFFIX,
|
suffix=scu.XLSX_SUFFIX,
|
||||||
mime=scu.XLSX_MIMETYPE,
|
mime=scu.XLSX_MIMETYPE,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
return xls
|
return xls
|
||||||
elif fmt == "text":
|
elif fmt == "text":
|
||||||
return self.text()
|
return self.text()
|
||||||
|
@ -811,7 +814,10 @@ if __name__ == "__main__":
|
||||||
document,
|
document,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
document.build(objects)
|
document.build(objects)
|
||||||
|
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
||||||
|
raise ScoPDFFormatError(str(exc)) from exc
|
||||||
data = doc.getvalue()
|
data = doc.getvalue()
|
||||||
with open("/tmp/gen_table.pdf", "wb") as f:
|
with open("/tmp/gen_table.pdf", "wb") as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
|
@ -25,12 +25,11 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""HTML Header/Footer for ScoDoc pages
|
"""HTML Header/Footer for ScoDoc pages"""
|
||||||
"""
|
|
||||||
|
|
||||||
import html
|
import html
|
||||||
|
|
||||||
from flask import g, render_template
|
from flask import g, render_template, url_for
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -163,7 +162,7 @@ def sco_header(
|
||||||
params = {
|
params = {
|
||||||
"page_title": page_title or sco_version.SCONAME,
|
"page_title": page_title or sco_version.SCONAME,
|
||||||
"no_side_bar": no_side_bar,
|
"no_side_bar": no_side_bar,
|
||||||
"ScoURL": scu.ScoURL(),
|
"ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
|
||||||
"encoding": scu.SCO_ENCODING,
|
"encoding": scu.SCO_ENCODING,
|
||||||
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
|
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
|
||||||
"authuser": current_user.user_name,
|
"authuser": current_user.user_name,
|
||||||
|
@ -179,6 +178,7 @@ def sco_header(
|
||||||
|
|
||||||
H = [
|
H = [
|
||||||
"""<!DOCTYPE html><html lang="fr">
|
"""<!DOCTYPE html><html lang="fr">
|
||||||
|
<!-- ScoDoc legacy -->
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<title>%(page_title)s</title>
|
<title>%(page_title)s</title>
|
||||||
|
@ -217,9 +217,9 @@ 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="{scu.ScoURL()}";
|
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}";
|
||||||
</script>"""
|
</script>"""
|
||||||
)
|
)
|
||||||
|
@ -303,13 +303,16 @@ def sco_header(
|
||||||
# div pour affichage messages temporaires
|
# div pour affichage messages temporaires
|
||||||
H.append('<div id="sco_msg" class="head_message"></div>')
|
H.append('<div id="sco_msg" class="head_message"></div>')
|
||||||
#
|
#
|
||||||
|
H.append('<div class="sco-app-content">')
|
||||||
return "".join(H)
|
return "".join(H)
|
||||||
|
|
||||||
|
|
||||||
def sco_footer():
|
def sco_footer():
|
||||||
"""Main HTMl pages footer"""
|
"""Main HTMl pages footer"""
|
||||||
return (
|
return (
|
||||||
"""</div><!-- /gtrcontent -->""" + scu.CUSTOM_HTML_FOOTER + """</body></html>"""
|
"""</div></div><!-- /gtrcontent -->"""
|
||||||
|
+ scu.CUSTOM_HTML_FOOTER
|
||||||
|
+ """</body></html>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -108,19 +109,27 @@ def sidebar_common():
|
||||||
</div>
|
</div>
|
||||||
{sidebar_dept()}
|
{sidebar_dept()}
|
||||||
<h2 class="insidebar">Scolarité</h2>
|
<h2 class="insidebar">Scolarité</h2>
|
||||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
<a href="{
|
||||||
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br>
|
url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
|
||||||
|
}" class="sidebar">Semestres</a> <br>
|
||||||
|
<a href="{
|
||||||
|
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||||
|
}" class="sidebar">Formations</a> <br>
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
if current_user.has_permission(Permission.AbsChange):
|
if current_user.has_permission(Permission.AbsChange):
|
||||||
H.append(
|
H.append(
|
||||||
f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduité</a> <br> """
|
f""" <a href="{
|
||||||
|
url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)
|
||||||
|
}" class="sidebar">Assiduité</a> <br> """
|
||||||
)
|
)
|
||||||
if current_user.has_permission(
|
if current_user.has_permission(
|
||||||
Permission.UsersAdmin
|
Permission.UsersAdmin
|
||||||
) or current_user.has_permission(Permission.UsersView):
|
) or current_user.has_permission(Permission.UsersView):
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>"""
|
f"""<a href="{
|
||||||
|
url_for("users.index_html", scodoc_dept=g.scodoc_dept)
|
||||||
|
}" class="sidebar">Utilisateurs</a> <br>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
if current_user.has_permission(Permission.EditPreferences):
|
if current_user.has_permission(Permission.EditPreferences):
|
||||||
|
@ -141,7 +150,9 @@ def sidebar(etudid: int = None):
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
H = [
|
H = [
|
||||||
f"""<div class="sidebar">
|
f"""
|
||||||
|
<!-- sidebar py -->
|
||||||
|
<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"
|
||||||
|
@ -175,18 +186,17 @@ def sidebar(etudid: int = None):
|
||||||
inscription = etud.inscription_courante()
|
inscription = etud.inscription_courante()
|
||||||
if inscription:
|
if inscription:
|
||||||
formsemestre = inscription.formsemestre
|
formsemestre = inscription.formsemestre
|
||||||
nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count(
|
nbabsnj, nbabsjust, _ = sco_assiduites.formsemestre_get_assiduites_count(
|
||||||
etudid, formsemestre
|
etudid, formsemestre
|
||||||
)
|
)
|
||||||
nbabsnj = nbabs - nbabsjust
|
|
||||||
H.append(
|
H.append(
|
||||||
f"""<span title="absences du {
|
f"""<span title="absences du {
|
||||||
formsemestre.date_debut.strftime("%d/%m/%Y")
|
formsemestre.date_debut.strftime(scu.DATE_FMT)
|
||||||
} au {
|
} au {
|
||||||
formsemestre.date_fin.strftime("%d/%m/%Y")
|
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:1.0f} J., {nbabsnj:1.0f} N.J.</span>"""
|
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
|
||||||
)
|
)
|
||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
if current_user.has_permission(Permission.AbsChange):
|
if current_user.has_permission(Permission.AbsChange):
|
||||||
|
@ -218,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>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import psycopg2.extras
|
||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
|
from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
quote_html = html.escape
|
quote_html = html.escape
|
||||||
|
|
||||||
|
@ -460,7 +461,8 @@ def dictfilter(d, fields, filter_nulls=True):
|
||||||
# --- Misc Tools
|
# --- Misc Tools
|
||||||
|
|
||||||
|
|
||||||
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None: # XXX deprecated
|
# XXX deprecated, voir convert_fr_date
|
||||||
|
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None:
|
||||||
"""Convert date string from french format (or ISO) to ISO.
|
"""Convert date string from french format (or ISO) to ISO.
|
||||||
If null_is_empty (default false), returns "" if no input.
|
If null_is_empty (default false), returns "" if no input.
|
||||||
"""
|
"""
|
||||||
|
@ -474,7 +476,7 @@ def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None: # XXX deprecated
|
||||||
if not isinstance(dmy, str):
|
if not isinstance(dmy, str):
|
||||||
raise ScoValueError(f'Date (j/m/a) invalide: "{dmy}"')
|
raise ScoValueError(f'Date (j/m/a) invalide: "{dmy}"')
|
||||||
try:
|
try:
|
||||||
dt = datetime.datetime.strptime(dmy, "%d/%m/%Y")
|
dt = datetime.datetime.strptime(dmy, scu.DATE_FMT)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
dt = datetime.datetime.fromisoformat(dmy)
|
dt = datetime.datetime.fromisoformat(dmy)
|
||||||
|
|
|
@ -34,6 +34,7 @@ from app.models.absences import BilletAbsence
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
def query_billets_etud(etudid: int = None, etat: bool = None) -> Query:
|
def query_billets_etud(etudid: int = None, etat: bool = None) -> Query:
|
||||||
|
@ -89,12 +90,12 @@ def table_billets(
|
||||||
m = " matin"
|
m = " matin"
|
||||||
else:
|
else:
|
||||||
m = " après-midi"
|
m = " après-midi"
|
||||||
billet_dict["abs_begin_str"] = billet.abs_begin.strftime("%d/%m/%Y") + m
|
billet_dict["abs_begin_str"] = billet.abs_begin.strftime(scu.DATE_FMT) + m
|
||||||
if billet.abs_end.hour < 12:
|
if billet.abs_end.hour < 12:
|
||||||
m = " matin"
|
m = " matin"
|
||||||
else:
|
else:
|
||||||
m = " après-midi"
|
m = " après-midi"
|
||||||
billet_dict["abs_end_str"] = billet.abs_end.strftime("%d/%m/%Y") + m
|
billet_dict["abs_end_str"] = billet.abs_end.strftime(scu.DATE_FMT) + m
|
||||||
if billet.etat == 0:
|
if billet.etat == 0:
|
||||||
if billet.justified:
|
if billet.justified:
|
||||||
billet_dict["etat_str"] = "à traiter"
|
billet_dict["etat_str"] = "à traiter"
|
||||||
|
@ -156,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
|
||||||
|
|
|
@ -67,7 +67,7 @@ def abs_notify(etudid: int, date: str | datetime.datetime):
|
||||||
if not formsemestre:
|
if not formsemestre:
|
||||||
return # non inscrit a la date, pas de notification
|
return # non inscrit a la date, pas de notification
|
||||||
|
|
||||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count_in_interval(
|
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count_in_interval(
|
||||||
etudid,
|
etudid,
|
||||||
metrique=scu.translate_assiduites_metric(
|
metrique=scu.translate_assiduites_metric(
|
||||||
sco_preferences.get_preference(
|
sco_preferences.get_preference(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -515,11 +515,13 @@ class ApoEtud(dict):
|
||||||
# ne trouve pas de semestre impair
|
# ne trouve pas de semestre impair
|
||||||
self.validation_annee_but = None
|
self.validation_annee_but = None
|
||||||
return
|
return
|
||||||
self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
|
self.validation_annee_but: ApcValidationAnnee = (
|
||||||
|
ApcValidationAnnee.query.filter_by(
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
etudid=self.etud["etudid"],
|
etudid=self.etud["etudid"],
|
||||||
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
|
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
|
||||||
).first()
|
).first()
|
||||||
|
)
|
||||||
self.is_nar = (
|
self.is_nar = (
|
||||||
self.validation_annee_but and self.validation_annee_but.code == NAR
|
self.validation_annee_but and self.validation_annee_but.code == NAR
|
||||||
)
|
)
|
||||||
|
@ -915,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
|
||||||
|
@ -967,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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1003,7 +1007,7 @@ def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
|
||||||
def nar_etuds_table(apo_data, nar_etuds):
|
def nar_etuds_table(apo_data, nar_etuds):
|
||||||
"""Liste les NAR -> excel table"""
|
"""Liste les NAR -> excel table"""
|
||||||
code_etape = apo_data.etape_apogee
|
code_etape = apo_data.etape_apogee
|
||||||
today = datetime.datetime.today().strftime("%d/%m/%y")
|
today = datetime.datetime.today().strftime(scu.DATE_FMT)
|
||||||
rows = []
|
rows = []
|
||||||
nar_etuds.sort(key=lambda k: k["nom"])
|
nar_etuds.sort(key=lambda k: k["nom"])
|
||||||
for e in nar_etuds:
|
for e in nar_etuds:
|
||||||
|
@ -1052,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()
|
||||||
|
|
|
@ -49,11 +49,13 @@
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import glob
|
import glob
|
||||||
|
import gzip
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
|
import zlib
|
||||||
|
|
||||||
import chardet
|
import chardet
|
||||||
|
|
||||||
|
@ -62,7 +64,7 @@ from flask import g
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from config import Config
|
from config import Config
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||||
|
|
||||||
|
|
||||||
class BaseArchiver:
|
class BaseArchiver:
|
||||||
|
@ -241,11 +243,13 @@ class BaseArchiver:
|
||||||
filename: str,
|
filename: str,
|
||||||
data: str | bytes,
|
data: str | bytes,
|
||||||
dept_id: int = None,
|
dept_id: int = None,
|
||||||
|
compress=False,
|
||||||
):
|
):
|
||||||
"""Store data in archive, under given filename.
|
"""Store data in archive, under given filename.
|
||||||
Filename may be modified (sanitized): return used filename
|
Filename may be modified (sanitized): return used filename
|
||||||
The file is created or replaced.
|
The file is created or replaced.
|
||||||
data may be str or bytes
|
data may be str or bytes
|
||||||
|
If compress, data is gziped and filename suffix ".gz" added.
|
||||||
"""
|
"""
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.encode(scu.SCO_ENCODING)
|
data = data.encode(scu.SCO_ENCODING)
|
||||||
|
@ -255,6 +259,12 @@ class BaseArchiver:
|
||||||
try:
|
try:
|
||||||
scu.GSL.acquire()
|
scu.GSL.acquire()
|
||||||
fname = os.path.join(archive_id, filename)
|
fname = os.path.join(archive_id, filename)
|
||||||
|
if compress:
|
||||||
|
if not fname.endswith(".gz"):
|
||||||
|
fname += ".gz"
|
||||||
|
with gzip.open(fname, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
else:
|
||||||
with open(fname, "wb") as f:
|
with open(fname, "wb") as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
|
@ -274,6 +284,15 @@ class BaseArchiver:
|
||||||
fname = os.path.join(archive_id, filename)
|
fname = os.path.join(archive_id, filename)
|
||||||
log(f"reading archive file {fname}")
|
log(f"reading archive file {fname}")
|
||||||
try:
|
try:
|
||||||
|
if fname.endswith(".gz"):
|
||||||
|
try:
|
||||||
|
with gzip.open(fname) as f:
|
||||||
|
data = f.read()
|
||||||
|
except (OSError, EOFError, zlib.error) as exc:
|
||||||
|
raise ScoValueError(
|
||||||
|
f"Erreur lecture archive ({fname} invalide)"
|
||||||
|
) from exc
|
||||||
|
else:
|
||||||
with open(fname, "rb") as f:
|
with open(fname, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
|
@ -288,6 +307,8 @@ class BaseArchiver:
|
||||||
"""
|
"""
|
||||||
archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
|
archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
|
||||||
data = self.get(archive_id, filename)
|
data = self.get(archive_id, filename)
|
||||||
|
if filename.endswith(".gz"):
|
||||||
|
filename = filename[:-3]
|
||||||
mime = mimetypes.guess_type(filename)[0]
|
mime = mimetypes.guess_type(filename)[0]
|
||||||
if mime is None:
|
if mime is None:
|
||||||
mime = "application/octet-stream"
|
mime = "application/octet-stream"
|
||||||
|
|
|
@ -68,7 +68,7 @@ PV_ARCHIVER = SemsArchiver()
|
||||||
|
|
||||||
|
|
||||||
def do_formsemestre_archive(
|
def do_formsemestre_archive(
|
||||||
formsemestre_id,
|
formsemestre: FormSemestre,
|
||||||
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
|
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
|
||||||
description="",
|
description="",
|
||||||
date_jury="",
|
date_jury="",
|
||||||
|
@ -92,19 +92,18 @@ def do_formsemestre_archive(
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"do_formsemestre_archive: version de bulletin demandée invalide"
|
"do_formsemestre_archive: version de bulletin demandée invalide"
|
||||||
)
|
)
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
||||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
sem_archive_id = formsemestre_id
|
sem_archive_id = formsemestre.id
|
||||||
archive_id = PV_ARCHIVER.create_obj_archive(
|
archive_id = PV_ARCHIVER.create_obj_archive(
|
||||||
sem_archive_id, description, formsemestre.dept_id
|
sem_archive_id, description, formsemestre.dept_id
|
||||||
)
|
)
|
||||||
date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
|
date = PV_ARCHIVER.get_archive_date(archive_id).strftime(scu.DATEATIME_FMT)
|
||||||
|
|
||||||
if not group_ids:
|
if not group_ids:
|
||||||
# tous les inscrits du semestre
|
# tous les inscrits du semestre
|
||||||
group_ids = [sco_groups.get_default_group(formsemestre_id)]
|
group_ids = [sco_groups.get_default_group(formsemestre.id)]
|
||||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||||
group_ids, formsemestre_id=formsemestre_id
|
group_ids, formsemestre_id=formsemestre.id
|
||||||
)
|
)
|
||||||
groups_filename = "-" + groups_infos.groups_filename
|
groups_filename = "-" + groups_infos.groups_filename
|
||||||
etudids = [m["etudid"] for m in groups_infos.members]
|
etudids = [m["etudid"] for m in groups_infos.members]
|
||||||
|
@ -142,19 +141,23 @@ def do_formsemestre_archive(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bulletins en JSON
|
# Bulletins en JSON
|
||||||
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
data = gen_formsemestre_recapcomplet_json(formsemestre.id, xml_with_decisions=True)
|
||||||
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
|
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
|
||||||
if data:
|
if data:
|
||||||
PV_ARCHIVER.store(
|
PV_ARCHIVER.store(
|
||||||
archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id
|
archive_id,
|
||||||
|
"Bulletins.json",
|
||||||
|
data_js,
|
||||||
|
dept_id=formsemestre.dept_id,
|
||||||
|
compress=True,
|
||||||
)
|
)
|
||||||
# Décisions de jury, en XLS
|
# Décisions de jury, en XLS
|
||||||
if formsemestre.formation.is_apc():
|
if formsemestre.formation.is_apc():
|
||||||
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
|
response = jury_but_pv.pvjury_page_but(formsemestre.id, fmt="xls")
|
||||||
data = response.get_data()
|
data = response.get_data()
|
||||||
else: # formations classiques
|
else: # formations classiques
|
||||||
data = sco_pv_forms.formsemestre_pvjury(
|
data = sco_pv_forms.formsemestre_pvjury(
|
||||||
formsemestre_id, fmt="xls", publish=False
|
formsemestre.id, fmt="xls", publish=False
|
||||||
)
|
)
|
||||||
if data:
|
if data:
|
||||||
PV_ARCHIVER.store(
|
PV_ARCHIVER.store(
|
||||||
|
@ -165,7 +168,7 @@ def do_formsemestre_archive(
|
||||||
)
|
)
|
||||||
# Classeur bulletins (PDF)
|
# Classeur bulletins (PDF)
|
||||||
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
|
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
|
||||||
formsemestre_id, version=bul_version
|
formsemestre.id, version=bul_version
|
||||||
)
|
)
|
||||||
if data:
|
if data:
|
||||||
PV_ARCHIVER.store(
|
PV_ARCHIVER.store(
|
||||||
|
@ -173,10 +176,11 @@ def do_formsemestre_archive(
|
||||||
"Bulletins.pdf",
|
"Bulletins.pdf",
|
||||||
data,
|
data,
|
||||||
dept_id=formsemestre.dept_id,
|
dept_id=formsemestre.dept_id,
|
||||||
|
compress=True,
|
||||||
)
|
)
|
||||||
# Lettres individuelles (PDF):
|
# Lettres individuelles (PDF):
|
||||||
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
|
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
|
||||||
formsemestre_id,
|
formsemestre.id,
|
||||||
etudids=etudids,
|
etudids=etudids,
|
||||||
date_jury=date_jury,
|
date_jury=date_jury,
|
||||||
date_commission=date_commission,
|
date_commission=date_commission,
|
||||||
|
@ -217,7 +221,7 @@ def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
|
||||||
"""Make and store new archive for this formsemestre.
|
"""Make and store new archive for this formsemestre.
|
||||||
(all students or only selected groups)
|
(all students or only selected groups)
|
||||||
"""
|
"""
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
if not formsemestre.can_edit_pv():
|
if not formsemestre.can_edit_pv():
|
||||||
raise ScoPermissionDenied(
|
raise ScoPermissionDenied(
|
||||||
dest_url=url_for(
|
dest_url=url_for(
|
||||||
|
@ -320,7 +324,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
||||||
else:
|
else:
|
||||||
tf[2]["anonymous"] = False
|
tf[2]["anonymous"] = False
|
||||||
do_formsemestre_archive(
|
do_formsemestre_archive(
|
||||||
formsemestre_id,
|
formsemestre,
|
||||||
group_ids=group_ids,
|
group_ids=group_ids,
|
||||||
description=tf[2]["description"],
|
description=tf[2]["description"],
|
||||||
date_jury=tf[2]["date_jury"],
|
date_jury=tf[2]["date_jury"],
|
||||||
|
@ -352,7 +356,7 @@ def formsemestre_list_archives(formsemestre_id):
|
||||||
"""Page listing archives"""
|
"""Page listing archives"""
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
sem_archive_id = formsemestre_id
|
sem_archive_id = formsemestre_id
|
||||||
L = []
|
archives_descr = []
|
||||||
for archive_id in PV_ARCHIVER.list_obj_archives(
|
for archive_id in PV_ARCHIVER.list_obj_archives(
|
||||||
sem_archive_id, dept_id=formsemestre.dept_id
|
sem_archive_id, dept_id=formsemestre.dept_id
|
||||||
):
|
):
|
||||||
|
@ -366,28 +370,30 @@ def formsemestre_list_archives(formsemestre_id):
|
||||||
archive_id, dept_id=formsemestre.dept_id
|
archive_id, dept_id=formsemestre.dept_id
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
L.append(a)
|
archives_descr.append(a)
|
||||||
|
|
||||||
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
|
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
|
||||||
if not L:
|
if not archives_descr:
|
||||||
H.append("<p>aucune archive enregistrée</p>")
|
H.append("<p>aucune archive enregistrée</p>")
|
||||||
else:
|
else:
|
||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
for a in L:
|
for a in archives_descr:
|
||||||
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
|
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
|
||||||
H.append(
|
H.append(
|
||||||
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
|
f"""<li>{a["date"].strftime("%d/%m/%Y %H:%M")} : <em>{a["description"]}</em>
|
||||||
% (
|
(<a href="{ url_for( "notes.formsemestre_delete_archive", scodoc_dept=g.scodoc_dept,
|
||||||
a["date"].strftime("%d/%m/%Y %H:%M"),
|
formsemestre_id=formsemestre_id, archive_name=archive_name
|
||||||
a["description"],
|
)}">supprimer</a>)
|
||||||
formsemestre_id,
|
<ul>"""
|
||||||
archive_name,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for filename in a["content"]:
|
for filename in a["content"]:
|
||||||
H.append(
|
H.append(
|
||||||
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
|
f"""<li><a href="{
|
||||||
% (formsemestre_id, archive_name, filename, filename)
|
url_for( "notes.formsemestre_get_archived_file", scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
archive_name=archive_name,
|
||||||
|
filename=filename
|
||||||
|
)}">{filename[:-3] if filename.endswith(".gz") else filename}</a></li>"""
|
||||||
)
|
)
|
||||||
if not a["content"]:
|
if not a["content"]:
|
||||||
H.append("<li><em>aucun fichier !</em></li>")
|
H.append("<li><em>aucun fichier !</em></li>")
|
||||||
|
@ -399,7 +405,7 @@ def formsemestre_list_archives(formsemestre_id):
|
||||||
|
|
||||||
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
|
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
|
||||||
"""Send file to client."""
|
"""Send file to client."""
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
sem_archive_id = formsemestre.id
|
sem_archive_id = formsemestre.id
|
||||||
return PV_ARCHIVER.get_archived_file(
|
return PV_ARCHIVER.get_archived_file(
|
||||||
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id
|
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id
|
||||||
|
|
|
@ -17,7 +17,7 @@ from app.models import (
|
||||||
ModuleImplInscription,
|
ModuleImplInscription,
|
||||||
ScoDocSiteConfig,
|
ScoDocSiteConfig,
|
||||||
)
|
)
|
||||||
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
|
@ -372,12 +372,38 @@ def str_to_time(time_str: str) -> time:
|
||||||
def get_assiduites_stats(
|
def get_assiduites_stats(
|
||||||
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
||||||
) -> dict[str, int | float]:
|
) -> dict[str, int | float]:
|
||||||
"""Compte les assiduités en fonction des filtres"""
|
"""
|
||||||
# XXX TODO-assiduite : documenter !!!
|
Calcule les statistiques sur les assiduités
|
||||||
# Que sont les filtres ? Quelles valeurs ?
|
(nombre de jours, demi-journées et heures passées,
|
||||||
# documenter permet de faire moins de bug: qualité du code non satisfaisante.
|
non justifiées, justifiées et total)
|
||||||
#
|
|
||||||
# + on se perd entre les clés en majuscules et en minuscules. Pourquoi
|
Les filtres :
|
||||||
|
- etat : filtre les assiduités par leur état
|
||||||
|
valeur : (absent, present, retard)
|
||||||
|
- date_debut/date_fin : prend les assiduités qui se trouvent entre les dates
|
||||||
|
valeur : datetime.datetime
|
||||||
|
- moduleimpl_id : filtre les assiduités en fonction du moduleimpl_id
|
||||||
|
valeur : int | None
|
||||||
|
- formsemestre : prend les assiduités du formsemestre donné
|
||||||
|
valeur : FormSemestre
|
||||||
|
- formsemestre_modimpls : prend les assiduités avec un moduleimpl du formsemestre
|
||||||
|
valeur : FormSemestre
|
||||||
|
- est_just : filtre les assiduités en fonction de si elles sont justifiées ou non
|
||||||
|
valeur : bool
|
||||||
|
- user_id : filtre les assiduités en fonction de l'utilisateur qui les a créées
|
||||||
|
valeur : int
|
||||||
|
- split : effectue un comptage par état d'assiduité
|
||||||
|
valeur : str (du moment que la clé est présente dans filtered)
|
||||||
|
|
||||||
|
Les métriques :
|
||||||
|
- journee : comptage en nombre de journée
|
||||||
|
- demi : comptage en nombre de demi journée
|
||||||
|
- heure : comptage en heure
|
||||||
|
- compte : nombre d'objets
|
||||||
|
- all : renvoi toute les métriques
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
if filtered is not None:
|
if filtered is not None:
|
||||||
deb, fin = None, None
|
deb, fin = None, None
|
||||||
|
@ -414,34 +440,71 @@ def get_assiduites_stats(
|
||||||
calculator: CountCalculator = CountCalculator()
|
calculator: CountCalculator = CountCalculator()
|
||||||
calculator.compute_assiduites(assiduites)
|
calculator.compute_assiduites(assiduites)
|
||||||
|
|
||||||
|
# S'il n'y a pas de filtre ou que le filtre split n'est pas dans les filtres
|
||||||
if filtered is None or "split" not in filtered:
|
if filtered is None or "split" not in filtered:
|
||||||
|
# On récupère le comptage total
|
||||||
|
# only_total permet de ne récupérer que le total
|
||||||
count: dict = calculator.to_dict(only_total=True)
|
count: dict = calculator.to_dict(only_total=True)
|
||||||
|
|
||||||
|
# On ne garde que les métriques demandées
|
||||||
for key, val in count.items():
|
for key, val in count.items():
|
||||||
if key in metrics:
|
if key in metrics:
|
||||||
output[key] = val
|
output[key] = val
|
||||||
|
# On renvoie le total si on a rien demandé (ou que metrics == ["all"])
|
||||||
return output if output else count
|
return output if output else count
|
||||||
|
|
||||||
# Récupération des états
|
|
||||||
etats: list[str] = (
|
|
||||||
filtered["etat"].split(",") if "etat" in filtered else scu.EtatAssiduite.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
# être sur que les états sont corrects
|
|
||||||
etats = [etat for etat in etats if etat.upper() in scu.EtatAssiduite.all()]
|
|
||||||
|
|
||||||
# Préparation du dictionnaire de retour avec les valeurs du calcul
|
# Préparation du dictionnaire de retour avec les valeurs du calcul
|
||||||
count: dict = calculator.to_dict(only_total=False)
|
count: dict = calculator.to_dict(only_total=False)
|
||||||
|
|
||||||
|
# Récupération des états depuis la saisie utilisateur
|
||||||
|
etats: list[str] = (
|
||||||
|
filtered["etat"].split(",") if "etat" in filtered else scu.EtatAssiduite.all()
|
||||||
|
)
|
||||||
for etat in etats:
|
for etat in etats:
|
||||||
# TODO-assiduite: on se perd entre les lower et upper.
|
# On vérifie que l'état est bien un état d'assiduité
|
||||||
# Pourquoi EtatAssiduite est en majuscules si tout le reste est en minuscules ?
|
# sinon on passe à l'état suivant
|
||||||
etat = etat.lower()
|
if not scu.EtatAssiduite.contains(etat):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# On récupère le comptage pour chaque état
|
||||||
if etat != "present":
|
if etat != "present":
|
||||||
output[etat] = count[etat]
|
output[etat] = count[etat]
|
||||||
output[etat]["justifie"] = count[etat + "_just"]
|
output[etat]["justifie"] = count[etat + "_just"]
|
||||||
output[etat]["non_justifie"] = count[etat + "_non_just"]
|
output[etat]["non_justifie"] = count[etat + "_non_just"]
|
||||||
else:
|
else:
|
||||||
output[etat] = count[etat]
|
output[etat] = count[etat]
|
||||||
|
|
||||||
output["total"] = count["total"]
|
output["total"] = count["total"]
|
||||||
|
|
||||||
|
# le dictionnaire devrait ressembler à :
|
||||||
|
# {
|
||||||
|
# "absent": {
|
||||||
|
# "journee": 1,
|
||||||
|
# "demi": 2,
|
||||||
|
# "heure": 3,
|
||||||
|
# "compte": 4,
|
||||||
|
# "justifie": {
|
||||||
|
# "journee": 1,
|
||||||
|
# "demi": 2,
|
||||||
|
# "heure": 3,
|
||||||
|
# "compte": 4
|
||||||
|
# },
|
||||||
|
# "non_justifie": {
|
||||||
|
# "journee": 1,
|
||||||
|
# "demi": 2,
|
||||||
|
# "heure": 3,
|
||||||
|
# "compte": 4
|
||||||
|
# }
|
||||||
|
# },
|
||||||
|
# ...
|
||||||
|
# "total": {
|
||||||
|
# "journee": 1,
|
||||||
|
# "demi": 2,
|
||||||
|
# "heure": 3,
|
||||||
|
# "compte": 4
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
@ -661,7 +724,7 @@ def create_absence_billet(
|
||||||
db.session.add(justi)
|
db.session.add(justi)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
compute_assiduites_justified(etud.id, [justi])
|
justi.justifier_assiduites()
|
||||||
|
|
||||||
calculator: CountCalculator = CountCalculator()
|
calculator: CountCalculator = CountCalculator()
|
||||||
calculator.compute_assiduites([assiduite_unique])
|
calculator.compute_assiduites([assiduite_unique])
|
||||||
|
@ -669,9 +732,9 @@ def create_absence_billet(
|
||||||
|
|
||||||
|
|
||||||
# Gestion du cache
|
# Gestion du cache
|
||||||
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
|
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int, int]:
|
||||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||||
tuple (nb abs non justifiées, nb abs justifiées)
|
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
|
metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
|
||||||
|
@ -685,19 +748,19 @@ def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
|
||||||
|
|
||||||
def formsemestre_get_assiduites_count(
|
def formsemestre_get_assiduites_count(
|
||||||
etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
|
etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
|
||||||
) -> tuple[int, int]:
|
) -> tuple[int, int, int]:
|
||||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||||
tuple (nb abs non justifiées, nb abs justifiées)
|
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
|
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
|
||||||
return get_assiduites_count_in_interval(
|
return get_assiduites_count_in_interval(
|
||||||
etudid,
|
etudid,
|
||||||
date_debut=scu.localize_datetime(
|
date_debut=scu.localize_datetime(
|
||||||
datetime.combine(formsemestre.date_debut, time(8, 0))
|
datetime.combine(formsemestre.date_debut, time(0, 0))
|
||||||
),
|
),
|
||||||
date_fin=scu.localize_datetime(
|
date_fin=scu.localize_datetime(
|
||||||
datetime.combine(formsemestre.date_fin, time(18, 0))
|
datetime.combine(formsemestre.date_fin, time(23, 0))
|
||||||
),
|
),
|
||||||
metrique=scu.translate_assiduites_metric(metrique),
|
metrique=scu.translate_assiduites_metric(metrique),
|
||||||
moduleimpl_id=moduleimpl_id,
|
moduleimpl_id=moduleimpl_id,
|
||||||
|
@ -712,14 +775,14 @@ def get_assiduites_count_in_interval(
|
||||||
date_debut: datetime = None,
|
date_debut: datetime = None,
|
||||||
date_fin: datetime = None,
|
date_fin: datetime = None,
|
||||||
moduleimpl_id: int = None,
|
moduleimpl_id: int = None,
|
||||||
):
|
) -> tuple[int, int, int]:
|
||||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||||
tuple (nb abs, nb abs justifiées)
|
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||||
On peut spécifier les dates comme datetime ou iso.
|
On peut spécifier les dates comme datetime ou iso.
|
||||||
Utilise un cache.
|
Utilise un cache.
|
||||||
"""
|
"""
|
||||||
date_debut_iso = date_debut_iso or date_debut.isoformat()
|
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
|
||||||
date_fin_iso = date_fin_iso or date_fin.isoformat()
|
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
|
||||||
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
|
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
|
||||||
|
|
||||||
r = sco_cache.AbsSemEtudCache.get(key)
|
r = sco_cache.AbsSemEtudCache.get(key)
|
||||||
|
@ -744,9 +807,10 @@ def get_assiduites_count_in_interval(
|
||||||
if not ans:
|
if not ans:
|
||||||
log("warning: get_assiduites_count failed to cache")
|
log("warning: get_assiduites_count failed to cache")
|
||||||
|
|
||||||
nb_abs: dict = r["absent"][metrique]
|
nb_abs: int = r["absent"][metrique]
|
||||||
nb_abs_just: dict = r["absent_just"][metrique]
|
nb_abs_nj: int = r["absent_non_just"][metrique]
|
||||||
return (nb_abs, nb_abs_just)
|
nb_abs_just: int = r["absent_just"][metrique]
|
||||||
|
return (nb_abs_nj, nb_abs_just, nb_abs)
|
||||||
|
|
||||||
|
|
||||||
def invalidate_assiduites_count(etudid: int, sem: dict):
|
def invalidate_assiduites_count(etudid: int, sem: dict):
|
||||||
|
|
102
app/scodoc/sco_bug_report.py
Normal file
102
app/scodoc/sco_bug_report.py
Normal 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
|
|
@ -126,7 +126,7 @@ def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict:
|
||||||
|
|
||||||
# ajoute date courante
|
# ajoute date courante
|
||||||
t = time.localtime()
|
t = time.localtime()
|
||||||
C["date_dmy"] = time.strftime("%d/%m/%Y", t)
|
C["date_dmy"] = time.strftime(scu.DATE_FMT, t)
|
||||||
C["date_iso"] = time.strftime("%Y-%m-%d", t)
|
C["date_iso"] = time.strftime("%Y-%m-%d", t)
|
||||||
|
|
||||||
return C
|
return C
|
||||||
|
@ -196,7 +196,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||||
pid = partition["partition_id"]
|
pid = partition["partition_id"]
|
||||||
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
||||||
# --- Absences
|
# --- Absences
|
||||||
I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
_, I["nbabsjust"], I["nbabs"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||||
|
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
infos, dpv = etud_descr_situation_semestre(
|
infos, dpv = etud_descr_situation_semestre(
|
||||||
|
@ -446,7 +446,8 @@ def _ue_mod_bulletin(
|
||||||
):
|
):
|
||||||
"""Infos sur les modules (et évaluations) dans une UE
|
"""Infos sur les modules (et évaluations) dans une UE
|
||||||
(ajoute les informations aux modimpls)
|
(ajoute les informations aux modimpls)
|
||||||
Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit).
|
Result: liste de modules de l'UE avec les infos dans chacun (seulement
|
||||||
|
ceux où l'étudiant est inscrit).
|
||||||
"""
|
"""
|
||||||
bul_show_mod_rangs = sco_preferences.get_preference(
|
bul_show_mod_rangs = sco_preferences.get_preference(
|
||||||
"bul_show_mod_rangs", formsemestre_id
|
"bul_show_mod_rangs", formsemestre_id
|
||||||
|
@ -471,7 +472,7 @@ def _ue_mod_bulletin(
|
||||||
) # peut etre 'NI'
|
) # peut etre 'NI'
|
||||||
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
||||||
if bul_show_abs_modules:
|
if bul_show_abs_modules:
|
||||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
mod_abs = [nbabs, nbabsjust]
|
mod_abs = [nbabs, nbabsjust]
|
||||||
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
|
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -61,7 +61,7 @@ from flask_login import current_user
|
||||||
|
|
||||||
from app.models import FormSemestre, Identite, ScoDocSiteConfig
|
from app.models import FormSemestre, Identite, ScoDocSiteConfig
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import NoteProcessError
|
from app.scodoc.sco_exceptions import NoteProcessError, ScoPDFFormatError
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_pdf
|
from app.scodoc import sco_pdf
|
||||||
|
@ -226,9 +226,18 @@ 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:
|
||||||
document.build(story)
|
document.build(story)
|
||||||
|
except (
|
||||||
|
ValueError,
|
||||||
|
KeyError,
|
||||||
|
reportlab.platypus.doctemplate.LayoutError,
|
||||||
|
) as exc:
|
||||||
|
raise ScoPDFFormatError(str(exc)) from exc
|
||||||
|
|
||||||
data = report.getvalue()
|
data = report.getvalue()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ def formsemestre_bulletinetud_published_dict(
|
||||||
version="long",
|
version="long",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Dictionnaire representant les informations _publiees_ du bulletin de notes
|
"""Dictionnaire representant les informations _publiees_ du bulletin de notes
|
||||||
Utilisé pour JSON, devrait l'être aussi pour XML. (todo)
|
Utilisé pour JSON des formations classiques (mais pas pour le XML, qui est deprecated).
|
||||||
|
|
||||||
version:
|
version:
|
||||||
short (sans les évaluations)
|
short (sans les évaluations)
|
||||||
|
@ -114,10 +114,8 @@ def formsemestre_bulletinetud_published_dict(
|
||||||
if etudid not in nt.identdict:
|
if etudid not in nt.identdict:
|
||||||
abort(404, "etudiant non inscrit dans ce semestre")
|
abort(404, "etudiant non inscrit dans ce semestre")
|
||||||
d = {"type": "classic", "version": "0"}
|
d = {"type": "classic", "version": "0"}
|
||||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
|
||||||
published = True
|
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||||
else:
|
|
||||||
published = False
|
|
||||||
if xml_nodate:
|
if xml_nodate:
|
||||||
docdate = ""
|
docdate = ""
|
||||||
else:
|
else:
|
||||||
|
@ -171,6 +169,21 @@ def formsemestre_bulletinetud_published_dict(
|
||||||
pid = partition["partition_id"]
|
pid = partition["partition_id"]
|
||||||
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
||||||
|
|
||||||
|
# Il serait préférable de factoriser et d'avoir la même section
|
||||||
|
# "semestre" que celle des bulletins BUT.
|
||||||
|
etud_groups = sco_groups.get_etud_formsemestre_groups(
|
||||||
|
etud, formsemestre, only_to_show=True
|
||||||
|
)
|
||||||
|
d["semestre"] = {
|
||||||
|
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
|
||||||
|
"date_debut": formsemestre.date_debut.isoformat(),
|
||||||
|
"date_fin": formsemestre.date_fin.isoformat(),
|
||||||
|
"annee_universitaire": formsemestre.annee_scolaire_str(),
|
||||||
|
"numero": formsemestre.semestre_id,
|
||||||
|
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
|
||||||
|
"groupes": [group.to_dict() for group in etud_groups],
|
||||||
|
}
|
||||||
|
|
||||||
ues_stat = nt.get_ues_stat_dict()
|
ues_stat = nt.get_ues_stat_dict()
|
||||||
modimpls = nt.get_modimpls_dict()
|
modimpls = nt.get_modimpls_dict()
|
||||||
nbetuds = len(nt.etud_moy_gen_ranks)
|
nbetuds = len(nt.etud_moy_gen_ranks)
|
||||||
|
@ -296,7 +309,7 @@ def formsemestre_bulletinetud_published_dict(
|
||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
if prefs["bul_show_abs"]:
|
if prefs["bul_show_abs"]:
|
||||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
||||||
|
|
||||||
# --- Décision Jury
|
# --- Décision Jury
|
||||||
|
|
|
@ -352,7 +352,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
|
||||||
H.append(
|
H.append(
|
||||||
f"""<p>
|
f"""<p>
|
||||||
<span class="bull_appreciations_date">{
|
<span class="bull_appreciations_date">{
|
||||||
appreciation.date.strftime("%d/%m/%y") if appreciation.date else ""
|
appreciation.date.strftime(scu.DATE_FMT) if appreciation.date else ""
|
||||||
}</span>
|
}</span>
|
||||||
{appreciation.comment_safe()}
|
{appreciation.comment_safe()}
|
||||||
<span class="bull_appreciations_link">{mlink}</span>
|
<span class="bull_appreciations_link">{mlink}</span>
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -182,7 +183,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||||
H.append(
|
H.append(
|
||||||
f"""<p>
|
f"""<p>
|
||||||
<span class="bull_appreciations_date">{
|
<span class="bull_appreciations_date">{
|
||||||
appreciation.date.strftime("%d/%m/%Y")
|
appreciation.date.strftime(scu.DATE_FMT)
|
||||||
if appreciation.date else ""}</span>
|
if appreciation.date else ""}</span>
|
||||||
{appreciation.comment_safe()}
|
{appreciation.comment_safe()}
|
||||||
<span class="bull_appreciations_link">{mlink}</span>
|
<span class="bull_appreciations_link">{mlink}</span>
|
||||||
|
|
|
@ -260,7 +260,7 @@ def make_xml_formsemestre_bulletinetud(
|
||||||
numero=str(mod["numero"]),
|
numero=str(mod["numero"]),
|
||||||
titre=quote_xml_attr(mod["titre"]),
|
titre=quote_xml_attr(mod["titre"]),
|
||||||
abbrev=quote_xml_attr(mod["abbrev"]),
|
abbrev=quote_xml_attr(mod["abbrev"]),
|
||||||
code_apogee=quote_xml_attr(mod["code_apogee"])
|
code_apogee=quote_xml_attr(mod["code_apogee"]),
|
||||||
# ects=ects ects des modules maintenant inutilisés
|
# ects=ects ects des modules maintenant inutilisés
|
||||||
)
|
)
|
||||||
x_ue.append(x_mod)
|
x_ue.append(x_mod)
|
||||||
|
@ -347,7 +347,7 @@ def make_xml_formsemestre_bulletinetud(
|
||||||
|
|
||||||
# --- Absences
|
# --- Absences
|
||||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
|
||||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -55,7 +55,6 @@ from flask import g
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.scodoc import notesdb as ndb
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
|
|
||||||
|
@ -174,17 +173,15 @@ class EvaluationCache(ScoDocCache):
|
||||||
@classmethod
|
@classmethod
|
||||||
def invalidate_all_sems(cls):
|
def invalidate_all_sems(cls):
|
||||||
"delete all evaluations in current dept from cache"
|
"delete all evaluations in current dept from cache"
|
||||||
|
from app.models.evaluations import Evaluation
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
|
|
||||||
evaluation_ids = [
|
evaluation_ids = [
|
||||||
x[0]
|
e.id
|
||||||
for x in ndb.SimpleQuery(
|
for e in Evaluation.query.join(ModuleImpl)
|
||||||
"""SELECT e.id
|
.join(FormSemestre)
|
||||||
FROM notes_evaluation e, notes_moduleimpl mi, notes_formsemestre s
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
WHERE s.dept_id=%(dept_id)s
|
|
||||||
AND s.id = mi.formsemestre_id
|
|
||||||
AND mi.id = e.moduleimpl_id;
|
|
||||||
""",
|
|
||||||
{"dept_id": g.scodoc_dept_id},
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
cls.delete_many(evaluation_ids)
|
cls.delete_many(evaluation_ids)
|
||||||
|
|
||||||
|
@ -277,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
|
||||||
|
|
||||||
|
@ -318,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)
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
import datetime
|
|
||||||
import html
|
import html
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -231,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
|
||||||
|
@ -323,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()
|
||||||
|
|
||||||
|
@ -354,10 +356,9 @@ 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), "%d/%m/%Y")
|
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
|
||||||
)
|
)
|
||||||
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
||||||
if day in weekend:
|
if day in weekend:
|
||||||
|
@ -368,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", " "))
|
||||||
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 = (
|
|
||||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
legend = " " # 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), "%d/%m/%Y")
|
|
||||||
)
|
|
||||||
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
|
||||||
if day in weekend:
|
|
||||||
bgcolor = WEEKENDCOLOR
|
|
||||||
weekclass = "wkend"
|
|
||||||
attrs = ""
|
|
||||||
else:
|
|
||||||
bgcolor = WEEKDAYCOLOR
|
|
||||||
weekclass = "wk" + str(monday).replace("/", "_")
|
|
||||||
attrs = trattributes
|
|
||||||
if (
|
|
||||||
weeknum == current_weeknum
|
|
||||||
and current_year == year
|
|
||||||
and weekclass != "wkend"
|
|
||||||
):
|
|
||||||
weeknum += " currentweek"
|
|
||||||
|
|
||||||
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 = (
|
|
||||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
legend = " " # 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)
|
|
||||||
|
|
|
@ -30,17 +30,18 @@
|
||||||
|
|
||||||
(coût théorique en heures équivalent TD)
|
(coût théorique en heures équivalent TD)
|
||||||
"""
|
"""
|
||||||
from flask import request
|
from flask import request, Response
|
||||||
|
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
from app.scodoc.gen_tables import GenTable
|
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc.gen_tables import GenTable
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import sco_version
|
import sco_version
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_table_estim_cost(
|
def formsemestre_table_estim_cost(
|
||||||
formsemestre_id,
|
formsemestre: FormSemestre,
|
||||||
n_group_td=1,
|
n_group_td=1,
|
||||||
n_group_tp=1,
|
n_group_tp=1,
|
||||||
coef_tp=1,
|
coef_tp=1,
|
||||||
|
@ -55,8 +56,6 @@ def formsemestre_table_estim_cost(
|
||||||
peut conduire à une sur-estimation du coût s'il y a des modules optionnels
|
peut conduire à une sur-estimation du coût s'il y a des modules optionnels
|
||||||
(dans ce cas, retoucher le tableau excel exporté).
|
(dans ce cas, retoucher le tableau excel exporté).
|
||||||
"""
|
"""
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for modimpl in formsemestre.modimpls:
|
for modimpl in formsemestre.modimpls:
|
||||||
rows.append(
|
rows.append(
|
||||||
|
@ -76,14 +75,14 @@ def formsemestre_table_estim_cost(
|
||||||
+ coef_cours * row["heures_cours"]
|
+ coef_cours * row["heures_cours"]
|
||||||
+ coef_tp * row["heures_tp"]
|
+ coef_tp * row["heures_tp"]
|
||||||
)
|
)
|
||||||
sum_cours = sum([t["heures_cours"] for t in rows])
|
sum_cours = sum(t["heures_cours"] for t in rows)
|
||||||
sum_td = sum([t["heures_td"] for t in rows])
|
sum_td = sum(t["heures_td"] for t in rows)
|
||||||
sum_tp = sum([t["heures_tp"] for t in rows])
|
sum_tp = sum(t["heures_tp"] for t in rows)
|
||||||
sum_heqtd = sum_td + coef_cours * sum_cours + coef_tp * sum_tp
|
sum_heqtd = sum_td + coef_cours * sum_cours + coef_tp * sum_tp
|
||||||
assert abs(sum([t["HeqTD"] for t in rows]) - sum_heqtd) < 0.01, "%s != %s" % (
|
# assert abs(sum(t["HeqTD"] for t in rows) - sum_heqtd) < 0.01, "%s != %s" % (
|
||||||
sum([t["HeqTD"] for t in rows]),
|
# sum(t["HeqTD"] for t in rows),
|
||||||
sum_heqtd,
|
# sum_heqtd,
|
||||||
)
|
# )
|
||||||
|
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
|
@ -117,7 +116,7 @@ def formsemestre_table_estim_cost(
|
||||||
),
|
),
|
||||||
rows=rows,
|
rows=rows,
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
||||||
html_class="table_leftalign table_listegroupe",
|
html_class="table_leftalign table_listegroupe",
|
||||||
xls_before_table=[
|
xls_before_table=[
|
||||||
[formsemestre.titre_annee()],
|
[formsemestre.titre_annee()],
|
||||||
|
@ -142,51 +141,50 @@ 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
|
||||||
def formsemestre_estim_cost(
|
def formsemestre_estim_cost(
|
||||||
formsemestre_id,
|
formsemestre_id: int,
|
||||||
n_group_td=1,
|
n_group_td: int | str = 1,
|
||||||
n_group_tp=1,
|
n_group_tp: int | str = 1,
|
||||||
coef_tp=1,
|
coef_tp: float | str = 1.0,
|
||||||
coef_cours=1.5,
|
coef_cours: float | str = 1.5,
|
||||||
fmt="html",
|
fmt="html",
|
||||||
):
|
) -> str | Response:
|
||||||
"""Page (formulaire) estimation coûts"""
|
"""Page (formulaire) estimation coûts"""
|
||||||
|
try:
|
||||||
n_group_td = int(n_group_td)
|
n_group_td = int(n_group_td)
|
||||||
n_group_tp = int(n_group_tp)
|
n_group_tp = int(n_group_tp)
|
||||||
coef_tp = float(coef_tp)
|
coef_tp = float(coef_tp)
|
||||||
coef_cours = float(coef_cours)
|
coef_cours = float(coef_cours)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ScoValueError("paramètre invalide: utiliser des nombres") from exc
|
||||||
|
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
|
||||||
tab = formsemestre_table_estim_cost(
|
tab = formsemestre_table_estim_cost(
|
||||||
formsemestre_id,
|
formsemestre,
|
||||||
n_group_td=n_group_td,
|
n_group_td=n_group_td,
|
||||||
n_group_tp=n_group_tp,
|
n_group_tp=n_group_tp,
|
||||||
coef_tp=coef_tp,
|
coef_tp=coef_tp,
|
||||||
coef_cours=coef_cours,
|
coef_cours=coef_cours,
|
||||||
)
|
)
|
||||||
h = """
|
tab.html_before_table = f"""
|
||||||
<form name="f" method="get" action="%s">
|
<form name="f" method="get" action="{request.base_url}">
|
||||||
<input type="hidden" name="formsemestre_id" value="%s"></input>
|
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}"></input>
|
||||||
Nombre de groupes de TD: <input type="text" name="n_group_td" value="%s" onchange="document.f.submit()"/><br>
|
Nombre de groupes de TD: <input type="text" name="n_group_td" value="{n_group_td}" onchange="document.f.submit()"/><br>
|
||||||
Nombre de groupes de TP: <input type="text" name="n_group_tp" value="%s" onchange="document.f.submit()"/>
|
Nombre de groupes de TP: <input type="text" name="n_group_tp" value="{n_group_tp}" onchange="document.f.submit()"/>
|
||||||
Coefficient heures TP: <input type="text" name="coef_tp" value="%s" onchange="document.f.submit()"/>
|
Coefficient heures TP: <input type="text" name="coef_tp" value="{coef_tp}" onchange="document.f.submit()"/>
|
||||||
<br>
|
<br>
|
||||||
</form>
|
</form>
|
||||||
""" % (
|
"""
|
||||||
request.base_url,
|
|
||||||
formsemestre_id,
|
|
||||||
n_group_td,
|
|
||||||
n_group_tp,
|
|
||||||
coef_tp,
|
|
||||||
)
|
|
||||||
tab.html_before_table = h
|
|
||||||
tab.base_url = "%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s" % (
|
tab.base_url = "%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s" % (
|
||||||
request.base_url,
|
request.base_url,
|
||||||
formsemestre_id,
|
formsemestre.id,
|
||||||
n_group_td,
|
n_group_td,
|
||||||
n_group_tp,
|
n_group_tp,
|
||||||
coef_tp,
|
coef_tp,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -71,12 +71,10 @@ def report_debouche_date(start_year=None, fmt="html"):
|
||||||
etudids = get_etudids_with_debouche(start_year)
|
etudids = get_etudids_with_debouche(start_year)
|
||||||
tab = table_debouche_etudids(etudids, keep_numeric=keep_numeric)
|
tab = table_debouche_etudids(etudids, keep_numeric=keep_numeric)
|
||||||
|
|
||||||
tab.filename = scu.make_filename("debouche_scodoc_%s" % start_year)
|
tab.filename = scu.make_filename(f"debouche_scodoc_{start_year}")
|
||||||
tab.origin = (
|
tab.origin = f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}"
|
||||||
"Généré par %s le " % sco_version.SCONAME + scu.timedate_human_repr() + ""
|
tab.caption = f"Récapitulatif débouchés à partir du 1/1/{start_year}."
|
||||||
)
|
tab.base_url = f"{request.base_url}?start_year={start_year}"
|
||||||
tab.caption = "Récapitulatif débouchés à partir du 1/1/%s." % start_year
|
|
||||||
tab.base_url = "%s?start_year=%s" % (request.base_url, start_year)
|
|
||||||
return tab.make_page(
|
return tab.make_page(
|
||||||
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
|
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
|
||||||
init_qtip=True,
|
init_qtip=True,
|
||||||
|
@ -118,7 +116,16 @@ def get_etudids_with_debouche(start_year):
|
||||||
|
|
||||||
def table_debouche_etudids(etudids, keep_numeric=True):
|
def table_debouche_etudids(etudids, keep_numeric=True):
|
||||||
"""Rapport pour ces étudiants"""
|
"""Rapport pour ces étudiants"""
|
||||||
L = []
|
rows = []
|
||||||
|
# Recherche les débouchés:
|
||||||
|
itemsuivi_etuds = {etudid: itemsuivi_list_etud(etudid) for etudid in etudids}
|
||||||
|
all_tags = set()
|
||||||
|
for debouche in itemsuivi_etuds.values():
|
||||||
|
if debouche:
|
||||||
|
for it in debouche:
|
||||||
|
all_tags.update(tag.strip() for tag in it["tags"].split(","))
|
||||||
|
all_tags = tuple(sorted(all_tags))
|
||||||
|
|
||||||
for etudid in etudids:
|
for etudid in etudids:
|
||||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||||
# retrouve le "dernier" semestre (au sens de la date de fin)
|
# retrouve le "dernier" semestre (au sens de la date de fin)
|
||||||
|
@ -152,10 +159,14 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||||
"sem_ident": "%s %s"
|
"sem_ident": "%s %s"
|
||||||
% (last_sem["date_debut_iso"], last_sem["titre"]), # utile pour tris
|
% (last_sem["date_debut_iso"], last_sem["titre"]), # utile pour tris
|
||||||
}
|
}
|
||||||
|
|
||||||
# recherche des débouchés
|
# recherche des débouchés
|
||||||
debouche = itemsuivi_list_etud(etudid) # liste de plusieurs items
|
debouche = itemsuivi_etuds[etudid] # liste de plusieurs items
|
||||||
if debouche:
|
if debouche:
|
||||||
|
if keep_numeric: # pour excel:
|
||||||
|
row["debouche"] = "\n".join(
|
||||||
|
f"""{it["item_date"]}: {it["situation"]}""" for it in debouche
|
||||||
|
)
|
||||||
|
else:
|
||||||
row["debouche"] = "<br>".join(
|
row["debouche"] = "<br>".join(
|
||||||
[
|
[
|
||||||
str(it["item_date"])
|
str(it["item_date"])
|
||||||
|
@ -166,11 +177,15 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||||
+ "</i>"
|
+ "</i>"
|
||||||
for it in debouche
|
for it in debouche
|
||||||
]
|
]
|
||||||
) #
|
)
|
||||||
|
for it in debouche:
|
||||||
|
for tag in it["tags"].split(","):
|
||||||
|
tag = tag.strip()
|
||||||
|
row[f"tag_{tag}"] = tag
|
||||||
else:
|
else:
|
||||||
row["debouche"] = "non renseigné"
|
row["debouche"] = "non renseigné"
|
||||||
L.append(row)
|
rows.append(row)
|
||||||
L.sort(key=lambda x: x["sem_ident"])
|
rows.sort(key=lambda x: x["sem_ident"])
|
||||||
|
|
||||||
titles = {
|
titles = {
|
||||||
"civilite": "",
|
"civilite": "",
|
||||||
|
@ -184,8 +199,7 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||||
"effectif": "Eff.",
|
"effectif": "Eff.",
|
||||||
"debouche": "Débouché",
|
"debouche": "Débouché",
|
||||||
}
|
}
|
||||||
tab = GenTable(
|
columns_ids = [
|
||||||
columns_ids=(
|
|
||||||
"semestre",
|
"semestre",
|
||||||
"semestre_id",
|
"semestre_id",
|
||||||
"periode",
|
"periode",
|
||||||
|
@ -196,13 +210,19 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||||
"rang",
|
"rang",
|
||||||
"effectif",
|
"effectif",
|
||||||
"debouche",
|
"debouche",
|
||||||
),
|
]
|
||||||
|
for tag in all_tags:
|
||||||
|
titles[f"tag_{tag}"] = tag
|
||||||
|
columns_ids.append(f"tag_{tag}")
|
||||||
|
tab = GenTable(
|
||||||
|
columns_ids=columns_ids,
|
||||||
titles=titles,
|
titles=titles,
|
||||||
rows=L,
|
rows=rows,
|
||||||
# html_col_width='4em',
|
# html_col_width='4em',
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
|
@ -28,272 +28,177 @@
|
||||||
"""Page accueil département (liste des semestres, etc)
|
"""Page accueil département (liste des semestres, etc)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import g
|
from sqlalchemy import desc
|
||||||
from flask import url_for
|
from flask import g, url_for, render_template
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import log
|
from app import log
|
||||||
from app.models import ScolarNews
|
from app.models import FormSemestre, ScolarNews, ScoDocSiteConfig
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc import html_sco_header
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
|
||||||
from app.scodoc import sco_modalites
|
from app.scodoc import sco_modalites
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_users
|
from app.scodoc import sco_users
|
||||||
|
from app.views import ScoData
|
||||||
|
|
||||||
|
|
||||||
def index_html(showcodes=0, showsemtable=0):
|
def index_html(showcodes=0, showsemtable=0, export_table_formsemestres=False):
|
||||||
"Page accueil département (liste des semestres)"
|
"Page accueil département (liste des semestres)"
|
||||||
showcodes = int(showcodes)
|
showcodes = int(showcodes)
|
||||||
showsemtable = int(showsemtable)
|
showsemtable = int(showsemtable) or export_table_formsemestres
|
||||||
H = []
|
|
||||||
|
|
||||||
# News:
|
# Liste tous les formsemestres du dept, le plus récent d'abord
|
||||||
H.append(ScolarNews.scolar_news_summary_html())
|
current_formsemestres = (
|
||||||
|
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id, etat=True)
|
||||||
|
.filter(FormSemestre.modalite != "EXT")
|
||||||
|
.order_by(desc(FormSemestre.date_debut))
|
||||||
|
)
|
||||||
|
locked_formsemestres = (
|
||||||
|
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id, etat=False)
|
||||||
|
.filter(FormSemestre.modalite != "EXT")
|
||||||
|
.order_by(desc(FormSemestre.date_debut))
|
||||||
|
)
|
||||||
|
formsemestres = (
|
||||||
|
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
.filter(FormSemestre.modalite != "EXT")
|
||||||
|
.order_by(desc(FormSemestre.date_debut))
|
||||||
|
)
|
||||||
|
if showsemtable: # table de tous les formsemestres
|
||||||
|
table = _sem_table_gt(
|
||||||
|
formsemestres,
|
||||||
|
showcodes=showcodes,
|
||||||
|
fmt="xlsx" if export_table_formsemestres else "html",
|
||||||
|
)
|
||||||
|
if export_table_formsemestres:
|
||||||
|
return table # cas spécial: on renvoie juste cette table
|
||||||
|
html_table_formsemestres = table.html()
|
||||||
|
else:
|
||||||
|
html_table_formsemestres = None
|
||||||
|
|
||||||
# Avertissement de mise à jour:
|
current_formsemestres_by_modalite, modalites = (
|
||||||
H.append("""<div id="update_warning"></div>""")
|
sco_modalites.group_formsemestres_by_modalite(current_formsemestres)
|
||||||
|
)
|
||||||
|
passerelle_disabled = ScoDocSiteConfig.is_passerelle_disabled()
|
||||||
|
return render_template(
|
||||||
|
"scolar/index.j2",
|
||||||
|
current_user=current_user,
|
||||||
|
current_formsemestres=current_formsemestres,
|
||||||
|
current_formsemestres_by_modalite=current_formsemestres_by_modalite,
|
||||||
|
dept_name=sco_preferences.get_preference("DeptName"),
|
||||||
|
emptygroupicon=scu.icontag(
|
||||||
|
"emptygroupicon_img", title="Pas d'inscrits", border="0"
|
||||||
|
),
|
||||||
|
formsemestres=formsemestres,
|
||||||
|
groupicon=scu.icontag("groupicon_img", title="Inscrits", border="0"),
|
||||||
|
html_table_formsemestres=html_table_formsemestres,
|
||||||
|
icon_hidden="" if passerelle_disabled else scu.ICON_HIDDEN,
|
||||||
|
icon_published="" if passerelle_disabled else scu.ICON_PUBLISHED,
|
||||||
|
locked_formsemestres=locked_formsemestres,
|
||||||
|
modalites=modalites,
|
||||||
|
nb_locked=locked_formsemestres.count(),
|
||||||
|
nb_user_accounts=sco_users.get_users_count(dept=g.scodoc_dept),
|
||||||
|
page_title=f"ScoDoc {g.scodoc_dept}",
|
||||||
|
Permission=Permission,
|
||||||
|
scolar_news_summary=ScolarNews.scolar_news_summary_html(),
|
||||||
|
showcodes=showcodes,
|
||||||
|
showsemtable=showsemtable,
|
||||||
|
sco=ScoData(),
|
||||||
|
)
|
||||||
|
|
||||||
# Liste de toutes les sessions:
|
|
||||||
sems = sco_formsemestre.do_formsemestre_list()
|
def _convert_formsemestres_to_dicts(
|
||||||
cursems = [] # semestres "courants"
|
formsemestres: Query, showcodes: bool, fmt: str = "html"
|
||||||
othersems = [] # autres (verrouillés)
|
) -> list[dict]:
|
||||||
# icon image:
|
""" """
|
||||||
|
if fmt == "html":
|
||||||
|
# icon images:
|
||||||
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
|
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
|
||||||
emptygroupicon = scu.icontag(
|
emptygroupicon = scu.icontag(
|
||||||
"emptygroupicon_img", title="Pas d'inscrits", border="0"
|
"emptygroupicon_img", title="Pas d'inscrits", border="0"
|
||||||
)
|
)
|
||||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||||
# Sélection sur l'etat du semestre
|
|
||||||
for sem in sems:
|
|
||||||
if sem["etat"] and sem["modalite"] != "EXT":
|
|
||||||
sem["lockimg"] = ""
|
|
||||||
cursems.append(sem)
|
|
||||||
else:
|
else:
|
||||||
sem["lockimg"] = lockicon
|
groupicon = "X"
|
||||||
othersems.append(sem)
|
emptygroupicon = ""
|
||||||
# Responsable de formation:
|
lockicon = "X"
|
||||||
sco_formsemestre.sem_set_responsable_name(sem)
|
# génère liste de dict
|
||||||
|
sems = []
|
||||||
if showcodes:
|
formsemestre: FormSemestre
|
||||||
sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
|
for formsemestre in formsemestres:
|
||||||
else:
|
nb_inscrits = len(formsemestre.inscriptions)
|
||||||
sem["tmpcode"] = ""
|
formation = formsemestre.formation
|
||||||
# Nombre d'inscrits:
|
sem = {
|
||||||
args = {"formsemestre_id": sem["formsemestre_id"]}
|
"anneescolaire": formsemestre.annee_scolaire(),
|
||||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args)
|
"anneescolaire_str": formsemestre.annee_scolaire_str(),
|
||||||
nb = len(ins) # nb etudiants
|
"bul_hide_xml": formsemestre.bul_hide_xml,
|
||||||
sem["nb_inscrits"] = nb
|
"dateord": formsemestre.date_debut,
|
||||||
if nb > 0:
|
"elt_annee_apo": formsemestre.elt_annee_apo,
|
||||||
sem["groupicon"] = groupicon
|
"elt_sem_apo": formsemestre.elt_sem_apo,
|
||||||
else:
|
"etapes_apo_str": formsemestre.etapes_apo_str(),
|
||||||
sem["groupicon"] = emptygroupicon
|
"formation": f"{formation.acronyme} v{formation.version}",
|
||||||
|
"_formation_target": url_for(
|
||||||
# S'il n'y a pas d'utilisateurs dans la base, affiche message
|
"notes.ue_table",
|
||||||
if not sco_users.get_users_count(dept=g.scodoc_dept):
|
scodoc_dept=g.scodoc_dept,
|
||||||
H.append(
|
formation_id=formation.id,
|
||||||
"""<h2>Aucun utilisateur défini !</h2><p>Pour définir des utilisateurs
|
semestre_idx=formsemestre.semestre_id,
|
||||||
<a href="Users">passez par la page Utilisateurs</a>.
|
),
|
||||||
<br>
|
"formsemestre_id": formsemestre.id,
|
||||||
Définissez au moins un utilisateur avec le rôle AdminXXX
|
"groupicon": groupicon if nb_inscrits > 0 else emptygroupicon,
|
||||||
(le responsable du département XXX).
|
"lockimg": "" if formsemestre.etat else lockicon,
|
||||||
</p>
|
"modalite": formsemestre.modalite,
|
||||||
"""
|
"mois_debut": formsemestre.mois_debut(),
|
||||||
)
|
"mois_fin": formsemestre.mois_fin(),
|
||||||
|
"nb_inscrits": nb_inscrits,
|
||||||
# Liste des formsemestres "courants"
|
"responsable_name": formsemestre.responsables_str(),
|
||||||
if cursems:
|
"semestre_id": formsemestre.semestre_id,
|
||||||
H.append('<h2 class="listesems">Sessions en cours</h2>')
|
"session_id": formsemestre.session_id(),
|
||||||
H.append(_sem_table(cursems))
|
"titre_num": formsemestre.titre_num(),
|
||||||
else:
|
"tmpcode": (f"<td><tt>{formsemestre.id}</tt></td>" if showcodes else ""),
|
||||||
# aucun semestre courant: affiche aide
|
}
|
||||||
H.append(
|
sems.append(sem)
|
||||||
"""<h2 class="listesems">Aucune session en cours !</h2>
|
return sems
|
||||||
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
|
|
||||||
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
|
|
||||||
</p><p>
|
|
||||||
Là, en bas de page, suivez le lien
|
|
||||||
"<em>Mettre en place un nouveau semestre de formation...</em>"
|
|
||||||
</p>"""
|
|
||||||
)
|
|
||||||
|
|
||||||
if showsemtable:
|
|
||||||
H.append(
|
|
||||||
f"""<hr>
|
|
||||||
<h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
|
|
||||||
H.append("</table>")
|
|
||||||
if not showsemtable:
|
|
||||||
H.append(
|
|
||||||
f"""<hr>
|
|
||||||
<p><a class="stdlink" href="{url_for('scolar.index_html',
|
|
||||||
scodoc_dept=g.scodoc_dept, showsemtable=1)
|
|
||||||
}">Voir table des semestres (dont {len(othersems)}
|
|
||||||
verrouillé{'s' if len(othersems) else ''})</a>
|
|
||||||
</p>"""
|
|
||||||
)
|
|
||||||
|
|
||||||
H.append(
|
|
||||||
f"""<p>
|
|
||||||
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
|
|
||||||
Chercher étape courante:
|
|
||||||
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
|
|
||||||
</form>
|
|
||||||
</p>"""
|
|
||||||
)
|
|
||||||
#
|
|
||||||
H.append(
|
|
||||||
"""<hr>
|
|
||||||
<h3>Gestion des étudiants</h3>
|
|
||||||
<ul>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
if current_user.has_permission(Permission.EtudInscrit):
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
<li><a class="stdlink" href="{
|
|
||||||
url_for("scolar.etudident_create_form", scodoc_dept=g.scodoc_dept)
|
|
||||||
}">créer <em>un</em> nouvel étudiant</a>
|
|
||||||
</li>
|
|
||||||
<li><a class="stdlink" href="{
|
|
||||||
url_for("scolar.form_students_import_excel", scodoc_dept=g.scodoc_dept)
|
|
||||||
}">importer de nouveaux étudiants</a>
|
|
||||||
(<em>ne pas utiliser</em> sauf cas particulier : utilisez plutôt le lien dans
|
|
||||||
le tableau de bord semestre si vous souhaitez inscrire les
|
|
||||||
étudiants importés à un semestre)
|
|
||||||
</li>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
<li><a class="stdlink" href="{
|
|
||||||
url_for("scolar.export_etudiants_courants", scodoc_dept=g.scodoc_dept)
|
|
||||||
}">exporter tableau des étudiants des semestres en cours</a>
|
|
||||||
</li>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
if current_user.has_permission(
|
|
||||||
Permission.EtudInscrit
|
|
||||||
) and sco_preferences.get_preference("portal_url"):
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
<li><a class="stdlink" href="{
|
|
||||||
url_for("scolar.formsemestre_import_etud_admission",
|
|
||||||
scodoc_dept=g.scodoc_dept, tous_courants=1)
|
|
||||||
}">resynchroniser les données étudiants des semestres en cours depuis le portail</a>
|
|
||||||
</li>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
H.append("</ul>")
|
|
||||||
#
|
|
||||||
if current_user.has_permission(Permission.EditApogee):
|
|
||||||
H.append(
|
|
||||||
f"""<hr>
|
|
||||||
<h3>Exports Apogée</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
|
|
||||||
}">Années scolaires / exports Apogée</a></li>
|
|
||||||
</ul>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
#
|
|
||||||
H.append(
|
|
||||||
"""<hr>
|
|
||||||
<h3>Assistance</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a></li>
|
|
||||||
</ul>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
#
|
|
||||||
return (
|
|
||||||
html_sco_header.sco_header(
|
|
||||||
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
|
|
||||||
)
|
|
||||||
+ "\n".join(H)
|
|
||||||
+ html_sco_header.sco_footer()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _sem_table(sems):
|
def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable:
|
||||||
"""Affiche liste des semestres, utilisée pour semestres en cours"""
|
"""Table des semestres
|
||||||
tmpl = """<tr class="%(trclass)s">%(tmpcode)s
|
|
||||||
<td class="semicon">%(lockimg)s <a href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
|
|
||||||
<td class="datesem">%(mois_debut)s <a title="%(session_id)s">-</a> %(mois_fin)s</td>
|
|
||||||
<td><a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
|
|
||||||
<span class="respsem">(%(responsable_name)s)</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Liste des semestres, groupés par modalités
|
|
||||||
sems_by_mod, modalites = sco_modalites.group_sems_by_modalite(sems)
|
|
||||||
|
|
||||||
H = ['<table class="listesems">']
|
|
||||||
for modalite in modalites:
|
|
||||||
if len(modalites) > 1:
|
|
||||||
H.append('<tr><th colspan="3">%s</th></tr>' % modalite["titre"])
|
|
||||||
|
|
||||||
if sems_by_mod[modalite["modalite"]]:
|
|
||||||
cur_idx = sems_by_mod[modalite["modalite"]][0]["semestre_id"]
|
|
||||||
for sem in sems_by_mod[modalite["modalite"]]:
|
|
||||||
if cur_idx != sem["semestre_id"]:
|
|
||||||
sem["trclass"] = "firstsem" # separe les groupes de semestres
|
|
||||||
cur_idx = sem["semestre_id"]
|
|
||||||
else:
|
|
||||||
sem["trclass"] = ""
|
|
||||||
sem["notes_url"] = scu.NotesURL()
|
|
||||||
H.append(tmpl % sem)
|
|
||||||
H.append("</table>")
|
|
||||||
return "\n".join(H)
|
|
||||||
|
|
||||||
|
|
||||||
def _sem_table_gt(sems, showcodes=False):
|
|
||||||
"""Nouvelle version de la table des semestres
|
|
||||||
Utilise une datatables.
|
Utilise une datatables.
|
||||||
"""
|
"""
|
||||||
_style_sems(sems)
|
sems = _style_sems(
|
||||||
columns_ids = (
|
_convert_formsemestres_to_dicts(formsemestres, showcodes, fmt=fmt), fmt=fmt
|
||||||
"lockimg",
|
)
|
||||||
|
sems.sort(
|
||||||
|
key=lambda s: (
|
||||||
|
-s["anneescolaire"],
|
||||||
|
s["semestre_id"] if s["semestre_id"] > 0 else -s["semestre_id"] * 1000,
|
||||||
|
s["modalite"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
columns_ids = ["lockimg"]
|
||||||
|
if not ScoDocSiteConfig.is_passerelle_disabled():
|
||||||
|
columns_ids.append("published")
|
||||||
|
columns_ids += [
|
||||||
|
"dash_mois_fin",
|
||||||
"semestre_id_n",
|
"semestre_id_n",
|
||||||
"modalite",
|
"modalite",
|
||||||
#'mois_debut',
|
|
||||||
"dash_mois_fin",
|
|
||||||
"titre_resp",
|
"titre_resp",
|
||||||
"nb_inscrits",
|
"nb_inscrits",
|
||||||
|
"formation",
|
||||||
"etapes_apo_str",
|
"etapes_apo_str",
|
||||||
"elt_annee_apo",
|
"elt_annee_apo",
|
||||||
"elt_sem_apo",
|
"elt_sem_apo",
|
||||||
)
|
]
|
||||||
if showcodes:
|
if showcodes:
|
||||||
columns_ids = ("formsemestre_id",) + columns_ids
|
columns_ids.insert(0, "formsemestre_id") # prepend
|
||||||
|
|
||||||
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
|
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
|
||||||
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(
|
||||||
titles={
|
|
||||||
"formsemestre_id": "id",
|
|
||||||
"semestre_id_n": "S#",
|
|
||||||
"modalite": "",
|
|
||||||
"mois_debut": "Début",
|
|
||||||
"dash_mois_fin": "Année",
|
|
||||||
"titre_resp": "Semestre",
|
|
||||||
"nb_inscrits": "N",
|
|
||||||
"etapes_apo_str": "Étape Apo.",
|
|
||||||
"elt_annee_apo": "Elt. année Apo.",
|
|
||||||
"elt_sem_apo": "Elt. sem. Apo.",
|
|
||||||
},
|
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
rows=sems,
|
|
||||||
table_id="semlist",
|
|
||||||
html_class_ignore_default=True,
|
html_class_ignore_default=True,
|
||||||
html_class=html_class,
|
html_class=html_class,
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
|
@ -304,27 +209,59 @@ def _sem_table_gt(sems, showcodes=False):
|
||||||
""",
|
""",
|
||||||
html_with_td_classes=True,
|
html_with_td_classes=True,
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
rows=sems,
|
||||||
|
titles={
|
||||||
|
"formsemestre_id": "id",
|
||||||
|
"semestre_id_n": "S#",
|
||||||
|
"modalite": "" if fmt == "html" else "Modalité",
|
||||||
|
"mois_debut": "Début",
|
||||||
|
"dash_mois_fin": "Année",
|
||||||
|
"titre_resp": "Semestre",
|
||||||
|
"nb_inscrits": "N",
|
||||||
|
"etapes_apo_str": "Étape Apo.",
|
||||||
|
"elt_annee_apo": "Elt. année Apo.",
|
||||||
|
"elt_sem_apo": "Elt. sem. Apo.",
|
||||||
|
"formation": "Formation",
|
||||||
|
},
|
||||||
|
table_id="semlist",
|
||||||
)
|
)
|
||||||
|
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
||||||
def _style_sems(sems):
|
def _style_sems(sems: list[dict], fmt="html") -> list[dict]:
|
||||||
"""ajoute quelques attributs de présentation pour la table"""
|
"""ajoute quelques attributs de présentation pour la table"""
|
||||||
|
is_h = fmt == "html"
|
||||||
|
if is_h:
|
||||||
|
icon_published = scu.ICON_PUBLISHED
|
||||||
|
icon_hidden = scu.ICON_HIDDEN
|
||||||
|
else:
|
||||||
|
icon_published = "publié"
|
||||||
|
icon_hidden = "non publié"
|
||||||
for sem in sems:
|
for sem in sems:
|
||||||
sem["notes_url"] = scu.NotesURL()
|
status_url = url_for(
|
||||||
sem["_groupicon_target"] = (
|
"notes.formsemestre_status",
|
||||||
"%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
|
scodoc_dept=g.scodoc_dept,
|
||||||
% sem
|
formsemestre_id=sem["formsemestre_id"],
|
||||||
)
|
)
|
||||||
|
sem["_groupicon_target"] = status_url
|
||||||
sem["_formsemestre_id_class"] = "blacktt"
|
sem["_formsemestre_id_class"] = "blacktt"
|
||||||
sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
|
sem["dash_mois_fin"] = (
|
||||||
|
(f"""<a title="{sem['session_id']}">{sem['anneescolaire_str']}</a>""")
|
||||||
|
if is_h
|
||||||
|
else sem["anneescolaire_str"]
|
||||||
|
)
|
||||||
sem["_dash_mois_fin_class"] = "datesem"
|
sem["_dash_mois_fin_class"] = "datesem"
|
||||||
sem["titre_resp"] = (
|
sem["titre_resp"] = (
|
||||||
"""<a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
|
(
|
||||||
<span class="respsem">(%(responsable_name)s)</span>"""
|
f"""<a class="stdlink" href="{status_url}">{sem['titre_num']}</a>
|
||||||
% sem
|
<span class="respsem">({sem['responsable_name']})</span>"""
|
||||||
)
|
)
|
||||||
|
if is_h
|
||||||
|
else f"""{sem['titre_num']} ({sem["responsable_name"]})"""
|
||||||
|
)
|
||||||
|
sem["published"] = icon_hidden if sem["bul_hide_xml"] else icon_published
|
||||||
|
|
||||||
sem["_css_row_class"] = "css_S%d css_M%s" % (
|
sem["_css_row_class"] = "css_S%d css_M%s" % (
|
||||||
sem["semestre_id"],
|
sem["semestre_id"],
|
||||||
sem["modalite"],
|
sem["modalite"],
|
||||||
|
@ -345,6 +282,7 @@ def _style_sems(sems):
|
||||||
sem["_elt_sem_apo_td_attrs"] = (
|
sem["_elt_sem_apo_td_attrs"] = (
|
||||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
|
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
|
||||||
)
|
)
|
||||||
|
return sems
|
||||||
|
|
||||||
|
|
||||||
def delete_dept(dept_id: int) -> str:
|
def delete_dept(dept_id: int) -> str:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -58,21 +58,20 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||||
html_sco_header.sco_header(page_title="Suppression d'une formation"),
|
html_sco_header.sco_header(page_title="Suppression d'une formation"),
|
||||||
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
|
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
|
||||||
]
|
]
|
||||||
|
formsemestres = formation.formsemestres.all()
|
||||||
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id})
|
if formsemestres:
|
||||||
if sems:
|
|
||||||
H.append(
|
H.append(
|
||||||
"""<p class="warning">Impossible de supprimer cette formation,
|
"""<p class="warning">Impossible de supprimer cette formation,
|
||||||
car les sessions suivantes l'utilisent:</p>
|
car les sessions suivantes l'utilisent:</p>
|
||||||
<ul>"""
|
<ul>"""
|
||||||
)
|
)
|
||||||
for sem in sems:
|
for formsemestre in formsemestres:
|
||||||
|
H.append(f"""<li>{formsemestre.html_link_status()}</li>""")
|
||||||
H.append(
|
H.append(
|
||||||
'<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
|
f"""</ul>
|
||||||
% sem
|
<p><a class="stdlink" href="{
|
||||||
)
|
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||||
H.append(
|
}">Revenir</a></p>"""
|
||||||
'</ul><p><a class="stdlink" href="%s">Revenir</a></p>' % scu.NotesURL()
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
|
@ -85,14 +84,16 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||||
</p>
|
</p>
|
||||||
""",
|
""",
|
||||||
OK="Supprimer cette formation",
|
OK="Supprimer cette formation",
|
||||||
cancel_url=scu.NotesURL(),
|
cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
|
||||||
parameters={"formation_id": formation_id},
|
parameters={"formation_id": formation_id},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
do_formation_delete(formation_id)
|
do_formation_delete(formation_id)
|
||||||
H.append(
|
H.append(
|
||||||
f"""<p>OK, formation supprimée.</p>
|
f"""<p>OK, formation supprimée.</p>
|
||||||
<p><a class="stdlink" href="{scu.NotesURL()}">continuer</a></p>"""
|
<p><a class="stdlink" href="{
|
||||||
|
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||||
|
}">continuer</a></p>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
H.append(html_sco_header.sco_footer())
|
H.append(html_sco_header.sco_footer())
|
||||||
|
@ -252,7 +253,7 @@ def formation_edit(formation_id=None, create=False):
|
||||||
if tf[0] == 0:
|
if tf[0] == 0:
|
||||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||||
elif tf[0] == -1:
|
elif tf[0] == -1:
|
||||||
return flask.redirect(scu.NotesURL())
|
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
|
||||||
else:
|
else:
|
||||||
# check unicity : constraint UNIQUE(acronyme,titre,version)
|
# check unicity : constraint UNIQUE(acronyme,titre,version)
|
||||||
if create:
|
if create:
|
||||||
|
@ -325,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(
|
||||||
|
|
|
@ -448,7 +448,7 @@ def module_edit(
|
||||||
(
|
(
|
||||||
"titre",
|
"titre",
|
||||||
{
|
{
|
||||||
"size": 30,
|
"size": 64,
|
||||||
"explanation": """nom du module. Exemple:
|
"explanation": """nom du module. Exemple:
|
||||||
<em>Introduction à la démarche ergonomique</em>""",
|
<em>Introduction à la démarche ergonomique</em>""",
|
||||||
},
|
},
|
||||||
|
@ -456,8 +456,8 @@ def module_edit(
|
||||||
(
|
(
|
||||||
"abbrev",
|
"abbrev",
|
||||||
{
|
{
|
||||||
"size": 20,
|
"size": 32,
|
||||||
"explanation": """nom abrégé (pour bulletins).
|
"explanation": """(optionnel) nom abrégé pour bulletins.
|
||||||
Exemple: <em>Intro. à l'ergonomie</em>""",
|
Exemple: <em>Intro. à l'ergonomie</em>""",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -298,27 +298,6 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||||
cursus = formation.get_cursus()
|
cursus = formation.get_cursus()
|
||||||
is_apc = cursus.APC_SAE
|
is_apc = cursus.APC_SAE
|
||||||
semestres_indices = list(range(1, cursus.NB_SEM + 1))
|
semestres_indices = list(range(1, cursus.NB_SEM + 1))
|
||||||
H = [
|
|
||||||
html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"]),
|
|
||||||
"<h2>" + title,
|
|
||||||
f" (formation {formation.acronyme}, version {formation.version})</h2>",
|
|
||||||
"""
|
|
||||||
<p class="help">Les UE sont des groupes de modules dans une formation donnée,
|
|
||||||
utilisés pour la validation (on calcule des moyennes par UE et applique des
|
|
||||||
seuils ("barres")).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
|
|
||||||
Seuls les <em>modules</em> ont des coefficients.
|
|
||||||
</p>""",
|
|
||||||
(
|
|
||||||
f"""
|
|
||||||
<h4>UE du semestre S{ue.semestre_idx}</h4>
|
|
||||||
"""
|
|
||||||
if is_apc and ue
|
|
||||||
else ""
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
ue_types = cursus.ALLOWED_UE_TYPES
|
ue_types = cursus.ALLOWED_UE_TYPES
|
||||||
ue_types.sort()
|
ue_types.sort()
|
||||||
|
@ -489,7 +468,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||||
if ue and is_apc:
|
if ue and is_apc:
|
||||||
ue_parcours_div = apc_edit_ue.form_ue_choix_parcours(ue)
|
ue_parcours_div = apc_edit_ue.form_ue_choix_parcours(ue)
|
||||||
if ue and ue.modules.count() and ue.semestre_idx is not None:
|
if ue and ue.modules.count() and ue.semestre_idx is not None:
|
||||||
modules_div = f"""<div id="ue_list_modules">
|
modules_div = f"""<div class="scobox" id="ue_list_modules">
|
||||||
<div><b>{ue.modules.count()} modules sont rattachés
|
<div><b>{ue.modules.count()} modules sont rattachés
|
||||||
à cette UE</b> du semestre S{ue.semestre_idx},
|
à cette UE</b> du semestre S{ue.semestre_idx},
|
||||||
elle ne peut donc pas être changée de semestre.</div>
|
elle ne peut donc pas être changée de semestre.</div>
|
||||||
|
@ -511,18 +490,34 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
clone_form = ""
|
clone_form = ""
|
||||||
bonus_div = """<div id="bonus_description"></div>"""
|
|
||||||
ue_div = """<div id="ue_list_code" class="sco_box sco_green_bg"></div>"""
|
return f"""
|
||||||
return (
|
{html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"])}
|
||||||
"\n".join(H)
|
<h2>{title}, (formation {formation.acronyme}, version {formation.version})</h2>
|
||||||
+ tf[1]
|
<p class="help">Les UEs sont des groupes de modules dans une formation donnée,
|
||||||
+ clone_form
|
utilisés pour la validation (on calcule des moyennes par UE et applique des
|
||||||
+ ue_parcours_div
|
seuils ("barres")).
|
||||||
+ modules_div
|
</p>
|
||||||
+ bonus_div
|
|
||||||
+ ue_div
|
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
|
||||||
+ html_sco_header.sco_footer()
|
Seuls les <em>modules</em> ont des coefficients.
|
||||||
)
|
</p>
|
||||||
|
|
||||||
|
<div class="scobox">
|
||||||
|
<div class="scobox-title">
|
||||||
|
Édition de l'UE {('du semestre S'+str(ue.semestre_idx)) if is_apc and ue else ''}
|
||||||
|
</div>
|
||||||
|
{tf[1]}
|
||||||
|
</div>
|
||||||
|
{clone_form}
|
||||||
|
{ue_parcours_div}
|
||||||
|
{modules_div}
|
||||||
|
|
||||||
|
<div id="bonus_description"></div>
|
||||||
|
<div id="ue_list_code" class="sco_box sco_green_bg"></div>
|
||||||
|
|
||||||
|
{html_sco_header.sco_footer()}
|
||||||
|
"""
|
||||||
elif tf[0] == 1:
|
elif tf[0] == 1:
|
||||||
if create:
|
if create:
|
||||||
if not tf[2]["ue_code"]:
|
if not tf[2]["ue_code"]:
|
||||||
|
@ -756,7 +751,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||||
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/ue_table.css"],
|
||||||
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
||||||
+ [
|
+ [
|
||||||
"libjs/jinplace-1.2.1.min.js",
|
"libjs/jinplace-1.2.1.min.js",
|
||||||
|
@ -842,8 +837,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||||
<a href="{url_for('notes.refcomp_show',
|
<a href="{url_for('notes.refcomp_show',
|
||||||
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
|
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
|
||||||
class="stdlink">
|
class="stdlink">
|
||||||
{formation.referentiel_competence.type_titre}
|
{formation.referentiel_competence.get_title()}
|
||||||
{formation.referentiel_competence.specialite_long}
|
|
||||||
</a> """
|
</a> """
|
||||||
msg_refcomp = "changer"
|
msg_refcomp = "changer"
|
||||||
H.append(f"""<ul><li>{descr_refcomp}""")
|
H.append(f"""<ul><li>{descr_refcomp}""")
|
||||||
|
@ -1170,14 +1164,17 @@ def _ue_table_ues(
|
||||||
if has_perm_change:
|
if has_perm_change:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a class="stdlink" href="{
|
f"""<a class="stdlink" href="{
|
||||||
url_for("notes.ue_set_internal", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
|
url_for("notes.ue_set_internal",
|
||||||
|
scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
|
||||||
}">transformer en UE ordinaire</a> """
|
}">transformer en UE ordinaire</a> """
|
||||||
)
|
)
|
||||||
H.append("</span>")
|
H.append("</span>")
|
||||||
ue_editable = editable and not ue_is_locked(ue["ue_id"])
|
ue_editable = editable and not ue_is_locked(ue["ue_id"])
|
||||||
if ue_editable:
|
if ue_editable:
|
||||||
H.append(
|
H.append(
|
||||||
'<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % ue
|
f"""<a class="stdlink" href="{
|
||||||
|
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
|
||||||
|
}">modifier</a>"""
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
H.append('<span class="locked">[verrouillé]</span>')
|
H.append('<span class="locked">[verrouillé]</span>')
|
||||||
|
|
|
@ -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
|
||||||
|
@ -478,11 +479,11 @@ def convert_ics(
|
||||||
"heure_deb": event.decoded("dtstart")
|
"heure_deb": event.decoded("dtstart")
|
||||||
.replace(tzinfo=timezone.utc)
|
.replace(tzinfo=timezone.utc)
|
||||||
.astimezone(tz=None)
|
.astimezone(tz=None)
|
||||||
.strftime("%H:%M"),
|
.strftime(scu.TIME_FMT),
|
||||||
"heure_fin": event.decoded("dtend")
|
"heure_fin": event.decoded("dtend")
|
||||||
.replace(tzinfo=timezone.utc)
|
.replace(tzinfo=timezone.utc)
|
||||||
.astimezone(tz=None)
|
.astimezone(tz=None)
|
||||||
.strftime("%H:%M"),
|
.strftime(scu.TIME_FMT),
|
||||||
"jour": event.decoded("dtstart").date().isoformat(),
|
"jour": event.decoded("dtstart").date().isoformat(),
|
||||||
"start": event.decoded("dtstart").isoformat(),
|
"start": event.decoded("dtstart").isoformat(),
|
||||||
"end": event.decoded("dtend").isoformat(),
|
"end": event.decoded("dtend").isoformat(),
|
||||||
|
|
|
@ -452,7 +452,7 @@ def table_apo_csv_list(semset):
|
||||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
|
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
|
||||||
t["filename"] = apo_data.apo_csv.titles["apoC_Fichier_Exp"]
|
t["filename"] = apo_data.apo_csv.titles["apoC_Fichier_Exp"]
|
||||||
t["nb_etuds"] = len(apo_data.etuds)
|
t["nb_etuds"] = len(apo_data.etuds)
|
||||||
t["date_str"] = t["date"].strftime("%d/%m/%Y à %H:%M")
|
t["date_str"] = t["date"].strftime(scu.DATEATIME_FMT)
|
||||||
view_link = url_for(
|
view_link = url_for(
|
||||||
"notes.view_apo_csv",
|
"notes.view_apo_csv",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
@ -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":
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -122,16 +122,14 @@ def format_pays(s):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def etud_sort_key(etud: dict) -> tuple:
|
def etud_sort_key(etud: dict) -> str:
|
||||||
"""Clé de tri pour les étudiants représentés par des dict (anciens codes).
|
"""Clé de tri pour les étudiants représentés par des dict (anciens codes).
|
||||||
Equivalent moderne: identite.sort_key
|
Equivalent moderne: identite.sort_key
|
||||||
"""
|
"""
|
||||||
return (
|
return scu.sanitize_string(
|
||||||
scu.sanitize_string(
|
(etud.get("nom_usuel") or etud["nom"] or "") + ";" + (etud["prenom"] or ""),
|
||||||
etud.get("nom_usuel") or etud["nom"] or "", remove_spaces=False
|
remove_spaces=False,
|
||||||
).lower(),
|
).lower()
|
||||||
scu.sanitize_string(etud["prenom"] or "", remove_spaces=False).lower(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_identiteEditor = ndb.EditableTable(
|
_identiteEditor = ndb.EditableTable(
|
||||||
|
|
|
@ -135,7 +135,7 @@ def evaluation_check_absences_html(
|
||||||
f"""<h2 class="eval_check_absences">{
|
f"""<h2 class="eval_check_absences">{
|
||||||
evaluation.description or "évaluation"
|
evaluation.description or "évaluation"
|
||||||
} du {
|
} du {
|
||||||
evaluation.date_debut.strftime("%d/%m/%Y") if evaluation.date_debut else ""
|
evaluation.date_debut.strftime(scu.DATE_FMT) if evaluation.date_debut else ""
|
||||||
} """
|
} """
|
||||||
]
|
]
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -90,7 +90,7 @@ def evaluation_create_form(
|
||||||
raise ValueError("missing moduleimpl_id parameter")
|
raise ValueError("missing moduleimpl_id parameter")
|
||||||
numeros = [(e.numero or 0) for e in modimpl.evaluations]
|
numeros = [(e.numero or 0) for e in modimpl.evaluations]
|
||||||
initvalues = {
|
initvalues = {
|
||||||
"jour": time.strftime("%d/%m/%Y", time.localtime()),
|
"jour": time.strftime(scu.DATE_FMT, time.localtime()),
|
||||||
"note_max": 20,
|
"note_max": 20,
|
||||||
"numero": (max(numeros) + 1) if numeros else 0,
|
"numero": (max(numeros) + 1) if numeros else 0,
|
||||||
"publish_incomplete": is_malus,
|
"publish_incomplete": is_malus,
|
||||||
|
@ -144,7 +144,7 @@ def evaluation_create_form(
|
||||||
if edit:
|
if edit:
|
||||||
initvalues["blocked"] = evaluation.is_blocked()
|
initvalues["blocked"] = evaluation.is_blocked()
|
||||||
initvalues["blocked_until"] = (
|
initvalues["blocked_until"] = (
|
||||||
evaluation.blocked_until.strftime("%d/%m/%Y")
|
evaluation.blocked_until.strftime(scu.DATE_FMT)
|
||||||
if evaluation.blocked_until
|
if evaluation.blocked_until
|
||||||
and evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
|
and evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
|
||||||
else ""
|
else ""
|
||||||
|
@ -231,7 +231,13 @@ def evaluation_create_form(
|
||||||
{
|
{
|
||||||
"input_type": "boolcheckbox",
|
"input_type": "boolcheckbox",
|
||||||
"title": "Prise en compte immédiate",
|
"title": "Prise en compte immédiate",
|
||||||
"explanation": "notes utilisées même si incomplètes",
|
"explanation": """notes utilisées même si incomplètes (dangereux,
|
||||||
|
à n'utiliser que dans des cas particuliers
|
||||||
|
<a target="_blank" rel="noopener noreferrer"
|
||||||
|
href="https://scodoc.org/Evaluation/#pourquoi-eviter-dutiliser-prise-en-compte-immediate"
|
||||||
|
>voir la documentation</a>
|
||||||
|
)
|
||||||
|
""",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -361,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
|
||||||
)
|
)
|
||||||
|
@ -374,13 +383,7 @@ def evaluation_create_form(
|
||||||
args = tf[2]
|
args = tf[2]
|
||||||
# modifie le codage des dates
|
# modifie le codage des dates
|
||||||
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
|
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
|
||||||
if args.get("jour"):
|
date_debut = scu.convert_fr_date(args["jour"]) if args.get("jour") else None
|
||||||
try:
|
|
||||||
date_debut = datetime.datetime.strptime(args["jour"], "%d/%m/%Y")
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ScoValueError("Date (j/m/a) invalide") from exc
|
|
||||||
else:
|
|
||||||
date_debut = None
|
|
||||||
args["date_debut"] = date_debut
|
args["date_debut"] = date_debut
|
||||||
args["date_fin"] = date_debut # même jour
|
args["date_fin"] = date_debut # même jour
|
||||||
args.pop("jour", None)
|
args.pop("jour", None)
|
||||||
|
@ -405,7 +408,7 @@ def evaluation_create_form(
|
||||||
if args.get("blocked_until"):
|
if args.get("blocked_until"):
|
||||||
try:
|
try:
|
||||||
args["blocked_until"] = datetime.datetime.strptime(
|
args["blocked_until"] = datetime.datetime.strptime(
|
||||||
args["blocked_until"], "%d/%m/%Y"
|
args["blocked_until"], scu.DATE_FMT
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc
|
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc
|
||||||
|
|
|
@ -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": "",
|
||||||
|
@ -126,13 +129,16 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||||
evaluation_id=evaluation_id,
|
evaluation_id=evaluation_id,
|
||||||
),
|
),
|
||||||
"_titre_target_attrs": 'class="discretelink"',
|
"_titre_target_attrs": 'class="discretelink"',
|
||||||
"date": e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "",
|
"date": e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "",
|
||||||
"_date_order": e.date_debut.isoformat() if e.date_debut else "",
|
"_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": 'class="bull_link" title="prise en compte dans les moyennes"'
|
"_complete_target_attrs": (
|
||||||
|
'class="bull_link" title="prise en compte dans les moyennes"'
|
||||||
if eval_etat.is_complete
|
if eval_etat.is_complete
|
||||||
else 'class="bull_link incomplete" title="il manque des notes"',
|
else 'class="bull_link incomplete" title="il manque des notes"'
|
||||||
|
),
|
||||||
"manquantes": len(modimpl_results.evals_etudids_sans_note[e.id]),
|
"manquantes": len(modimpl_results.evals_etudids_sans_note[e.id]),
|
||||||
"inscrits": modimpl_results.nb_inscrits_module,
|
"inscrits": modimpl_results.nb_inscrits_module,
|
||||||
"nb_abs": sum(modimpl_results.evals_notes[e.id] == scu.NOTES_ABSENCE),
|
"nb_abs": sum(modimpl_results.evals_notes[e.id] == scu.NOTES_ABSENCE),
|
||||||
|
|
|
@ -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
|
||||||
|
@ -279,11 +280,18 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
|
||||||
nb_eval_completes (= prises en compte)
|
nb_eval_completes (= prises en compte)
|
||||||
nb_evals_en_cours (= avec des notes, mais pas complete)
|
nb_evals_en_cours (= avec des notes, mais pas complete)
|
||||||
nb_evals_vides (= sans aucune note)
|
nb_evals_vides (= sans aucune note)
|
||||||
|
nb_evals_attente (= avec des notes en ATTente et pas bloquée)
|
||||||
date derniere modif
|
date derniere modif
|
||||||
|
|
||||||
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
|
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
|
||||||
"""
|
"""
|
||||||
nb_evals_completes, nb_evals_en_cours, nb_evals_vides, nb_evals_blocked = 0, 0, 0, 0
|
(
|
||||||
|
nb_evals_completes,
|
||||||
|
nb_evals_en_cours,
|
||||||
|
nb_evals_vides,
|
||||||
|
nb_evals_blocked,
|
||||||
|
nb_evals_attente,
|
||||||
|
) = (0, 0, 0, 0, 0)
|
||||||
dates = []
|
dates = []
|
||||||
for e in etat_evals:
|
for e in etat_evals:
|
||||||
if e["etat"]["blocked"]:
|
if e["etat"]["blocked"]:
|
||||||
|
@ -294,6 +302,8 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
|
||||||
nb_evals_vides += 1
|
nb_evals_vides += 1
|
||||||
elif not e["etat"]["blocked"]:
|
elif not e["etat"]["blocked"]:
|
||||||
nb_evals_en_cours += 1
|
nb_evals_en_cours += 1
|
||||||
|
if e["etat"]["nb_attente"] and not e["etat"]["blocked"]:
|
||||||
|
nb_evals_attente += 1
|
||||||
last_modif = e["etat"]["last_modif"]
|
last_modif = e["etat"]["last_modif"]
|
||||||
if last_modif is not None:
|
if last_modif is not None:
|
||||||
dates.append(e["etat"]["last_modif"])
|
dates.append(e["etat"]["last_modif"])
|
||||||
|
@ -303,6 +313,7 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"nb_evals": len(etat_evals),
|
"nb_evals": len(etat_evals),
|
||||||
|
"nb_evals_attente": nb_evals_attente,
|
||||||
"nb_evals_blocked": nb_evals_blocked,
|
"nb_evals_blocked": nb_evals_blocked,
|
||||||
"nb_evals_completes": nb_evals_completes,
|
"nb_evals_completes": nb_evals_completes,
|
||||||
"nb_evals_en_cours": nb_evals_en_cours,
|
"nb_evals_en_cours": nb_evals_en_cours,
|
||||||
|
@ -350,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)
|
||||||
|
@ -358,56 +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("%Hh%M") if e.date_debut else "?"
|
|
||||||
heure_fin_txt = e.date_fin.strftime("%Hh%M") if e.date_fin else "?"
|
|
||||||
|
|
||||||
description = f"""{
|
|
||||||
e.moduleimpl.module.titre
|
|
||||||
}, de {heure_debut_txt} à {heure_fin_txt}"""
|
|
||||||
|
|
||||||
# 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"""
|
||||||
{
|
{
|
||||||
|
@ -423,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>
|
||||||
|
@ -516,7 +580,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
||||||
"date_first_complete": date_first_complete,
|
"date_first_complete": date_first_complete,
|
||||||
"delai_correction": delai_correction,
|
"delai_correction": delai_correction,
|
||||||
"jour": (
|
"jour": (
|
||||||
e.date_debut.strftime("%d/%m/%Y") if e.date_debut else "sans date"
|
e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "sans date"
|
||||||
),
|
),
|
||||||
"_jour_target": url_for(
|
"_jour_target": url_for(
|
||||||
"notes.evaluation_listenotes",
|
"notes.evaluation_listenotes",
|
||||||
|
@ -529,7 +593,9 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
moduleimpl_id=e.moduleimpl.id,
|
moduleimpl_id=e.moduleimpl.id,
|
||||||
),
|
),
|
||||||
"module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre,
|
"module_titre": e.moduleimpl.module.abbrev
|
||||||
|
or e.moduleimpl.module.titre
|
||||||
|
or "",
|
||||||
"responsable_id": e.moduleimpl.responsable_id,
|
"responsable_id": e.moduleimpl.responsable_id,
|
||||||
"responsable_nomplogin": sco_users.user_info(
|
"responsable_nomplogin": sco_users.user_info(
|
||||||
e.moduleimpl.responsable_id
|
e.moduleimpl.responsable_id
|
||||||
|
@ -567,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)
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ def xldate_as_datetime(xldate, datemode=0):
|
||||||
Peut lever une ValueError
|
Peut lever une ValueError
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return datetime.datetime.strptime(xldate, "%d/%m/%Y")
|
return datetime.datetime.strptime(xldate, scu.DATE_FMT)
|
||||||
except:
|
except:
|
||||||
return openpyxl.utils.datetime.from_ISO8601(xldate)
|
return openpyxl.utils.datetime.from_ISO8601(xldate)
|
||||||
|
|
||||||
|
|
|
@ -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 où 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):
|
||||||
|
@ -103,7 +107,7 @@ class ScoPDFFormatError(ScoValueError):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
f"""Erreur dans un format pdf:
|
f"""Erreur dans un format pdf:
|
||||||
<p>{msg}</p>
|
<p>{msg}</p>
|
||||||
<p>Vérifiez les paramètres (polices de caractères, balisage)
|
<p>Vérifiez les paramètres (polices de caractères, balisage, réglages bulletins...)
|
||||||
dans les paramètres ou préférences.
|
dans les paramètres ou préférences.
|
||||||
</p>
|
</p>
|
||||||
""",
|
""",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -489,9 +489,10 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||||
return formation.id, modules_old2new, ues_old2new
|
return formation.id, modules_old2new, ues_old2new
|
||||||
|
|
||||||
|
|
||||||
def formation_list_table() -> GenTable:
|
def formation_list_table(detail: bool) -> GenTable:
|
||||||
"""List formation, grouped by titre and sorted by versions
|
"""List formation, grouped by titre and sorted by versions
|
||||||
and listing associated semestres
|
and listing associated semestres.
|
||||||
|
If detail, add column with more details.
|
||||||
returns a table
|
returns a table
|
||||||
"""
|
"""
|
||||||
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
|
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
@ -507,6 +508,7 @@ def formation_list_table() -> GenTable:
|
||||||
)
|
)
|
||||||
|
|
||||||
editable = current_user.has_permission(Permission.EditFormation)
|
editable = current_user.has_permission(Permission.EditFormation)
|
||||||
|
can_implement = current_user.has_permission(Permission.EditFormSemestre)
|
||||||
|
|
||||||
# Traduit/ajoute des champs à afficher:
|
# Traduit/ajoute des champs à afficher:
|
||||||
rows = []
|
rows = []
|
||||||
|
@ -527,6 +529,21 @@ def formation_list_table() -> GenTable:
|
||||||
"_titre_id": f"""titre-{acronyme_no_spaces}""",
|
"_titre_id": f"""titre-{acronyme_no_spaces}""",
|
||||||
"version": formation.version or 0,
|
"version": formation.version or 0,
|
||||||
"commentaire": formation.commentaire or "",
|
"commentaire": formation.commentaire or "",
|
||||||
|
"referentiel": (
|
||||||
|
f"""{formation.referentiel_competence.specialite} {
|
||||||
|
formation.referentiel_competence.get_version()}"""
|
||||||
|
if formation.referentiel_competence
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"_referentiel_target": (
|
||||||
|
url_for(
|
||||||
|
"notes.refcomp_show",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
refcomp_id=formation.referentiel_competence.id,
|
||||||
|
)
|
||||||
|
if formation.referentiel_competence
|
||||||
|
else ""
|
||||||
|
),
|
||||||
}
|
}
|
||||||
# Ajoute les semestres associés à chaque formation:
|
# Ajoute les semestres associés à chaque formation:
|
||||||
row["formsemestres"] = formation.formsemestres.order_by(
|
row["formsemestres"] = formation.formsemestres.order_by(
|
||||||
|
@ -541,20 +558,28 @@ def formation_list_table() -> GenTable:
|
||||||
)}">{s.session_id()}</a>"""
|
)}">{s.session_id()}</a>"""
|
||||||
for s in row["formsemestres"]
|
for s in row["formsemestres"]
|
||||||
]
|
]
|
||||||
+ [
|
+ (
|
||||||
f"""<a class="stdlink" id="add-semestre-{
|
[
|
||||||
formation.acronyme.lower().replace(" ", "-")}"
|
f"""<a class="stdlink"
|
||||||
href="{ url_for("notes.formsemestre_createwithmodules",
|
href="{ url_for("notes.formsemestre_createwithmodules",
|
||||||
scodoc_dept=g.scodoc_dept, formation_id=formation.id, semestre_id=1
|
scodoc_dept=g.scodoc_dept, formation_id=formation.id, semestre_id=1
|
||||||
)
|
)
|
||||||
}">ajouter</a>
|
}">ajouter</a>
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
|
if can_implement
|
||||||
|
else []
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
# Répartition des UEs dans les semestres
|
||||||
|
# utilise pour voir si la formation couvre tous les semestres
|
||||||
|
row["semestres_ues"] = ", ".join(
|
||||||
|
"S" + str(x if (x is not None and x > 0) else "-")
|
||||||
|
for x in sorted({(ue.semestre_idx or 0) for ue in formation.ues})
|
||||||
|
)
|
||||||
|
# Date surtout utilisées pour le tri:
|
||||||
if row["formsemestres"]:
|
if row["formsemestres"]:
|
||||||
row["date_fin_dernier_sem"] = (
|
row["date_fin_dernier_sem"] = row["formsemestres"][-1].date_fin.isoformat()
|
||||||
row["formsemestres"][-1].date_fin.isoformat(),
|
|
||||||
)
|
|
||||||
row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year
|
row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year
|
||||||
else:
|
else:
|
||||||
row["date_fin_dernier_sem"] = ""
|
row["date_fin_dernier_sem"] = ""
|
||||||
|
@ -603,9 +628,12 @@ def formation_list_table() -> GenTable:
|
||||||
"formation_code",
|
"formation_code",
|
||||||
"version",
|
"version",
|
||||||
"titre",
|
"titre",
|
||||||
|
"referentiel",
|
||||||
"commentaire",
|
"commentaire",
|
||||||
"sems_list_txt",
|
"sems_list_txt",
|
||||||
)
|
)
|
||||||
|
if detail:
|
||||||
|
columns_ids += ("annee_dernier_sem", "semestres_ues")
|
||||||
titles = {
|
titles = {
|
||||||
"buttons": "",
|
"buttons": "",
|
||||||
"commentaire": "Commentaire",
|
"commentaire": "Commentaire",
|
||||||
|
@ -615,22 +643,26 @@ def formation_list_table() -> GenTable:
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"formation_code": "Code",
|
"formation_code": "Code",
|
||||||
"sems_list_txt": "Semestres",
|
"sems_list_txt": "Semestres",
|
||||||
|
"referentiel": "Réf.",
|
||||||
|
"date_fin_dernier_sem": "Fin dernier sem.",
|
||||||
|
"annee_dernier_sem": "Année dernier sem.",
|
||||||
|
"semestres_ues": "Semestres avec UEs",
|
||||||
}
|
}
|
||||||
return GenTable(
|
return GenTable(
|
||||||
columns_ids=columns_ids,
|
base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
|
||||||
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,
|
||||||
table_id="formation_list_table",
|
|
||||||
html_class="formation_list_table table_leftalign",
|
html_class="formation_list_table table_leftalign",
|
||||||
html_with_td_classes=True,
|
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
base_url=f"{request.base_url}",
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -494,7 +494,7 @@ def table_formsemestres(
|
||||||
):
|
):
|
||||||
"""Une table presentant des semestres"""
|
"""Une table presentant des semestres"""
|
||||||
for sem in sems:
|
for sem in sems:
|
||||||
sem_set_responsable_name(sem)
|
sem_set_responsable_name(sem) # TODO utiliser formsemestre.responsables_str()
|
||||||
sem["_titre_num_target"] = url_for(
|
sem["_titre_num_target"] = url_for(
|
||||||
"notes.formsemestre_status",
|
"notes.formsemestre_status",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -573,7 +573,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||||
"input_type": "checkbox",
|
"input_type": "checkbox",
|
||||||
"title": "Publication",
|
"title": "Publication",
|
||||||
"allowed_values": ["X"],
|
"allowed_values": ["X"],
|
||||||
"explanation": "publier le bulletin sur le portail étudiants",
|
"explanation": "publier le bulletin sur la passerelle étudiants",
|
||||||
"labels": [""],
|
"labels": [""],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -812,14 +812,18 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||||
)
|
)
|
||||||
msg = ""
|
msg = ""
|
||||||
if tf[0] == 1:
|
if tf[0] == 1:
|
||||||
# check dates
|
# convert and check dates
|
||||||
if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
|
tf[2]["date_debut"] = scu.convert_fr_date(tf[2]["date_debut"])
|
||||||
msg = '<ul class="tf-msg"><li class="tf-msg">Dates de début et fin incompatibles !</li></ul>'
|
tf[2]["date_fin"] = scu.convert_fr_date(tf[2]["date_fin"])
|
||||||
|
if tf[2]["date_debut"] > tf[2]["date_fin"]:
|
||||||
|
msg = """<ul class="tf-msg">
|
||||||
|
<li class="tf-msg">Dates de début et fin incompatibles !</li>
|
||||||
|
</ul>"""
|
||||||
|
|
||||||
if (
|
if (
|
||||||
sco_preferences.get_preference("always_require_apo_sem_codes")
|
sco_preferences.get_preference("always_require_apo_sem_codes")
|
||||||
and not any(
|
and not any(
|
||||||
[tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)]
|
tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)
|
||||||
)
|
)
|
||||||
# n'impose pas d'Apo pour les sem. extérieurs
|
# n'impose pas d'Apo pour les sem. extérieurs
|
||||||
and ((formsemestre is None) or formsemestre.modalite != "EXT")
|
and ((formsemestre is None) or formsemestre.modalite != "EXT")
|
||||||
|
@ -1427,18 +1431,25 @@ Ceci n'est possible que si :
|
||||||
|
|
||||||
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
|
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
|
||||||
"""Delete a formsemestre (confirmation)"""
|
"""Delete a formsemestre (confirmation)"""
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
# Confirmation dialog
|
# Confirmation dialog
|
||||||
if not dialog_confirmed:
|
if not dialog_confirmed:
|
||||||
return scu.confirm_dialog(
|
return scu.confirm_dialog(
|
||||||
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""",
|
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2>
|
||||||
|
<p>(opération irréversible)</p>
|
||||||
|
""",
|
||||||
dest_url="",
|
dest_url="",
|
||||||
cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
|
cancel_url=url_for(
|
||||||
parameters={"formsemestre_id": formsemestre_id},
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
),
|
||||||
|
parameters={"formsemestre_id": formsemestre.id},
|
||||||
)
|
)
|
||||||
# Bon, s'il le faut...
|
# Bon, s'il le faut...
|
||||||
do_formsemestre_delete(formsemestre_id)
|
do_formsemestre_delete(formsemestre.id)
|
||||||
flash("Semestre supprimé !")
|
flash("Semestre supprimé !")
|
||||||
return flask.redirect(scu.ScoURL())
|
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_has_decisions_or_compensations(
|
def formsemestre_has_decisions_or_compensations(
|
||||||
|
|
|
@ -521,7 +521,7 @@ def _record_ue_validations_and_coefs(
|
||||||
coef = _convert_field_to_float(coef)
|
coef = _convert_field_to_float(coef)
|
||||||
if coef == "" or coef is False:
|
if coef == "" or coef is False:
|
||||||
coef = None
|
coef = None
|
||||||
now_dmy = time.strftime("%d/%m/%Y")
|
now_dmy = time.strftime(scu.DATE_FMT)
|
||||||
log(
|
log(
|
||||||
f"_record_ue_validations_and_coefs: {formsemestre.id} etudid={etud.id} ue_id={ue.id} moy_ue={note} ue_coef={coef}"
|
f"_record_ue_validations_and_coefs: {formsemestre.id} etudid={etud.id} ue_id={ue.id} moy_ue={note} ue_coef={coef}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -106,7 +106,7 @@ def do_formsemestre_inscription_create(args, method=None):
|
||||||
cnx,
|
cnx,
|
||||||
args={
|
args={
|
||||||
"etudid": args["etudid"],
|
"etudid": args["etudid"],
|
||||||
"event_date": time.strftime("%d/%m/%Y"),
|
"event_date": time.strftime(scu.DATE_FMT),
|
||||||
"formsemestre_id": args["formsemestre_id"],
|
"formsemestre_id": args["formsemestre_id"],
|
||||||
"event_type": "INSCRIPTION",
|
"event_type": "INSCRIPTION",
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,8 +25,7 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Tableau de bord semestre
|
"""Tableau de bord semestre"""
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
@ -65,14 +64,11 @@ from app.scodoc import sco_archives_formsemestre
|
||||||
from app.scodoc import sco_bulletins
|
from app.scodoc import sco_bulletins
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_compute_moy
|
from app.scodoc import sco_compute_moy
|
||||||
from app.scodoc import sco_edit_ue
|
|
||||||
from app.scodoc import sco_evaluations
|
from app.scodoc import sco_evaluations
|
||||||
from app.scodoc import sco_evaluation_db
|
|
||||||
from app.scodoc import sco_formations
|
from app.scodoc import sco_formations
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
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_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
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
|
@ -147,8 +143,10 @@ def _build_menu_stats(formsemestre: FormSemestre):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
|
||||||
"""HTML to render menubar"""
|
"""HTML to render menubar"""
|
||||||
|
if formsemestre is None:
|
||||||
|
return ""
|
||||||
formsemestre_id = formsemestre.id
|
formsemestre_id = formsemestre.id
|
||||||
if formsemestre.etat:
|
if formsemestre.etat:
|
||||||
change_lock_msg = "Verrouiller"
|
change_lock_msg = "Verrouiller"
|
||||||
|
@ -636,7 +634,7 @@ def formsemestre_description_table(
|
||||||
"UE": modimpl.module.ue.acronyme,
|
"UE": modimpl.module.ue.acronyme,
|
||||||
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
|
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
|
||||||
"Code": modimpl.module.code or "",
|
"Code": modimpl.module.code or "",
|
||||||
"Module": modimpl.module.abbrev or modimpl.module.titre,
|
"Module": modimpl.module.abbrev or modimpl.module.titre or "",
|
||||||
"_Module_class": "scotext",
|
"_Module_class": "scotext",
|
||||||
"Inscrits": mod_nb_inscrits,
|
"Inscrits": mod_nb_inscrits,
|
||||||
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],
|
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],
|
||||||
|
@ -693,7 +691,7 @@ def formsemestre_description_table(
|
||||||
)
|
)
|
||||||
e["_date_evaluation_order"] = e["jour"].isoformat()
|
e["_date_evaluation_order"] = e["jour"].isoformat()
|
||||||
e["date_evaluation"] = (
|
e["date_evaluation"] = (
|
||||||
e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
|
e["jour"].strftime(scu.DATE_FMT) if e["jour"] else ""
|
||||||
)
|
)
|
||||||
e["UE"] = row["UE"]
|
e["UE"] = row["UE"]
|
||||||
e["_UE_td_attrs"] = row["_UE_td_attrs"]
|
e["_UE_td_attrs"] = row["_UE_td_attrs"]
|
||||||
|
@ -728,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -798,7 +797,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||||
'Tous les étudiants'}
|
'Tous les étudiants'}
|
||||||
</div>
|
</div>
|
||||||
<div class="sem-groups-partition-titre">{
|
<div class="sem-groups-partition-titre">{
|
||||||
"Gestion de l'assiduité" if not partition_is_empty else ""
|
"Assiduité" if not partition_is_empty else ""
|
||||||
}</div>
|
}</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -823,15 +822,36 @@ 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>
|
<div>
|
||||||
<a class="btn" href="{
|
<a class="stdlink" href="{
|
||||||
url_for("assiduites.visu_assi_group",
|
url_for("assiduites.signal_assiduites_group",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
date_debut=formsemestre.date_debut.isoformat(),
|
day=datetime.date.today().isoformat(),
|
||||||
date_fin=formsemestre.date_fin.isoformat(),
|
formsemestre_id=formsemestre.id,
|
||||||
group_ids=group.id,
|
group_ids=group.id,
|
||||||
)}">
|
)}">
|
||||||
<button>Bilan assiduité</button></a>
|
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>
|
</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -839,42 +859,27 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<div>
|
<div>
|
||||||
<a class="btn" href="{
|
<a class="stdlink" href="{
|
||||||
url_for("assiduites.visu_assiduites_group",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
jour = datetime.date.today().isoformat(),
|
|
||||||
group_ids=group.id,
|
|
||||||
)}">
|
|
||||||
<button>Visualiser</button></a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a class="btn" href="{
|
|
||||||
url_for("assiduites.signal_assiduites_group",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
jour=datetime.date.today().isoformat(),
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
group_ids=group.id,
|
|
||||||
)}">
|
|
||||||
<button>Saisie journalière</button></a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a class="btn" href="{
|
|
||||||
url_for("assiduites.signal_assiduites_diff",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
group_ids=group.id,
|
|
||||||
)}">
|
|
||||||
<button>Saisie différée</button></a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a class="btn" href="{
|
|
||||||
url_for("assiduites.bilan_dept",
|
url_for("assiduites.bilan_dept",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
group_ids=group.id,
|
group_ids=group.id,
|
||||||
)}">
|
)}">
|
||||||
<button>Justificatifs en attente</button></a>
|
Justificatifs en attente</a>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div>
|
||||||
|
<a class="stdlink" href="{
|
||||||
|
url_for("assiduites.visu_assi_group",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
date_debut=formsemestre.date_debut.isoformat(),
|
||||||
|
date_fin=formsemestre.date_fin.isoformat(),
|
||||||
|
group_ids=group.id,
|
||||||
|
)}">
|
||||||
|
Bilan</a>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -1128,6 +1133,19 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||||
_make_listes_sem(formsemestre),
|
_make_listes_sem(formsemestre),
|
||||||
"</div>",
|
"</div>",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# --- Lien Traitement Justificatifs:
|
||||||
|
|
||||||
|
if current_user.has_permission(Permission.AbsJustifView):
|
||||||
|
H.append(
|
||||||
|
f"""<p>
|
||||||
|
<a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id)}">
|
||||||
|
Traitement des justificatifs d'absence</a>
|
||||||
|
</p>"""
|
||||||
|
)
|
||||||
|
|
||||||
# --- Lien mail enseignants:
|
# --- Lien mail enseignants:
|
||||||
adrlist = list(mails_enseignants - {None, ""})
|
adrlist = list(mails_enseignants - {None, ""})
|
||||||
if adrlist:
|
if adrlist:
|
||||||
|
@ -1175,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"]
|
||||||
|
@ -1312,7 +1320,9 @@ def formsemestre_tableau_modules(
|
||||||
if etat["attente"]:
|
if etat["attente"]:
|
||||||
H.append(
|
H.append(
|
||||||
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
|
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
|
||||||
title="Il y a des notes en attente"><span class="evals_attente">en attente</span></a></span>"""
|
title="Il y a des notes en attente"><span class="evals_attente">{
|
||||||
|
etat["nb_evals_attente"]
|
||||||
|
} en attente</span></a></span>"""
|
||||||
)
|
)
|
||||||
if not mod_is_conforme:
|
if not mod_is_conforme:
|
||||||
H.append(
|
H.append(
|
||||||
|
@ -1481,7 +1491,12 @@ def formsemestre_note_etuds_sans_notes(
|
||||||
</div>
|
</div>
|
||||||
{message}
|
{message}
|
||||||
|
|
||||||
<form method="post">
|
<style>
|
||||||
|
.sco-std-form select, .sco-std-form input[type="submit"] {{
|
||||||
|
height: 24px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
<form class="sco-std-form" method="post">
|
||||||
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}">
|
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}">
|
||||||
<input type="hidden" name="etudid" value="{etudid or ""}">
|
<input type="hidden" name="etudid" value="{etudid or ""}">
|
||||||
|
|
||||||
|
@ -1492,7 +1507,7 @@ def formsemestre_note_etuds_sans_notes(
|
||||||
<option value="ATT" selected>ATT (en attente)</option>
|
<option value="ATT" selected>ATT (en attente)</option>
|
||||||
<option value="EXC">EXC (neutralisée)</option>
|
<option value="EXC">EXC (neutralisée)</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="submit" name="enregistrer">
|
<input type="submit" value="Enregistrer">
|
||||||
</form>
|
</form>
|
||||||
{html_sco_header.sco_footer()}
|
{html_sco_header.sco_footer()}
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -57,14 +57,12 @@ from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_assiduites
|
from app.scodoc import sco_assiduites
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
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_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
from app.scodoc import sco_cursus
|
from app.scodoc import sco_cursus
|
||||||
from app.scodoc import sco_cursus_dut
|
from app.scodoc import sco_cursus_dut
|
||||||
from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
|
from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
|
||||||
from app.scodoc import sco_photos
|
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_pv_dict
|
from app.scodoc import sco_pv_dict
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
@ -722,8 +720,8 @@ def formsemestre_recap_parcours_table(
|
||||||
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
||||||
)
|
)
|
||||||
# Absences (nb d'abs non just. dans ce semestre)
|
# Absences (nb d'abs non just. dans ce semestre)
|
||||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
|
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
|
||||||
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
|
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
|
||||||
|
|
||||||
# UEs
|
# UEs
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
|
@ -1162,7 +1160,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
||||||
"input_type": "date",
|
"input_type": "date",
|
||||||
"size": 9,
|
"size": 9,
|
||||||
"explanation": "j/m/a",
|
"explanation": "j/m/a",
|
||||||
"default": time.strftime("%d/%m/%Y"),
|
"default": time.strftime(scu.DATE_FMT),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -1210,7 +1208,9 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
||||||
|
|
||||||
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
|
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
|
||||||
<em>dans un semestre hors ScoDoc</em>.</p>
|
<em>dans un semestre hors ScoDoc</em>.</p>
|
||||||
<p class="expl"><b>Les UE validées dans ScoDoc sont
|
|
||||||
|
<div class="scobox explanation">
|
||||||
|
<p><b>Les UE validées dans ScoDoc sont
|
||||||
automatiquement prises en compte</b>.
|
automatiquement prises en compte</b>.
|
||||||
</p>
|
</p>
|
||||||
<p>Cette page est surtout utile pour les étudiants ayant
|
<p>Cette page est surtout utile pour les étudiants ayant
|
||||||
|
@ -1227,11 +1227,12 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
||||||
l'attribution des ECTS si le code jury est validant (ADM).
|
l'attribution des ECTS si le code jury est validant (ADM).
|
||||||
</p>
|
</p>
|
||||||
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
|
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{_get_etud_ue_cap_html(etud, formsemestre)}
|
{_get_etud_ue_cap_html(etud, formsemestre)}
|
||||||
|
|
||||||
<div class="sco_box">
|
<div class="scobox">
|
||||||
<div class="sco_box_title">
|
<div class="scobox-title">
|
||||||
Enregistrer une UE antérieure
|
Enregistrer une UE antérieure
|
||||||
</div>
|
</div>
|
||||||
{tf[1]}
|
{tf[1]}
|
||||||
|
|
214
app/scodoc/sco_gen_cal.py
Normal file
214
app/scodoc/sco_gen_cal.py
Normal 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,
|
||||||
|
)
|
|
@ -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)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user