forked from ScoDoc/ScoDoc
Compare commits
274 Commits
master
...
iziram-rev
Author | SHA1 | Date | |
---|---|---|---|
8ebe3fa6f3 | |||
|
21f57aab8f | ||
|
53c9658ce1 | ||
|
e18990d804 | ||
|
c11599b64f | ||
|
095eb6ce20 | ||
5a58346282 | |||
|
61d4186ad3 | ||
2fc978e515 | |||
|
4d72fec42d | ||
a63e14ce06 | |||
ba909d72f0 | |||
44a72c1ab9 | |||
|
2fd1b039f4 | ||
0f8998c891 | |||
be367de2a1 | |||
e81ad610b6 | |||
0f45101000 | |||
1224b46846 | |||
6b2ea5c5bc | |||
|
a7b856b1ec | ||
338c24a9c1 | |||
43849007fb | |||
|
547040bb93 | ||
|
8bc780f2cf | ||
86f5751e79 | |||
b160f64e4f | |||
ee2ac9d986 | |||
fc78484186 | |||
21b5474a6f | |||
3c1acc9c00 | |||
2548a97515 | |||
ce1cb7516b | |||
|
cf3258f5f9 | ||
3998b5a366 | |||
728010bf69 | |||
e9f23d8b3e | |||
f6d442beb4 | |||
b19c94a1f4 | |||
9fb70aef5d | |||
c8c05ecd77 | |||
efa8f617bb | |||
|
02ec55ca18 | ||
b30d5eb996 | |||
e33bc1e303 | |||
d2362c1080 | |||
6e4bf424e5 | |||
2170b06e33 | |||
696f4a5410 | |||
9880645c01 | |||
4d0ea06559 | |||
f46e3f6db5 | |||
4d66fb13ee | |||
138f9597f5 | |||
91e8c9185b | |||
f3b2c6d4fe | |||
7277c9f999 | |||
9a19919bae | |||
d97c0c08aa | |||
325978a175 | |||
135ca9fc1c | |||
a4072efe4c | |||
4430eb9a61 | |||
073c3c7c44 | |||
75b87b24de | |||
e0f6b022b1 | |||
98c6761f6a | |||
53514ef919 | |||
294ce1d708 | |||
cf63e1c038 | |||
584a7af2a1 | |||
635320fd62 | |||
6867974957 | |||
6ad415dfca | |||
2919ff517c | |||
89948db135 | |||
bdf90dfd69 | |||
0b9c9be222 | |||
b5cf210112 | |||
6833a28274 | |||
5753ac92f4 | |||
16cc35f63c | |||
5e0922a4bf | |||
10148bc7c0 | |||
556d8e7cbf | |||
c6b2af5635 | |||
cc0c544519 | |||
71116e6b39 | |||
3121a6d54c | |||
83afc1d6a0 | |||
5fc08b9716 | |||
e7559b7a78 | |||
d8a98b6e5b | |||
1287aecc4b | |||
549323e781 | |||
0ff5fa46d9 | |||
8d124eca3e | |||
6a7638d7ff | |||
452bbf2885 | |||
4915852d66 | |||
85f00c7cb6 | |||
b8b3fbb324 | |||
dd93d952d7 | |||
00c09b1eb8 | |||
0e628273cf | |||
664d5483fc | |||
b4eab5fcbc | |||
c551634417 | |||
efe8673e8a | |||
f17b10da3b | |||
cf18520e9c | |||
cda20c27b2 | |||
b7983a8d59 | |||
47b3eec14b | |||
9f6068caa2 | |||
04277d1f57 | |||
f9d15da553 | |||
a7126990f0 | |||
42d92cb998 | |||
2d3d7d49fc | |||
277e87add9 | |||
fffb07d612 | |||
afe9ae69a9 | |||
16f953caf6 | |||
|
f3b1b8a3cb | ||
7adc7d824b | |||
cf900d2027 | |||
1fa8375b11 | |||
bdefa111a7 | |||
c5b2df379e | |||
3460b217dd | |||
18aed44644 | |||
ec632dd43c | |||
acc1ecf906 | |||
9566551e7e | |||
7e1b0177f0 | |||
8e6dc37a87 | |||
a4840f494b | |||
a28f58a443 | |||
2a41cf972c | |||
e2ca9d417f | |||
|
f96571f520 | ||
|
4df1bdda8e | ||
6c88dfa722 | |||
|
b9f3db91d4 | ||
3e2631b94d | |||
9251810814 | |||
4c83c69f7c | |||
b09dc63fe3 | |||
075d864de3 | |||
6440ca4a1f | |||
7d2d19f3a8 | |||
882d131837 | |||
3012fc465d | |||
7069fb6e31 | |||
be2d7926bf | |||
bc6d9d5442 | |||
a0a6dbea00 | |||
872e741d9f | |||
5258a570a6 | |||
f0da8434a9 | |||
e995228ca7 | |||
e59fce5f6b | |||
0d9338dc0a | |||
f1fd4d98d7 | |||
c6e35dd4cd | |||
cb8d313dc7 | |||
3e3b09134d | |||
7b3c50620b | |||
63fb09348d | |||
7a9dc11af3 | |||
d178c636bf | |||
c6a06266fa | |||
e1504adc03 | |||
|
a9615bc077 | ||
|
3ff4abd19c | ||
0a1a847044 | |||
f5442b924f | |||
bec4cd7978 | |||
f63fa43862 | |||
ca20c303f0 | |||
014886c288 | |||
e2110f4abb | |||
ff12f4312e | |||
930a96b984 | |||
60fa12df81 | |||
0324771aa2 | |||
688fc5401f | |||
c1cbd6bce0 | |||
f2ffd69fe6 | |||
ba5b5cdb6f | |||
51b0ca088c | |||
9c618692d1 | |||
a0c33b3c19 | |||
ef1b28fe27 | |||
f246d9e82c | |||
cd36737460 | |||
acb8e6aab2 | |||
8ef19b14c7 | |||
7af381becc | |||
c9b4058717 | |||
42b03dbdfa | |||
a87dbd9927 | |||
ae9aad0619 | |||
e38d4bde81 | |||
25d1132a06 | |||
4c730a6302 | |||
287e4df74e | |||
77348c2cdf | |||
f67a11519e | |||
f9a9c2088d | |||
afbb1fb0e2 | |||
346701d91e | |||
f647ff1139 | |||
f5988b9e34 | |||
f318f35c1b | |||
4626cb9a3e | |||
0a58437fa9 | |||
c906cd7f16 | |||
ee86fba3d3 | |||
5018298d12 | |||
9d64caa749 | |||
6f257dc80d | |||
49d176c603 | |||
559b66de8b | |||
d7f1114a42 | |||
a2ea7d7a02 | |||
f6d8de5a20 | |||
2bf678ac50 | |||
|
63c0667694 | ||
|
3f7f4172b5 | ||
528d5c8863 | |||
7b28e0ba6b | |||
588f2f26eb | |||
4940decf57 | |||
d055c17c6b | |||
ea0a49d837 | |||
59a6ee3b3e | |||
111634db99 | |||
26dcc31ffb | |||
18f4b9cd42 | |||
dbc9aab7c3 | |||
|
9c50d03dd8 | ||
2d76cc0ad1 | |||
|
b7fb8879df | ||
3e0f43d5ea | |||
dcdd83d2e8 | |||
4d453d5d14 | |||
20b13b05cf | |||
a730bf759b | |||
|
a63349382e | ||
dab6bad08f | |||
e435dd10db | |||
95100ed429 | |||
155a093635 | |||
|
c85a51a8c5 | ||
d50107079b | |||
9535ff1e91 | |||
f4d8f4dded | |||
ba003d7c02 | |||
7ca3290357 | |||
7cb98e3f31 | |||
365e54f7e1 | |||
9da5506361 | |||
979359257b | |||
cc674b4e65 | |||
c103111aa1 | |||
b9d6688250 | |||
93e54982b6 | |||
eefdd5458e | |||
072d839b75 | |||
0de35b5400 | |||
cfcb100ab7 | |||
|
1c48758940 |
@ -1,4 +1,8 @@
|
|||||||
[[MESSAGES CONTROL]
|
|
||||||
|
[MASTER]
|
||||||
|
load-plugins=pylint_flask_sqlalchemy,pylint_flask
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
# pylint and black disagree...
|
# pylint and black disagree...
|
||||||
disable=bad-continuation
|
disable=bad-continuation
|
||||||
|
|
||||||
|
65
README.md
65
README.md
@ -1,5 +1,4 @@
|
|||||||
i
|
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
||||||
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
|
||||||
|
|
||||||
(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt).
|
(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt).
|
||||||
|
|
||||||
@ -9,39 +8,34 @@ Documentation utilisateur: <https://scodoc.org>
|
|||||||
|
|
||||||
## Version ScoDoc 9
|
## Version ScoDoc 9
|
||||||
|
|
||||||
La version ScoDoc 9 est parue en septembre 2021.
|
La version ScoDoc 9 est parue en septembre 2021. Elle représente une évolution
|
||||||
Elle représente une évolution majeure du projet, maintenant basé sur
|
majeure du projet, maintenant basé sur Flask (au lieu de Zope) et sur **python
|
||||||
Flask (au lieu de Zope) et sur **python 3.9+**.
|
3.9+**.
|
||||||
|
|
||||||
La version 9.0 s'efforce de reproduire presque à l'identique le fonctionnement
|
La version 9.0 s'efforce de reproduire presque à l'identique le fonctionnement
|
||||||
de ScoDoc7, avec des composants logiciels différents (Debian 11, Python 3,
|
de ScoDoc7, avec des composants logiciels différents (Debian 11, Python 3,
|
||||||
Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
||||||
|
|
||||||
|
### État actuel (dec 22)
|
||||||
|
|
||||||
|
- 9.4.x est en production
|
||||||
|
- le prochain jalon est 9.5. Voir branches sur gitea.
|
||||||
|
|
||||||
### État actuel (26 jan 22)
|
|
||||||
|
|
||||||
- 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
|
|
||||||
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
|
|
||||||
|
|
||||||
- 9.2 (branche dev92) est la version de développement.
|
|
||||||
|
|
||||||
|
|
||||||
### Lignes de commandes
|
### Lignes de commandes
|
||||||
|
|
||||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||||
|
|
||||||
|
|
||||||
## Organisation des fichiers
|
## Organisation des fichiers
|
||||||
|
|
||||||
L'installation comporte les fichiers de l'application, sous `/opt/scodoc/`, et
|
L'installation comporte les fichiers de l'application, sous `/opt/scodoc/`, et
|
||||||
les fichiers locaux (archives, photos, configurations, logs) sous
|
les fichiers locaux (archives, photos, configurations, logs) sous
|
||||||
`/opt/scodoc-data`. Par ailleurs, il y a évidemment les bases de données
|
`/opt/scodoc-data`. Par ailleurs, il y a évidemment les bases de données
|
||||||
postgresql et la configuration du système Linux.
|
postgresql et la configuration du système Linux.
|
||||||
|
|
||||||
### Fichiers locaux
|
### Fichiers locaux
|
||||||
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
|
|
||||||
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
|
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
|
||||||
|
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
|
||||||
`/opt/scodoc-data/config`.
|
`/opt/scodoc-data/config`.
|
||||||
|
|
||||||
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||||
@ -62,7 +56,7 @@ Principaux contenus:
|
|||||||
|
|
||||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
||||||
|
|
||||||
Puis remplacer `/opt/scodoc` par un clone du git.
|
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||||
|
|
||||||
sudo su
|
sudo su
|
||||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||||
@ -76,7 +70,7 @@ Puis remplacer `/opt/scodoc` par un clone du git.
|
|||||||
|
|
||||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||||
chown -R scodoc.scodoc /opt/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:
|
||||||
|
|
||||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||||
@ -100,14 +94,14 @@ Avant le premier lancement, créer cette base ainsi:
|
|||||||
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.
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
flask delete-dept 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:
|
||||||
|
|
||||||
@ -117,24 +111,24 @@ Ou avec couverture (`pip install pytest-cov`)
|
|||||||
|
|
||||||
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 développement dans un état connu, par exemple pour éviter de
|
|
||||||
recréer à la main étudiants et semestres quand on développe.
|
|
||||||
|
|
||||||
Il suffit de positionner une variable d'environnement indiquant la BD
|
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||||
utilisée par les tests:
|
développement dans un état connu, par exemple pour éviter de recréer à la main
|
||||||
|
étudiants et semestres quand on développe.
|
||||||
|
|
||||||
|
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||||
|
par les tests:
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
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)
|
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||||
un utilisateur:
|
utilisateur:
|
||||||
|
|
||||||
flask user-password admin
|
flask user-password admin
|
||||||
|
|
||||||
@ -178,12 +172,10 @@ Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bie
|
|||||||
|
|
||||||
pip install snakeviz
|
pip install snakeviz
|
||||||
|
|
||||||
puis
|
puis
|
||||||
|
|
||||||
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 11
|
# Paquet Debian 11
|
||||||
|
|
||||||
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
|
||||||
@ -191,5 +183,4 @@ 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
|
||||||
`tools/build_release.sh`.
|
`tools/build_release.sh`.
|
||||||
|
|
||||||
|
@ -26,11 +26,13 @@ from flask_mail import Mail
|
|||||||
from flask_bootstrap import Bootstrap
|
from flask_bootstrap import Bootstrap
|
||||||
from flask_moment import Moment
|
from flask_moment import Moment
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
|
from jinja2 import select_autoescape
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
ScoBugCatcher,
|
ScoBugCatcher,
|
||||||
|
ScoException,
|
||||||
ScoGenError,
|
ScoGenError,
|
||||||
ScoValueError,
|
ScoValueError,
|
||||||
APIInvalidParams,
|
APIInvalidParams,
|
||||||
@ -60,11 +62,11 @@ cache = Cache(
|
|||||||
|
|
||||||
|
|
||||||
def handle_sco_value_error(exc):
|
def handle_sco_value_error(exc):
|
||||||
return render_template("sco_value_error.html", exc=exc), 404
|
return render_template("sco_value_error.j2", exc=exc), 404
|
||||||
|
|
||||||
|
|
||||||
def handle_access_denied(exc):
|
def handle_access_denied(exc):
|
||||||
return render_template("error_access_denied.html", exc=exc), 403
|
return render_template("error_access_denied.j2", exc=exc), 403
|
||||||
|
|
||||||
|
|
||||||
def internal_server_error(exc):
|
def internal_server_error(exc):
|
||||||
@ -74,7 +76,7 @@ def internal_server_error(exc):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
render_template(
|
render_template(
|
||||||
"error_500.html",
|
"error_500.j2",
|
||||||
SCOVERSION=sco_version.SCOVERSION,
|
SCOVERSION=sco_version.SCOVERSION,
|
||||||
date=datetime.datetime.now().isoformat(),
|
date=datetime.datetime.now().isoformat(),
|
||||||
exc=exc,
|
exc=exc,
|
||||||
@ -92,9 +94,12 @@ def handle_sco_bug(exc):
|
|||||||
"""Un bug, en général rare, sur lequel les dev cherchent des
|
"""Un bug, en général rare, sur lequel les dev cherchent des
|
||||||
informations pour le corriger.
|
informations pour le corriger.
|
||||||
"""
|
"""
|
||||||
Thread(
|
if current_app.config["TESTING"] or current_app.config["DEBUG"]:
|
||||||
target=_async_dump, args=(current_app._get_current_object(), request.url)
|
raise ScoException # for development servers only
|
||||||
).start()
|
else:
|
||||||
|
Thread(
|
||||||
|
target=_async_dump, args=(current_app._get_current_object(), request.url)
|
||||||
|
).start()
|
||||||
|
|
||||||
return internal_server_error(exc)
|
return internal_server_error(exc)
|
||||||
|
|
||||||
@ -142,7 +147,7 @@ def render_raw_html(template_filename: str, **args) -> str:
|
|||||||
|
|
||||||
def postgresql_server_error(e):
|
def postgresql_server_error(e):
|
||||||
"""Erreur de connection au serveur postgresql (voir notesdb.open_db_connection)"""
|
"""Erreur de connection au serveur postgresql (voir notesdb.open_db_connection)"""
|
||||||
return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503
|
return render_raw_html("error_503.j2", SCOVERSION=sco_version.SCOVERSION), 503
|
||||||
|
|
||||||
|
|
||||||
class LogRequestFormatter(logging.Formatter):
|
class LogRequestFormatter(logging.Formatter):
|
||||||
@ -271,6 +276,9 @@ 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
|
||||||
|
|
||||||
|
# Enable autoescaping of all templates, including .j2
|
||||||
|
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
|
||||||
|
|
||||||
# https://scodoc.fr/ScoDoc
|
# https://scodoc.fr/ScoDoc
|
||||||
app.register_blueprint(scodoc_bp)
|
app.register_blueprint(scodoc_bp)
|
||||||
# https://scodoc.fr/ScoDoc/RT/Scolarite/...
|
# https://scodoc.fr/ScoDoc/RT/Scolarite/...
|
||||||
@ -435,8 +443,6 @@ def initialize_scodoc_database(erase=False, create_all=False):
|
|||||||
SQL tables and functions.
|
SQL tables and functions.
|
||||||
If erase is True, _erase_ all database content.
|
If erase is True, _erase_ all database content.
|
||||||
"""
|
"""
|
||||||
from app import models
|
|
||||||
|
|
||||||
# - ERASE (the truncation sql function has been defined above)
|
# - ERASE (the truncation sql function has been defined above)
|
||||||
if erase:
|
if erase:
|
||||||
truncate_database()
|
truncate_database()
|
||||||
@ -463,6 +469,26 @@ def truncate_database():
|
|||||||
except:
|
except:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise
|
raise
|
||||||
|
# Remet les compteurs (séquences sql) à zéro
|
||||||
|
db.session.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
|
||||||
|
DECLARE
|
||||||
|
statements CURSOR FOR
|
||||||
|
SELECT sequence_name
|
||||||
|
FROM information_schema.sequences
|
||||||
|
ORDER BY sequence_name ;
|
||||||
|
BEGIN
|
||||||
|
FOR stmt IN statements LOOP
|
||||||
|
EXECUTE 'ALTER SEQUENCE ' || quote_ident(stmt.sequence_name) || ' RESTART;';
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
SELECT reset_sequences('scodoc');
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def clear_scodoc_cache():
|
def clear_scodoc_cache():
|
||||||
@ -480,12 +506,10 @@ def clear_scodoc_cache():
|
|||||||
|
|
||||||
|
|
||||||
# --------- Logging
|
# --------- Logging
|
||||||
def log(msg: str, silent_test=True):
|
def log(msg: str):
|
||||||
"""log a message.
|
"""log a message.
|
||||||
If Flask app, use configured logger, else stderr.
|
If Flask app, use configured logger, else stderr.
|
||||||
"""
|
"""
|
||||||
if silent_test and current_app and current_app.config["TESTING"]:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
dept = getattr(g, "scodoc_dept", "")
|
dept = getattr(g, "scodoc_dept", "")
|
||||||
msg = f" ({dept}) {msg}"
|
msg = f" ({dept}) {msg}"
|
||||||
@ -530,3 +554,22 @@ def scodoc_flash_status_messages():
|
|||||||
f"Mode test: mails redirigés vers {email_test_mode_address}",
|
f"Mode test: mails redirigés vers {email_test_mode_address}",
|
||||||
category="warning",
|
category="warning",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def critical_error(msg):
|
||||||
|
"""Handle a critical error: flush all caches, display message to the user"""
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||||
|
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||||
|
clear_scodoc_cache()
|
||||||
|
raise ScoValueError(
|
||||||
|
f"""
|
||||||
|
Une erreur est survenue.
|
||||||
|
|
||||||
|
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||||
|
{scu.SCO_DISCORD_ASSISTANCE}
|
||||||
|
|
||||||
|
{msg}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import request
|
from flask import request, g, jsonify
|
||||||
|
from app import db
|
||||||
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
|
||||||
|
|
||||||
@ -31,9 +32,26 @@ def requested_format(default_format="json", allowed_formats=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||||
|
"""
|
||||||
|
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
||||||
|
|
||||||
|
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemstre) -> join_cls
|
||||||
|
|
||||||
|
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
||||||
|
"""
|
||||||
|
query = model_cls.query.filter_by(id=model_id)
|
||||||
|
if g.scodoc_dept and join_cls is not None:
|
||||||
|
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
unique: model_cls = query.first_or_404()
|
||||||
|
|
||||||
|
return jsonify(unique.to_dict(format_api=True))
|
||||||
|
|
||||||
|
|
||||||
from app.api import tokens
|
from app.api import tokens
|
||||||
from app.api import (
|
from app.api import (
|
||||||
absences,
|
absences,
|
||||||
|
assiduites,
|
||||||
billets_absences,
|
billets_absences,
|
||||||
departements,
|
departements,
|
||||||
etudiants,
|
etudiants,
|
||||||
@ -41,6 +59,7 @@ from app.api import (
|
|||||||
formations,
|
formations,
|
||||||
formsemestres,
|
formsemestres,
|
||||||
jury,
|
jury,
|
||||||
|
justificatifs,
|
||||||
logos,
|
logos,
|
||||||
partitions,
|
partitions,
|
||||||
users,
|
users,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 API : Absences
|
"""ScoDoc 9 API : Absences
|
||||||
|
575
app/api/assiduites.py
Normal file
575
app/api/assiduites.py
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
"""ScoDoc 9 API : Assiduités
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import g, jsonify, request
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app import db
|
||||||
|
from app.api import api_bp as bp
|
||||||
|
from app.api import api_web_bp
|
||||||
|
from app.api import get_model_api_object
|
||||||
|
from app.decorators import permission_required, scodoc
|
||||||
|
from app.models import Assiduite, FormSemestre, Identite, ModuleImpl
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:assiduite_id>")
|
||||||
|
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduite(assiduite_id: int = None):
|
||||||
|
"""Retourne un objet assiduité à partir de son id
|
||||||
|
|
||||||
|
Exemple de résultat:
|
||||||
|
{
|
||||||
|
"assiduite_id": 1,
|
||||||
|
"etudid": 2,
|
||||||
|
"moduleimpl_id": 3,
|
||||||
|
"date_debut": "2022-10-31T08:00+01:00",
|
||||||
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
|
"etat": "retard",
|
||||||
|
"desc": "une description",
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||||
|
@bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne le nombre d'assiduités d'un étudiant
|
||||||
|
chemin : /assiduites/<int:etudid>/count
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /assiduites/<int:etudid>/count/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
|
||||||
|
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
|
||||||
|
ex: .../query?type=heure
|
||||||
|
Comportement par défaut : compte le nombre d'assiduité enregistrée
|
||||||
|
|
||||||
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=present,retard
|
||||||
|
Date debut
|
||||||
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||||
|
query?moduleimpl_id=[- int ou vide -]
|
||||||
|
ex: query?moduleimpl_id=1234
|
||||||
|
query?moduleimpl_od=
|
||||||
|
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||||
|
query?formsemstre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
filtered: dict[str, object] = {}
|
||||||
|
metric: str = "all"
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
metric, filtered = _count_manager(request)
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
scass.get_assiduites_stats(
|
||||||
|
assiduites=etud.assiduites, metric=metric, filtered=filtered
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduites(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /assiduites/<int:etudid>
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /assiduites/<int:etudid>/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=present,retard
|
||||||
|
Date debut
|
||||||
|
(date de début de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin de l'assiduité, sont affichés les assiduités
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||||
|
query?moduleimpl_id=[- int ou vide -]
|
||||||
|
ex: query?moduleimpl_id=1234
|
||||||
|
query?moduleimpl_od=
|
||||||
|
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||||
|
query?formsemstre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
assiduites_query = etud.assiduites
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
assiduites_query = _filter_manager(request, assiduites_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for ass in assiduites_query.all():
|
||||||
|
data = ass.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||||
|
)
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(Assiduite.query, formsemestre)
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
assiduites_query = _filter_manager(request, assiduites_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for ass in assiduites_query.all():
|
||||||
|
data = ass.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||||
|
defaults={"with_query": False},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||||
|
defaults={"with_query": False},
|
||||||
|
)
|
||||||
|
@bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||||
|
defaults={"with_query": True},
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def count_assiduites_formsemestre(
|
||||||
|
formsemestre_id: int = None, with_query: bool = False
|
||||||
|
):
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
|
etuds = formsemestre.etuds.all()
|
||||||
|
etuds_id = [etud.id for etud in etuds]
|
||||||
|
|
||||||
|
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||||
|
metric: str = "all"
|
||||||
|
filtered: dict = {}
|
||||||
|
if with_query:
|
||||||
|
metric, filtered = _count_manager(request)
|
||||||
|
|
||||||
|
return jsonify(scass.get_assiduites_stats(assiduites_query, metric, filtered))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_create(etudid: int = None):
|
||||||
|
"""
|
||||||
|
Création d'une assiduité pour l'étudiant (etudid)
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"moduleimpl_id": int,
|
||||||
|
"desc":str,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||||
|
|
||||||
|
create_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(create_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
code, obj = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
|
||||||
|
return jsonify({"errors": errors, "success": success})
|
||||||
|
|
||||||
|
|
||||||
|
def _create_singular(
|
||||||
|
data: dict,
|
||||||
|
etud: Identite,
|
||||||
|
) -> tuple[int, object]:
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# -- vérifications de l'objet json --
|
||||||
|
# cas 1 : ETAT
|
||||||
|
etat = data.get("etat", None)
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': manquant")
|
||||||
|
elif not scu.EtatAssiduite.contains(etat):
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
|
||||||
|
etat = scu.EtatAssiduite.get(etat)
|
||||||
|
|
||||||
|
# cas 2 : date_debut
|
||||||
|
date_debut = data.get("date_debut", None)
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
# cas 3 : date_fin
|
||||||
|
date_fin = data.get("date_fin", None)
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
|
||||||
|
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||||
|
moduleimpl: ModuleImpl = None
|
||||||
|
|
||||||
|
if moduleimpl_id is not False:
|
||||||
|
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
|
|
||||||
|
# cas 5 : desc
|
||||||
|
|
||||||
|
desc: str = data.get("desc", None)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err)
|
||||||
|
|
||||||
|
# TOUT EST OK
|
||||||
|
|
||||||
|
try:
|
||||||
|
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
|
||||||
|
date_debut=deb,
|
||||||
|
date_fin=fin,
|
||||||
|
etat=etat,
|
||||||
|
etud=etud,
|
||||||
|
moduleimpl=moduleimpl,
|
||||||
|
description=desc,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_assiduite)
|
||||||
|
db.session.commit()
|
||||||
|
return (200, {"assiduite_id": nouv_assiduite.assiduite_id})
|
||||||
|
except ScoValueError as excp:
|
||||||
|
return (
|
||||||
|
404,
|
||||||
|
excp.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/delete", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_cdelete():
|
||||||
|
"""
|
||||||
|
Suppression d'une assiduité à partir de son id
|
||||||
|
|
||||||
|
Forme des données envoyées :
|
||||||
|
|
||||||
|
[
|
||||||
|
<assiduite_id:int>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
assiduites_list: list[int] = request.get_json(force=True)
|
||||||
|
if not isinstance(assiduites_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
output = {"errors": {}, "success": {}}
|
||||||
|
|
||||||
|
for i, ass in enumerate(assiduites_list):
|
||||||
|
code, msg = _delete_singular(ass, db)
|
||||||
|
if code == 404:
|
||||||
|
output["errors"][f"{i}"] = msg
|
||||||
|
else:
|
||||||
|
output["success"][f"{i}"] = {"OK": True}
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(output)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_singular(assiduite_id: int, database):
|
||||||
|
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
||||||
|
if assiduite_unique is None:
|
||||||
|
return (404, "Assiduite non existante")
|
||||||
|
database.session.delete(assiduite_unique)
|
||||||
|
return (200, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def assiduite_edit(assiduite_id: int):
|
||||||
|
"""
|
||||||
|
Edition d'une assiduité à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
{
|
||||||
|
"etat"?: str,
|
||||||
|
"moduleimpl_id"?: int
|
||||||
|
"desc"?: str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||||
|
id=assiduite_id
|
||||||
|
).first_or_404()
|
||||||
|
errors: list[str] = []
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
|
||||||
|
# Vérifications de data
|
||||||
|
|
||||||
|
# Cas 1 : Etat
|
||||||
|
if data.get("etat") is not None:
|
||||||
|
etat = scu.EtatAssiduite.get(data.get("etat"))
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
else:
|
||||||
|
assiduite_unique.etat = etat
|
||||||
|
|
||||||
|
# Cas 2 : Moduleimpl_id
|
||||||
|
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||||
|
moduleimpl: ModuleImpl = None
|
||||||
|
|
||||||
|
if moduleimpl_id is not False:
|
||||||
|
if moduleimpl_id is not None:
|
||||||
|
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||||
|
if moduleimpl is None:
|
||||||
|
errors.append("param 'moduleimpl_id': invalide")
|
||||||
|
else:
|
||||||
|
if not moduleimpl.est_inscrit(
|
||||||
|
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||||
|
):
|
||||||
|
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
else:
|
||||||
|
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||||
|
|
||||||
|
# Cas 3 : desc
|
||||||
|
desc = data.get("desc", False)
|
||||||
|
if desc is not False:
|
||||||
|
assiduite_unique.desc = desc
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return json_error(404, err)
|
||||||
|
|
||||||
|
db.session.add(assiduite_unique)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"OK": True})
|
||||||
|
|
||||||
|
|
||||||
|
# -- Utils --
|
||||||
|
|
||||||
|
|
||||||
|
def _count_manager(requested) -> tuple[str, dict]:
|
||||||
|
"""
|
||||||
|
Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête
|
||||||
|
"""
|
||||||
|
filtered: dict = {}
|
||||||
|
# cas 1 : etat assiduite
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
filtered["etat"] = etat
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
if deb is not None:
|
||||||
|
filtered["date_debut"] = deb
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if fin is not None:
|
||||||
|
filtered["date_fin"] = fin
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
module = requested.args.get("moduleimpl_id", False)
|
||||||
|
try:
|
||||||
|
if module is False:
|
||||||
|
raise ValueError
|
||||||
|
if module != "":
|
||||||
|
module = int(module)
|
||||||
|
else:
|
||||||
|
module = None
|
||||||
|
except ValueError:
|
||||||
|
module = False
|
||||||
|
|
||||||
|
if module is not False:
|
||||||
|
filtered["moduleimpl_id"] = module
|
||||||
|
|
||||||
|
# cas 5 : formsemestre_id
|
||||||
|
formsemestre_id = requested.args.get("formsemestre_id")
|
||||||
|
|
||||||
|
if formsemestre_id is not None:
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
filtered["formsemestre"] = formsemestre
|
||||||
|
|
||||||
|
# cas 6 : type
|
||||||
|
metric = requested.args.get("metric", "all")
|
||||||
|
|
||||||
|
return (metric, filtered)
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_manager(requested, assiduites_query: Assiduite):
|
||||||
|
"""
|
||||||
|
Retourne les assiduites entrées filtrées en fonction de la request
|
||||||
|
"""
|
||||||
|
# cas 1 : etat assiduite
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat)
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
assiduites_query: Assiduite = scass.filter_by_date(
|
||||||
|
assiduites_query, Assiduite, deb, fin
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 4 : moduleimpl_id
|
||||||
|
module = requested.args.get("moduleimpl_id", False)
|
||||||
|
try:
|
||||||
|
if module is False:
|
||||||
|
raise ValueError
|
||||||
|
if module != "":
|
||||||
|
module = int(module)
|
||||||
|
else:
|
||||||
|
module = None
|
||||||
|
except ValueError:
|
||||||
|
module = False
|
||||||
|
|
||||||
|
if module is not False:
|
||||||
|
assiduites_query = scass.filter_by_module_impl(assiduites_query, module)
|
||||||
|
|
||||||
|
# cas 5 : formsemestre_id
|
||||||
|
formsemestre_id = requested.args.get("formsemestre_id")
|
||||||
|
|
||||||
|
if formsemestre_id is not None:
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||||
|
|
||||||
|
return assiduites_query
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -10,12 +10,13 @@
|
|||||||
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
|
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
|
||||||
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
|
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
|
||||||
"""
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import jsonify, request
|
from flask import jsonify, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import db
|
||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
@ -42,7 +43,7 @@ def get_departement(dept_ident: str) -> Departement:
|
|||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def departements_list():
|
def departements_list():
|
||||||
"""Liste les départements"""
|
"""Liste les départements"""
|
||||||
return jsonify([dept.to_dict() for dept in Departement.query])
|
return jsonify([dept.to_dict(with_dept_name=True) for dept in Departement.query])
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/departements_ids")
|
@bp.route("/departements_ids")
|
||||||
@ -66,13 +67,14 @@ def departement(acronym: str):
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"acronym": "TAPI",
|
"acronym": "TAPI",
|
||||||
|
"dept_name" : "TEST",
|
||||||
"description": null,
|
"description": null,
|
||||||
"visible": true,
|
"visible": true,
|
||||||
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||||
return jsonify(dept.to_dict())
|
return jsonify(dept.to_dict(with_dept_name=True))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/departement/id/<int:dept_id>")
|
@bp.route("/departement/id/<int:dept_id>")
|
||||||
@ -256,15 +258,18 @@ def dept_formsemestres_courants(acronym: str):
|
|||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||||
|
date_courante = request.args.get("date_courante")
|
||||||
|
if date_courante:
|
||||||
|
test_date = datetime.fromisoformat(date_courante)
|
||||||
|
else:
|
||||||
|
test_date = app.db.func.now()
|
||||||
# Les semestres en cours de ce département
|
# Les semestres en cours de ce département
|
||||||
formsemestres = FormSemestre.query.filter(
|
formsemestres = FormSemestre.query.filter(
|
||||||
FormSemestre.dept_id == dept.id,
|
FormSemestre.dept_id == dept.id,
|
||||||
FormSemestre.date_debut <= app.db.func.now(),
|
FormSemestre.date_debut <= test_date,
|
||||||
FormSemestre.date_fin >= app.db.func.now(),
|
FormSemestre.date_fin >= test_date,
|
||||||
)
|
)
|
||||||
|
return jsonify([d.to_dict_api() for d in formsemestres])
|
||||||
return jsonify([d.to_dict(convert_objects=True) for d in formsemestres])
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||||
@ -277,12 +282,16 @@ def dept_formsemestres_courants_by_id(dept_id: int):
|
|||||||
"""
|
"""
|
||||||
# Le département, spécifié par un id ou un acronyme
|
# Le département, spécifié par un id ou un acronyme
|
||||||
dept = Departement.query.get_or_404(dept_id)
|
dept = Departement.query.get_or_404(dept_id)
|
||||||
|
date_courante = request.args.get("date_courante")
|
||||||
|
if date_courante:
|
||||||
|
test_date = datetime.fromisoformat(date_courante)
|
||||||
|
else:
|
||||||
|
test_date = app.db.func.now()
|
||||||
# Les semestres en cours de ce département
|
# Les semestres en cours de ce département
|
||||||
formsemestres = FormSemestre.query.filter(
|
formsemestres = FormSemestre.query.filter(
|
||||||
FormSemestre.dept_id == dept.id,
|
FormSemestre.dept_id == dept.id,
|
||||||
FormSemestre.date_debut <= app.db.func.now(),
|
FormSemestre.date_debut <= test_date,
|
||||||
FormSemestre.date_fin >= app.db.func.now(),
|
FormSemestre.date_fin >= test_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify([d.to_dict(convert_objects=True) for d in formsemestres])
|
return jsonify([d.to_dict_api() for d in formsemestres])
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
API : accès aux étudiants
|
API : accès aux étudiants
|
||||||
"""
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import g, jsonify
|
from flask import abort, g, jsonify, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from sqlalchemy import desc, or_
|
from sqlalchemy import desc, or_
|
||||||
@ -75,11 +76,16 @@ def etudiants_courants(long=False):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||||
|
date_courante = request.args.get("date_courante")
|
||||||
|
if date_courante:
|
||||||
|
test_date = datetime.fromisoformat(date_courante)
|
||||||
|
else:
|
||||||
|
test_date = app.db.func.now()
|
||||||
etuds = Identite.query.filter(
|
etuds = Identite.query.filter(
|
||||||
Identite.id == FormSemestreInscription.etudid,
|
Identite.id == FormSemestreInscription.etudid,
|
||||||
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||||
FormSemestre.date_debut <= app.db.func.now(),
|
FormSemestre.date_debut <= test_date,
|
||||||
FormSemestre.date_fin >= app.db.func.now(),
|
FormSemestre.date_fin >= test_date,
|
||||||
)
|
)
|
||||||
if not None in allowed_depts:
|
if not None in allowed_depts:
|
||||||
# restreint aux départements autorisés:
|
# restreint aux départements autorisés:
|
||||||
@ -204,160 +210,75 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
|||||||
|
|
||||||
|
|
||||||
@bp.route(
|
@bp.route(
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "long", "pdf": False},
|
|
||||||
)
|
)
|
||||||
@bp.route(
|
@bp.route(
|
||||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin",
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "long", "pdf": False},
|
|
||||||
)
|
)
|
||||||
@bp.route(
|
@bp.route(
|
||||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin",
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
|
||||||
methods=["GET"],
|
defaults={"pdf": True},
|
||||||
defaults={"version": "long", "pdf": False},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "long", "pdf": True},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": False},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": False},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": False},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": True},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": True},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": True},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "long", "pdf": True},
|
|
||||||
)
|
|
||||||
@bp.route(
|
|
||||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "long", "pdf": True},
|
|
||||||
)
|
)
|
||||||
@api_web_bp.route(
|
@api_web_bp.route(
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "long", "pdf": False},
|
|
||||||
)
|
)
|
||||||
@api_web_bp.route(
|
@api_web_bp.route(
|
||||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin",
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "long", "pdf": False},
|
|
||||||
)
|
)
|
||||||
@api_web_bp.route(
|
@api_web_bp.route(
|
||||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin",
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
|
||||||
methods=["GET"],
|
defaults={"pdf": True},
|
||||||
defaults={"version": "long", "pdf": False},
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "long", "pdf": True},
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": False},
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": False},
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": False},
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": True},
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": True},
|
|
||||||
)
|
|
||||||
@api_web_bp.route(
|
|
||||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
|
||||||
methods=["GET"],
|
|
||||||
defaults={"version": "short", "pdf": True},
|
|
||||||
)
|
)
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def etudiant_bulletin_semestre(
|
def bulletin(
|
||||||
formsemestre_id,
|
code_type: str = "etudid",
|
||||||
etudid: int = None,
|
code: str = None,
|
||||||
nip: str = None,
|
formsemestre_id: int = None,
|
||||||
ine: str = None,
|
version: str = "long",
|
||||||
version="long",
|
|
||||||
pdf: bool = False,
|
pdf: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné
|
Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné
|
||||||
|
|
||||||
formsemestre_id : l'id d'un formsemestre
|
formsemestre_id : l'id d'un formsemestre
|
||||||
etudid : l'etudid d'un étudiant
|
code_type : "etudid", "nip" ou "ine"
|
||||||
nip : le code nip d'un étudiant
|
code : valeur du code INE, NIP ou etudid, selon code_type.
|
||||||
ine : le code ine d'un étudiant
|
version : type de bulletin (par défaut, "long"): short, long, long_mat
|
||||||
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
||||||
|
|
||||||
|
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||||
"""
|
"""
|
||||||
|
if version == "pdf":
|
||||||
|
version = "long"
|
||||||
|
pdf = True
|
||||||
|
# return f"{code_type}={code}, version={version}, pdf={pdf}"
|
||||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||||
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 non trouve")
|
return json_error(404, "formsemestre non trouve")
|
||||||
if etudid is not None:
|
app.set_sco_dept(dept.acronym)
|
||||||
query = Identite.query.filter_by(id=etudid)
|
|
||||||
elif nip is not None:
|
|
||||||
query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id)
|
|
||||||
elif ine is not None:
|
|
||||||
query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id)
|
|
||||||
else:
|
|
||||||
return json_error(404, message="parametre manquant")
|
|
||||||
|
|
||||||
|
if code_type == "nip":
|
||||||
|
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
||||||
|
elif code_type == "etudid":
|
||||||
|
try:
|
||||||
|
etudid = int(code)
|
||||||
|
except ValueError:
|
||||||
|
return json_error(404, "invalid etudid type")
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
elif code_type == "ine":
|
||||||
|
query = Identite.query.filter_by(code_ine=code, dept_id=dept.id)
|
||||||
|
else:
|
||||||
|
return json_error(404, "invalid code_type")
|
||||||
etud = query.first()
|
etud = query.first()
|
||||||
if etud is None:
|
if etud is None:
|
||||||
return json_error(404, message="etudiant inexistant")
|
return json_error(404, message="etudiant inexistant")
|
||||||
|
|
||||||
app.set_sco_dept(dept.acronym)
|
|
||||||
|
|
||||||
if pdf:
|
if pdf:
|
||||||
pdf_response, _ = do_formsemestre_bulletinetud(
|
pdf_response, _ = do_formsemestre_bulletinetud(
|
||||||
formsemestre, etud.id, version=version, format="pdf"
|
formsemestre, etud.id, version=version, format="pdf"
|
||||||
)
|
)
|
||||||
return pdf_response
|
return pdf_response
|
||||||
|
|
||||||
return sco_bulletins.get_formsemestre_bulletin_etud_json(
|
return sco_bulletins.get_formsemestre_bulletin_etud_json(
|
||||||
formsemestre, etud, version=version
|
formsemestre, etud, version=version
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -22,6 +22,44 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/evaluation/<int:evaluation_id>")
|
||||||
|
@api_web_bp.route("/evaluation/<int:evaluation_id>")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def evaluation(evaluation_id: int):
|
||||||
|
"""Description d'une évaluation.
|
||||||
|
|
||||||
|
{
|
||||||
|
'coefficient': 1.0,
|
||||||
|
'date_debut': '2016-01-04T08:30:00',
|
||||||
|
'date_fin': '2016-01-04T12:30:00',
|
||||||
|
'description': 'TP NI9219 Température',
|
||||||
|
'evaluation_type': 0,
|
||||||
|
'id': 15797,
|
||||||
|
'moduleimpl_id': 1234,
|
||||||
|
'note_max': 20.0,
|
||||||
|
'numero': 3,
|
||||||
|
'poids': {
|
||||||
|
'UE1.1': 1.0,
|
||||||
|
'UE1.2': 1.0,
|
||||||
|
'UE1.3': 1.0
|
||||||
|
},
|
||||||
|
'publish_incomplete': False,
|
||||||
|
'visi_bulletin': True
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = (
|
||||||
|
query.join(ModuleImpl)
|
||||||
|
.join(FormSemestre)
|
||||||
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
)
|
||||||
|
e = query.first_or_404()
|
||||||
|
return jsonify(e.to_dict_api())
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
||||||
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
||||||
@login_required
|
@login_required
|
||||||
@ -33,39 +71,16 @@ def evaluations(moduleimpl_id: int):
|
|||||||
|
|
||||||
moduleimpl_id : l'id d'un moduleimpl
|
moduleimpl_id : l'id d'un moduleimpl
|
||||||
|
|
||||||
Exemple de résultat :
|
Exemple de résultat : voir /evaluation
|
||||||
[
|
|
||||||
{
|
|
||||||
"moduleimpl_id": 1,
|
|
||||||
"jour": "20/04/2022",
|
|
||||||
"heure_debut": "08h00",
|
|
||||||
"description": "eval1",
|
|
||||||
"coefficient": 1.0,
|
|
||||||
"publish_incomplete": false,
|
|
||||||
"numero": 0,
|
|
||||||
"id": 1,
|
|
||||||
"heure_fin": "09h00",
|
|
||||||
"note_max": 20.0,
|
|
||||||
"visibulletin": true,
|
|
||||||
"evaluation_type": 0,
|
|
||||||
"evaluation_id": 1,
|
|
||||||
"jouriso": "2022-04-20",
|
|
||||||
"duree": "1h",
|
|
||||||
"descrheure": " de 08h00 à 09h00",
|
|
||||||
"matin": 1,
|
|
||||||
"apresmidi": 0
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
"""
|
"""
|
||||||
query = Evaluation.query.filter_by(id=moduleimpl_id)
|
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = (
|
query = (
|
||||||
query.join(ModuleImpl)
|
query.join(ModuleImpl)
|
||||||
.join(FormSemestre)
|
.join(FormSemestre)
|
||||||
.filter_by(dept_id=g.scodoc_dept_id)
|
.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
return jsonify([d.to_dict() for d in query])
|
return jsonify([e.to_dict_api() for e in query])
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -22,6 +22,8 @@ from app.models import (
|
|||||||
Evaluation,
|
Evaluation,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
FormSemestreEtape,
|
FormSemestreEtape,
|
||||||
|
FormSemestreInscription,
|
||||||
|
Identite,
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
NotesNotes,
|
NotesNotes,
|
||||||
)
|
)
|
||||||
@ -95,11 +97,14 @@ def formsemestres_query():
|
|||||||
annee_scolaire : année de début de l'année scolaire
|
annee_scolaire : année de début de l'année scolaire
|
||||||
dept_acronym : acronyme du département (eg "RT")
|
dept_acronym : acronyme du département (eg "RT")
|
||||||
dept_id : id du département
|
dept_id : id du département
|
||||||
|
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
||||||
"""
|
"""
|
||||||
etape_apo = request.args.get("etape_apo")
|
etape_apo = request.args.get("etape_apo")
|
||||||
annee_scolaire = request.args.get("annee_scolaire")
|
annee_scolaire = request.args.get("annee_scolaire")
|
||||||
dept_acronym = request.args.get("dept_acronym")
|
dept_acronym = request.args.get("dept_acronym")
|
||||||
dept_id = request.args.get("dept_id")
|
dept_id = request.args.get("dept_id")
|
||||||
|
nip = request.args.get("nip")
|
||||||
|
ine = request.args.get("ine")
|
||||||
formsemestres = FormSemestre.query
|
formsemestres = FormSemestre.query
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id)
|
formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
@ -125,16 +130,30 @@ def formsemestres_query():
|
|||||||
formsemestres = formsemestres.join(FormSemestreEtape).filter(
|
formsemestres = formsemestres.join(FormSemestreEtape).filter(
|
||||||
FormSemestreEtape.etape_apo == etape_apo
|
FormSemestreEtape.etape_apo == etape_apo
|
||||||
)
|
)
|
||||||
|
inscr_joined = False
|
||||||
|
if nip is not None:
|
||||||
|
formsemestres = (
|
||||||
|
formsemestres.join(FormSemestreInscription)
|
||||||
|
.join(Identite)
|
||||||
|
.filter_by(code_nip=nip)
|
||||||
|
)
|
||||||
|
inscr_joined = True
|
||||||
|
if ine is not None:
|
||||||
|
if not inscr_joined:
|
||||||
|
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
|
||||||
|
formsemestres = formsemestres.filter_by(code_ine=ine)
|
||||||
|
|
||||||
return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres])
|
return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres])
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||||
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||||
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||||
@login_required
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def bulletins(formsemestre_id: int):
|
def bulletins(formsemestre_id: int, version: str = "long"):
|
||||||
"""
|
"""
|
||||||
Retourne les bulletins d'un formsemestre donné
|
Retourne les bulletins d'un formsemestre donné
|
||||||
|
|
||||||
@ -145,12 +164,16 @@ def bulletins(formsemestre_id: int):
|
|||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first()
|
||||||
|
if formsemestre is None:
|
||||||
|
return json_error(404, "formsemestre non trouve")
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for etu in formsemestre.etuds:
|
for etu in formsemestre.etuds:
|
||||||
bul_etu = get_formsemestre_bulletin_etud_json(formsemestre, etu)
|
bul_etu = get_formsemestre_bulletin_etud_json(
|
||||||
|
formsemestre, etu, version=version
|
||||||
|
)
|
||||||
data.append(bul_etu.json)
|
data.append(bul_etu.json)
|
||||||
|
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
@ -381,7 +404,7 @@ def etat_evals(formsemestre_id: int):
|
|||||||
for evaluation_id in modimpl_results.evaluations_etat:
|
for evaluation_id in modimpl_results.evaluations_etat:
|
||||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||||
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||||
eval_dict = evaluation.to_dict()
|
eval_dict = evaluation.to_dict_api()
|
||||||
eval_dict["etat"] = eval_etat.to_dict()
|
eval_dict["etat"] = eval_etat.to_dict()
|
||||||
|
|
||||||
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
|
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
580
app/api/justificatifs.py
Normal file
580
app/api/justificatifs.py
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
"""ScoDoc 9 API : Assiduités
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import g, jsonify, request
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app import db
|
||||||
|
from app.api import api_bp as bp
|
||||||
|
from app.api import api_web_bp
|
||||||
|
from app.api import get_model_api_object
|
||||||
|
from app.decorators import permission_required, scodoc
|
||||||
|
from app.models import Identite, Justificatif
|
||||||
|
from app.models.assiduites import is_period_conflicting
|
||||||
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
|
|
||||||
|
|
||||||
|
# @bp.route("/justificatif/remove")
|
||||||
|
# @api_web_bp.route("/justificatif/remove")
|
||||||
|
# @scodoc
|
||||||
|
# def justremove():
|
||||||
|
# """ """
|
||||||
|
# archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
|
||||||
|
# archiver.delete_justificatif(etudid=1, archive_id="2023-02-01-10-29-20")
|
||||||
|
# return jsonify("done")
|
||||||
|
|
||||||
|
# Partie Modèle
|
||||||
|
@bp.route("/justificatif/<int:justif_id>")
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatif(justif_id: int = None):
|
||||||
|
"""Retourne un objet justificatif à partir de son id
|
||||||
|
|
||||||
|
Exemple de résultat:
|
||||||
|
{
|
||||||
|
"justif_id": 1,
|
||||||
|
"etudid": 2,
|
||||||
|
"date_debut": "2022-10-31T08:00+01:00",
|
||||||
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
|
"etat": "valide",
|
||||||
|
"fichier": "archive_id",
|
||||||
|
"raison": "une raison",
|
||||||
|
"entry_date": "2022-10-31T08:00+01:00",
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@api_web_bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||||
|
@api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def justificatifs(etudid: int = None, with_query: bool = False):
|
||||||
|
"""
|
||||||
|
Retourne toutes les assiduités d'un étudiant
|
||||||
|
chemin : /justificatifs/<int:etudid>
|
||||||
|
|
||||||
|
Un filtrage peut être donné avec une query
|
||||||
|
chemin : /justificatifs/<int:etudid>/query?
|
||||||
|
|
||||||
|
Les différents filtres :
|
||||||
|
Etat (etat du justificatif -> validé, non validé, modifé, en attente):
|
||||||
|
query?etat=[- liste des états séparé par une virgule -]
|
||||||
|
ex: .../query?etat=validé,modifié
|
||||||
|
Date debut
|
||||||
|
(date de début du justificatif, sont affichés les justificatifs
|
||||||
|
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||||
|
query?date_debut=[- date au format iso -]
|
||||||
|
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||||
|
Date fin
|
||||||
|
(date de fin du justificatif, sont affichés les justificatifs
|
||||||
|
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||||
|
query?date_fin=[- date au format iso -]
|
||||||
|
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Identite.query.filter_by(id=etudid)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
etud: Identite = query.first_or_404(etudid)
|
||||||
|
justificatifs_query = etud.justificatifs
|
||||||
|
|
||||||
|
if with_query:
|
||||||
|
justificatifs_query = _filter_manager(request, justificatifs_query)
|
||||||
|
|
||||||
|
data_set: list[dict] = []
|
||||||
|
for just in justificatifs_query.all():
|
||||||
|
data = just.to_dict(format_api=True)
|
||||||
|
data_set.append(data)
|
||||||
|
|
||||||
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_create(etudid: int = None):
|
||||||
|
"""
|
||||||
|
Création d'un justificatif pour l'étudiant (etudid)
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_debut": str,
|
||||||
|
"date_fin": str,
|
||||||
|
"etat": str,
|
||||||
|
"raison":str,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||||
|
|
||||||
|
create_list: list[object] = request.get_json(force=True)
|
||||||
|
|
||||||
|
if not isinstance(create_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
errors: dict[int, str] = {}
|
||||||
|
success: dict[int, object] = {}
|
||||||
|
for i, data in enumerate(create_list):
|
||||||
|
code, obj = _create_singular(data, etud)
|
||||||
|
if code == 404:
|
||||||
|
errors[i] = obj
|
||||||
|
else:
|
||||||
|
success[i] = obj
|
||||||
|
|
||||||
|
return jsonify({"errors": errors, "success": success})
|
||||||
|
|
||||||
|
|
||||||
|
def _create_singular(
|
||||||
|
data: dict,
|
||||||
|
etud: Identite,
|
||||||
|
) -> tuple[int, object]:
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# -- vérifications de l'objet json --
|
||||||
|
# cas 1 : ETAT
|
||||||
|
etat = data.get("etat", None)
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': manquant")
|
||||||
|
elif not scu.EtatJustificatif.contains(etat):
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
|
||||||
|
etat = scu.EtatJustificatif.get(etat)
|
||||||
|
|
||||||
|
# cas 2 : date_debut
|
||||||
|
date_debut = data.get("date_debut", None)
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
# cas 3 : date_fin
|
||||||
|
date_fin = data.get("date_fin", None)
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
|
||||||
|
# cas 4 : raison
|
||||||
|
|
||||||
|
raison: str = data.get("raison", None)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return (404, err)
|
||||||
|
|
||||||
|
# TOUT EST OK
|
||||||
|
|
||||||
|
try:
|
||||||
|
nouv_justificatif: Justificatif = Justificatif.create_justificatif(
|
||||||
|
date_debut=deb,
|
||||||
|
date_fin=fin,
|
||||||
|
etat=etat,
|
||||||
|
etud=etud,
|
||||||
|
raison=raison,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_justificatif)
|
||||||
|
db.session.commit()
|
||||||
|
return (200, {"justif_id": nouv_justificatif.id})
|
||||||
|
except ScoValueError as excp:
|
||||||
|
return (
|
||||||
|
404,
|
||||||
|
excp.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_edit(justif_id: int):
|
||||||
|
"""
|
||||||
|
Edition d'un justificatif à partir de son id
|
||||||
|
La requête doit avoir un content type "application/json":
|
||||||
|
|
||||||
|
{
|
||||||
|
"etat"?: str,
|
||||||
|
"raison"?: str
|
||||||
|
"date_debut"?: str
|
||||||
|
"date_fin"?: str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||||
|
id=justif_id
|
||||||
|
).first_or_404()
|
||||||
|
errors: list[str] = []
|
||||||
|
data = request.get_json(force=True)
|
||||||
|
|
||||||
|
# Vérifications de data
|
||||||
|
|
||||||
|
# Cas 1 : Etat
|
||||||
|
if data.get("etat") is not None:
|
||||||
|
etat = scu.EtatJustificatif.get(data.get("etat"))
|
||||||
|
if etat is None:
|
||||||
|
errors.append("param 'etat': invalide")
|
||||||
|
else:
|
||||||
|
justificatif_unique.etat = etat
|
||||||
|
|
||||||
|
# Cas 2 : raison
|
||||||
|
raison = data.get("raison", False)
|
||||||
|
if raison is not False:
|
||||||
|
justificatif_unique.raison = raison
|
||||||
|
|
||||||
|
deb, fin = None, None
|
||||||
|
|
||||||
|
# cas 3 : date_debut
|
||||||
|
date_debut = data.get("date_debut", False)
|
||||||
|
if date_debut is not False:
|
||||||
|
if date_debut is None:
|
||||||
|
errors.append("param 'date_debut': manquant")
|
||||||
|
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||||
|
if deb is None:
|
||||||
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
|
if justificatif_unique.date_fin >= deb:
|
||||||
|
errors.append("param 'date_debut': date de début située après date de fin ")
|
||||||
|
|
||||||
|
# cas 4 : date_fin
|
||||||
|
date_fin = data.get("date_fin", False)
|
||||||
|
if date_fin is not False:
|
||||||
|
if date_fin is None:
|
||||||
|
errors.append("param 'date_fin': manquant")
|
||||||
|
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||||
|
if fin is None:
|
||||||
|
errors.append("param 'date_fin': format invalide")
|
||||||
|
if justificatif_unique.date_debut <= fin:
|
||||||
|
errors.append("param 'date_fin': date de fin située avant date de début ")
|
||||||
|
|
||||||
|
# Vérification du conflit d'horaire
|
||||||
|
if (deb is not None) or (fin is not None):
|
||||||
|
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||||
|
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||||
|
|
||||||
|
justificatifs_list: list[Justificatif] = Justificatif.query.filter_by(
|
||||||
|
etuid=justificatif_unique.etudid
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if is_period_conflicting(deb, fin, justificatifs_list):
|
||||||
|
errors.append(
|
||||||
|
"Modification de la plage horaire impossible: conflit avec les autres justificatifs"
|
||||||
|
)
|
||||||
|
justificatif_unique.date_debut = deb
|
||||||
|
justificatif_unique.date_fin = fin
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
err: str = ", ".join(errors)
|
||||||
|
return json_error(404, err)
|
||||||
|
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"OK": True})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/delete", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_delete():
|
||||||
|
"""
|
||||||
|
Suppression d'un justificatif à partir de son id
|
||||||
|
|
||||||
|
Forme des données envoyées :
|
||||||
|
|
||||||
|
[
|
||||||
|
<justif_id:int>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
justificatifs_list: list[int] = request.get_json(force=True)
|
||||||
|
if not isinstance(justificatifs_list, list):
|
||||||
|
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||||
|
|
||||||
|
output = {"errors": {}, "success": {}}
|
||||||
|
|
||||||
|
for i, ass in enumerate(justificatifs_list):
|
||||||
|
code, msg = _delete_singular(ass, db)
|
||||||
|
if code == 404:
|
||||||
|
output["errors"][f"{i}"] = msg
|
||||||
|
else:
|
||||||
|
output["success"][f"{i}"] = {"OK": True}
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(output)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_singular(justif_id: int, database):
|
||||||
|
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||||
|
id=justif_id
|
||||||
|
).first()
|
||||||
|
if justificatif_unique is None:
|
||||||
|
return (404, "Justificatif non existant")
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
if archive_name is not None:
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
archiver.delete_justificatif(justificatif_unique.etudid, archive_name)
|
||||||
|
|
||||||
|
database.session.delete(justificatif_unique)
|
||||||
|
return (200, "OK")
|
||||||
|
|
||||||
|
|
||||||
|
# Partie archivage
|
||||||
|
@bp.route("/justificatif/import/<int:justif_id>", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/import/<int:justif_id>", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_import(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Importation d'un fichier (création d'archive)
|
||||||
|
"""
|
||||||
|
if len(request.files) == 0:
|
||||||
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
|
file = list(request.files.values())[0]
|
||||||
|
if file.filename == "":
|
||||||
|
return json_error(404, "Il n'y a pas de fichier joint")
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
try:
|
||||||
|
fname: str
|
||||||
|
archive_name, fname = archiver.save_justificatif(
|
||||||
|
etudid=justificatif_unique.etudid,
|
||||||
|
filename=file.filename,
|
||||||
|
data=file.stream.read(),
|
||||||
|
archive_name=archive_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
justificatif_unique.fichier = archive_name
|
||||||
|
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"filename": fname})
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_export(justif_id: int = None, filename: str = None):
|
||||||
|
"""
|
||||||
|
Retourne un fichier d'une archive d'un justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
if archive_name is None:
|
||||||
|
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return archiver.get_justificatif_file(
|
||||||
|
archive_name, justificatif_unique.etudid, filename
|
||||||
|
)
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/remove/<int:justif_id>", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/remove/<int:justif_id>", methods=["POST"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_remove(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Supression d'un fichier ou d'une archive
|
||||||
|
# TOTALK: Doc, expliquer les noms coté server
|
||||||
|
{
|
||||||
|
"remove": <"all"/"list">
|
||||||
|
|
||||||
|
"filenames"?: [
|
||||||
|
<filename:str>,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: dict = request.get_json(force=True)
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
if archive_name is None:
|
||||||
|
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||||
|
|
||||||
|
remove: str = data.get("remove")
|
||||||
|
if remove is None or remove not in ("all", "list"):
|
||||||
|
return json_error(404, "param 'remove': Valeur invalide")
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
etudid: int = justificatif_unique.etudid
|
||||||
|
try:
|
||||||
|
if remove == "all":
|
||||||
|
archiver.delete_justificatif(etudid=etudid, archive_name=archive_name)
|
||||||
|
justificatif_unique.fichier = None
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
else:
|
||||||
|
for fname in data.get("filenames", []):
|
||||||
|
archiver.delete_justificatif(
|
||||||
|
etudid=etudid,
|
||||||
|
archive_name=archive_name,
|
||||||
|
filename=fname,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(archiver.list_justificatifs(archive_name, etudid)) == 0:
|
||||||
|
archiver.delete_justificatif(etudid, archive_name)
|
||||||
|
justificatif_unique.fichier = None
|
||||||
|
db.session.add(justificatif_unique)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
except ScoValueError as err:
|
||||||
|
return json_error(404, err.args[0])
|
||||||
|
|
||||||
|
return jsonify({"response": "removed"})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/justificatif/list/<int:justif_id>", methods=["GET"])
|
||||||
|
@api_web_bp.route("/justificatif/list/<int:justif_id>", methods=["GET"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_list(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Liste les fichiers du justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
archive_name: str = justificatif_unique.fichier
|
||||||
|
|
||||||
|
filenames: list[str] = []
|
||||||
|
|
||||||
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||||
|
if archive_name is not None:
|
||||||
|
filenames = archiver.list_justificatifs(
|
||||||
|
archive_name, justificatif_unique.etudid
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(filenames)
|
||||||
|
|
||||||
|
|
||||||
|
# Partie justification
|
||||||
|
@bp.route("/justificatif/justified/<int:justif_id>", methods=["GET"])
|
||||||
|
@api_web_bp.route("/justificatif/justified/<int:justif_id>", methods=["GET"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_justified(justif_id: int = None):
|
||||||
|
"""
|
||||||
|
Liste assiduite_id justifiées par le justificatif
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Justificatif.query.filter_by(id=justif_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
|
justificatif_unique: Justificatif = query.first_or_404()
|
||||||
|
|
||||||
|
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||||
|
|
||||||
|
return jsonify(assiduites_list)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Utils --
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_manager(requested, justificatifs_query):
|
||||||
|
"""
|
||||||
|
Retourne les justificatifs entrés filtrés en fonction de la request
|
||||||
|
"""
|
||||||
|
# cas 1 : etat justificatif
|
||||||
|
etat = requested.args.get("etat")
|
||||||
|
if etat is not None:
|
||||||
|
justificatifs_query = scass.filter_justificatifs_by_etat(
|
||||||
|
justificatifs_query, etat
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 2 : date de début
|
||||||
|
deb = requested.args.get("date_debut")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
justificatifs_query: Justificatif = scass.filter_by_date(
|
||||||
|
justificatifs_query, Justificatif, deb, fin
|
||||||
|
)
|
||||||
|
|
||||||
|
return justificatifs_query
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -48,6 +48,7 @@ from app.scodoc.sco_permissions import Permission
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoSuperAdmin)
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
def api_get_glob_logos():
|
def api_get_glob_logos():
|
||||||
|
"""Liste tous les logos"""
|
||||||
logos = list_logos()[None]
|
logos = list_logos()[None]
|
||||||
return jsonify(list(logos.keys()))
|
return jsonify(list(logos.keys()))
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ from app.models import FormSemestre, FormSemestreInscription, Identite
|
|||||||
from app.models import GroupDescr, Partition
|
from app.models import GroupDescr, Partition
|
||||||
from app.models.groups import group_membership
|
from app.models.groups import group_membership
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
|
from app.scodoc import sco_groups
|
||||||
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 import sco_utils as scu
|
||||||
|
|
||||||
@ -170,24 +171,15 @@ def set_etud_group(etudid: int, group_id: int):
|
|||||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
group = query.first_or_404()
|
group = query.first_or_404()
|
||||||
|
if not group.partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
|
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
|
||||||
return json_error(404, "etud non inscrit au formsemestre du groupe")
|
return json_error(404, "etud non inscrit au formsemestre du groupe")
|
||||||
groups = (
|
|
||||||
GroupDescr.query.filter_by(partition_id=group.partition.id)
|
sco_groups.change_etud_group_in_partition(
|
||||||
.join(group_membership)
|
etudid, group_id, group.partition.to_dict()
|
||||||
.filter_by(etudid=etudid)
|
|
||||||
)
|
)
|
||||||
ok = False
|
|
||||||
for other_group in groups:
|
|
||||||
if other_group.id == group_id:
|
|
||||||
ok = True
|
|
||||||
else:
|
|
||||||
other_group.etuds.remove(etud)
|
|
||||||
if not ok:
|
|
||||||
group.etuds.append(etud)
|
|
||||||
log(f"set_etud_group({etud}, {group})")
|
|
||||||
db.session.commit()
|
|
||||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
|
||||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||||
|
|
||||||
|
|
||||||
@ -207,6 +199,8 @@ def group_remove_etud(group_id: int, etudid: int):
|
|||||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
group = query.first_or_404()
|
group = query.first_or_404()
|
||||||
|
if not group.partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
if etud in group.etuds:
|
if etud in group.etuds:
|
||||||
group.etuds.remove(etud)
|
group.etuds.remove(etud)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
partition = query.first_or_404()
|
partition = query.first_or_404()
|
||||||
|
if not partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
groups = (
|
groups = (
|
||||||
GroupDescr.query.filter_by(partition_id=partition_id)
|
GroupDescr.query.filter_by(partition_id=partition_id)
|
||||||
.join(group_membership)
|
.join(group_membership)
|
||||||
@ -262,8 +258,10 @@ def group_create(partition_id: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
partition: Partition = query.first_or_404()
|
partition: Partition = query.first_or_404()
|
||||||
|
if not partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
if not partition.groups_editable:
|
if not partition.groups_editable:
|
||||||
return json_error(404, "partition non editable")
|
return json_error(403, "partition non editable")
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
group_name = data.get("group_name")
|
group_name = data.get("group_name")
|
||||||
if group_name is None:
|
if group_name is None:
|
||||||
@ -294,8 +292,10 @@ def group_delete(group_id: int):
|
|||||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
group: GroupDescr = query.first_or_404()
|
group: GroupDescr = query.first_or_404()
|
||||||
|
if not group.partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
if not group.partition.groups_editable:
|
if not group.partition.groups_editable:
|
||||||
return json_error(404, "partition non editable")
|
return json_error(403, "partition non editable")
|
||||||
formsemestre_id = group.partition.formsemestre_id
|
formsemestre_id = group.partition.formsemestre_id
|
||||||
log(f"deleting {group}")
|
log(f"deleting {group}")
|
||||||
db.session.delete(group)
|
db.session.delete(group)
|
||||||
@ -318,8 +318,10 @@ def group_edit(group_id: int):
|
|||||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
)
|
)
|
||||||
group: GroupDescr = query.first_or_404()
|
group: GroupDescr = query.first_or_404()
|
||||||
|
if not group.partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
if not group.partition.groups_editable:
|
if not group.partition.groups_editable:
|
||||||
return json_error(404, "partition non editable")
|
return json_error(403, "partition non editable")
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
group_name = data.get("group_name")
|
group_name = data.get("group_name")
|
||||||
if group_name is not None:
|
if group_name is not None:
|
||||||
@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
|
if not formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
partition_name = data.get("partition_name")
|
partition_name = data.get("partition_name")
|
||||||
if partition_name is None:
|
if partition_name is None:
|
||||||
@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
|
if not formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
if not isinstance(partition_ids, int) and not all(
|
if not isinstance(partition_ids, int) and not all(
|
||||||
isinstance(x, int) for x in partition_ids
|
isinstance(x, int) for x in partition_ids
|
||||||
@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
partition: Partition = query.first_or_404()
|
partition: Partition = query.first_or_404()
|
||||||
|
if not partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
if not isinstance(group_ids, int) and not all(
|
if not isinstance(group_ids, int) and not all(
|
||||||
isinstance(x, int) for x in group_ids
|
isinstance(x, int) for x in group_ids
|
||||||
@ -484,6 +492,8 @@ def partition_edit(partition_id: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
partition: Partition = query.first_or_404()
|
partition: Partition = query.first_or_404()
|
||||||
|
if not partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
modified = False
|
modified = False
|
||||||
partition_name = data.get("partition_name")
|
partition_name = data.get("partition_name")
|
||||||
@ -542,6 +552,8 @@ def partition_delete(partition_id: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
partition: Partition = query.first_or_404()
|
partition: Partition = query.first_or_404()
|
||||||
|
if not partition.formsemestre.etat:
|
||||||
|
return json_error(403, "formsemestre verrouillé")
|
||||||
if not partition.partition_name:
|
if not partition.partition_name:
|
||||||
return json_error(404, "ne peut pas supprimer la partition par défaut")
|
return json_error(404, "ne peut pas supprimer la partition par défaut")
|
||||||
is_parcours = partition.is_parcours()
|
is_parcours = partition.is_parcours()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 API : outils
|
"""ScoDoc 9 API : outils
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -11,5 +11,5 @@ def send_password_reset_email(user):
|
|||||||
sender=current_app.config["SCODOC_MAIL_FROM"],
|
sender=current_app.config["SCODOC_MAIL_FROM"],
|
||||||
recipients=[user.email],
|
recipients=[user.email],
|
||||||
text_body=render_template("email/reset_password.txt", user=user, token=token),
|
text_body=render_template("email/reset_password.txt", user=user, token=token),
|
||||||
html_body=render_template("email/reset_password.html", user=user, token=token),
|
html_body=render_template("email/reset_password.j2", user=user, token=token),
|
||||||
)
|
)
|
||||||
|
@ -42,7 +42,7 @@ def login():
|
|||||||
return form.redirect("scodoc.index")
|
return form.redirect("scodoc.index")
|
||||||
message = request.args.get("message", "")
|
message = request.args.get("message", "")
|
||||||
return render_template(
|
return render_template(
|
||||||
"auth/login.html", title=_("Sign In"), form=form, message=message
|
"auth/login.j2", title=_("Sign In"), form=form, message=message
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -65,9 +65,7 @@ def create_user():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f"Utilisateur {user.user_name} créé")
|
flash(f"Utilisateur {user.user_name} créé")
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
return render_template(
|
return render_template("auth/register.j2", title="Création utilisateur", form=form)
|
||||||
"auth/register.html", title="Création utilisateur", form=form
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/reset_password_request", methods=["GET", "POST"])
|
@bp.route("/reset_password_request", methods=["GET", "POST"])
|
||||||
@ -98,7 +96,7 @@ def reset_password_request():
|
|||||||
)
|
)
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
return render_template(
|
return render_template(
|
||||||
"auth/reset_password_request.html", title=_("Reset Password"), form=form
|
"auth/reset_password_request.j2", title=_("Reset Password"), form=form
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -116,7 +114,7 @@ def reset_password(token):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_("Votre mot de passe a été changé."))
|
flash(_("Votre mot de passe a été changé."))
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
return render_template("auth/reset_password.html", form=form, user=user)
|
return render_template("auth/reset_password.j2", form=form, user=user)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
|
@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -8,14 +8,14 @@
|
|||||||
Edition associations UE <-> Ref. Compétence
|
Edition associations UE <-> Ref. Compétence
|
||||||
"""
|
"""
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
from app import db, log
|
from app.models import ApcReferentielCompetences, Formation, UniteEns
|
||||||
from app.models import Formation, UniteEns
|
|
||||||
from app.models.but_refcomp import ApcNiveau
|
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
|
|
||||||
|
|
||||||
def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
def form_ue_choix_niveau(ue: UniteEns) -> str:
|
||||||
"""Form. HTML pour associer une UE à un niveau de compétence"""
|
"""Form. HTML pour associer une UE à un niveau de compétence.
|
||||||
|
Le menu select lui meême est vide et rempli en JS par appel à get_ue_niveaux_options_html
|
||||||
|
"""
|
||||||
if ue.type != sco_codes_parcours.UE_STANDARD:
|
if ue.type != sco_codes_parcours.UE_STANDARD:
|
||||||
return ""
|
return ""
|
||||||
ref_comp = ue.formation.referentiel_competence
|
ref_comp = ue.formation.referentiel_competence
|
||||||
@ -27,11 +27,70 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
|||||||
}">associer un référentiel de compétence</a>
|
}">associer un référentiel de compétence</a>
|
||||||
</div>
|
</div>
|
||||||
</div>"""
|
</div>"""
|
||||||
annee = 1 if ue.semestre_idx is None else (ue.semestre_idx + 1) // 2 # 1, 2, 3
|
# Les parcours:
|
||||||
niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
|
parcours_options = []
|
||||||
|
for parcour in ref_comp.parcours:
|
||||||
|
parcours_options.append(
|
||||||
|
f"""<option value="{parcour.id}" {
|
||||||
|
'selected' if ue.parcour == parcour else ''}
|
||||||
|
>{parcour.libelle} ({parcour.code})
|
||||||
|
</option>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
newline = "\n"
|
||||||
|
return f"""
|
||||||
|
<div class="ue_choix_niveau">
|
||||||
|
<form class="form_ue_choix_niveau">
|
||||||
|
<div class="cont_ue_choix_niveau">
|
||||||
|
<div>
|
||||||
|
<b>Parcours :</b>
|
||||||
|
<select class="select_parcour"
|
||||||
|
onchange="set_ue_parcour(this);"
|
||||||
|
data-ue_id="{ue.id}"
|
||||||
|
data-setter="{
|
||||||
|
url_for( "notes.set_ue_parcours", scodoc_dept=g.scodoc_dept)
|
||||||
|
}">
|
||||||
|
<option value="" {
|
||||||
|
'selected' if ue.parcour is None else ''
|
||||||
|
}>Tous</option>
|
||||||
|
{newline.join(parcours_options)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Niveau de compétence :</b>
|
||||||
|
<select class="select_niveau_ue"
|
||||||
|
onchange="set_ue_niveau_competence(this);"
|
||||||
|
data-ue_id="{ue.id}"
|
||||||
|
data-setter="{
|
||||||
|
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
|
||||||
|
}">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
||||||
|
"""fragment html avec les options du menu de sélection du
|
||||||
|
niveau de compétences associé à une UE.
|
||||||
|
|
||||||
|
Si l'UE n'a pas de parcours associé: présente les niveaux
|
||||||
|
de tous les parcours.
|
||||||
|
Si l'UE a un parcours: seulement les niveaux de ce parcours.
|
||||||
|
"""
|
||||||
|
ref_comp: ApcReferentielCompetences = ue.formation.referentiel_competence
|
||||||
|
if ref_comp is None:
|
||||||
|
return ""
|
||||||
|
# Les niveaux:
|
||||||
|
annee = ue.annee() # 1, 2, 3
|
||||||
|
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(
|
||||||
|
annee, parcour=ue.parcour
|
||||||
|
)
|
||||||
|
|
||||||
# Les niveaux déjà associés à d'autres UE du même semestre
|
# Les niveaux déjà associés à d'autres UE du même semestre
|
||||||
autres_ues = formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||||
niveaux_autres_ues = {
|
niveaux_autres_ues = {
|
||||||
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
||||||
}
|
}
|
||||||
@ -39,18 +98,14 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
|||||||
if niveaux_by_parcours["TC"]: # TC pour Tronc Commun
|
if niveaux_by_parcours["TC"]: # TC pour Tronc Commun
|
||||||
options.append("""<optgroup label="Tronc commun">""")
|
options.append("""<optgroup label="Tronc commun">""")
|
||||||
for n in niveaux_by_parcours["TC"]:
|
for n in niveaux_by_parcours["TC"]:
|
||||||
if n.id in niveaux_autres_ues:
|
|
||||||
disabled = "disabled"
|
|
||||||
else:
|
|
||||||
disabled = ""
|
|
||||||
options.append(
|
options.append(
|
||||||
f"""<option value="{n.id}" {'selected'
|
f"""<option value="{n.id}" {
|
||||||
if ue.niveau_competence == n else ''}
|
'selected' if ue.niveau_competence == n else ''}
|
||||||
{disabled}>{n.annee} {n.competence.titre_long}
|
>{n.annee} {n.competence.titre_long}
|
||||||
niveau {n.ordre}</option>"""
|
niveau {n.ordre}</option>"""
|
||||||
)
|
)
|
||||||
options.append("""</optgroup>""")
|
options.append("""</optgroup>""")
|
||||||
for parcour in ref_comp.parcours:
|
for parcour in parcours:
|
||||||
if len(niveaux_by_parcours[parcour.id]):
|
if len(niveaux_by_parcours[parcour.id]):
|
||||||
options.append(f"""<optgroup label="Parcours {parcour.libelle}">""")
|
options.append(f"""<optgroup label="Parcours {parcour.libelle}">""")
|
||||||
for n in niveaux_by_parcours[parcour.id]:
|
for n in niveaux_by_parcours[parcour.id]:
|
||||||
@ -65,46 +120,7 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
|||||||
niveau {n.ordre}</option>"""
|
niveau {n.ordre}</option>"""
|
||||||
)
|
)
|
||||||
options.append("""</optgroup>""")
|
options.append("""</optgroup>""")
|
||||||
options_str = "\n".join(options)
|
return (
|
||||||
return f"""
|
f"""<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>"""
|
||||||
<div class="ue_choix_niveau">
|
+ "\n".join(options)
|
||||||
<form class="form_ue_choix_niveau">
|
)
|
||||||
<b>Niveau de compétence associé:</b>
|
|
||||||
<select onchange="set_ue_niveau_competence(this);"
|
|
||||||
data-ue_id="{ue.id}"
|
|
||||||
data-setter="{
|
|
||||||
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
|
|
||||||
}">
|
|
||||||
<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>
|
|
||||||
{options_str}
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def set_ue_niveau_competence(ue_id: int, niveau_id: int):
|
|
||||||
"""Associe le niveau et l'UE"""
|
|
||||||
ue = UniteEns.query.get_or_404(ue_id)
|
|
||||||
|
|
||||||
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
|
||||||
niveaux_autres_ues = {
|
|
||||||
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
|
||||||
}
|
|
||||||
if niveau_id in niveaux_autres_ues:
|
|
||||||
log(
|
|
||||||
f"set_ue_niveau_competence: denying association of {ue} to already associated {niveau_id}"
|
|
||||||
)
|
|
||||||
return "", 409 # conflict
|
|
||||||
if niveau_id == "":
|
|
||||||
niveau = ""
|
|
||||||
# suppression de l'association
|
|
||||||
ue.niveau_competence = None
|
|
||||||
else:
|
|
||||||
niveau = ApcNiveau.query.get_or_404(niveau_id)
|
|
||||||
ue.niveau_competence = niveau
|
|
||||||
db.session.add(ue)
|
|
||||||
db.session.commit()
|
|
||||||
log(f"set_ue_niveau_competence( {ue}, {niveau} )")
|
|
||||||
|
|
||||||
return "", 204
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -80,6 +80,9 @@ class BulletinBUT:
|
|||||||
"""
|
"""
|
||||||
res = self.res
|
res = self.res
|
||||||
|
|
||||||
|
if (etud.id, ue.id) in self.res.dispense_ues:
|
||||||
|
return {}
|
||||||
|
|
||||||
if ue.type == UE_SPORT:
|
if ue.type == UE_SPORT:
|
||||||
modimpls_spo = [
|
modimpls_spo = [
|
||||||
modimpl
|
modimpl
|
||||||
@ -239,6 +242,7 @@ class BulletinBUT:
|
|||||||
self.etud_eval_results(etud, e)
|
self.etud_eval_results(etud, e)
|
||||||
for e in modimpl.evaluations
|
for e in modimpl.evaluations
|
||||||
if (e.visibulletin or version == "long")
|
if (e.visibulletin or version == "long")
|
||||||
|
and (e.id in modimpl_results.evaluations_etat)
|
||||||
and (
|
and (
|
||||||
modimpl_results.evaluations_etat[e.id].is_complete
|
modimpl_results.evaluations_etat[e.id].is_complete
|
||||||
or self.prefs["bul_show_all_evals"]
|
or self.prefs["bul_show_all_evals"]
|
||||||
@ -256,10 +260,11 @@ class BulletinBUT:
|
|||||||
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[e.moduleimpl_id]
|
||||||
try:
|
try:
|
||||||
|
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][e.id]
|
||||||
for ue in self.res.ues
|
for ue in self.res.ues
|
||||||
if ue.type != UE_SPORT
|
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)
|
||||||
@ -356,7 +361,7 @@ class BulletinBUT:
|
|||||||
"formsemestre_id": formsemestre.id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etat_inscription": etat_inscription,
|
"etat_inscription": etat_inscription,
|
||||||
"options": sco_preferences.bulletin_option_affichage(
|
"options": sco_preferences.bulletin_option_affichage(
|
||||||
formsemestre.id, self.prefs
|
formsemestre, self.prefs
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
if not published:
|
if not published:
|
||||||
@ -460,6 +465,7 @@ class BulletinBUT:
|
|||||||
"ressources": {},
|
"ressources": {},
|
||||||
"saes": {},
|
"saes": {},
|
||||||
"ues": {},
|
"ues": {},
|
||||||
|
"ues_capitalisees": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -43,13 +43,13 @@ from app.but import bulletin_but
|
|||||||
from app.models import FormSemestre, Identite
|
from app.models import FormSemestre, Identite
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc import sco_abs
|
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_edit_ue
|
from app.scodoc import sco_edit_ue
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_photos
|
from app.scodoc import sco_photos
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
|
from app.scodoc.sco_xml import quote_xml_attr
|
||||||
|
|
||||||
|
|
||||||
def bulletin_but_xml_compat(
|
def bulletin_but_xml_compat(
|
||||||
@ -108,13 +108,13 @@ def bulletin_but_xml_compat(
|
|||||||
etudid=str(etudid),
|
etudid=str(etudid),
|
||||||
code_nip=etud.code_nip or "",
|
code_nip=etud.code_nip or "",
|
||||||
code_ine=etud.code_ine or "",
|
code_ine=etud.code_ine or "",
|
||||||
nom=scu.quote_xml_attr(etud.nom),
|
nom=quote_xml_attr(etud.nom),
|
||||||
prenom=scu.quote_xml_attr(etud.prenom),
|
prenom=quote_xml_attr(etud.prenom),
|
||||||
civilite=scu.quote_xml_attr(etud.civilite_str),
|
civilite=quote_xml_attr(etud.civilite_str),
|
||||||
sexe=scu.quote_xml_attr(etud.civilite_str), # compat
|
sexe=quote_xml_attr(etud.civilite_str), # compat
|
||||||
photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
|
photo_url=quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
|
||||||
email=scu.quote_xml_attr(etud.get_first_email() or ""),
|
email=quote_xml_attr(etud.get_first_email() or ""),
|
||||||
emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""),
|
emailperso=quote_xml_attr(etud.get_first_email("emailperso") or ""),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Disponible pour publication ?
|
# Disponible pour publication ?
|
||||||
@ -153,10 +153,10 @@ def bulletin_but_xml_compat(
|
|||||||
x_ue = Element(
|
x_ue = Element(
|
||||||
"ue",
|
"ue",
|
||||||
id=str(ue.id),
|
id=str(ue.id),
|
||||||
numero=scu.quote_xml_attr(ue.numero),
|
numero=quote_xml_attr(ue.numero),
|
||||||
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
|
acronyme=quote_xml_attr(ue.acronyme or ""),
|
||||||
titre=scu.quote_xml_attr(ue.titre or ""),
|
titre=quote_xml_attr(ue.titre or ""),
|
||||||
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
|
code_apogee=quote_xml_attr(ue.code_apogee or ""),
|
||||||
)
|
)
|
||||||
doc.append(x_ue)
|
doc.append(x_ue)
|
||||||
if ue.type != sco_codes_parcours.UE_SPORT:
|
if ue.type != sco_codes_parcours.UE_SPORT:
|
||||||
@ -192,11 +192,9 @@ def bulletin_but_xml_compat(
|
|||||||
code=str(modimpl.module.code or ""),
|
code=str(modimpl.module.code or ""),
|
||||||
coefficient=str(coef),
|
coefficient=str(coef),
|
||||||
numero=str(modimpl.module.numero or 0),
|
numero=str(modimpl.module.numero or 0),
|
||||||
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
|
titre=quote_xml_attr(modimpl.module.titre or ""),
|
||||||
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
|
abbrev=quote_xml_attr(modimpl.module.abbrev or ""),
|
||||||
code_apogee=scu.quote_xml_attr(
|
code_apogee=quote_xml_attr(modimpl.module.code_apogee or ""),
|
||||||
modimpl.module.code_apogee or ""
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
# XXX TODO rangs et effectifs
|
# XXX TODO rangs et effectifs
|
||||||
# --- notes de chaque eval:
|
# --- notes de chaque eval:
|
||||||
@ -215,7 +213,7 @@ def bulletin_but_xml_compat(
|
|||||||
coefficient=str(e.coefficient),
|
coefficient=str(e.coefficient),
|
||||||
# pas les poids en XML compat
|
# pas les poids en XML compat
|
||||||
evaluation_type=str(e.evaluation_type),
|
evaluation_type=str(e.evaluation_type),
|
||||||
description=scu.quote_xml_attr(e.description),
|
description=quote_xml_attr(e.description),
|
||||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||||
note_max_origin=str(e.note_max),
|
note_max_origin=str(e.note_max),
|
||||||
)
|
)
|
||||||
@ -262,7 +260,7 @@ def bulletin_but_xml_compat(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
x_situation = Element("situation")
|
x_situation = Element("situation")
|
||||||
x_situation.text = scu.quote_xml_attr(infos["situation"])
|
x_situation.text = quote_xml_attr(infos["situation"])
|
||||||
doc.append(x_situation)
|
doc.append(x_situation)
|
||||||
if dpv:
|
if dpv:
|
||||||
decision = dpv["decisions"][0]
|
decision = dpv["decisions"][0]
|
||||||
@ -297,9 +295,9 @@ def bulletin_but_xml_compat(
|
|||||||
Element(
|
Element(
|
||||||
"decision_ue",
|
"decision_ue",
|
||||||
ue_id=str(ue["ue_id"]),
|
ue_id=str(ue["ue_id"]),
|
||||||
numero=scu.quote_xml_attr(ue["numero"]),
|
numero=quote_xml_attr(ue["numero"]),
|
||||||
acronyme=scu.quote_xml_attr(ue["acronyme"]),
|
acronyme=quote_xml_attr(ue["acronyme"]),
|
||||||
titre=scu.quote_xml_attr(ue["titre"]),
|
titre=quote_xml_attr(ue["titre"]),
|
||||||
code=decision["decisions_ue"][ue_id]["code"],
|
code=decision["decisions_ue"][ue_id]["code"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -322,7 +320,7 @@ def bulletin_but_xml_compat(
|
|||||||
"appreciation",
|
"appreciation",
|
||||||
date=ndb.DateDMYtoISO(appr["date"]),
|
date=ndb.DateDMYtoISO(appr["date"]),
|
||||||
)
|
)
|
||||||
x_appr.text = scu.quote_xml_attr(appr["comment"])
|
x_appr.text = quote_xml_attr(appr["comment"])
|
||||||
doc.append(x_appr)
|
doc.append(x_appr)
|
||||||
|
|
||||||
if is_appending:
|
if is_appending:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7:
|
|||||||
avec la même interface.
|
avec la même interface.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import collections
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
@ -47,12 +47,14 @@ from app.models.validations import ScolarFormSemestreValidation
|
|||||||
from app.scodoc import sco_codes_parcours as sco_codes
|
from app.scodoc import sco_codes_parcours as sco_codes
|
||||||
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
|
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||||
|
|
||||||
from app.scodoc import sco_cursus_dut
|
from app.scodoc import sco_cursus_dut
|
||||||
|
|
||||||
|
|
||||||
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||||
|
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
|
||||||
|
|
||||||
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||||
super().__init__(etud, formsemestre_id, res)
|
super().__init__(etud, formsemestre_id, res)
|
||||||
# Ajustements pour le BUT
|
# Ajustements pour le BUT
|
||||||
@ -65,3 +67,117 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
|||||||
def parcours_validated(self):
|
def parcours_validated(self):
|
||||||
"True si le parcours est validé"
|
"True si le parcours est validé"
|
||||||
return False # XXX TODO
|
return False # XXX TODO
|
||||||
|
|
||||||
|
|
||||||
|
class EtudCursusBUT:
|
||||||
|
"""L'état de l'étudiant dans son cursus BUT
|
||||||
|
Liste des niveaux validés/à valider
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, etud: Identite, formation: Formation):
|
||||||
|
"""formation indique la spécialité préparée"""
|
||||||
|
# Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
|
||||||
|
if formation.id not in (
|
||||||
|
ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
|
||||||
|
):
|
||||||
|
raise ScoValueError(
|
||||||
|
f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}"
|
||||||
|
)
|
||||||
|
if not formation.referentiel_competence:
|
||||||
|
raise ScoNoReferentielCompetences(formation=formation)
|
||||||
|
#
|
||||||
|
self.etud = etud
|
||||||
|
self.formation = formation
|
||||||
|
self.inscriptions = sorted(
|
||||||
|
[
|
||||||
|
ins
|
||||||
|
for ins in etud.formsemestre_inscriptions
|
||||||
|
if ins.formsemestre.formation.referentiel_competence
|
||||||
|
and (
|
||||||
|
ins.formsemestre.formation.referentiel_competence.id
|
||||||
|
== formation.referentiel_competence.id
|
||||||
|
)
|
||||||
|
],
|
||||||
|
key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut),
|
||||||
|
)
|
||||||
|
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
||||||
|
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
||||||
|
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||||
|
self.niveaux_by_annee = {}
|
||||||
|
"{ annee : liste des niveaux à valider }"
|
||||||
|
self.niveaux: dict[int, ApcNiveau] = {}
|
||||||
|
"cache les niveaux"
|
||||||
|
for annee in (1, 2, 3):
|
||||||
|
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||||
|
annee, self.parcour
|
||||||
|
)[1]
|
||||||
|
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||||
|
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||||
|
niveaux_d[self.parcour.id] if self.parcour else []
|
||||||
|
)
|
||||||
|
self.niveaux.update(
|
||||||
|
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||||
|
)
|
||||||
|
# Probablement inutile:
|
||||||
|
# # Cherche les validations de jury enregistrées pour chaque niveau
|
||||||
|
# self.validations_by_niveau = collections.defaultdict(lambda: [])
|
||||||
|
# " { niveau_id : [ ApcValidationRCUE ] }"
|
||||||
|
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||||
|
# self.validations_by_niveau[validation_rcue.niveau().id].append(
|
||||||
|
# validation_rcue
|
||||||
|
# )
|
||||||
|
# self.validation_by_niveau = {
|
||||||
|
# niveau_id: sorted(
|
||||||
|
# validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
|
||||||
|
# )[0]
|
||||||
|
# for niveau_id, validations in self.validations_by_niveau.items()
|
||||||
|
# }
|
||||||
|
# "{ niveau_id : meilleure validation pour ce niveau }"
|
||||||
|
|
||||||
|
self.validation_par_competence_et_annee = {}
|
||||||
|
"{ competence_id : { 'BUT1' : validation_rcue, ... } }"
|
||||||
|
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||||
|
niveau = validation_rcue.niveau()
|
||||||
|
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||||
|
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||||
|
previous_validation = self.validation_par_competence_et_annee.get(
|
||||||
|
niveau.competence.id
|
||||||
|
).get(validation_rcue.annee())
|
||||||
|
# prend la "meilleure" validation
|
||||||
|
if (not previous_validation) or (
|
||||||
|
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||||
|
> sco_codes.BUT_CODES_ORDERED[previous_validation.code]
|
||||||
|
):
|
||||||
|
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||||
|
niveau.annee
|
||||||
|
] = validation_rcue
|
||||||
|
|
||||||
|
self.competences = {
|
||||||
|
competence.id: competence
|
||||||
|
for competence in (
|
||||||
|
self.parcour.query_competences()
|
||||||
|
if self.parcour
|
||||||
|
else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"cache { competence_id : competence }"
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
competence_id : {
|
||||||
|
annee : meilleure_validation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
competence.id: {
|
||||||
|
annee: {
|
||||||
|
self.validation_par_competence_et_annee.get(competence.id, {}).get(
|
||||||
|
annee
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for annee in ("BUT1", "BUT2", "BUT3")
|
||||||
|
}
|
||||||
|
for competence in self.competences.values()
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
from flask_wtf.file import FileField, FileAllowed
|
||||||
from wtforms import SelectField, SubmitField
|
from wtforms import SelectField, SubmitField
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Jury BUT: table recap annuelle et liens saisie
|
"""Jury BUT: table recap annuelle et liens saisie
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
@ -31,7 +32,7 @@ from app.scodoc.sco_codes_parcours import (
|
|||||||
from app.scodoc import sco_formsemestre_status
|
from app.scodoc import sco_formsemestre_status
|
||||||
from app.scodoc import sco_pvjury
|
from app.scodoc import sco_pvjury
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_saisie_jury_but(
|
def formsemestre_saisie_jury_but(
|
||||||
@ -58,20 +59,13 @@ def formsemestre_saisie_jury_but(
|
|||||||
# DecisionsProposeesAnnee(etud, formsemestre2)
|
# DecisionsProposeesAnnee(etud, formsemestre2)
|
||||||
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
|
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
|
||||||
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
|
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
|
||||||
if formsemestre2.semestre_id % 2 != 0:
|
# XXX if formsemestre2.semestre_id % 2 != 0:
|
||||||
raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
|
# raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
|
||||||
|
|
||||||
if formsemestre2.formation.referentiel_competence is None:
|
if formsemestre2.formation.referentiel_competence is None:
|
||||||
raise ScoValueError(
|
raise ScoNoReferentielCompetences(formation=formsemestre2.formation)
|
||||||
"""
|
|
||||||
<p>Pas de référentiel de compétences associé à la formation !</p>
|
|
||||||
<p>Pour associer un référentiel, passer par le menu <b>Semestre /
|
|
||||||
Voir la formation... </b> et suivre le lien <em>"associer à un référentiel
|
|
||||||
de compétences"</em>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
rows, titles, column_ids = get_jury_but_table(
|
rows, titles, column_ids, jury_stats = get_jury_but_table(
|
||||||
formsemestre2, read_only=read_only, mode=mode
|
formsemestre2, read_only=read_only, mode=mode
|
||||||
)
|
)
|
||||||
if not rows:
|
if not rows:
|
||||||
@ -153,6 +147,28 @@ def formsemestre_saisie_jury_but(
|
|||||||
f"""
|
f"""
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="jury_stats">
|
||||||
|
<div>Nb d'étudiants avec décision annuelle:
|
||||||
|
{sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]}
|
||||||
|
</div>
|
||||||
|
<div><b>Codes annuels octroyés:</b></div>
|
||||||
|
<table class="jury_stats_codes">
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for code in sorted(jury_stats["codes_annuels"].keys()):
|
||||||
|
H.append(
|
||||||
|
f"""<tr>
|
||||||
|
<td>{code}</td>
|
||||||
|
<td style="text-align:right">{jury_stats["codes_annuels"][code]}</td>
|
||||||
|
<td style="text-align:right">{
|
||||||
|
(100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}%
|
||||||
|
</td>
|
||||||
|
</tr>"""
|
||||||
|
)
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{html_sco_header.sco_footer()}
|
{html_sco_header.sco_footer()}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -262,12 +278,16 @@ class RowCollector:
|
|||||||
# --- Codes (seront cachés, mais exportés en excel)
|
# --- Codes (seront cachés, mais exportés en excel)
|
||||||
self.add_cell("etudid", "etudid", etud.id, "codes")
|
self.add_cell("etudid", "etudid", etud.id, "codes")
|
||||||
self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
|
self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
|
||||||
# --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO)
|
# --- Identité étudiant (adapté de res_common/get_table_recap, à factoriser XXX TODO)
|
||||||
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||||
self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
||||||
self["_nom_disp_order"] = etud.sort_key
|
self["_nom_disp_order"] = etud.sort_key
|
||||||
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||||
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
||||||
|
self["_nom_short_data"] = {
|
||||||
|
"etudid": etud.id,
|
||||||
|
"nomprenom": etud.nomprenom,
|
||||||
|
}
|
||||||
if with_links:
|
if with_links:
|
||||||
self["_nom_short_order"] = etud.sort_key
|
self["_nom_short_order"] = etud.sort_key
|
||||||
self["_nom_short_target"] = url_for(
|
self["_nom_short_target"] = url_for(
|
||||||
@ -352,10 +372,6 @@ class RowCollector:
|
|||||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
||||||
"col_rcue col_rcues_validables" + klass,
|
"col_rcue col_rcues_validables" + klass,
|
||||||
)
|
)
|
||||||
self["_rcues_validables_data"] = {
|
|
||||||
"etudid": deca.etud.id,
|
|
||||||
"nomprenom": deca.etud.nomprenom,
|
|
||||||
}
|
|
||||||
if len(deca.rcues_annee) > 0:
|
if len(deca.rcues_annee) > 0:
|
||||||
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
|
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
|
||||||
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
|
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
|
||||||
@ -377,10 +393,17 @@ class RowCollector:
|
|||||||
|
|
||||||
def get_jury_but_table(
|
def get_jury_but_table(
|
||||||
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
|
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
|
||||||
) -> tuple[list[dict], list[str], list[str]]:
|
) -> tuple[list[dict], list[str], list[str], dict]:
|
||||||
"""Construit la table des résultats annuels pour le jury BUT"""
|
"""Construit la table des résultats annuels pour le jury BUT
|
||||||
|
=> rows_dict, titles, column_ids, jury_stats
|
||||||
|
où jury_stats est un dict donnant des comptages sur le jury.
|
||||||
|
"""
|
||||||
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
||||||
titles = {} # column_id : title
|
titles = {} # column_id : title
|
||||||
|
jury_stats = {
|
||||||
|
"nb_etuds": len(formsemestre2.etuds_inscriptions),
|
||||||
|
"codes_annuels": collections.Counter(),
|
||||||
|
}
|
||||||
column_classes = {}
|
column_classes = {}
|
||||||
rows = []
|
rows = []
|
||||||
for etudid in formsemestre2.etuds_inscriptions:
|
for etudid in formsemestre2.etuds_inscriptions:
|
||||||
@ -417,6 +440,8 @@ def get_jury_but_table(
|
|||||||
f"""{deca.code_valide or ''}""",
|
f"""{deca.code_valide or ''}""",
|
||||||
"col_code_annee",
|
"col_code_annee",
|
||||||
)
|
)
|
||||||
|
if deca.code_valide:
|
||||||
|
jury_stats["codes_annuels"][deca.code_valide] += 1
|
||||||
# --- Le lien de saisie
|
# --- Le lien de saisie
|
||||||
if mode != "recap" and with_links:
|
if mode != "recap" and with_links:
|
||||||
row.add_cell(
|
row.add_cell(
|
||||||
@ -439,11 +464,14 @@ def get_jury_but_table(
|
|||||||
rows.append(row)
|
rows.append(row)
|
||||||
rows_dict = [row.get_row_dict() for row in rows]
|
rows_dict = [row.get_row_dict() for row in rows]
|
||||||
if len(rows_dict) > 0:
|
if len(rows_dict) > 0:
|
||||||
res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1)
|
col_idx = res2.recap_add_partitions(
|
||||||
|
rows_dict, titles, col_idx=row.last_etud_cell_idx + 1
|
||||||
|
)
|
||||||
|
res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1)
|
||||||
column_ids = [title for title in titles if not title.startswith("_")]
|
column_ids = [title for title in titles if not title.startswith("_")]
|
||||||
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
|
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
|
||||||
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
|
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
|
||||||
return rows_dict, titles, column_ids
|
return rows_dict, titles, column_ids, jury_stats
|
||||||
|
|
||||||
|
|
||||||
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -15,20 +15,32 @@ from app.scodoc import sco_cache
|
|||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_validation_auto_but(formsemestre: FormSemestre) -> int:
|
def formsemestre_validation_auto_but(
|
||||||
"""Calcul automatique des décisions de jury sur une année BUT.
|
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
|
||||||
Returns: nombre d'étudiants "admis"
|
) -> int:
|
||||||
|
"""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
|
||||||
|
si on a des RCUE "à cheval".
|
||||||
|
- Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
|
||||||
|
ce qui est utilisé pour certains tests unitaires).
|
||||||
|
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
|
||||||
|
de droit: ADM ou CMP.
|
||||||
|
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||||
|
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||||
|
|
||||||
|
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
|
||||||
"""
|
"""
|
||||||
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_admis = 0
|
nb_etud_modif = 0
|
||||||
with sco_cache.DeferredSemCacheManager():
|
with sco_cache.DeferredSemCacheManager():
|
||||||
for etudid in formsemestre.etuds_inscriptions:
|
for etudid in formsemestre.etuds_inscriptions:
|
||||||
etud: Identite = Identite.query.get(etudid)
|
etud: Identite = Identite.query.get(etudid)
|
||||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
if deca.admis: # année réussie
|
nb_etud_modif += deca.record_all(
|
||||||
deca.record_all()
|
no_overwrite=no_overwrite, only_validantes=only_adm
|
||||||
nb_admis += 1
|
)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return nb_admis
|
return nb_etud_modif
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -8,25 +8,34 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import flash, url_for
|
from flask import flash, render_template, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.but import jury_but
|
from app.but import jury_but
|
||||||
from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE
|
from app.but.jury_but import (
|
||||||
|
DecisionsProposeesAnnee,
|
||||||
|
DecisionsProposeesRCUE,
|
||||||
|
DecisionsProposeesUE,
|
||||||
|
)
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.models import (
|
from app.models import (
|
||||||
|
ApcNiveau,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
FormSemestreInscription,
|
FormSemestreInscription,
|
||||||
Identite,
|
Identite,
|
||||||
UniteEns,
|
UniteEns,
|
||||||
ScolarAutorisationInscription,
|
ScolarAutorisationInscription,
|
||||||
|
ScolarFormSemestreValidation,
|
||||||
)
|
)
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -35,37 +44,50 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
Si pas read_only, menus sélection codes jury.
|
Si pas read_only, menus sélection codes jury.
|
||||||
"""
|
"""
|
||||||
H = []
|
H = []
|
||||||
if deca.code_valide and not read_only:
|
|
||||||
erase_span = f"""<a href="{
|
|
||||||
url_for("notes.formsemestre_jury_but_erase",
|
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id,
|
|
||||||
etudid=deca.etud.id)}" class="stdlink">effacer décisions</a>"""
|
|
||||||
else:
|
|
||||||
erase_span = ""
|
|
||||||
|
|
||||||
H.append(
|
if deca.jury_annuel:
|
||||||
f"""
|
H.append(
|
||||||
|
f"""
|
||||||
<div class="but_section_annee">
|
<div class="but_section_annee">
|
||||||
<div>
|
<div>
|
||||||
<b>Décision de jury pour l'année :</b> {
|
<b>Décision de jury pour l'année :</b> {
|
||||||
_gen_but_select("code_annee", deca.codes, deca.code_valide,
|
_gen_but_select("code_annee", deca.codes, deca.code_valide,
|
||||||
disabled=True, klass="manual")
|
disabled=True, klass="manual")
|
||||||
}
|
}
|
||||||
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
|
<span>({deca.code_valide or 'non'} enregistrée)</span>
|
||||||
<span>{erase_span}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="but_explanation">{deca.explanation}</div>
|
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
formsemestre_1 = deca.formsemestre_impair
|
||||||
|
formsemestre_2 = deca.formsemestre_pair
|
||||||
|
# Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval):
|
||||||
|
reverse_semestre = (
|
||||||
|
deca.formsemestre_pair
|
||||||
|
and deca.formsemestre_impair
|
||||||
|
and deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut
|
||||||
|
)
|
||||||
|
if reverse_semestre:
|
||||||
|
formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<div><b>Niveaux de compétences et unités d'enseignement :</b></div>
|
<div class="titre_niveaux">
|
||||||
|
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
|
||||||
|
</div>
|
||||||
|
<div class="but_explanation">{deca.explanation}</div>
|
||||||
<div class="but_annee">
|
<div class="but_annee">
|
||||||
<div class="titre"></div>
|
<div class="titre"></div>
|
||||||
<div class="titre">S{1}</div>
|
<div class="titre">{"S" +str(formsemestre_1.semestre_id)
|
||||||
<div class="titre">S{2}</div>
|
if formsemestre_1 else "-"}
|
||||||
|
<span class="avertissement_redoublement">{formsemestre_1.annee_scolaire_str()
|
||||||
|
if formsemestre_1 else ""}</span>
|
||||||
|
</div>
|
||||||
|
<div class="titre">{"S"+str(formsemestre_2.semestre_id)
|
||||||
|
if formsemestre_2 else "-"}
|
||||||
|
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
||||||
|
if formsemestre_2 else ""}</span>
|
||||||
|
</div>
|
||||||
<div class="titre">RCUE</div>
|
<div class="titre">RCUE</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -75,44 +97,52 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||||||
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
||||||
</div>"""
|
</div>"""
|
||||||
)
|
)
|
||||||
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id)
|
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None
|
||||||
if dec_rcue is None:
|
ues = [
|
||||||
break
|
ue
|
||||||
# Semestre impair
|
for ue in deca.ues_impair
|
||||||
H.append(
|
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||||
_gen_but_niveau_ue(
|
]
|
||||||
dec_rcue.rcue.ue_1,
|
ue_impair = ues[0] if ues else None
|
||||||
deca.decisions_ues[dec_rcue.rcue.ue_1.id].moy_ue,
|
ues = [
|
||||||
# dec_rcue.rcue.moy_ue_1,
|
ue
|
||||||
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
|
for ue in deca.ues_pair
|
||||||
disabled=read_only,
|
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||||
)
|
]
|
||||||
)
|
ue_pair = ues[0] if ues else None
|
||||||
# Semestre pair
|
# Les UEs à afficher,
|
||||||
H.append(
|
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
|
||||||
_gen_but_niveau_ue(
|
ues_ro = [
|
||||||
dec_rcue.rcue.ue_2,
|
(
|
||||||
deca.decisions_ues[dec_rcue.rcue.ue_2.id].moy_ue,
|
ue_impair,
|
||||||
# dec_rcue.rcue.moy_ue_2,
|
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
|
||||||
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
|
),
|
||||||
disabled=read_only,
|
(
|
||||||
)
|
ue_pair,
|
||||||
)
|
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
|
||||||
# RCUE
|
),
|
||||||
H.append(
|
]
|
||||||
f"""<div class="but_niveau_rcue
|
# Ordonne selon les dates des 2 semestres considérés:
|
||||||
{'recorded' if dec_rcue.code_valide is not None else ''}
|
if reverse_semestre:
|
||||||
">
|
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
|
||||||
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
# Colonnes d'UE:
|
||||||
<div class="but_code">{
|
for ue, ue_read_only in ues_ro:
|
||||||
_gen_but_select("code_rcue_"+str(niveau.id),
|
if ue:
|
||||||
dec_rcue.codes,
|
H.append(
|
||||||
dec_rcue.code_valide,
|
_gen_but_niveau_ue(
|
||||||
disabled=True, klass="manual"
|
ue,
|
||||||
|
deca.decisions_ues[ue.id],
|
||||||
|
disabled=read_only or ue_read_only,
|
||||||
|
annee_prec=ue_read_only,
|
||||||
|
niveau_id=ue.niveau_competence.id,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}</div>
|
else:
|
||||||
</div>"""
|
H.append("""<div class="niveau_vide"></div>""")
|
||||||
)
|
|
||||||
|
# Colonne RCUE
|
||||||
|
H.append(_gen_but_rcue(dec_rcue, niveau))
|
||||||
|
|
||||||
H.append("</div>") # but_annee
|
H.append("</div>") # but_annee
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
@ -123,59 +153,155 @@ def _gen_but_select(
|
|||||||
code_valide: str,
|
code_valide: str,
|
||||||
disabled: bool = False,
|
disabled: bool = False,
|
||||||
klass: str = "",
|
klass: str = "",
|
||||||
|
data: dict = {},
|
||||||
) -> str:
|
) -> str:
|
||||||
"Le menu html select avec les codes"
|
"Le menu html select avec les codes"
|
||||||
h = "\n".join(
|
# if disabled: # mauvaise idée car le disabled est traité en JS
|
||||||
|
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
|
||||||
|
options_htm = "\n".join(
|
||||||
[
|
[
|
||||||
f"""<option value="{code}"
|
f"""<option value="{code}"
|
||||||
{'selected' if code == code_valide else ''}
|
{'selected' if code == code_valide else ''}
|
||||||
class="{'recorded' if code == code_valide else ''}"
|
class="{'recorded' if code == code_valide else ''}"
|
||||||
>{code}</option>"""
|
>{code}</option>"""
|
||||||
for code in codes
|
for code in codes
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return f"""<select required name="{name}"
|
return f"""<select required name="{name}"
|
||||||
class="but_code {klass}"
|
class="but_code {klass}"
|
||||||
|
data-orig_code="{code_valide or (codes[0] if codes else '')}"
|
||||||
|
data-orig_recorded="{code_valide or ''}"
|
||||||
onchange="change_menu_code(this);"
|
onchange="change_menu_code(this);"
|
||||||
{"disabled" if disabled else ""}
|
{"disabled" if disabled else ""}
|
||||||
>{h}</select>
|
{" ".join( f'data-{k}="{v}"' for (k,v) in data.items() )}
|
||||||
|
>{options_htm}</select>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _gen_but_niveau_ue(
|
def _gen_but_niveau_ue(
|
||||||
ue: UniteEns, moy_ue: float, dec_ue: DecisionsProposeesUE, disabled=False
|
ue: UniteEns,
|
||||||
):
|
dec_ue: DecisionsProposeesUE,
|
||||||
|
disabled: bool = False,
|
||||||
|
annee_prec: bool = False,
|
||||||
|
niveau_id: int = None,
|
||||||
|
) -> str:
|
||||||
|
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
|
||||||
|
moy_ue_str = f"""<span class="ue_cap">{
|
||||||
|
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
|
||||||
|
scoplement = f"""<div class="scoplement">
|
||||||
|
<div>
|
||||||
|
<b>UE {ue.acronyme} capitalisée </b>
|
||||||
|
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>UE en cours
|
||||||
|
{ "sans notes" if np.isnan(dec_ue.moy_ue)
|
||||||
|
else
|
||||||
|
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
||||||
|
if dec_ue.code_valide:
|
||||||
|
scoplement = f"""<div class="scoplement">
|
||||||
|
<div>Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||||
|
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
scoplement = ""
|
||||||
|
|
||||||
return f"""<div class="but_niveau_ue {
|
return f"""<div class="but_niveau_ue {
|
||||||
'recorded' if dec_ue.code_valide is not None else ''}
|
'recorded' if dec_ue.code_valide is not None else ''}
|
||||||
|
{'annee_prec' if annee_prec else ''}
|
||||||
">
|
">
|
||||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||||
<div class="but_note">{scu.fmt_note(moy_ue)}</div>
|
<div class="but_note with_scoplement">
|
||||||
|
<div>{moy_ue_str}</div>
|
||||||
|
{scoplement}
|
||||||
|
</div>
|
||||||
<div class="but_code">{
|
<div class="but_code">{
|
||||||
_gen_but_select("code_ue_"+str(ue.id),
|
_gen_but_select("code_ue_"+str(ue.id),
|
||||||
dec_ue.codes,
|
dec_ue.codes,
|
||||||
dec_ue.code_valide, disabled=disabled
|
dec_ue.code_valide,
|
||||||
|
disabled=disabled,
|
||||||
|
klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else ""
|
||||||
)
|
)
|
||||||
}</div>
|
}</div>
|
||||||
|
|
||||||
</div>"""
|
</div>"""
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||||
|
if dec_rcue is None:
|
||||||
|
return """
|
||||||
|
<div class="but_niveau_rcue niveau_vide with_scoplement">
|
||||||
|
<div></div>
|
||||||
|
<div class="scoplement">Pas de RCUE (UE non capitalisée ?)</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
scoplement = (
|
||||||
|
f"""<div class="scoplement">{
|
||||||
|
dec_rcue.validation.to_html()
|
||||||
|
}</div>"""
|
||||||
|
if dec_rcue.validation
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Déjà enregistré ?
|
||||||
|
niveau_rcue_class = ""
|
||||||
|
if dec_rcue.code_valide is not None and dec_rcue.codes:
|
||||||
|
if dec_rcue.code_valide == dec_rcue.codes[0]:
|
||||||
|
niveau_rcue_class = "recorded"
|
||||||
|
else:
|
||||||
|
niveau_rcue_class = "recorded_different"
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<div class="but_niveau_rcue {niveau_rcue_class}
|
||||||
|
">
|
||||||
|
<div class="but_note with_scoplement">
|
||||||
|
<div>{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
||||||
|
{scoplement}
|
||||||
|
</div>
|
||||||
|
<div class="but_code">
|
||||||
|
{_gen_but_select("code_rcue_"+str(niveau.id),
|
||||||
|
dec_rcue.codes,
|
||||||
|
dec_rcue.code_valide,
|
||||||
|
disabled=True,
|
||||||
|
klass="manual code_rcue",
|
||||||
|
data = { "niveau_id" : str(niveau.id)}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def jury_but_semestriel(
|
def jury_but_semestriel(
|
||||||
formsemestre: FormSemestre, etud: Identite, read_only: bool
|
formsemestre: FormSemestre,
|
||||||
|
etud: Identite,
|
||||||
|
read_only: bool,
|
||||||
|
navigation_div: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)"""
|
"""Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)."""
|
||||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||||
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
|
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
|
||||||
inscription_etat = etud.inscription_etat(formsemestre.id)
|
inscription_etat = etud.inscription_etat(formsemestre.id)
|
||||||
semestre_terminal = (
|
semestre_terminal = (
|
||||||
formsemestre.semestre_id >= formsemestre.formation.get_parcours().NB_SEM
|
formsemestre.semestre_id >= formsemestre.formation.get_parcours().NB_SEM
|
||||||
)
|
)
|
||||||
est_autorise_a_passer = (formsemestre.semestre_id + 1) in (
|
autorisations_passage = ScolarAutorisationInscription.query.filter_by(
|
||||||
a.semestre_id
|
etudid=etud.id,
|
||||||
for a in ScolarAutorisationInscription.query.filter_by(
|
origin_formsemestre_id=formsemestre.id,
|
||||||
etudid=etud.id,
|
).all()
|
||||||
origin_formsemestre_id=formsemestre.id,
|
# Par défaut: autorisé à passer dans le semestre suivant si sem. impair,
|
||||||
)
|
# ou si décision déjà enregistrée:
|
||||||
)
|
est_autorise_a_passer = (formsemestre.semestre_id % 2) or (
|
||||||
|
formsemestre.semestre_id + 1
|
||||||
|
) in (a.semestre_id for a in autorisations_passage)
|
||||||
decisions_ues = {
|
decisions_ues = {
|
||||||
ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
|
ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
|
||||||
for ue in ues
|
for ue in ues
|
||||||
@ -188,9 +314,9 @@ def jury_but_semestriel(
|
|||||||
for key in request.form:
|
for key in request.form:
|
||||||
code = request.form[key]
|
code = request.form[key]
|
||||||
# Codes d'UE
|
# Codes d'UE
|
||||||
m = re.match(r"^code_ue_(\d+)$", key)
|
code_match = re.match(r"^code_ue_(\d+)$", key)
|
||||||
if m:
|
if code_match:
|
||||||
ue_id = int(m.group(1))
|
ue_id = int(code_match.group(1))
|
||||||
dec_ue = decisions_ues.get(ue_id)
|
dec_ue = decisions_ues.get(ue_id)
|
||||||
if not dec_ue:
|
if not dec_ue:
|
||||||
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
||||||
@ -199,7 +325,9 @@ def jury_but_semestriel(
|
|||||||
flash("codes enregistrés")
|
flash("codes enregistrés")
|
||||||
if not semestre_terminal:
|
if not semestre_terminal:
|
||||||
if request.form.get("autorisation_passage"):
|
if request.form.get("autorisation_passage"):
|
||||||
if not est_autorise_a_passer:
|
if not formsemestre.semestre_id + 1 in (
|
||||||
|
a.semestre_id for a in autorisations_passage
|
||||||
|
):
|
||||||
ScolarAutorisationInscription.autorise_etud(
|
ScolarAutorisationInscription.autorise_etud(
|
||||||
etud.id,
|
etud.id,
|
||||||
formsemestre.formation.formation_code,
|
formsemestre.formation.formation_code,
|
||||||
@ -208,7 +336,8 @@ def jury_but_semestriel(
|
|||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(
|
flash(
|
||||||
f"autorisation de passage en S{formsemestre.semestre_id + 1} enregistrée"
|
f"""autorisation de passage en S{formsemestre.semestre_id + 1
|
||||||
|
} enregistrée"""
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if est_autorise_a_passer:
|
if est_autorise_a_passer:
|
||||||
@ -237,7 +366,7 @@ def jury_but_semestriel(
|
|||||||
warning = ""
|
warning = ""
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
page_title="Validation BUT",
|
page_title=f"Validation BUT S{formsemestre.semestre_id}",
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
cssstyles=("css/jury_but.css",),
|
cssstyles=("css/jury_but.css",),
|
||||||
@ -246,90 +375,139 @@ def jury_but_semestriel(
|
|||||||
f"""
|
f"""
|
||||||
<div class="jury_but">
|
<div class="jury_but">
|
||||||
<div>
|
<div>
|
||||||
<div class="bull_head">
|
<div class="bull_head">
|
||||||
<div>
|
<div>
|
||||||
<div class="titre_parcours">Jury BUT S{formsemestre.id}
|
<div class="titre_parcours">Jury BUT S{formsemestre.id}
|
||||||
- Parcours {(parcour.libelle if parcour else False) or "non spécifié"}
|
- Parcours {(parcour.libelle if parcour else False) or "non spécifié"}
|
||||||
|
</div>
|
||||||
|
<div class="nom_etud">{etud.nomprenom}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bull_photo"><a href="{
|
||||||
|
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||||
|
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nom_etud">{etud.nomprenom}</div>
|
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
|
||||||
</div>
|
{warning}
|
||||||
<div class="bull_photo"><a href="{
|
|
||||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
|
||||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3>Jury sur un semestre BUT isolé</h3>
|
|
||||||
{warning}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="post" id="jury_but">
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
if (not read_only) and any([dec.code_valide for dec in decisions_ues.values()]):
|
|
||||||
erase_span = f"""<a href="{
|
erase_span = ""
|
||||||
url_for("notes.formsemestre_jury_but_erase",
|
if not read_only:
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id,
|
# Requête toutes les validations (pas seulement celles du deca courant),
|
||||||
etudid=etud.id, only_one_sem=1)}" class="stdlink">effacer décisions</a>"""
|
# au cas où: changement d'architecture, saisie en mode classique, ...
|
||||||
else:
|
validations = ScolarFormSemestreValidation.query.filter_by(
|
||||||
erase_span = "aucune décision enregistrée pour ce semestre"
|
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||||
|
).all()
|
||||||
|
if validations:
|
||||||
|
erase_span = f"""<a href="{
|
||||||
|
url_for("notes.formsemestre_jury_but_erase",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id,
|
||||||
|
etudid=etud.id, only_one_sem=1)
|
||||||
|
}" class="stdlink">effacer les décisions enregistrées</a>"""
|
||||||
|
else:
|
||||||
|
erase_span = (
|
||||||
|
"Cet étudiant n'a aucune décision enregistrée pour ce semestre."
|
||||||
|
)
|
||||||
|
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<div class="but_section_annee">
|
<div class="but_section_annee">
|
||||||
<span>{erase_span}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div>
|
<div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div>
|
||||||
<div class="but_annee">
|
|
||||||
<div class="titre"></div>
|
|
||||||
<div class="titre"></div>
|
|
||||||
<div class="titre"></div>
|
|
||||||
<div class="titre"></div>
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
for ue in ues:
|
if not ues:
|
||||||
dec_ue = decisions_ues[ue.id]
|
|
||||||
H.append("""<div class="but_niveau_titre"><div></div></div>""")
|
|
||||||
H.append(
|
H.append(
|
||||||
_gen_but_niveau_ue(
|
"""<div class="warning">Aucune UE ! Vérifiez votre programme de
|
||||||
ue,
|
formation, et l'association UEs / Niveaux de compétences</div>"""
|
||||||
dec_ue.moy_ue,
|
)
|
||||||
dec_ue,
|
else:
|
||||||
disabled=read_only,
|
H.append(
|
||||||
|
"""
|
||||||
|
<div class="but_annee">
|
||||||
|
<div class="titre"></div>
|
||||||
|
<div class="titre"></div>
|
||||||
|
<div class="titre"></div>
|
||||||
|
<div class="titre"></div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for ue in ues:
|
||||||
|
dec_ue = decisions_ues[ue.id]
|
||||||
|
H.append("""<div class="but_niveau_titre"><div></div></div>""")
|
||||||
|
H.append(
|
||||||
|
_gen_but_niveau_ue(
|
||||||
|
ue,
|
||||||
|
dec_ue,
|
||||||
|
disabled=read_only,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
H.append(
|
||||||
H.append(
|
"""<div style=""></div>
|
||||||
"""<div style=""></div>
|
<div class=""></div>"""
|
||||||
<div class=""></div>"""
|
)
|
||||||
)
|
H.append("</div>") # but_annee
|
||||||
H.append("</div>") # but_annee
|
|
||||||
|
div_autorisations_passage = (
|
||||||
|
f"""
|
||||||
|
<div class="but_autorisations_passage">
|
||||||
|
<span>Autorisé à passer en :</span>
|
||||||
|
{ ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
if autorisations_passage
|
||||||
|
else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>"""
|
||||||
|
)
|
||||||
|
H.append(div_autorisations_passage)
|
||||||
|
|
||||||
if read_only:
|
if read_only:
|
||||||
H.append(
|
H.append(
|
||||||
"""<div class="but_explanation">
|
f"""<div class="but_explanation">
|
||||||
Vous n'avez pas la permission de modifier ces décisions.
|
{"Vous n'avez pas la permission de modifier ces décisions."
|
||||||
Les champs entourés en vert sont enregistrés.</div>"""
|
if formsemestre.etat
|
||||||
|
else "Semestre verrouillé."}
|
||||||
|
Les champs entourés en vert sont enregistrés.
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM:
|
if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM:
|
||||||
H.append(
|
H.append(
|
||||||
f"""
|
f"""
|
||||||
<div class="but_settings">
|
<div class="but_settings">
|
||||||
<input type="checkbox" name="autorisation_passage" value="1" {
|
<input type="checkbox" name="autorisation_passage" value="1" {
|
||||||
"checked" if est_autorise_a_passer else ""}>
|
"checked" if est_autorise_a_passer else ""}>
|
||||||
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
|
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
|
||||||
</input>
|
</input>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
H.append("""<div class="help">dernier semestre de la formation.</div>""")
|
H.append("""<div class="help">dernier semestre de la formation.</div>""")
|
||||||
H.append(
|
H.append(
|
||||||
"""
|
f"""
|
||||||
<div class="but_buttons">
|
<div class="but_buttons">
|
||||||
<input type="submit" value="Enregistrer ces décisions">
|
<span><input type="submit" value="Enregistrer ces décisions"></span>
|
||||||
|
<span>{erase_span}</span>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
H.append(navigation_div)
|
||||||
|
H.append("</div>")
|
||||||
|
H.append(
|
||||||
|
render_template(
|
||||||
|
"but/documentation_codes_jury.j2",
|
||||||
|
nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
|
||||||
|
or sco_preferences.get_preference("UnivName")
|
||||||
|
or "Apogée"}""",
|
||||||
|
codes=ScoDocSiteConfig.get_codes_apo_dict(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
@ -355,11 +533,10 @@ def infos_fiche_etud_html(etudid: int) -> str:
|
|||||||
# temporaire quick & dirty: affiche le dernier
|
# temporaire quick & dirty: affiche le dernier
|
||||||
try:
|
try:
|
||||||
deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1])
|
deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1])
|
||||||
if len(deca.rcues_annee) > 0:
|
return f"""<div class="infos_but">
|
||||||
return f"""<div class="infos_but">
|
|
||||||
{show_etud(deca, read_only=True)}
|
{show_etud(deca, read_only=True)}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
except ScoValueError:
|
except ScoValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -18,21 +18,11 @@ import pandas as pd
|
|||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
from app.scodoc.sco_codes_parcours import ParcoursDUT, ParcoursDUTMono
|
from app.scodoc.sco_codes_parcours import ParcoursDUT, ParcoursDUTMono
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
def get_bonus_sport_class_from_name(dept_id):
|
|
||||||
"""La classe de bonus sport pour le département indiqué.
|
|
||||||
Note: en ScoDoc 9, le bonus sport est défini gloabelement et
|
|
||||||
ne dépend donc pas du département.
|
|
||||||
Résultat: une sous-classe de BonusSport
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class BonusSport:
|
class BonusSport:
|
||||||
"""Calcul du bonus sport.
|
"""Calcul du bonus sport.
|
||||||
|
|
||||||
@ -65,7 +55,7 @@ class BonusSport:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
formsemestre: FormSemestre,
|
formsemestre: "FormSemestre",
|
||||||
sem_modimpl_moys: np.array,
|
sem_modimpl_moys: np.array,
|
||||||
ues: list,
|
ues: list,
|
||||||
modimpl_inscr_df: pd.DataFrame,
|
modimpl_inscr_df: pd.DataFrame,
|
||||||
@ -362,18 +352,37 @@ class BonusAisneStQuentin(BonusSportAdditif):
|
|||||||
|
|
||||||
|
|
||||||
class BonusAmiens(BonusSportAdditif):
|
class BonusAmiens(BonusSportAdditif):
|
||||||
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
|
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...)
|
||||||
|
|
||||||
Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
|
<p><b>À partir d'août 2022:</b></p>
|
||||||
|
<p>
|
||||||
|
Deux activités optionnelles sont possibles chaque semestre, et peuvent donner lieu à une bonification de 0,1 chacune sur la moyenne de chaque UE.
|
||||||
|
</p><p>
|
||||||
|
La note saisie peut valoir 0 (pas de bonus), 1 (bonus de 0,1 points) ou 2 (bonus de 0,2 points).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><b>Avant juillet 2022:</b></p>
|
||||||
|
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
|
||||||
sur toutes les moyennes d'UE.
|
sur toutes les moyennes d'UE.
|
||||||
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "bonus_amiens"
|
name = "bonus_amiens"
|
||||||
displayed_name = "IUT d'Amiens"
|
displayed_name = "IUT d'Amiens"
|
||||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
|
||||||
proportion_point = 1e10
|
|
||||||
bonus_max = 0.1
|
|
||||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||||
|
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||||
|
|
||||||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
|
"""calcul du bonus, avec réglage différent suivant la date"""
|
||||||
|
|
||||||
|
if self.formsemestre.date_debut > datetime.date(2022, 8, 1):
|
||||||
|
self.proportion_point = 0.1
|
||||||
|
self.bonus_max = 0.2
|
||||||
|
else: # anciens semestres
|
||||||
|
self.proportion_point = 1e10
|
||||||
|
self.bonus_max = 0.1
|
||||||
|
|
||||||
|
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||||
|
|
||||||
|
|
||||||
# Finalement ils n'en veulent pas.
|
# Finalement ils n'en veulent pas.
|
||||||
@ -421,6 +430,22 @@ class BonusAmiens(BonusSportAdditif):
|
|||||||
# )
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
class BonusBesanconVesoul(BonusSportAdditif):
|
||||||
|
"""Bonus IUT Besançon - Vesoul pour les UE libres
|
||||||
|
|
||||||
|
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
|
||||||
|
sur toutes les moyennes d'UE.
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_besancon_vesoul"
|
||||||
|
displayed_name = "IUT de Besançon - Vesoul"
|
||||||
|
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||||
|
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||||
|
proportion_point = 1e10 # infini
|
||||||
|
bonus_max = 0.2
|
||||||
|
|
||||||
|
|
||||||
class BonusBethune(BonusSportMultiplicatif):
|
class BonusBethune(BonusSportMultiplicatif):
|
||||||
"""
|
"""
|
||||||
Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
|
Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
|
||||||
@ -638,7 +663,10 @@ class BonusCalais(BonusSportAdditif):
|
|||||||
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||||
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b> (ex : UE2.1BS, UE32BS)
|
</li>
|
||||||
|
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b>
|
||||||
|
(ex : UE2.1BS, UE32BS)
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -1199,7 +1227,7 @@ class BonusStDenis(BonusSportAdditif):
|
|||||||
bonus_max = 0.5
|
bonus_max = 0.5
|
||||||
|
|
||||||
|
|
||||||
class BonusStNazaire(BonusSportMultiplicatif):
|
class BonusStNazaire(BonusSport):
|
||||||
"""IUT de Saint-Nazaire
|
"""IUT de Saint-Nazaire
|
||||||
|
|
||||||
Trois bonifications sont possibles : sport, culture et engagement citoyen
|
Trois bonifications sont possibles : sport, culture et engagement citoyen
|
||||||
@ -1221,9 +1249,37 @@ class BonusStNazaire(BonusSportMultiplicatif):
|
|||||||
name = "bonus_iutSN"
|
name = "bonus_iutSN"
|
||||||
displayed_name = "IUT de Saint-Nazaire"
|
displayed_name = "IUT de Saint-Nazaire"
|
||||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||||
seuil_moy_gen = 0.0 # tous les points comptent
|
|
||||||
amplitude = 0.01 / 4 # 4pt => 1%
|
amplitude = 0.01 / 4 # 4pt => 1%
|
||||||
factor_max = 0.1 # 10% max
|
factor_max = 0.1 # 10% max
|
||||||
|
# Modifié 2022-11-29: calculer chaque bonus
|
||||||
|
# (de 1 à 3 modules) séparément.
|
||||||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
|
"""Calcul du bonus St Nazaire 2022
|
||||||
|
sem_modimpl_moys_inscrits: les notes de sport
|
||||||
|
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
|
||||||
|
En classic: ndarray (nb_etuds, nb_mod_sport)
|
||||||
|
"""
|
||||||
|
if 0 in sem_modimpl_moys_inscrits.shape:
|
||||||
|
# pas d'étudiants ou pas d'UE ou pas de module...
|
||||||
|
return
|
||||||
|
# Prend les 3 premiers bonus trouvés
|
||||||
|
# ignore les coefficients
|
||||||
|
bonus_mod_moys = sem_modimpl_moys_inscrits[:, :3]
|
||||||
|
bonus_mod_moys = np.nan_to_num(bonus_mod_moys, copy=False)
|
||||||
|
factor = bonus_mod_moys * self.amplitude
|
||||||
|
# somme les bonus:
|
||||||
|
factor = factor.sum(axis=1)
|
||||||
|
# et limite à 10%:
|
||||||
|
factor.clip(0.0, self.factor_max, out=factor)
|
||||||
|
|
||||||
|
# Applique aux moyennes d'UE
|
||||||
|
if len(factor.shape) == 1: # classic
|
||||||
|
factor = factor[:, np.newaxis]
|
||||||
|
bonus = self.etud_moy_ue * factor
|
||||||
|
self.bonus_ues = bonus # DataFrame
|
||||||
|
|
||||||
|
# Les bonus multiplicatifs ne s'appliquent pas à la moyenne générale
|
||||||
|
self.bonus_moy_gen = None
|
||||||
|
|
||||||
|
|
||||||
class BonusTarbes(BonusIUTRennes1):
|
class BonusTarbes(BonusIUTRennes1):
|
||||||
@ -1302,7 +1358,45 @@ class BonusIUTvannes(BonusSportAdditif):
|
|||||||
classic_use_bonus_ues = False # seulement sur moy gen.
|
classic_use_bonus_ues = False # seulement sur moy gen.
|
||||||
|
|
||||||
|
|
||||||
class BonusVilleAvray(BonusSport):
|
class BonusValenciennes(BonusDirect):
|
||||||
|
"""Article 7 des RCC de l’IUT de Valenciennes
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Une bonification maximale de 0.25 point (1/4 de point) peut être ajoutée
|
||||||
|
à la moyenne de chaque Unité d’Enseignement pour :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>l'engagement citoyen ;</li>
|
||||||
|
<li>la participation à un module de sport.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Une bonification accordée par la commission des sports de l’UPHF peut être attribuée
|
||||||
|
aux sportifs de haut niveau. Cette bonification est appliquée à l’ensemble des
|
||||||
|
Unités d’Enseignement. Ce bonus est :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li> 0.5 pour la catégorie <em>or</em> (sportif inscrit sur liste ministérielle
|
||||||
|
jeunesse et sport) ;
|
||||||
|
</li>
|
||||||
|
<li> 0.45 pour la catégorie <em>argent</em> (sportif en club professionnel) ;
|
||||||
|
</li>
|
||||||
|
<li> 0.40 pour le <em>bronze</em> (sportif de niveau départemental, régional ou national).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>Le cumul de bonifications est possible mais ne peut excéder 0.5 point (un demi-point).
|
||||||
|
</p>
|
||||||
|
<p><em>Dans ScoDoc, saisir directement la valeur désirée du bonus
|
||||||
|
dans une évaluation notée sur 20.</em>
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_valenciennes"
|
||||||
|
displayed_name = "IUT de Valenciennes"
|
||||||
|
bonus_max = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
class BonusVilleAvray(BonusSportAdditif):
|
||||||
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.
|
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.
|
||||||
|
|
||||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||||
@ -1351,7 +1445,7 @@ class BonusIUTV(BonusSportAdditif):
|
|||||||
|
|
||||||
name = "bonus_iutv"
|
name = "bonus_iutv"
|
||||||
displayed_name = "IUT de Villetaneuse"
|
displayed_name = "IUT de Villetaneuse"
|
||||||
pass # oui, c'est le bonus par défaut
|
# c'est le bonus par défaut: aucune méthode à surcharger
|
||||||
|
|
||||||
|
|
||||||
def get_bonus_class_dict(start=BonusSport, d=None):
|
def get_bonus_class_dict(start=BonusSport, d=None):
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -122,6 +122,10 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
|||||||
event_date :
|
event_date :
|
||||||
} ]
|
} ]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Note: pour récupérer aussi les UE validées en CMp ou ADJ, changer une ligne
|
||||||
|
# and ( SFV.code = 'ADM' or SFV.code = 'ADJ' or SFV.code = 'CMP' )
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
SELECT DISTINCT SFV.*, ue.ue_code
|
SELECT DISTINCT SFV.*, ue.ue_code
|
||||||
FROM
|
FROM
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -35,14 +35,16 @@ moyenne générale d'une UE.
|
|||||||
"""
|
"""
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
import app
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
|
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
||||||
|
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.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
@ -217,12 +219,19 @@ class ModuleImplResults:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
|
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
|
||||||
"""Coefficients des évaluations, met à zéro ceux des évals incomplètes.
|
"""Coefficients des évaluations.
|
||||||
|
Les coefs des évals incomplètes et non "normales" (session 2, rattrapage)
|
||||||
|
sont zéro.
|
||||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
np.array(
|
np.array(
|
||||||
[e.coefficient for e in moduleimpl.evaluations],
|
[
|
||||||
|
e.coefficient
|
||||||
|
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||||
|
else 0.0
|
||||||
|
for e in moduleimpl.evaluations
|
||||||
|
],
|
||||||
dtype=float,
|
dtype=float,
|
||||||
)
|
)
|
||||||
* self.evaluations_completes
|
* self.evaluations_completes
|
||||||
@ -236,8 +245,8 @@ class ModuleImplResults:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
|
def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
|
||||||
"""Les notes des évaluations,
|
"""Les notes de toutes les évaluations du module, complètes ou non.
|
||||||
remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
|
Remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
|
||||||
Résultat: 2d array of floats, shape nb_etuds x nb_evaluations
|
Résultat: 2d array of floats, shape nb_etuds x nb_evaluations
|
||||||
"""
|
"""
|
||||||
return np.where(
|
return np.where(
|
||||||
@ -368,7 +377,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||||||
etuds_moy_module = np.where(
|
etuds_moy_module = np.where(
|
||||||
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||||
)
|
)
|
||||||
# Serie indiquant que l'étudiant utilise une note de rattarage sur l'une des UE:
|
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
|
||||||
self.etuds_use_rattrapage = pd.Series(
|
self.etuds_use_rattrapage = pd.Series(
|
||||||
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
||||||
)
|
)
|
||||||
@ -429,7 +438,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||||||
|
|
||||||
|
|
||||||
def moduleimpl_is_conforme(
|
def moduleimpl_is_conforme(
|
||||||
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
|
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
|
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
|
||||||
au PN.
|
au PN.
|
||||||
@ -438,7 +447,7 @@ def moduleimpl_is_conforme(
|
|||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||||
modules_coefficients: DataFrame, cols module_id, lignes UEs
|
modimpl_coefs_df: DataFrame, cols: modimpl_id, lignes: UEs du formsemestre
|
||||||
NB: les UEs dans evals_poids sont sans le bonus sport
|
NB: les UEs dans evals_poids sont sans le bonus sport
|
||||||
"""
|
"""
|
||||||
nb_evals, nb_ues = evals_poids.shape
|
nb_evals, nb_ues = evals_poids.shape
|
||||||
@ -446,18 +455,18 @@ def moduleimpl_is_conforme(
|
|||||||
return True # modules vides conformes
|
return True # modules vides conformes
|
||||||
if nb_ues == 0:
|
if nb_ues == 0:
|
||||||
return False # situation absurde (pas d'UE)
|
return False # situation absurde (pas d'UE)
|
||||||
if len(modules_coefficients) != 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")
|
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
|
||||||
|
|
||||||
if moduleimpl.module_id not in modules_coefficients:
|
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")
|
raise ScoBugCatcher("Erreur 454 - merci de ré-essayer")
|
||||||
|
|
||||||
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||||
return all((modules_coefficients[moduleimpl.module_id] != 0).eq(module_evals_poids))
|
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||||
|
|
||||||
|
|
||||||
class ModuleImplResultsClassic(ModuleImplResults):
|
class ModuleImplResultsClassic(ModuleImplResults):
|
||||||
@ -476,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||||||
if nb_etuds == 0:
|
if nb_etuds == 0:
|
||||||
return pd.Series()
|
return pd.Series()
|
||||||
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
|
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
|
||||||
assert evals_coefs.shape == (nb_evals,)
|
if evals_coefs.shape != (nb_evals,):
|
||||||
|
app.critical_error("compute_module_moy: vals_coefs.shape != nb_evals")
|
||||||
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
|
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
|
||||||
# Les coefs des évals pour chaque étudiant: là où il a des notes
|
# Les coefs des évals pour chaque étudiant: là où il a des notes
|
||||||
# non neutralisées
|
# non neutralisées
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -52,13 +52,16 @@ def compute_sem_moys_apc_using_coefs(
|
|||||||
|
|
||||||
|
|
||||||
def compute_sem_moys_apc_using_ects(
|
def compute_sem_moys_apc_using_ects(
|
||||||
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None, skip_empty_ues=False
|
etud_moy_ue_df: pd.DataFrame,
|
||||||
|
ects_df: pd.DataFrame,
|
||||||
|
formation_id=None,
|
||||||
|
skip_empty_ues=False,
|
||||||
) -> pd.Series:
|
) -> pd.Series:
|
||||||
"""Calcule les moyennes générales indicatives de tous les étudiants
|
"""Calcule les moyennes générales indicatives de tous les étudiants
|
||||||
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
|
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
|
||||||
|
|
||||||
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
|
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
|
||||||
ects: liste de floats ou None, 1 par UE
|
ects: DataFrame, col. ue_id, lignes etudid, valeur float ou None
|
||||||
|
|
||||||
Si skip_empty_ues: ne compte pas les UE non notées.
|
Si skip_empty_ues: ne compte pas les UE non notées.
|
||||||
Sinon (par défaut), une UE non notée compte comme zéro.
|
Sinon (par défaut), une UE non notée compte comme zéro.
|
||||||
@ -68,11 +71,11 @@ def compute_sem_moys_apc_using_ects(
|
|||||||
try:
|
try:
|
||||||
if skip_empty_ues:
|
if skip_empty_ues:
|
||||||
# annule les coefs des UE sans notes (NaN)
|
# annule les coefs des UE sans notes (NaN)
|
||||||
ects = np.where(etud_moy_ue_df.isna(), 0.0, np.array(ects, dtype=float))
|
ects = np.where(etud_moy_ue_df.isna(), 0.0, ects_df.to_numpy())
|
||||||
# ects est devenu nb_etuds x nb_ues
|
|
||||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
|
||||||
else:
|
else:
|
||||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
|
ects = ects_df.to_numpy()
|
||||||
|
# ects est maintenant un array nb_etuds x nb_ues
|
||||||
|
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
if None in ects:
|
if None in ects:
|
||||||
formation = Formation.query.get(formation_id)
|
formation = Formation.query.get(formation_id)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -32,9 +32,14 @@ import pandas as pd
|
|||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import models
|
from app import models
|
||||||
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
|
from app.models import (
|
||||||
|
FormSemestre,
|
||||||
|
Module,
|
||||||
|
ModuleImpl,
|
||||||
|
ModuleUECoef,
|
||||||
|
UniteEns,
|
||||||
|
)
|
||||||
from app.comp import moy_mod
|
from app.comp import moy_mod
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
@ -69,9 +74,7 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
|||||||
& (UniteEns.type == sco_codes_parcours.UE_SPORT)
|
& (UniteEns.type == sco_codes_parcours.UE_SPORT)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by(
|
.order_by(Module.semestre_id, Module.module_type, Module.numero, Module.code)
|
||||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if semestre_idx is not None:
|
if semestre_idx is not None:
|
||||||
ues = ues.filter_by(semestre_idx=semestre_idx)
|
ues = ues.filter_by(semestre_idx=semestre_idx)
|
||||||
@ -140,7 +143,8 @@ def df_load_modimpl_coefs(
|
|||||||
mod_coef.ue_id
|
mod_coef.ue_id
|
||||||
] = mod_coef.coef
|
] = mod_coef.coef
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# il peut y avoir en base des coefs sur des modules ou UE qui ont depuis été retirés de la formation
|
# il peut y avoir en base des coefs sur des modules ou UE
|
||||||
|
# qui ont depuis été retirés de la formation
|
||||||
pass
|
pass
|
||||||
# Initialisation des poids non fixés:
|
# Initialisation des poids non fixés:
|
||||||
# 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
|
||||||
@ -199,7 +203,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
|||||||
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)
|
||||||
if len(modimpls_notes):
|
if len(modimpls_notes) > 0:
|
||||||
cube = notes_sem_assemble_cube(modimpls_notes)
|
cube = notes_sem_assemble_cube(modimpls_notes)
|
||||||
else:
|
else:
|
||||||
nb_etuds = formsemestre.etuds.count()
|
nb_etuds = formsemestre.etuds.count()
|
||||||
@ -215,10 +219,11 @@ def compute_ue_moys_apc(
|
|||||||
sem_cube: np.array,
|
sem_cube: np.array,
|
||||||
etuds: list,
|
etuds: list,
|
||||||
modimpls: list,
|
modimpls: list,
|
||||||
ues: list,
|
|
||||||
modimpl_inscr_df: pd.DataFrame,
|
modimpl_inscr_df: pd.DataFrame,
|
||||||
modimpl_coefs_df: pd.DataFrame,
|
modimpl_coefs_df: pd.DataFrame,
|
||||||
modimpl_mask: np.array,
|
modimpl_mask: np.array,
|
||||||
|
dispense_ues: set[tuple[int, int]],
|
||||||
|
block: bool = False,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
"""Calcul de la moyenne d'UE en mode APC (BUT).
|
"""Calcul de la moyenne d'UE en mode APC (BUT).
|
||||||
La moyenne d'UE est un nombre (note/20), ou NaN si pas de notes disponibles
|
La moyenne d'UE est un nombre (note/20), ou NaN si pas de notes disponibles
|
||||||
@ -229,18 +234,17 @@ def compute_ue_moys_apc(
|
|||||||
etuds : liste des étudiants (dim. 0 du cube)
|
etuds : liste des étudiants (dim. 0 du cube)
|
||||||
modimpls : liste des module_impl (dim. 1 du cube)
|
modimpls : liste des module_impl (dim. 1 du cube)
|
||||||
ues : liste des UE (dim. 2 du cube)
|
ues : liste des UE (dim. 2 du cube)
|
||||||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
|
||||||
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
|
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
|
||||||
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
|
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
|
||||||
(utilisé pour éliminer les bonus, et pourra servir à cacluler
|
(utilisé pour éliminer les bonus, et pourra servir à cacluler
|
||||||
sur des sous-ensembles de modules)
|
sur des sous-ensembles de modules)
|
||||||
|
block: si vrai, ne calcule rien et renvoie des NaNs
|
||||||
Résultat: DataFrame columns UE (sans bonus), rows etudid
|
Résultat: DataFrame columns UE (sans bonus), rows etudid
|
||||||
"""
|
"""
|
||||||
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
|
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
|
||||||
nb_ues_tot = len(ues)
|
|
||||||
assert len(modimpls) == nb_modules
|
assert len(modimpls) == nb_modules
|
||||||
if nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
|
if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
|
||||||
return pd.DataFrame(
|
return pd.DataFrame(
|
||||||
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||||
)
|
)
|
||||||
@ -277,11 +281,16 @@ def compute_ue_moys_apc(
|
|||||||
etud_moy_ue = np.sum(
|
etud_moy_ue = np.sum(
|
||||||
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
|
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
|
||||||
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
||||||
return pd.DataFrame(
|
etud_moy_ue_df = pd.DataFrame(
|
||||||
etud_moy_ue,
|
etud_moy_ue,
|
||||||
index=modimpl_inscr_df.index, # les etudids
|
index=modimpl_inscr_df.index, # les etudids
|
||||||
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
|
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
|
||||||
)
|
)
|
||||||
|
# Les "dispenses" sont très peu nombreuses et traitées en python:
|
||||||
|
for dispense_ue in dispense_ues:
|
||||||
|
etud_moy_ue_df[dispense_ue[1]][dispense_ue[0]] = 0.0
|
||||||
|
|
||||||
|
return etud_moy_ue_df
|
||||||
|
|
||||||
|
|
||||||
def compute_ue_moys_classic(
|
def compute_ue_moys_classic(
|
||||||
@ -291,6 +300,7 @@ def compute_ue_moys_classic(
|
|||||||
modimpl_inscr_df: pd.DataFrame,
|
modimpl_inscr_df: pd.DataFrame,
|
||||||
modimpl_coefs: np.array,
|
modimpl_coefs: np.array,
|
||||||
modimpl_mask: np.array,
|
modimpl_mask: np.array,
|
||||||
|
block: bool = False,
|
||||||
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
|
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
|
||||||
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
|
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
|
||||||
|
|
||||||
@ -312,6 +322,7 @@ def compute_ue_moys_classic(
|
|||||||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||||
modimpl_coefs: vecteur des coefficients de modules
|
modimpl_coefs: vecteur des coefficients de modules
|
||||||
modimpl_mask: masque des modimpls à prendre en compte
|
modimpl_mask: masque des modimpls à prendre en compte
|
||||||
|
block: si vrai, ne calcule rien et renvoie des NaNs
|
||||||
|
|
||||||
Résultat:
|
Résultat:
|
||||||
- moyennes générales: pd.Series, index etudid
|
- moyennes générales: pd.Series, index etudid
|
||||||
@ -320,13 +331,14 @@ def compute_ue_moys_classic(
|
|||||||
les coefficients effectifs de chaque UE pour chaque étudiant
|
les coefficients effectifs de chaque UE pour chaque étudiant
|
||||||
(sommes de coefs de modules pris en compte)
|
(sommes de coefs de modules pris en compte)
|
||||||
"""
|
"""
|
||||||
if (not len(modimpl_mask)) or (
|
if (
|
||||||
sem_matrix.shape[0] == 0
|
block or (len(modimpl_mask) == 0) or (sem_matrix.shape[0] == 0)
|
||||||
): # aucun module ou aucun étudiant
|
): # aucun module ou aucun étudiant
|
||||||
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
||||||
|
val = np.nan if block else 0.0
|
||||||
return (
|
return (
|
||||||
pd.Series(
|
pd.Series(
|
||||||
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
|
[val] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
|
||||||
),
|
),
|
||||||
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
|
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
|
||||||
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
|
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
|
||||||
@ -431,7 +443,7 @@ def compute_mat_moys_classic(
|
|||||||
Résultat:
|
Résultat:
|
||||||
- moyennes: pd.Series, index etudid
|
- moyennes: pd.Series, index etudid
|
||||||
"""
|
"""
|
||||||
if (not len(modimpl_mask)) or (
|
if (0 == len(modimpl_mask)) or (
|
||||||
sem_matrix.shape[0] == 0
|
sem_matrix.shape[0] == 0
|
||||||
): # aucun module ou aucun étudiant
|
): # aucun module ou aucun étudiant
|
||||||
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
||||||
@ -462,9 +474,10 @@ def compute_mat_moys_classic(
|
|||||||
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
|
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
|
||||||
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||||
|
|
||||||
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
|
with np.errstate(invalid="ignore"): # il peut y avoir des NaN
|
||||||
axis=1
|
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
|
||||||
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
|
axis=1
|
||||||
|
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
|
||||||
|
|
||||||
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
|
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ from app.comp.res_compat import NotesTableCompat
|
|||||||
from app.comp.bonus_spo import BonusSport
|
from app.comp.bonus_spo import BonusSport
|
||||||
from app.models import ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import DispenseUE, UniteEns
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
"""ndarray (etuds x modimpl x ue)"""
|
"""ndarray (etuds x modimpl x ue)"""
|
||||||
self.etuds_parcour_id = None
|
self.etuds_parcour_id = None
|
||||||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||||
|
|
||||||
if not self.load_cached():
|
if not self.load_cached():
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
self.compute()
|
self.compute()
|
||||||
@ -71,15 +72,18 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
modimpl.module.ue.type != UE_SPORT
|
modimpl.module.ue.type != UE_SPORT
|
||||||
for modimpl in self.formsemestre.modimpls_sorted
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
]
|
]
|
||||||
|
self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set(
|
||||||
|
self.formsemestre, self.modimpl_inscr_df.index, self.ues
|
||||||
|
)
|
||||||
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
||||||
self.sem_cube,
|
self.sem_cube,
|
||||||
self.etuds,
|
self.etuds,
|
||||||
self.formsemestre.modimpls_sorted,
|
self.formsemestre.modimpls_sorted,
|
||||||
self.ues,
|
|
||||||
self.modimpl_inscr_df,
|
self.modimpl_inscr_df,
|
||||||
self.modimpl_coefs_df,
|
self.modimpl_coefs_df,
|
||||||
modimpls_mask,
|
modimpls_mask,
|
||||||
|
self.dispense_ues,
|
||||||
|
block=self.formsemestre.block_moyennes,
|
||||||
)
|
)
|
||||||
# Les coefficients d'UE ne sont pas utilisés en APC
|
# Les coefficients d'UE ne sont pas utilisés en APC
|
||||||
self.etud_coef_ue_df = pd.DataFrame(
|
self.etud_coef_ue_df = pd.DataFrame(
|
||||||
@ -114,6 +118,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
|
|
||||||
# Nanifie les moyennes d'UE hors parcours pour chaque étudiant
|
# Nanifie les moyennes d'UE hors parcours pour chaque étudiant
|
||||||
self.etud_moy_ue *= self.ues_inscr_parcours_df
|
self.etud_moy_ue *= self.ues_inscr_parcours_df
|
||||||
|
# Les ects (utilisés comme coefs) sont nuls pour les UE hors parcours:
|
||||||
|
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
|
||||||
|
ue.ects for ue in self.ues if ue.type != UE_SPORT
|
||||||
|
]
|
||||||
|
|
||||||
# Moyenne générale indicative:
|
# Moyenne générale indicative:
|
||||||
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
||||||
@ -121,14 +129,19 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
|
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
|
||||||
# self.etud_moy_ue, self.modimpl_coefs_df
|
# self.etud_moy_ue, self.modimpl_coefs_df
|
||||||
# )
|
# )
|
||||||
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
|
if self.formsemestre.block_moyenne_generale or self.formsemestre.block_moyennes:
|
||||||
self.etud_moy_ue,
|
self.etud_moy_gen = pd.Series(
|
||||||
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
|
index=self.etud_moy_ue.index, dtype=float
|
||||||
formation_id=self.formsemestre.formation_id,
|
) # NaNs
|
||||||
skip_empty_ues=sco_preferences.get_preference(
|
else:
|
||||||
"but_moy_skip_empty_ues", self.formsemestre.id
|
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
|
||||||
),
|
self.etud_moy_ue,
|
||||||
)
|
ects,
|
||||||
|
formation_id=self.formsemestre.formation_id,
|
||||||
|
skip_empty_ues=sco_preferences.get_preference(
|
||||||
|
"but_moy_skip_empty_ues", self.formsemestre.id
|
||||||
|
),
|
||||||
|
)
|
||||||
# --- UE capitalisées
|
# --- UE capitalisées
|
||||||
self.apply_capitalisation()
|
self.apply_capitalisation()
|
||||||
|
|
||||||
@ -204,27 +217,33 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
}
|
}
|
||||||
self.etuds_parcour_id = etuds_parcour_id
|
self.etuds_parcour_id = etuds_parcour_id
|
||||||
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||||
# matrice de 1, inscrits par défaut à toutes les UE:
|
|
||||||
ues_inscr_parcours_df = pd.DataFrame(
|
|
||||||
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
|
||||||
)
|
|
||||||
if self.formsemestre.formation.referentiel_competence is None:
|
|
||||||
return ues_inscr_parcours_df
|
|
||||||
|
|
||||||
|
if self.formsemestre.formation.referentiel_competence is None:
|
||||||
|
return pd.DataFrame(
|
||||||
|
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||||
|
)
|
||||||
|
# matrice de NaN: inscrits par défaut à AUCUNE UE:
|
||||||
|
ues_inscr_parcours_df = pd.DataFrame(
|
||||||
|
np.nan, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float # XXX
|
||||||
|
)
|
||||||
|
# Construit pour chaque parcours du référentiel l'ensemble de ses UE
|
||||||
|
# (considère aussi le cas des semestres sans parcours: None)
|
||||||
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
|
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
|
||||||
for parcour in self.formsemestre.formation.referentiel_competence.parcours:
|
for (
|
||||||
ue_by_parcours[parcour.id] = {
|
parcour
|
||||||
|
) in self.formsemestre.formation.referentiel_competence.parcours.all() + [None]:
|
||||||
|
ue_by_parcours[None if parcour is None else parcour.id] = {
|
||||||
ue.id: 1.0
|
ue.id: 1.0
|
||||||
for ue in self.formsemestre.formation.query_ues_parcour(
|
for ue in self.formsemestre.formation.query_ues_parcour(
|
||||||
parcour
|
parcour
|
||||||
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
||||||
}
|
}
|
||||||
|
#
|
||||||
for etudid in etuds_parcour_id:
|
for etudid in etuds_parcour_id:
|
||||||
parcour = etuds_parcour_id[etudid]
|
parcour_id = etuds_parcour_id[etudid]
|
||||||
if parcour is not None:
|
if parcour_id in ue_by_parcours:
|
||||||
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[
|
if ue_by_parcours[parcour_id]:
|
||||||
etuds_parcour_id[etudid]
|
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[parcour_id]
|
||||||
]
|
|
||||||
return ues_inscr_parcours_df
|
return ues_inscr_parcours_df
|
||||||
|
|
||||||
def etud_ues_ids(self, etudid: int) -> list[int]:
|
def etud_ues_ids(self, etudid: int) -> list[int]:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -90,6 +90,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||||||
self.modimpl_inscr_df,
|
self.modimpl_inscr_df,
|
||||||
self.modimpl_coefs,
|
self.modimpl_coefs,
|
||||||
modimpl_standards_mask,
|
modimpl_standards_mask,
|
||||||
|
block=self.formsemestre.block_moyennes,
|
||||||
)
|
)
|
||||||
# --- Modules de MALUS sur les UEs et la moyenne générale
|
# --- Modules de MALUS sur les UEs et la moyenne générale
|
||||||
self.malus = moy_ue.compute_malus(
|
self.malus = moy_ue.compute_malus(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -48,12 +48,13 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
_cached_attrs = (
|
_cached_attrs = (
|
||||||
"bonus",
|
"bonus",
|
||||||
"bonus_ues",
|
"bonus_ues",
|
||||||
|
"dispense_ues",
|
||||||
|
"etud_coef_ue_df",
|
||||||
"etud_moy_gen_ranks",
|
"etud_moy_gen_ranks",
|
||||||
"etud_moy_gen",
|
"etud_moy_gen",
|
||||||
"etud_moy_ue",
|
"etud_moy_ue",
|
||||||
"modimpl_inscr_df",
|
"modimpl_inscr_df",
|
||||||
"modimpls_results",
|
"modimpls_results",
|
||||||
"etud_coef_ue_df",
|
|
||||||
"moyennes_matieres",
|
"moyennes_matieres",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,6 +67,8 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
"Bonus sur moy. gen. Series de float, index etudid"
|
"Bonus sur moy. gen. Series de float, index etudid"
|
||||||
self.bonus_ues: pd.DataFrame = None # virtuel
|
self.bonus_ues: pd.DataFrame = None # virtuel
|
||||||
"DataFrame de float, index etudid, columns: ue.id"
|
"DataFrame de float, index etudid, columns: ue.id"
|
||||||
|
self.dispense_ues: set[tuple[int, int]] = set()
|
||||||
|
"""set des dispenses d'UE: (etudid, ue_id), en APC seulement."""
|
||||||
# ResultatsSemestreBUT ou ResultatsSemestreClassic
|
# ResultatsSemestreBUT ou ResultatsSemestreClassic
|
||||||
self.etud_moy_ue = {}
|
self.etud_moy_ue = {}
|
||||||
"etud_moy_ue: DataFrame columns UE, rows etudid"
|
"etud_moy_ue: DataFrame columns UE, rows etudid"
|
||||||
@ -316,7 +319,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
"""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'est pas dans ce semestre.
|
||||||
"""
|
"""
|
||||||
ue = UniteEns.query.get(ue_id) # TODO cacher nos UEs ?
|
ue = UniteEns.query.get(ue_id)
|
||||||
if ue.type == UE_SPORT:
|
if ue.type == UE_SPORT:
|
||||||
return {
|
return {
|
||||||
"is_capitalized": False,
|
"is_capitalized": False,
|
||||||
@ -439,7 +442,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
allow_html=True,
|
allow_html=True,
|
||||||
):
|
):
|
||||||
"""Table récap. des résultats.
|
"""Table récap. des résultats.
|
||||||
allow_html: si vri, peut-mettre du HTML dans les valeurs
|
allow_html: si vrai, peut mettre du HTML dans les valeurs
|
||||||
|
|
||||||
Result: tuple avec
|
Result: tuple avec
|
||||||
- rows: liste de dicts { column_id : value }
|
- rows: liste de dicts { column_id : value }
|
||||||
@ -491,7 +494,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
classes: str = "",
|
classes: str = "",
|
||||||
idx: int = 100,
|
idx: int = 100,
|
||||||
):
|
):
|
||||||
"Add a row to our table. classes is a list of css class names"
|
"Add a cell to our table. classes is a list of css class names"
|
||||||
row[col_id] = content
|
row[col_id] = content
|
||||||
if classes:
|
if classes:
|
||||||
row[f"_{col_id}_class"] = classes + f" c{idx}"
|
row[f"_{col_id}_class"] = classes + f" c{idx}"
|
||||||
@ -516,10 +519,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
|
row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
|
||||||
)
|
)
|
||||||
# --- Rang
|
# --- Rang
|
||||||
idx = add_cell(
|
if not self.formsemestre.block_moyenne_generale:
|
||||||
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
|
idx = add_cell(
|
||||||
)
|
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
|
||||||
row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}"
|
)
|
||||||
|
row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}"
|
||||||
# --- Identité étudiant
|
# --- Identité étudiant
|
||||||
idx = add_cell(
|
idx = add_cell(
|
||||||
row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail", idx
|
row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail", idx
|
||||||
@ -539,32 +543,38 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
formsemestre_id=self.formsemestre.id,
|
formsemestre_id=self.formsemestre.id,
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
)
|
)
|
||||||
|
row["_nom_short_data"] = {
|
||||||
|
"etudid": etud.id,
|
||||||
|
"nomprenom": etud.nomprenom,
|
||||||
|
}
|
||||||
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
|
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
|
||||||
row["_nom_disp_target"] = row["_nom_short_target"]
|
row["_nom_disp_target"] = row["_nom_short_target"]
|
||||||
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
|
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
|
||||||
|
|
||||||
idx = 30 # début des colonnes de notes
|
idx = 30 # début des colonnes de notes
|
||||||
# --- Moyenne générale
|
# --- Moyenne générale
|
||||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
if not self.formsemestre.block_moyenne_generale:
|
||||||
note_class = ""
|
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||||
if moy_gen is False:
|
note_class = ""
|
||||||
moy_gen = NO_NOTE
|
if moy_gen is False:
|
||||||
elif isinstance(moy_gen, float) and moy_gen < barre_moy:
|
moy_gen = NO_NOTE
|
||||||
note_class = " moy_ue_warning" # en rouge
|
elif isinstance(moy_gen, float) and moy_gen < barre_moy:
|
||||||
idx = add_cell(
|
note_class = " moy_ue_warning" # en rouge
|
||||||
row,
|
idx = add_cell(
|
||||||
"moy_gen",
|
row,
|
||||||
"Moy",
|
"moy_gen",
|
||||||
fmt_note(moy_gen),
|
"Moy",
|
||||||
"col_moy_gen" + note_class,
|
fmt_note(moy_gen),
|
||||||
idx,
|
"col_moy_gen" + note_class,
|
||||||
)
|
idx,
|
||||||
titles_bot["_moy_gen_target_attrs"] = (
|
)
|
||||||
'title="moyenne indicative"' if self.is_apc else ""
|
titles_bot["_moy_gen_target_attrs"] = (
|
||||||
)
|
'title="moyenne indicative"' if self.is_apc else ""
|
||||||
|
)
|
||||||
# --- Moyenne d'UE
|
# --- Moyenne d'UE
|
||||||
nb_ues_validables, nb_ues_warning = 0, 0
|
nb_ues_validables, nb_ues_warning = 0, 0
|
||||||
for ue in ues_sans_bonus:
|
idx_ue_start = idx
|
||||||
|
for idx_ue, ue in enumerate(ues_sans_bonus):
|
||||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||||
if ue_status is not None:
|
if ue_status is not None:
|
||||||
col_id = f"moy_ue_{ue.id}"
|
col_id = f"moy_ue_{ue.id}"
|
||||||
@ -585,7 +595,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
ue.acronyme,
|
ue.acronyme,
|
||||||
fmt_note(val),
|
fmt_note(val),
|
||||||
"col_ue" + note_class,
|
"col_ue" + note_class,
|
||||||
idx,
|
idx_ue * 10000 + idx_ue_start,
|
||||||
)
|
)
|
||||||
titles_bot[
|
titles_bot[
|
||||||
f"_{col_id}_target_attrs"
|
f"_{col_id}_target_attrs"
|
||||||
@ -606,7 +616,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
f"Bonus {ue.acronyme}",
|
f"Bonus {ue.acronyme}",
|
||||||
val_fmt_html if allow_html else val_fmt,
|
val_fmt_html if allow_html else val_fmt,
|
||||||
"col_ue_bonus",
|
"col_ue_bonus",
|
||||||
idx,
|
idx_ue * 10000 + idx_ue_start + 1,
|
||||||
)
|
)
|
||||||
row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
|
row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
|
||||||
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
||||||
@ -651,7 +661,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
val_fmt_html,
|
val_fmt_html,
|
||||||
# class col_res mod_ue_123
|
# class col_res mod_ue_123
|
||||||
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
|
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
|
||||||
idx,
|
idx_ue * 10000
|
||||||
|
+ idx_ue_start
|
||||||
|
+ 1
|
||||||
|
+ (modimpl.module.module_type or 0) * 1000
|
||||||
|
+ (modimpl.module.numero or 0),
|
||||||
)
|
)
|
||||||
row[f"_{col_id}_xls"] = val_fmt
|
row[f"_{col_id}_xls"] = val_fmt
|
||||||
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
||||||
@ -701,7 +715,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
else:
|
else:
|
||||||
jury_code_sem = ""
|
jury_code_sem = ""
|
||||||
else:
|
else:
|
||||||
# formations classiqes: code semestre
|
# formations classiques: code semestre
|
||||||
dec_sem = self.validations.decisions_jury.get(etudid)
|
dec_sem = self.validations.decisions_jury.get(etudid)
|
||||||
jury_code_sem = dec_sem["code"] if dec_sem else ""
|
jury_code_sem = dec_sem["code"] if dec_sem else ""
|
||||||
idx = add_cell(
|
idx = add_cell(
|
||||||
@ -719,17 +733,22 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
|
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
|
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||||
)
|
)
|
||||||
}">{"saisir" if not jury_code_sem else "modifier"} décision</a>""",
|
}">{("saisir" if not jury_code_sem else "modifier")
|
||||||
|
if self.formsemestre.etat else "voir"} décisions</a>""",
|
||||||
"col_jury_link",
|
"col_jury_link",
|
||||||
idx,
|
idx,
|
||||||
)
|
)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
self.recap_add_partitions(rows, titles)
|
col_idx = self.recap_add_partitions(rows, titles)
|
||||||
|
self.recap_add_cursus(rows, titles, col_idx=col_idx + 1)
|
||||||
self._recap_add_admissions(rows, titles)
|
self._recap_add_admissions(rows, titles)
|
||||||
|
|
||||||
# tri par rang croissant
|
# tri par rang croissant
|
||||||
rows.sort(key=lambda e: e["_rang_order"])
|
if not self.formsemestre.block_moyenne_generale:
|
||||||
|
rows.sort(key=lambda e: e["_rang_order"])
|
||||||
|
else:
|
||||||
|
rows.sort(key=lambda e: e["_ues_validables_order"], reverse=True)
|
||||||
|
|
||||||
# INFOS POUR FOOTER
|
# INFOS POUR FOOTER
|
||||||
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
|
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
|
||||||
@ -746,6 +765,20 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
for row in bottom_infos.values():
|
for row in bottom_infos.values():
|
||||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
row[c_class] = row.get(c_class, "") + " col_empty"
|
||||||
|
|
||||||
|
# Ligne avec la classe de chaque colonne
|
||||||
|
# récupère le type à partir des classes css (hack...)
|
||||||
|
row_class = {}
|
||||||
|
for col_id in titles:
|
||||||
|
klass = titles.get(f"_{col_id}_class")
|
||||||
|
if klass:
|
||||||
|
row_class[col_id] = " ".join(
|
||||||
|
cls[4:] for cls in klass.split() if cls.startswith("col_")
|
||||||
|
)
|
||||||
|
# cette case (nb d'UE validables) a deux classes col_xxx, on en garde une seule:
|
||||||
|
if "ues_validables" in row_class[col_id]:
|
||||||
|
row_class[col_id] = "ues_validables"
|
||||||
|
bottom_infos["type_col"] = row_class
|
||||||
|
|
||||||
# --- TABLE FOOTER: ECTS, moyennes, min, max...
|
# --- TABLE FOOTER: ECTS, moyennes, min, max...
|
||||||
footer_rows = []
|
footer_rows = []
|
||||||
for (bottom_line, row) in bottom_infos.items():
|
for (bottom_line, row) in bottom_infos.items():
|
||||||
@ -769,7 +802,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
return (rows, footer_rows, titles, column_ids)
|
return (rows, footer_rows, titles, column_ids)
|
||||||
|
|
||||||
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
|
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
|
||||||
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
|
"""Les informations à mettre en bas de la table: min, max, moy, ECTS, Apo"""
|
||||||
row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
|
row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
|
||||||
{"_tr_class": "bottom_info", "_title": "Min."},
|
{"_tr_class": "bottom_info", "_title": "Min."},
|
||||||
{"_tr_class": "bottom_info"},
|
{"_tr_class": "bottom_info"},
|
||||||
@ -829,7 +862,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
row_moy[f"_{colid}_class"] = "col_empty"
|
row_moy[f"_{colid}_class"] = "col_empty"
|
||||||
row_apo[colid] = modimpl.module.code_apogee or ""
|
row_apo[colid] = modimpl.module.code_apogee or ""
|
||||||
|
|
||||||
return { # { key : row } avec key = min, max, moy, coef
|
return { # { key : row } avec key = min, max, moy, coef, ...
|
||||||
"min": row_min,
|
"min": row_min,
|
||||||
"max": row_max,
|
"max": row_max,
|
||||||
"moy": row_moy,
|
"moy": row_moy,
|
||||||
@ -877,7 +910,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
}
|
}
|
||||||
first = True
|
first = True
|
||||||
for i, cid in enumerate(fields):
|
for i, cid in enumerate(fields):
|
||||||
titles[f"_{cid}_col_order"] = 10000 + i # tout à droite
|
titles[f"_{cid}_col_order"] = 100000 + i # tout à droite
|
||||||
if first:
|
if first:
|
||||||
titles[f"_{cid}_class"] = "admission admission_first"
|
titles[f"_{cid}_class"] = "admission admission_first"
|
||||||
first = False
|
first = False
|
||||||
@ -896,10 +929,29 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
else:
|
else:
|
||||||
row[f"_{cid}_class"] = "admission"
|
row[f"_{cid}_class"] = "admission"
|
||||||
|
|
||||||
def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None):
|
def recap_add_cursus(self, rows: list[dict], titles: dict, col_idx: int = None):
|
||||||
|
"""Ajoute colonne avec code cursus, eg 'S1 S2 S1'"""
|
||||||
|
cid = "code_cursus"
|
||||||
|
titles[cid] = "Cursus"
|
||||||
|
titles[f"_{cid}_col_order"] = col_idx
|
||||||
|
formation_code = self.formsemestre.formation.formation_code
|
||||||
|
for row in rows:
|
||||||
|
etud = Identite.query.get(row["etudid"])
|
||||||
|
row[cid] = " ".join(
|
||||||
|
[
|
||||||
|
f"S{ins.formsemestre.semestre_id}"
|
||||||
|
for ins in reversed(etud.inscriptions())
|
||||||
|
if ins.formsemestre.formation.formation_code == formation_code
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def recap_add_partitions(
|
||||||
|
self, rows: list[dict], titles: dict, col_idx: int = None
|
||||||
|
) -> int:
|
||||||
"""Ajoute les colonnes indiquant les groupes
|
"""Ajoute les colonnes indiquant les groupes
|
||||||
rows est une liste de dict avec une clé "etudid"
|
rows est une liste de dict avec une clé "etudid"
|
||||||
Les colonnes ont la classe css "partition"
|
Les colonnes ont la classe css "partition"
|
||||||
|
Renvoie l'indice de la dernière colonne utilisée
|
||||||
"""
|
"""
|
||||||
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
||||||
self.formsemestre.id
|
self.formsemestre.id
|
||||||
@ -948,6 +1000,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
row[rg_cid] = rang.get(row["etudid"], "")
|
row[rg_cid] = rang.get(row["etudid"], "")
|
||||||
|
|
||||||
first_partition = False
|
first_partition = False
|
||||||
|
return col_order
|
||||||
|
|
||||||
def _recap_add_evaluations(
|
def _recap_add_evaluations(
|
||||||
self, rows: list[dict], titles: dict, bottom_infos: dict
|
self, rows: list[dict], titles: dict, bottom_infos: dict
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
"""Implementation partielle de NotesTable
|
"""Implementation partielle de NotesTable
|
||||||
|
|
||||||
Les méthodes définies dans cette classe sont là
|
Les méthodes définies dans cette classe sont là
|
||||||
pour conserver la compatibilité abvec les codes anciens et
|
pour conserver la compatibilité avec les codes anciens et
|
||||||
il n'est pas recommandé de les utiliser dans de nouveaux
|
il n'est pas recommandé de les utiliser dans de nouveaux
|
||||||
développements (API malcommode et peu efficace).
|
développements (API malcommode et peu efficace).
|
||||||
"""
|
"""
|
||||||
@ -103,10 +103,9 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
"""Stats (moy/min/max) sur la moyenne générale"""
|
"""Stats (moy/min/max) sur la moyenne générale"""
|
||||||
return StatsMoyenne(self.etud_moy_gen)
|
return StatsMoyenne(self.etud_moy_gen)
|
||||||
|
|
||||||
def get_ues_stat_dict(
|
def get_ues_stat_dict(self, filter_sport=False, check_apc_ects=True) -> list[dict]:
|
||||||
self, filter_sport=False, check_apc_ects=True
|
"""Liste des UEs de toutes les UEs du semestre (tous parcours),
|
||||||
) -> list[dict]: # was get_ues()
|
ordonnée par numero.
|
||||||
"""Liste des UEs, ordonnée par numero.
|
|
||||||
Si filter_sport, retire les UE de type SPORT.
|
Si filter_sport, retire les UE de type SPORT.
|
||||||
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import flask_login
|
|||||||
import app
|
import app
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
class ZUser(object):
|
class ZUser(object):
|
||||||
@ -180,19 +181,24 @@ def scodoc7func(func):
|
|||||||
else:
|
else:
|
||||||
arg_names = argspec.args
|
arg_names = argspec.args
|
||||||
for arg_name in arg_names: # pour chaque arg de la fonction vue
|
for arg_name in arg_names: # pour chaque arg de la fonction vue
|
||||||
if arg_name == "REQUEST": # ne devrait plus arriver !
|
# peut produire une KeyError s'il manque un argument attendu:
|
||||||
# debug check, TODO remove after tests
|
v = req_args[arg_name]
|
||||||
raise ValueError("invalid REQUEST parameter !")
|
# try to convert all arguments to INTEGERS
|
||||||
else:
|
# necessary for db ids and boolean values
|
||||||
# peut produire une KeyError s'il manque un argument attendu:
|
try:
|
||||||
v = req_args[arg_name]
|
v = int(v) if v else v
|
||||||
# try to convert all arguments to INTEGERS
|
except (ValueError, TypeError) as exc:
|
||||||
# necessary for db ids and boolean values
|
if arg_name in {
|
||||||
try:
|
"etudid",
|
||||||
v = int(v)
|
"formation_id",
|
||||||
except (ValueError, TypeError):
|
"formsemestre_id",
|
||||||
pass
|
"module_id",
|
||||||
pos_arg_values.append(v)
|
"moduleimpl_id",
|
||||||
|
"partition_id",
|
||||||
|
"ue_id",
|
||||||
|
}:
|
||||||
|
raise ScoValueError("page introuvable (id invalide)") from exc
|
||||||
|
pos_arg_values.append(v)
|
||||||
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
|
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
|
||||||
# current_app.logger.info("req_args=%s" % req_args)
|
# current_app.logger.info("req_args=%s" % req_args)
|
||||||
# Add keyword arguments
|
# Add keyword arguments
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -89,7 +89,7 @@ def index():
|
|||||||
visible=True, association=True, siret_provisoire=True
|
visible=True, association=True, siret_provisoire=True
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/entreprises.html",
|
"entreprises/entreprises.j2",
|
||||||
title="Entreprises",
|
title="Entreprises",
|
||||||
entreprises=entreprises,
|
entreprises=entreprises,
|
||||||
logs=logs,
|
logs=logs,
|
||||||
@ -109,7 +109,7 @@ def logs():
|
|||||||
EntrepriseHistorique.date.desc()
|
EntrepriseHistorique.date.desc()
|
||||||
).paginate(page=page, per_page=20)
|
).paginate(page=page, per_page=20)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/logs.html",
|
"entreprises/logs.j2",
|
||||||
title="Logs",
|
title="Logs",
|
||||||
logs=logs,
|
logs=logs,
|
||||||
)
|
)
|
||||||
@ -134,7 +134,7 @@ def correspondants():
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/correspondants.html",
|
"entreprises/correspondants.j2",
|
||||||
title="Correspondants",
|
title="Correspondants",
|
||||||
correspondants=correspondants,
|
correspondants=correspondants,
|
||||||
logs=logs,
|
logs=logs,
|
||||||
@ -149,7 +149,7 @@ def validation():
|
|||||||
"""
|
"""
|
||||||
entreprises = Entreprise.query.filter_by(visible=False).all()
|
entreprises = Entreprise.query.filter_by(visible=False).all()
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/entreprises_validation.html",
|
"entreprises/entreprises_validation.j2",
|
||||||
title="Validation entreprises",
|
title="Validation entreprises",
|
||||||
entreprises=entreprises,
|
entreprises=entreprises,
|
||||||
)
|
)
|
||||||
@ -167,7 +167,7 @@ def fiche_entreprise_validation(entreprise_id):
|
|||||||
description=f"fiche entreprise (validation) {entreprise_id} inconnue"
|
description=f"fiche entreprise (validation) {entreprise_id} inconnue"
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/fiche_entreprise_validation.html",
|
"entreprises/fiche_entreprise_validation.j2",
|
||||||
title="Validation fiche entreprise",
|
title="Validation fiche entreprise",
|
||||||
entreprise=entreprise,
|
entreprise=entreprise,
|
||||||
)
|
)
|
||||||
@ -205,7 +205,7 @@ def validate_entreprise(entreprise_id):
|
|||||||
flash("L'entreprise a été validé et ajouté à la liste.")
|
flash("L'entreprise a été validé et ajouté à la liste.")
|
||||||
return redirect(url_for("entreprises.validation"))
|
return redirect(url_for("entreprises.validation"))
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_validate_confirmation.html",
|
"entreprises/form_validate_confirmation.j2",
|
||||||
title="Validation entreprise",
|
title="Validation entreprise",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -242,7 +242,7 @@ def delete_validation_entreprise(entreprise_id):
|
|||||||
flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
|
flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
|
||||||
return redirect(url_for("entreprises.validation"))
|
return redirect(url_for("entreprises.validation"))
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Supression entreprise",
|
title="Supression entreprise",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||||
@ -282,7 +282,7 @@ def offres_recues():
|
|||||||
files.append(file)
|
files.append(file)
|
||||||
offres_recues_with_files.append([envoi_offre, offre, files, correspondant])
|
offres_recues_with_files.append([envoi_offre, offre, files, correspondant])
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/offres_recues.html",
|
"entreprises/offres_recues.j2",
|
||||||
title="Offres reçues",
|
title="Offres reçues",
|
||||||
offres_recues=offres_recues_with_files,
|
offres_recues=offres_recues_with_files,
|
||||||
)
|
)
|
||||||
@ -321,7 +321,7 @@ def preferences():
|
|||||||
form.mail_entreprise.data = EntreprisePreferences.get_email_notifications()
|
form.mail_entreprise.data = EntreprisePreferences.get_email_notifications()
|
||||||
form.check_siret.data = int(EntreprisePreferences.get_check_siret())
|
form.check_siret.data = int(EntreprisePreferences.get_check_siret())
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/preferences.html",
|
"entreprises/preferences.j2",
|
||||||
title="Préférences",
|
title="Préférences",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -357,7 +357,7 @@ def add_entreprise():
|
|||||||
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(
|
||||||
"entreprises/form_ajout_entreprise.html",
|
"entreprises/form_ajout_entreprise.j2",
|
||||||
title="Ajout entreprise avec correspondant",
|
title="Ajout entreprise avec correspondant",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -408,7 +408,7 @@ def add_entreprise():
|
|||||||
flash("L'entreprise a été ajouté à la liste pour la validation.")
|
flash("L'entreprise a été ajouté à la liste pour la validation.")
|
||||||
return redirect(url_for("entreprises.index"))
|
return redirect(url_for("entreprises.index"))
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_ajout_entreprise.html",
|
"entreprises/form_ajout_entreprise.j2",
|
||||||
title="Ajout entreprise avec correspondant",
|
title="Ajout entreprise avec correspondant",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -446,7 +446,7 @@ def fiche_entreprise(entreprise_id):
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/fiche_entreprise.html",
|
"entreprises/fiche_entreprise.j2",
|
||||||
title="Fiche entreprise",
|
title="Fiche entreprise",
|
||||||
entreprise=entreprise,
|
entreprise=entreprise,
|
||||||
offres=offres_with_files,
|
offres=offres_with_files,
|
||||||
@ -472,7 +472,7 @@ def logs_entreprise(entreprise_id):
|
|||||||
.paginate(page=page, per_page=20)
|
.paginate(page=page, per_page=20)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/logs_entreprise.html",
|
"entreprises/logs_entreprise.j2",
|
||||||
title="Logs",
|
title="Logs",
|
||||||
logs=logs,
|
logs=logs,
|
||||||
entreprise=entreprise,
|
entreprise=entreprise,
|
||||||
@ -490,7 +490,7 @@ def offres_expirees(entreprise_id):
|
|||||||
).first_or_404(description=f"fiche entreprise {entreprise_id} inconnue")
|
).first_or_404(description=f"fiche entreprise {entreprise_id} inconnue")
|
||||||
offres_with_files = are.get_offres_expirees_with_files(entreprise.offres)
|
offres_with_files = are.get_offres_expirees_with_files(entreprise.offres)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/offres_expirees.html",
|
"entreprises/offres_expirees.j2",
|
||||||
title="Offres expirées",
|
title="Offres expirées",
|
||||||
entreprise=entreprise,
|
entreprise=entreprise,
|
||||||
offres_expirees=offres_with_files,
|
offres_expirees=offres_with_files,
|
||||||
@ -574,7 +574,7 @@ def edit_entreprise(entreprise_id):
|
|||||||
form.pays.data = entreprise.pays
|
form.pays.data = entreprise.pays
|
||||||
form.association.data = entreprise.association
|
form.association.data = entreprise.association
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_modification_entreprise.html",
|
"entreprises/form_modification_entreprise.j2",
|
||||||
title="Modification entreprise",
|
title="Modification entreprise",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -610,7 +610,7 @@ def fiche_entreprise_desactiver(entreprise_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Désactiver entreprise",
|
title="Désactiver entreprise",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Modifier pour confirmer la désactivation",
|
info_message="Cliquez sur le bouton Modifier pour confirmer la désactivation",
|
||||||
@ -646,7 +646,7 @@ def fiche_entreprise_activer(entreprise_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Activer entreprise",
|
title="Activer entreprise",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Modifier pour confirmer l'activaction",
|
info_message="Cliquez sur le bouton Modifier pour confirmer l'activaction",
|
||||||
@ -692,7 +692,7 @@ def add_taxe_apprentissage(entreprise_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Ajout taxe apprentissage",
|
title="Ajout taxe apprentissage",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -735,7 +735,7 @@ def edit_taxe_apprentissage(entreprise_id, taxe_id):
|
|||||||
form.montant.data = taxe.montant
|
form.montant.data = taxe.montant
|
||||||
form.notes.data = taxe.notes
|
form.notes.data = taxe.notes
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Modification taxe apprentissage",
|
title="Modification taxe apprentissage",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -775,7 +775,7 @@ def delete_taxe_apprentissage(entreprise_id, taxe_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Supprimer taxe apprentissage",
|
title="Supprimer taxe apprentissage",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||||
@ -845,7 +845,7 @@ def add_offre(entreprise_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Ajout offre",
|
title="Ajout offre",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -921,7 +921,7 @@ def edit_offre(entreprise_id, offre_id):
|
|||||||
form.expiration_date.data = offre.expiration_date
|
form.expiration_date.data = offre.expiration_date
|
||||||
form.depts.data = offre_depts_list
|
form.depts.data = offre_depts_list
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Modification offre",
|
title="Modification offre",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -971,7 +971,7 @@ def delete_offre(entreprise_id, offre_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Supression offre",
|
title="Supression offre",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||||
@ -1047,7 +1047,7 @@ def add_site(entreprise_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Ajout site",
|
title="Ajout site",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1098,7 +1098,7 @@ def edit_site(entreprise_id, site_id):
|
|||||||
form.ville.data = site.ville
|
form.ville.data = site.ville
|
||||||
form.pays.data = site.pays
|
form.pays.data = site.pays
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Modification site",
|
title="Modification site",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1154,7 +1154,7 @@ def add_correspondant(entreprise_id, site_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=site.entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=site.entreprise_id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_ajout_correspondants.html",
|
"entreprises/form_ajout_correspondants.j2",
|
||||||
title="Ajout correspondant",
|
title="Ajout correspondant",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1234,7 +1234,7 @@ def edit_correspondant(entreprise_id, site_id, correspondant_id):
|
|||||||
form.origine.data = correspondant.origine
|
form.origine.data = correspondant.origine
|
||||||
form.notes.data = correspondant.notes
|
form.notes.data = correspondant.notes
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Modification correspondant",
|
title="Modification correspondant",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1290,7 +1290,7 @@ def delete_correspondant(entreprise_id, site_id, correspondant_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Supression correspondant",
|
title="Supression correspondant",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||||
@ -1308,7 +1308,7 @@ def contacts(entreprise_id):
|
|||||||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||||
contacts = EntrepriseContact.query.filter_by(entreprise=entreprise.id).all()
|
contacts = EntrepriseContact.query.filter_by(entreprise=entreprise.id).all()
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/contacts.html",
|
"entreprises/contacts.j2",
|
||||||
title="Liste des contacts",
|
title="Liste des contacts",
|
||||||
contacts=contacts,
|
contacts=contacts,
|
||||||
entreprise=entreprise,
|
entreprise=entreprise,
|
||||||
@ -1365,7 +1365,7 @@ def add_contact(entreprise_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise.id))
|
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise.id))
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Ajout contact",
|
title="Ajout contact",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1421,7 +1421,7 @@ def edit_contact(entreprise_id, contact_id):
|
|||||||
)
|
)
|
||||||
form.notes.data = contact.notes
|
form.notes.data = contact.notes
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Modification contact",
|
title="Modification contact",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1459,7 +1459,7 @@ def delete_contact(entreprise_id, contact_id):
|
|||||||
url_for("entreprises.contacts", entreprise_id=contact.entreprise)
|
url_for("entreprises.contacts", entreprise_id=contact.entreprise)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Supression contact",
|
title="Supression contact",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||||
@ -1525,7 +1525,7 @@ def add_stage_apprentissage(entreprise_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_ajout_stage_apprentissage.html",
|
"entreprises/form_ajout_stage_apprentissage.j2",
|
||||||
title="Ajout stage / apprentissage",
|
title="Ajout stage / apprentissage",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1599,7 +1599,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||||||
form.date_fin.data = stage_apprentissage.date_fin
|
form.date_fin.data = stage_apprentissage.date_fin
|
||||||
form.notes.data = stage_apprentissage.notes
|
form.notes.data = stage_apprentissage.notes
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_ajout_stage_apprentissage.html",
|
"entreprises/form_ajout_stage_apprentissage.j2",
|
||||||
title="Modification stage / apprentissage",
|
title="Modification stage / apprentissage",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1640,7 +1640,7 @@ def delete_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Supression stage/apprentissage",
|
title="Supression stage/apprentissage",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||||
@ -1690,7 +1690,7 @@ def envoyer_offre(entreprise_id, offre_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_envoi_offre.html",
|
"entreprises/form_envoi_offre.j2",
|
||||||
title="Envoyer une offre",
|
title="Envoyer une offre",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1816,7 +1816,7 @@ def import_donnees():
|
|||||||
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(
|
||||||
"entreprises/import_donnees.html",
|
"entreprises/import_donnees.j2",
|
||||||
title="Importation données",
|
title="Importation données",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1845,7 +1845,7 @@ def import_donnees():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f"Importation réussie")
|
flash(f"Importation réussie")
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/import_donnees.html",
|
"entreprises/import_donnees.j2",
|
||||||
title="Importation données",
|
title="Importation données",
|
||||||
form=form,
|
form=form,
|
||||||
entreprises_import=entreprises_import,
|
entreprises_import=entreprises_import,
|
||||||
@ -1853,7 +1853,7 @@ def import_donnees():
|
|||||||
correspondants_import=correspondants,
|
correspondants_import=correspondants,
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/import_donnees.html", title="Importation données", form=form
|
"entreprises/import_donnees.j2", title="Importation données", form=form
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1927,7 +1927,7 @@ def add_offre_file(entreprise_id, offre_id):
|
|||||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form.html",
|
"entreprises/form.j2",
|
||||||
title="Ajout fichier à une offre",
|
title="Ajout fichier à une offre",
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
@ -1969,7 +1969,7 @@ def delete_offre_file(entreprise_id, offre_id, filedir):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"entreprises/form_confirmation.html",
|
"entreprises/form_confirmation.j2",
|
||||||
title="Suppression fichier d'une offre",
|
title="Suppression fichier d'une offre",
|
||||||
form=form,
|
form=form,
|
||||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||||
@ -1981,4 +1981,4 @@ def not_found_error_handler(e):
|
|||||||
"""
|
"""
|
||||||
Renvoie une page d'erreur pour l'erreur 404
|
Renvoie une page d'erreur pour l'erreur 404
|
||||||
"""
|
"""
|
||||||
return render_template("entreprises/error.html", title="Erreur", e=e)
|
return render_template("entreprises/error.j2", title="Erreur", e=e)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -63,6 +63,7 @@ class CodesDecisionsForm(FlaskForm):
|
|||||||
ABL = _build_code_field("ABL")
|
ABL = _build_code_field("ABL")
|
||||||
ADC = _build_code_field("ADC")
|
ADC = _build_code_field("ADC")
|
||||||
ADJ = _build_code_field("ADJ")
|
ADJ = _build_code_field("ADJ")
|
||||||
|
ADJR = _build_code_field("ADJR")
|
||||||
ADM = _build_code_field("ADM")
|
ADM = _build_code_field("ADM")
|
||||||
AJ = _build_code_field("AJ")
|
AJ = _build_code_field("AJ")
|
||||||
ATB = _build_code_field("ATB")
|
ATB = _build_code_field("ATB")
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -171,7 +171,7 @@ class AddLogoForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class LogoForm(FlaskForm):
|
class LogoForm(FlaskForm):
|
||||||
"""Embed both presentation of a logo (cf. template file configuration.html)
|
"""Embed both presentation of a logo (cf. template file configuration.j2)
|
||||||
and all its data and UI action (change, delete)"""
|
and all its data and UI action (change, delete)"""
|
||||||
|
|
||||||
dept_key = HiddenField()
|
dept_key = HiddenField()
|
||||||
@ -434,7 +434,7 @@ def config_logos():
|
|||||||
scu.flash_errors(form)
|
scu.flash_errors(form)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"config_logos.html",
|
"config_logos.j2",
|
||||||
scodoc_dept=None,
|
scodoc_dept=None,
|
||||||
title="Configuration ScoDoc",
|
title="Configuration ScoDoc",
|
||||||
form=form,
|
form=form,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -54,6 +54,22 @@ class BonusConfigurationForm(FlaskForm):
|
|||||||
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>")
|
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
||||||
|
month_debut_annee_scolaire = SelectField(
|
||||||
|
label="Mois de début des années scolaires",
|
||||||
|
description="""Date pivot. En France métropolitaine, août.
|
||||||
|
S'applique à tous les départements.""",
|
||||||
|
choices=[
|
||||||
|
(i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
month_debut_periode2 = SelectField(
|
||||||
|
label="Mois de début deuxième période de l'année",
|
||||||
|
description="""Date pivot. En France métropolitaine, décembre.
|
||||||
|
S'applique à tous les départements.""",
|
||||||
|
choices=[
|
||||||
|
(i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1)
|
||||||
|
],
|
||||||
|
)
|
||||||
submit_scodoc = SubmitField("Valider")
|
submit_scodoc = SubmitField("Valider")
|
||||||
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
@ -67,7 +83,11 @@ def configuration():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
form_scodoc = ScoDocConfigurationForm(
|
form_scodoc = ScoDocConfigurationForm(
|
||||||
data={"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled()}
|
data={
|
||||||
|
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
|
||||||
|
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||||
|
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
if request.method == "POST" and (
|
if request.method == "POST" and (
|
||||||
form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data
|
form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data
|
||||||
@ -94,10 +114,26 @@ def configuration():
|
|||||||
"Module entreprise "
|
"Module entreprise "
|
||||||
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
||||||
)
|
)
|
||||||
|
if ScoDocSiteConfig.set_month_debut_annee_scolaire(
|
||||||
|
int(form_scodoc.data["month_debut_annee_scolaire"])
|
||||||
|
):
|
||||||
|
flash(
|
||||||
|
f"""Début des années scolaires fixé au mois de {
|
||||||
|
scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_annee_scolaire()-1]
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
if ScoDocSiteConfig.set_month_debut_periode2(
|
||||||
|
int(form_scodoc.data["month_debut_periode2"])
|
||||||
|
):
|
||||||
|
flash(
|
||||||
|
f"""Début des années scolaires fixé au mois de {
|
||||||
|
scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_periode2()-1]
|
||||||
|
}"""
|
||||||
|
)
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"configuration.html",
|
"configuration.j2",
|
||||||
form_bonus=form_bonus,
|
form_bonus=form_bonus,
|
||||||
form_scodoc=form_scodoc,
|
form_scodoc=form_scodoc,
|
||||||
scu=scu,
|
scu=scu,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -36,7 +36,7 @@ from app.models.etudiants import (
|
|||||||
from app.models.events import Scolog, ScolarNews
|
from app.models.events import Scolog, ScolarNews
|
||||||
from app.models.formations import Formation, Matiere
|
from app.models.formations import Formation, Matiere
|
||||||
from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags
|
from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import DispenseUE, UniteEns
|
||||||
from app.models.formsemestre import (
|
from app.models.formsemestre import (
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
FormSemestreEtape,
|
FormSemestreEtape,
|
||||||
@ -72,12 +72,15 @@ from app.models.validations import (
|
|||||||
from app.models.preferences import ScoPreference
|
from app.models.preferences import ScoPreference
|
||||||
|
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
ApcReferentielCompetences,
|
|
||||||
ApcCompetence,
|
|
||||||
ApcSituationPro,
|
|
||||||
ApcAppCritique,
|
ApcAppCritique,
|
||||||
|
ApcCompetence,
|
||||||
|
ApcNiveau,
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
|
ApcReferentielCompetences,
|
||||||
|
ApcSituationPro,
|
||||||
)
|
)
|
||||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
|
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
|
||||||
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
|
@ -15,8 +15,10 @@ class Absence(db.Model):
|
|||||||
db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True
|
db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True
|
||||||
)
|
)
|
||||||
jour = db.Column(db.Date)
|
jour = db.Column(db.Date)
|
||||||
|
# absent / justifié / absent+ justifié
|
||||||
estabs = db.Column(db.Boolean())
|
estabs = db.Column(db.Boolean())
|
||||||
estjust = db.Column(db.Boolean())
|
estjust = db.Column(db.Boolean())
|
||||||
|
|
||||||
matin = db.Column(db.Boolean())
|
matin = db.Column(db.Boolean())
|
||||||
# motif de l'absence:
|
# motif de l'absence:
|
||||||
description = db.Column(db.Text())
|
description = db.Column(db.Text())
|
||||||
|
277
app/models/assiduites.py
Normal file
277
app/models/assiduites.py
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import ModuleImpl
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc.sco_utils import (
|
||||||
|
EtatAssiduite,
|
||||||
|
EtatJustificatif,
|
||||||
|
is_period_overlapping,
|
||||||
|
localize_datetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Assiduite(db.Model):
|
||||||
|
"""
|
||||||
|
Représente une assiduité:
|
||||||
|
- une plage horaire lié à un état et un étudiant
|
||||||
|
- un module si spécifiée
|
||||||
|
- une description si spécifiée
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "assiduites"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
assiduite_id = db.synonym("id")
|
||||||
|
|
||||||
|
date_debut = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
date_fin = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
moduleimpl_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("notes_moduleimpl.id", ondelete="SET NULL"),
|
||||||
|
)
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
etat = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
|
desc = db.Column(db.Text)
|
||||||
|
|
||||||
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
|
||||||
|
def to_dict(self, format_api=True) -> dict:
|
||||||
|
etat = self.etat
|
||||||
|
|
||||||
|
if format_api:
|
||||||
|
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||||
|
data = {
|
||||||
|
"assiduite_id": self.assiduite_id,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"moduleimpl_id": self.moduleimpl_id,
|
||||||
|
"date_debut": self.date_debut,
|
||||||
|
"date_fin": self.date_fin,
|
||||||
|
"etat": etat,
|
||||||
|
"desc": self.desc,
|
||||||
|
"entry_date": self.entry_date,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_assiduite(
|
||||||
|
cls,
|
||||||
|
etud: Identite,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatAssiduite,
|
||||||
|
moduleimpl: ModuleImpl = None,
|
||||||
|
description: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
|
# Vérification de non duplication des périodes
|
||||||
|
assiduites: list[Assiduite] = etud.assiduites
|
||||||
|
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||||
|
raise ScoValueError(
|
||||||
|
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||||
|
)
|
||||||
|
if moduleimpl is not None:
|
||||||
|
# Vérification de l'existence du module pour l'étudiant
|
||||||
|
if moduleimpl.est_inscrit(etud):
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
moduleimpl_id=moduleimpl.id,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
|
||||||
|
else:
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_assiduite
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fast_create_assiduite(
|
||||||
|
cls,
|
||||||
|
etudid: int,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatAssiduite,
|
||||||
|
moduleimpl_id: int = None,
|
||||||
|
description: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||||
|
# Vérification de non duplication des périodes
|
||||||
|
|
||||||
|
nouv_assiduite = Assiduite(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudid=etudid,
|
||||||
|
moduleimpl_id=moduleimpl_id,
|
||||||
|
desc=description,
|
||||||
|
entry_date=entry_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_assiduite
|
||||||
|
|
||||||
|
|
||||||
|
class Justificatif(db.Model):
|
||||||
|
"""
|
||||||
|
Représente un justificatif:
|
||||||
|
- une plage horaire lié à un état et un étudiant
|
||||||
|
- une raison si spécifiée
|
||||||
|
- un fichier si spécifié
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "justificatifs"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
justif_id = db.synonym("id")
|
||||||
|
|
||||||
|
date_debut = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
date_fin = db.Column(
|
||||||
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
etat = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
|
||||||
|
raison = db.Column(db.Text())
|
||||||
|
|
||||||
|
# Archive_id -> sco_archives_justificatifs.py
|
||||||
|
fichier = db.Column(db.Text())
|
||||||
|
|
||||||
|
def to_dict(self, format_api: bool = False) -> dict:
|
||||||
|
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||||
|
|
||||||
|
etat = self.etat
|
||||||
|
|
||||||
|
if format_api:
|
||||||
|
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"justif_id": self.justif_id,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"date_debut": self.date_debut,
|
||||||
|
"date_fin": self.date_fin,
|
||||||
|
"etat": etat,
|
||||||
|
"raison": self.raison,
|
||||||
|
"fichier": self.fichier,
|
||||||
|
"entry_date": self.entry_date,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_justificatif(
|
||||||
|
cls,
|
||||||
|
etud: Identite,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatJustificatif,
|
||||||
|
raison: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||||
|
# Vérification de non duplication des périodes
|
||||||
|
justificatifs: list[Justificatif] = etud.justificatifs
|
||||||
|
if is_period_conflicting(date_debut, date_fin, justificatifs, Justificatif):
|
||||||
|
raise ScoValueError(
|
||||||
|
"Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
|
||||||
|
)
|
||||||
|
|
||||||
|
nouv_justificatif = Justificatif(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudiant=etud,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_justificatif
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fast_create_justificatif(
|
||||||
|
cls,
|
||||||
|
etudid: int,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatJustificatif,
|
||||||
|
raison: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
) -> object or int:
|
||||||
|
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||||
|
|
||||||
|
nouv_justificatif = Justificatif(
|
||||||
|
date_debut=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
etat=etat,
|
||||||
|
etudid=etudid,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nouv_justificatif
|
||||||
|
|
||||||
|
|
||||||
|
def is_period_conflicting(
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
collection: list[Assiduite or Justificatif],
|
||||||
|
collection_cls: Assiduite or Justificatif,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si une date n'entre pas en collision
|
||||||
|
avec les justificatifs ou assiduites déjà présentes
|
||||||
|
"""
|
||||||
|
|
||||||
|
date_debut = localize_datetime(date_debut)
|
||||||
|
date_fin = localize_datetime(date_fin)
|
||||||
|
|
||||||
|
if (
|
||||||
|
collection.filter_by(date_debut=date_debut, date_fin=date_fin).first()
|
||||||
|
is not None
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
count: int = collection.filter(
|
||||||
|
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return count > 0
|
@ -1,6 +1,6 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||||
@ -14,7 +14,7 @@ import sqlalchemy
|
|||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||||
|
|
||||||
|
|
||||||
# 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
|
||||||
@ -54,13 +54,15 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
"Référentiel de compétence d'une spécialité"
|
"Référentiel de compétence d'une spécialité"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||||
annexe = db.Column(db.Text())
|
annexe = db.Column(db.Text()) # '1', '22', ...
|
||||||
specialite = db.Column(db.Text())
|
specialite = db.Column(db.Text()) # 'CJ', 'RT', 'INFO', ...
|
||||||
specialite_long = db.Column(db.Text())
|
specialite_long = db.Column(
|
||||||
type_titre = db.Column(db.Text())
|
db.Text()
|
||||||
type_structure = db.Column(db.Text())
|
) # 'Carrière Juridique', 'Réseaux et télécommunications', ...
|
||||||
|
type_titre = db.Column(db.Text()) # 'B.U.T.'
|
||||||
|
type_structure = db.Column(db.Text()) # 'type1', 'type2', ...
|
||||||
type_departement = db.Column(db.Text()) # "secondaire", "tertiaire"
|
type_departement = db.Column(db.Text()) # "secondaire", "tertiaire"
|
||||||
version_orebut = db.Column(db.Text())
|
version_orebut = db.Column(db.Text()) # '2021-12-11 00:00:00'
|
||||||
_xml_attribs = { # Orébut xml attrib : attribute
|
_xml_attribs = { # Orébut xml attrib : attribute
|
||||||
"type": "type_titre",
|
"type": "type_titre",
|
||||||
"version": "version_orebut",
|
"version": "version_orebut",
|
||||||
@ -86,9 +88,16 @@ 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 to_dict(self):
|
def get_version(self) -> str:
|
||||||
|
"La version, normalement sous forme de date iso yyy-mm-dd"
|
||||||
|
if not self.version_orebut:
|
||||||
|
return ""
|
||||||
|
return self.version_orebut.split()[0]
|
||||||
|
|
||||||
|
def to_dict(self, parcours: list["ApcParcours"] = None, with_app_critiques=True):
|
||||||
"""Représentation complète du ref. de comp.
|
"""Représentation complète du ref. de comp.
|
||||||
comme un dict.
|
comme un dict.
|
||||||
|
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"dept_id": self.dept_id,
|
"dept_id": self.dept_id,
|
||||||
@ -103,29 +112,45 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
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": {x.titre: x.to_dict() for x in self.competences},
|
"competences": {
|
||||||
"parcours": {x.code: x.to_dict() for x in self.parcours},
|
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
||||||
|
for x in self.competences
|
||||||
|
},
|
||||||
|
"parcours": {
|
||||||
|
x.code: x.to_dict()
|
||||||
|
for x in (self.parcours if parcours is None else parcours)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_niveaux_by_parcours(self, annee) -> dict:
|
def get_niveaux_by_parcours(
|
||||||
|
self, annee: int, parcour: "ApcParcours" = None
|
||||||
|
) -> tuple[list["ApcParcours"], dict]:
|
||||||
"""
|
"""
|
||||||
Construit la liste des niveaux de compétences pour chaque parcours
|
Construit la liste des niveaux de compétences pour chaque parcours
|
||||||
de ce référentiel.
|
de ce référentiel, ou seulement pour le parcours donné.
|
||||||
|
|
||||||
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
|
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
|
||||||
|
|
||||||
Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut:
|
Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut:
|
||||||
on cherche les niveaux qui sont présents dans tous les parcours et les range sous
|
on cherche les niveaux qui sont présents dans tous les parcours et les range sous
|
||||||
la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun).
|
la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun).
|
||||||
|
|
||||||
résultat:
|
Résultat: couple
|
||||||
{
|
( [ ApcParcours ],
|
||||||
"TC" : [ ApcNiveau ],
|
{
|
||||||
parcour.id : [ ApcNiveau ]
|
"TC" : [ ApcNiveau ],
|
||||||
}
|
parcour.id : [ ApcNiveau ]
|
||||||
|
}
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
parcours = self.parcours.order_by(ApcParcours.numero).all()
|
parcours_ref = self.parcours.order_by(ApcParcours.numero).all()
|
||||||
|
if parcour is None:
|
||||||
|
parcours = parcours_ref
|
||||||
|
else:
|
||||||
|
parcours = [parcour]
|
||||||
niveaux_by_parcours = {
|
niveaux_by_parcours = {
|
||||||
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
|
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
|
||||||
for parcour in parcours
|
for parcour in parcours_ref
|
||||||
}
|
}
|
||||||
# Cherche tronc commun
|
# Cherche tronc commun
|
||||||
if niveaux_by_parcours:
|
if niveaux_by_parcours:
|
||||||
@ -154,7 +179,28 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc
|
niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc
|
||||||
]
|
]
|
||||||
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
|
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
|
||||||
return niveaux_by_parcours_no_tc
|
return parcours, niveaux_by_parcours_no_tc
|
||||||
|
|
||||||
|
def get_competences_tronc_commun(self) -> list["ApcCompetence"]:
|
||||||
|
"""Liste des compétences communes à tous les parcours du référentiel."""
|
||||||
|
parcours = self.parcours.all()
|
||||||
|
if not parcours:
|
||||||
|
return []
|
||||||
|
|
||||||
|
ids = set.intersection(
|
||||||
|
*[
|
||||||
|
{competence.id for competence in parcour.query_competences()}
|
||||||
|
for parcour in parcours
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return sorted(
|
||||||
|
[
|
||||||
|
competence
|
||||||
|
for competence in parcours[0].query_competences()
|
||||||
|
if competence.id in ids
|
||||||
|
],
|
||||||
|
key=lambda c: c.numero or 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ApcCompetence(db.Model, XMLModel):
|
class ApcCompetence(db.Model, XMLModel):
|
||||||
@ -197,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ApcCompetence {self.id} {self.titre!r}>"
|
return f"<ApcCompetence {self.id} {self.titre!r}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self, with_app_critiques=True):
|
||||||
"repr dict recursive sur situations, composantes, niveaux"
|
"repr dict recursive sur situations, composantes, niveaux"
|
||||||
return {
|
return {
|
||||||
"id_orebut": self.id_orebut,
|
"id_orebut": self.id_orebut,
|
||||||
@ -209,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel):
|
|||||||
"composantes_essentielles": [
|
"composantes_essentielles": [
|
||||||
x.to_dict() for x in self.composantes_essentielles
|
x.to_dict() for x in self.composantes_essentielles
|
||||||
],
|
],
|
||||||
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
|
"niveaux": {
|
||||||
|
x.annee: x.to_dict(with_app_critiques=with_app_critiques)
|
||||||
|
for x in self.niveaux
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_dict_bul(self) -> dict:
|
def to_dict_bul(self) -> dict:
|
||||||
@ -275,13 +324,15 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
||||||
self.annee!r} {self.competence!r}>"""
|
self.annee!r} {self.competence!r}>"""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self, with_app_critiques=True):
|
||||||
"as a dict, recursif sur les AC"
|
"as a dict, recursif (ou non) sur les AC"
|
||||||
return {
|
return {
|
||||||
"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
|
||||||
|
else {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_dict_bul(self):
|
def to_dict_bul(self):
|
||||||
@ -306,9 +357,8 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
if annee not in {1, 2, 3}:
|
if annee not in {1, 2, 3}:
|
||||||
raise ValueError("annee invalide pour un parcours BUT")
|
raise ValueError("annee invalide pour un parcours BUT")
|
||||||
if referentiel_competence is None:
|
if referentiel_competence is None:
|
||||||
raise ScoValueError(
|
raise ScoNoReferentielCompetences()
|
||||||
"Pas de référentiel de compétences associé à la formation !"
|
|
||||||
)
|
|
||||||
annee_formation = f"BUT{annee}"
|
annee_formation = f"BUT{annee}"
|
||||||
if parcour is None:
|
if parcour is None:
|
||||||
return ApcNiveau.query.filter(
|
return ApcNiveau.query.filter(
|
||||||
@ -436,6 +486,7 @@ class ApcParcours(db.Model, XMLModel):
|
|||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
|
ues = db.relationship("UniteEns", back_populates="parcour")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"
|
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"
|
||||||
@ -453,6 +504,14 @@ class ApcParcours(db.Model, XMLModel):
|
|||||||
d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
|
d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def query_competences(self) -> flask_sqlalchemy.BaseQuery:
|
||||||
|
"Les compétences associées à ce parcours"
|
||||||
|
return (
|
||||||
|
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||||
|
.filter_by(parcours_id=self.id)
|
||||||
|
.order_by(ApcCompetence.numero)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ApcAnneeParcours(db.Model, XMLModel):
|
class ApcAnneeParcours(db.Model, XMLModel):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -2,19 +2,17 @@
|
|||||||
|
|
||||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import flask_sqlalchemy
|
|
||||||
from sqlalchemy.sql import text
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from app import db
|
import flask_sqlalchemy
|
||||||
|
|
||||||
|
from app import db
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
from app.models.but_refcomp import ApcNiveau
|
from app.models.but_refcomp import ApcNiveau
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.ues import UniteEns
|
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_codes_parcours as sco_codes
|
from app.scodoc import sco_codes_parcours as sco_codes
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
@ -63,13 +61,32 @@ class ApcValidationRCUE(db.Model):
|
|||||||
self.ue1}/{self.ue2}:{self.code!r}>"""
|
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"""décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {self.code}"""
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||||
|
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
||||||
|
|
||||||
|
def to_html(self) -> str:
|
||||||
|
"description en HTML"
|
||||||
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||||
|
<b>{self.code}</b>
|
||||||
|
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
|
||||||
|
à {self.date.strftime("%Hh%M")}</em>"""
|
||||||
|
|
||||||
|
def annee(self) -> str:
|
||||||
|
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
||||||
|
niveau = self.niveau()
|
||||||
|
return niveau.annee if niveau else None
|
||||||
|
|
||||||
def niveau(self) -> ApcNiveau:
|
def niveau(self) -> ApcNiveau:
|
||||||
"""Le niveau de compétence associé à cet RCUE."""
|
"""Le niveau de compétence associé à cet RCUE."""
|
||||||
# Par convention, il est donné par la seconde UE
|
# Par convention, il est donné par la seconde UE
|
||||||
return self.ue2.niveau_competence
|
return self.ue2.niveau_competence
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"as a dict"
|
||||||
|
d = dict(self.__dict__)
|
||||||
|
d.pop("_sa_instance_state", None)
|
||||||
|
return d
|
||||||
|
|
||||||
def to_dict_bul(self) -> dict:
|
def to_dict_bul(self) -> dict:
|
||||||
"Export dict pour bulletins: le code et le niveau de compétence"
|
"Export dict pour bulletins: le code et le niveau de compétence"
|
||||||
niveau = self.niveau()
|
niveau = self.niveau()
|
||||||
@ -84,28 +101,24 @@ class RegroupementCoherentUE:
|
|||||||
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
||||||
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
|
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
|
||||||
|
|
||||||
La moyenne (10/20) au RCU déclenche la compensation des UE.
|
La moyenne (10/20) au RCUE déclenche la compensation des UE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
etud: Identite,
|
etud: Identite,
|
||||||
formsemestre_1: FormSemestre,
|
formsemestre_1: FormSemestre,
|
||||||
ue_1: UniteEns,
|
dec_ue_1: "DecisionsProposeesUE",
|
||||||
formsemestre_2: FormSemestre,
|
formsemestre_2: FormSemestre,
|
||||||
ue_2: UniteEns,
|
dec_ue_2: "DecisionsProposeesUE",
|
||||||
inscription_etat: str,
|
inscription_etat: str,
|
||||||
):
|
):
|
||||||
from app.comp import res_sem
|
ue_1 = dec_ue_1.ue
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
ue_2 = dec_ue_2.ue
|
||||||
|
|
||||||
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
||||||
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
|
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
|
||||||
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
|
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
|
||||||
(
|
(ue_2, formsemestre_2),
|
||||||
ue_2,
|
|
||||||
formsemestre_2,
|
|
||||||
),
|
|
||||||
(ue_1, formsemestre_1),
|
(ue_1, formsemestre_1),
|
||||||
)
|
)
|
||||||
assert formsemestre_1.semestre_id % 2 == 1
|
assert formsemestre_1.semestre_id % 2 == 1
|
||||||
@ -125,21 +138,12 @@ class RegroupementCoherentUE:
|
|||||||
self.moy_ue_1 = self.moy_ue_2 = "-"
|
self.moy_ue_1 = self.moy_ue_2 = "-"
|
||||||
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
|
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
|
||||||
return
|
return
|
||||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
|
self.moy_ue_1 = dec_ue_1.moy_ue_with_cap
|
||||||
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
|
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
||||||
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
|
self.moy_ue_2 = dec_ue_2.moy_ue_with_cap
|
||||||
self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN
|
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
|
||||||
else:
|
|
||||||
self.moy_ue_1 = None
|
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées)
|
||||||
self.moy_ue_1_val = 0.0
|
|
||||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2)
|
|
||||||
if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]:
|
|
||||||
self.moy_ue_2 = res.etud_moy_ue[ue_2.id][etud.id]
|
|
||||||
self.moy_ue_2_val = self.moy_ue_2
|
|
||||||
else:
|
|
||||||
self.moy_ue_2 = None
|
|
||||||
self.moy_ue_2_val = 0.0
|
|
||||||
# Calcul de la moyenne au RCUE
|
|
||||||
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
||||||
# Moyenne RCUE (les pondérations par défaut sont 1.)
|
# Moyenne RCUE (les pondérations par défaut sont 1.)
|
||||||
self.moy_rcue = (
|
self.moy_rcue = (
|
||||||
@ -149,7 +153,14 @@ class RegroupementCoherentUE:
|
|||||||
self.moy_rcue = None
|
self.moy_rcue = None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>"
|
return f"""<{self.__class__.__name__} {
|
||||||
|
self.ue_1.acronyme}({self.moy_ue_1}) {
|
||||||
|
self.ue_2.acronyme}({self.moy_ue_2})>"""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"""RCUE {
|
||||||
|
self.ue_1.acronyme}({self.moy_ue_1}) + {
|
||||||
|
self.ue_2.acronyme}({self.moy_ue_2})"""
|
||||||
|
|
||||||
def query_validations(
|
def query_validations(
|
||||||
self,
|
self,
|
||||||
@ -181,8 +192,9 @@ class RegroupementCoherentUE:
|
|||||||
return self.query_validations().count() > 0
|
return self.query_validations().count() > 0
|
||||||
|
|
||||||
def est_compensable(self):
|
def est_compensable(self):
|
||||||
"""Vrai si ce RCUE est validable par compensation
|
"""Vrai si ce RCUE est validable (uniquement) par compensation
|
||||||
c'est à dire que sa moyenne est > 10 avec une UE < 10
|
c'est à dire que sa moyenne est > 10 avec une UE < 10.
|
||||||
|
Note: si ADM, est_compensable est faux.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
(self.moy_rcue is not None)
|
(self.moy_rcue is not None)
|
||||||
@ -218,62 +230,62 @@ class RegroupementCoherentUE:
|
|||||||
|
|
||||||
|
|
||||||
# unused
|
# unused
|
||||||
def find_rcues(
|
# def find_rcues(
|
||||||
formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||||
) -> list[RegroupementCoherentUE]:
|
# ) -> list[RegroupementCoherentUE]:
|
||||||
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
# """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||||
ce semestre pour cette UE.
|
# ce semestre pour cette UE.
|
||||||
|
|
||||||
Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
# Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||||
En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
# En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||||
|
|
||||||
Résultat: la liste peut être vide.
|
# Résultat: la liste peut être vide.
|
||||||
"""
|
# """
|
||||||
if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
# if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||||
return []
|
# return []
|
||||||
|
|
||||||
if ue.semestre_idx % 2: # S1, S3, S5
|
# if ue.semestre_idx % 2: # S1, S3, S5
|
||||||
other_semestre_idx = ue.semestre_idx + 1
|
# other_semestre_idx = ue.semestre_idx + 1
|
||||||
else:
|
# else:
|
||||||
other_semestre_idx = ue.semestre_idx - 1
|
# other_semestre_idx = ue.semestre_idx - 1
|
||||||
|
|
||||||
cursor = db.session.execute(
|
# cursor = db.session.execute(
|
||||||
text(
|
# text(
|
||||||
"""SELECT
|
# """SELECT
|
||||||
ue.id, formsemestre.id
|
# ue.id, formsemestre.id
|
||||||
FROM
|
# FROM
|
||||||
notes_ue ue,
|
# notes_ue ue,
|
||||||
notes_formsemestre_inscription inscr,
|
# notes_formsemestre_inscription inscr,
|
||||||
notes_formsemestre formsemestre
|
# notes_formsemestre formsemestre
|
||||||
|
|
||||||
WHERE
|
# WHERE
|
||||||
inscr.etudid = :etudid
|
# inscr.etudid = :etudid
|
||||||
AND inscr.formsemestre_id = formsemestre.id
|
# AND inscr.formsemestre_id = formsemestre.id
|
||||||
|
|
||||||
AND formsemestre.semestre_id = :other_semestre_idx
|
# AND formsemestre.semestre_id = :other_semestre_idx
|
||||||
AND ue.formation_id = formsemestre.formation_id
|
# AND ue.formation_id = formsemestre.formation_id
|
||||||
AND ue.niveau_competence_id = :ue_niveau_competence_id
|
# AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||||
AND ue.semestre_idx = :other_semestre_idx
|
# AND ue.semestre_idx = :other_semestre_idx
|
||||||
"""
|
# """
|
||||||
),
|
# ),
|
||||||
{
|
# {
|
||||||
"etudid": etud.id,
|
# "etudid": etud.id,
|
||||||
"other_semestre_idx": other_semestre_idx,
|
# "other_semestre_idx": other_semestre_idx,
|
||||||
"ue_niveau_competence_id": ue.niveau_competence_id,
|
# "ue_niveau_competence_id": ue.niveau_competence_id,
|
||||||
},
|
# },
|
||||||
)
|
# )
|
||||||
rcues = []
|
# rcues = []
|
||||||
for ue_id, formsemestre_id in cursor:
|
# for ue_id, formsemestre_id in cursor:
|
||||||
other_ue = UniteEns.query.get(ue_id)
|
# other_ue = UniteEns.query.get(ue_id)
|
||||||
other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
# other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
rcues.append(
|
# rcues.append(
|
||||||
RegroupementCoherentUE(
|
# RegroupementCoherentUE(
|
||||||
etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
# etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
||||||
)
|
# )
|
||||||
)
|
# )
|
||||||
# safety check: 1 seul niveau de comp. concerné:
|
# # safety check: 1 seul niveau de comp. concerné:
|
||||||
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
# assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||||
return rcues
|
# return rcues
|
||||||
|
|
||||||
|
|
||||||
class ApcValidationAnnee(db.Model):
|
class ApcValidationAnnee(db.Model):
|
||||||
@ -281,7 +293,7 @@ class ApcValidationAnnee(db.Model):
|
|||||||
|
|
||||||
__tablename__ = "apc_validation_annee"
|
__tablename__ = "apc_validation_annee"
|
||||||
# Assure unicité de la décision:
|
# Assure unicité de la décision:
|
||||||
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire"),)
|
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),)
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
@ -303,7 +315,8 @@ class ApcValidationAnnee(db.Model):
|
|||||||
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"
|
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
||||||
|
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}"""
|
return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}"""
|
||||||
@ -340,7 +353,8 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|||||||
titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
|
titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
|
||||||
else:
|
else:
|
||||||
titres_rcues.append(
|
titres_rcues.append(
|
||||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {dec_rcue["code"]}"""
|
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {
|
||||||
|
dec_rcue["code"]}"""
|
||||||
)
|
)
|
||||||
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
||||||
decisions["descr_decisions_niveaux"] = (
|
decisions["descr_decisions_niveaux"] = (
|
||||||
|
@ -6,13 +6,14 @@
|
|||||||
from flask import flash
|
from flask import flash
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.comp import bonus_spo
|
from app.comp import bonus_spo
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
from app.scodoc.sco_codes_parcours import (
|
from app.scodoc.sco_codes_parcours import (
|
||||||
ABAN,
|
ABAN,
|
||||||
ABL,
|
ABL,
|
||||||
ADC,
|
ADC,
|
||||||
ADJ,
|
ADJ,
|
||||||
|
ADJR,
|
||||||
ADM,
|
ADM,
|
||||||
AJ,
|
AJ,
|
||||||
ATB,
|
ATB,
|
||||||
@ -34,6 +35,7 @@ CODES_SCODOC_TO_APO = {
|
|||||||
ABL: "ABL",
|
ABL: "ABL",
|
||||||
ADC: "ADMC",
|
ADC: "ADMC",
|
||||||
ADJ: "ADM",
|
ADJ: "ADM",
|
||||||
|
ADJR: "ADM",
|
||||||
ADM: "ADM",
|
ADM: "ADM",
|
||||||
AJ: "AJ",
|
AJ: "AJ",
|
||||||
ATB: "AJAC",
|
ATB: "AJAC",
|
||||||
@ -83,6 +85,8 @@ 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,
|
||||||
|
"month_debut_annee_scolaire": int,
|
||||||
|
"month_debut_periode2": int,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
@ -223,3 +227,73 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_int_field(cls, name: str, default=None) -> int:
|
||||||
|
"""Valeur d'un champs integer"""
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||||
|
if (cfg is None) or cfg.value is None:
|
||||||
|
return default
|
||||||
|
return int(cfg.value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _set_int_field(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
value: int,
|
||||||
|
default=None,
|
||||||
|
range_values: tuple = (),
|
||||||
|
) -> bool:
|
||||||
|
"""Set champs integer. True si changement."""
|
||||||
|
if value != cls._get_int_field(name, default=default):
|
||||||
|
if not isinstance(value, int) or (
|
||||||
|
range_values and (value < range_values[0]) or (value > range_values[1])
|
||||||
|
):
|
||||||
|
raise ValueError("invalid value")
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||||
|
if cfg is None:
|
||||||
|
cfg = ScoDocSiteConfig(name=name, value=str(value))
|
||||||
|
else:
|
||||||
|
cfg.value = str(value)
|
||||||
|
db.session.add(cfg)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_month_debut_annee_scolaire(cls) -> int:
|
||||||
|
"""Mois de début de l'année scolaire."""
|
||||||
|
return cls._get_int_field(
|
||||||
|
"month_debut_annee_scolaire", scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_month_debut_periode2(cls) -> int:
|
||||||
|
"""Mois de début de l'année scolaire."""
|
||||||
|
return cls._get_int_field("month_debut_periode2", scu.MONTH_DEBUT_PERIODE2)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_month_debut_annee_scolaire(
|
||||||
|
cls, month: int = scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||||
|
) -> bool:
|
||||||
|
"""Fixe le mois de début des années scolaires.
|
||||||
|
True si changement.
|
||||||
|
"""
|
||||||
|
if cls._set_int_field(
|
||||||
|
"month_debut_annee_scolaire", month, scu.MONTH_DEBUT_ANNEE_SCOLAIRE, (1, 12)
|
||||||
|
):
|
||||||
|
log(f"set_month_debut_annee_scolaire({month})")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_month_debut_periode2(cls, month: int = scu.MONTH_DEBUT_PERIODE2) -> bool:
|
||||||
|
"""Fixe le mois de début des années scolaires.
|
||||||
|
True si changement.
|
||||||
|
"""
|
||||||
|
if cls._set_int_field(
|
||||||
|
"month_debut_periode2", month, scu.MONTH_DEBUT_PERIODE2, (1, 12)
|
||||||
|
):
|
||||||
|
log(f"set_month_debut_periode2({month})")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
"""ScoDoc models : departements
|
"""ScoDoc models : departements
|
||||||
"""
|
"""
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
|
from app.models.preferences import ScoPreference
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ class Departement(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self, with_dept_name=True, with_dept_preferences=False):
|
||||||
data = {
|
data = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"acronym": self.acronym,
|
"acronym": self.acronym,
|
||||||
@ -47,6 +47,17 @@ class Departement(db.Model):
|
|||||||
"visible": self.visible,
|
"visible": self.visible,
|
||||||
"date_creation": self.date_creation,
|
"date_creation": self.date_creation,
|
||||||
}
|
}
|
||||||
|
if with_dept_name:
|
||||||
|
pref = ScoPreference.query.filter_by(
|
||||||
|
dept_id=self.id, name="DeptName"
|
||||||
|
).first()
|
||||||
|
data["dept_name"] = pref.value if pref else None
|
||||||
|
# Ceci n'est pas encore utilisé, mais pourrait être publié
|
||||||
|
# par l'API après nettoyage des préférences.
|
||||||
|
if with_dept_preferences:
|
||||||
|
data["preferences"] = {
|
||||||
|
p.name: p.value for p in ScoPreference.query.filter_by(dept_id=self.id)
|
||||||
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -58,6 +58,16 @@ class Identite(db.Model):
|
|||||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||||
#
|
#
|
||||||
admission = db.relationship("Admission", backref="identite", lazy="dynamic")
|
admission = db.relationship("Admission", backref="identite", lazy="dynamic")
|
||||||
|
dispense_ues = db.relationship(
|
||||||
|
"DispenseUE",
|
||||||
|
back_populates="etud",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relations avec les assiduites et les justificatifs
|
||||||
|
assiduites = db.relationship("Assiduite", backref="etudiant", lazy="dynamic")
|
||||||
|
justificatifs = db.relationship("Justificatif", backref="etudiant", lazy="dynamic")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
@ -73,6 +83,14 @@ class Identite(db.Model):
|
|||||||
args = make_etud_args(etudid=etudid, code_nip=code_nip)
|
args = make_etud_args(etudid=etudid, code_nip=code_nip)
|
||||||
return Identite.query.filter_by(**args).first_or_404()
|
return Identite.query.filter_by(**args).first_or_404()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_etud(cls, **args):
|
||||||
|
"Crée un étudiant, avec admission et adresse vides."
|
||||||
|
etud: Identite = cls(**args)
|
||||||
|
etud.adresses.append(Adresse())
|
||||||
|
etud.admission.append(Admission())
|
||||||
|
return etud
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def civilite_str(self):
|
def civilite_str(self):
|
||||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||||
|
@ -13,6 +13,8 @@ from app.models.ues import UniteEns
|
|||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
|
|
||||||
|
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
||||||
|
|
||||||
|
|
||||||
class Evaluation(db.Model):
|
class Evaluation(db.Model):
|
||||||
"""Evaluation (contrôle, examen, ...)"""
|
"""Evaluation (contrôle, examen, ...)"""
|
||||||
@ -51,7 +53,7 @@ class Evaluation(db.Model):
|
|||||||
self.description[:16] if self.description else ''}">"""
|
self.description[:16] if self.description else ''}">"""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"Représentation dict, pour json"
|
"Représentation dict (riche, compat ScoDoc 7)"
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
# ScoDoc7 output_formators
|
# ScoDoc7 output_formators
|
||||||
@ -71,6 +73,34 @@ class Evaluation(db.Model):
|
|||||||
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||||
return evaluation_enrich_dict(e)
|
return evaluation_enrich_dict(e)
|
||||||
|
|
||||||
|
def to_dict_api(self) -> dict:
|
||||||
|
"Représentation dict pour API JSON"
|
||||||
|
if self.jour is None:
|
||||||
|
date_debut = None
|
||||||
|
date_fin = None
|
||||||
|
else:
|
||||||
|
date_debut = datetime.datetime.combine(
|
||||||
|
self.jour, self.heure_debut or datetime.time(0, 0)
|
||||||
|
).isoformat()
|
||||||
|
date_fin = datetime.datetime.combine(
|
||||||
|
self.jour, self.heure_fin or datetime.time(0, 0)
|
||||||
|
).isoformat()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"coefficient": self.coefficient,
|
||||||
|
"date_debut": date_debut,
|
||||||
|
"date_fin": date_fin,
|
||||||
|
"description": self.description,
|
||||||
|
"evaluation_type": self.evaluation_type,
|
||||||
|
"id": self.id,
|
||||||
|
"moduleimpl_id": self.moduleimpl_id,
|
||||||
|
"note_max": self.note_max,
|
||||||
|
"numero": self.numero,
|
||||||
|
"poids": self.get_ue_poids_dict(),
|
||||||
|
"publish_incomplete": self.publish_incomplete,
|
||||||
|
"visi_bulletin": self.visibulletin,
|
||||||
|
}
|
||||||
|
|
||||||
def from_dict(self, data):
|
def from_dict(self, data):
|
||||||
"""Set evaluation attributes from given dict values."""
|
"""Set evaluation attributes from given dict values."""
|
||||||
check_evaluation_args(data)
|
check_evaluation_args(data)
|
||||||
@ -83,12 +113,24 @@ class Evaluation(db.Model):
|
|||||||
if self.heure_debut and (
|
if self.heure_debut and (
|
||||||
not self.heure_fin or self.heure_fin == self.heure_debut
|
not self.heure_fin or self.heure_fin == self.heure_debut
|
||||||
):
|
):
|
||||||
return f"""à {self.heure_debut.strftime("%H:%M")}"""
|
return f"""à {self.heure_debut.strftime("%Hh%M")}"""
|
||||||
elif self.heure_debut and self.heure_fin:
|
elif self.heure_debut and self.heure_fin:
|
||||||
return f"""de {self.heure_debut.strftime("%H:%M")} à {self.heure_fin.strftime("%H:%M")}"""
|
return f"""de {self.heure_debut.strftime("%Hh%M")} à {self.heure_fin.strftime("%Hh%M")}"""
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def descr_duree(self) -> str:
|
||||||
|
"Description de la durée pour affichages"
|
||||||
|
if self.heure_debut is None and self.heure_fin is None:
|
||||||
|
return ""
|
||||||
|
debut = self.heure_debut or DEFAULT_EVALUATION_TIME
|
||||||
|
fin = self.heure_fin or DEFAULT_EVALUATION_TIME
|
||||||
|
d = (fin.hour * 60 + fin.minute) - (debut.hour * 60 + debut.minute)
|
||||||
|
duree = f"{d//60}h"
|
||||||
|
if d % 60:
|
||||||
|
duree += f"{d%60:02d}"
|
||||||
|
return duree
|
||||||
|
|
||||||
def clone(self, not_copying=()):
|
def clone(self, not_copying=()):
|
||||||
"""Clone, not copying the given attrs
|
"""Clone, not copying the given attrs
|
||||||
Attention: la copie n'a pas d'id avant le prochain commit
|
Attention: la copie n'a pas d'id avant le prochain commit
|
||||||
@ -227,7 +269,7 @@ def evaluation_enrich_dict(e: dict):
|
|||||||
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
||||||
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
||||||
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
||||||
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
|
e["jour_iso"] = ndb.DateDMYtoISO(e["jour"])
|
||||||
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
||||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||||
if d is not None:
|
if d is not None:
|
||||||
|
@ -36,6 +36,7 @@ class Formation(db.Model):
|
|||||||
titre = db.Column(db.Text(), nullable=False)
|
titre = db.Column(db.Text(), nullable=False)
|
||||||
titre_officiel = db.Column(db.Text(), nullable=False)
|
titre_officiel = db.Column(db.Text(), nullable=False)
|
||||||
version = db.Column(db.Integer, default=1, server_default="1")
|
version = db.Column(db.Integer, default=1, server_default="1")
|
||||||
|
commentaire = db.Column(db.Text())
|
||||||
formation_code = db.Column(
|
formation_code = db.Column(
|
||||||
db.String(SHORT_STR_LEN),
|
db.String(SHORT_STR_LEN),
|
||||||
server_default=db.text("notes_newid_fcod()"),
|
server_default=db.text("notes_newid_fcod()"),
|
||||||
@ -55,18 +56,21 @@ class Formation(db.Model):
|
|||||||
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>"
|
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
|
||||||
|
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
|
||||||
|
|
||||||
def to_html(self) -> str:
|
def to_html(self) -> str:
|
||||||
"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}"""
|
||||||
|
|
||||||
def to_dict(self, with_refcomp_attrs=False):
|
def to_dict(self, with_refcomp_attrs=False):
|
||||||
""" "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.
|
||||||
"""
|
"""
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
|
if "referentiel_competence" in e:
|
||||||
|
e.pop("referentiel_competence")
|
||||||
e["departement"] = self.departement.to_dict()
|
e["departement"] = self.departement.to_dict()
|
||||||
e["formation_id"] = self.id # ScoDoc7 backward compat
|
e["formation_id"] = self.id # ScoDoc7 backward compat
|
||||||
if with_refcomp_attrs and self.referentiel_competence:
|
if with_refcomp_attrs and self.referentiel_competence:
|
||||||
@ -201,12 +205,17 @@ class Formation(db.Model):
|
|||||||
|
|
||||||
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:
|
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:
|
||||||
"""Les UEs d'un parcours de la formation.
|
"""Les UEs d'un parcours de la formation.
|
||||||
|
Si parcour est None, les UE sans parcours.
|
||||||
Exemple: pour avoir les UE du semestre 3, faire
|
Exemple: pour avoir les UE du semestre 3, faire
|
||||||
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
|
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
|
||||||
"""
|
"""
|
||||||
return UniteEns.query.filter_by(formation=self).filter(
|
if parcour is None:
|
||||||
|
return UniteEns.query.filter_by(
|
||||||
|
formation=self, type=UE_STANDARD, parcour_id=None
|
||||||
|
)
|
||||||
|
return UniteEns.query.filter_by(formation=self, type=UE_STANDARD).filter(
|
||||||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||||
UniteEns.type == UE_STANDARD,
|
(UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
|
||||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||||
ApcAnneeParcours.parcours_id == parcour.id,
|
ApcAnneeParcours.parcours_id == parcour.id,
|
||||||
@ -233,6 +242,21 @@ class Formation(db.Model):
|
|||||||
.filter(ApcAnneeParcours.parcours_id == parcour.id)
|
.filter(ApcAnneeParcours.parcours_id == parcour.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def refcomp_desassoc(self):
|
||||||
|
"""Désassocie la formation de son ref. de compétence"""
|
||||||
|
self.referentiel_competence = None
|
||||||
|
db.session.add(self)
|
||||||
|
# Niveaux des UE
|
||||||
|
for ue in self.ues:
|
||||||
|
ue.niveau_competence = None
|
||||||
|
db.session.add(ue)
|
||||||
|
# Parcours et AC des modules
|
||||||
|
for mod in self.modules:
|
||||||
|
mod.parcours = []
|
||||||
|
mod.app_critiques = []
|
||||||
|
db.session.add(mod)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
class Matiere(db.Model):
|
class Matiere(db.Model):
|
||||||
"""Matières: regroupe les modules d'une UE
|
"""Matières: regroupe les modules d'une UE
|
||||||
|
@ -1,48 +1,47 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
# pylint génère trop de faux positifs avec les colonnes date:
|
||||||
|
# pylint: disable=no-member,not-an-iterable
|
||||||
|
|
||||||
"""ScoDoc models: formsemestre
|
"""ScoDoc models: formsemestre
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from flask import flash, g
|
|
||||||
import flask_sqlalchemy
|
import flask_sqlalchemy
|
||||||
|
from flask import flash, g
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
from app import db
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
from app import db, log
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models import SHORT_STR_LEN
|
|
||||||
from app.models import CODE_STR_LEN
|
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
ApcAnneeParcours,
|
ApcAnneeParcours,
|
||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
ApcParcoursNiveauCompetence,
|
ApcParcoursNiveauCompetence,
|
||||||
ApcReferentielCompetences,
|
ApcReferentielCompetences,
|
||||||
|
parcours_formsemestre,
|
||||||
)
|
)
|
||||||
from app.models.groups import GroupDescr, Partition
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
from app.models.but_refcomp import parcours_formsemestre
|
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.modules import Module
|
from app.models.groups import GroupDescr, Partition
|
||||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||||
|
from app.models.modules import Module
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.validations import ScolarFormSemestreValidation
|
from app.models.validations import ScolarFormSemestreValidation
|
||||||
|
from app.scodoc import sco_codes_parcours, sco_preferences
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
||||||
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
|
|
||||||
|
|
||||||
class FormSemestre(db.Model):
|
class FormSemestre(db.Model):
|
||||||
@ -57,51 +56,58 @@ class FormSemestre(db.Model):
|
|||||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||||
titre = db.Column(db.Text())
|
titre = db.Column(db.Text(), nullable=False)
|
||||||
date_debut = db.Column(db.Date())
|
date_debut = db.Column(db.Date(), nullable=False)
|
||||||
date_fin = db.Column(db.Date())
|
date_fin = db.Column(db.Date(), nullable=False)
|
||||||
etat = db.Column(
|
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
||||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
"False si verrouillé"
|
||||||
) # False si verrouillé
|
|
||||||
modalite = db.Column(
|
modalite = db.Column(
|
||||||
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
|
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
|
||||||
) # "FI", "FAP", "FC", ...
|
)
|
||||||
# gestion compensation sem DUT:
|
"Modalité de formation: 'FI', 'FAP', 'FC', ..."
|
||||||
gestion_compensation = db.Column(
|
gestion_compensation = 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:
|
"gestion compensation sem DUT (inutilisé en APC)"
|
||||||
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"
|
||||||
)
|
)
|
||||||
# Bloque le calcul des moyennes (générale et d'UE)
|
"ne publie pas le bulletin XML ou JSON"
|
||||||
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"
|
||||||
)
|
)
|
||||||
# semestres decales (pour gestion jurys):
|
"Bloque le calcul des moyennes (générale et d'UE)"
|
||||||
|
block_moyenne_generale = db.Column(
|
||||||
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
|
)
|
||||||
|
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
|
||||||
gestion_semestrielle = db.Column(
|
gestion_semestrielle = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
# couleur fond bulletins HTML:
|
"Semestres décalés (pour gestion jurys DUT, pas implémenté ou utile en BUT)"
|
||||||
bul_bgcolor = db.Column(
|
bul_bgcolor = db.Column(
|
||||||
db.String(SHORT_STR_LEN), default="white", server_default="white"
|
db.String(SHORT_STR_LEN),
|
||||||
|
default="white",
|
||||||
|
server_default="white",
|
||||||
|
nullable=False,
|
||||||
)
|
)
|
||||||
# autorise resp. a modifier semestre:
|
"couleur fond bulletins HTML"
|
||||||
resp_can_edit = db.Column(
|
resp_can_edit = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
# autorise resp. a modifier slt les enseignants:
|
"autorise resp. à modifier le formsemestre"
|
||||||
resp_can_change_ens = db.Column(
|
resp_can_change_ens = db.Column(
|
||||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||||
)
|
)
|
||||||
# autorise les ens a creer des evals:
|
"autorise resp. a modifier slt les enseignants"
|
||||||
ens_can_edit_eval = db.Column(
|
ens_can_edit_eval = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="False"
|
db.Boolean(), nullable=False, default=False, server_default="False"
|
||||||
)
|
)
|
||||||
# code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'
|
"autorise les enseignants à créer des évals dans leurs modimpls"
|
||||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
||||||
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
|
"code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||||
elt_annee_apo = db.Column(db.Text())
|
elt_annee_apo = db.Column(db.Text())
|
||||||
|
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||||
|
|
||||||
# Relations:
|
# Relations:
|
||||||
etapes = db.relationship(
|
etapes = db.relationship(
|
||||||
@ -111,6 +117,7 @@ class FormSemestre(db.Model):
|
|||||||
"ModuleImpl",
|
"ModuleImpl",
|
||||||
backref="formsemestre",
|
backref="formsemestre",
|
||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
etuds = db.relationship(
|
etuds = db.relationship(
|
||||||
"Identite",
|
"Identite",
|
||||||
@ -148,7 +155,12 @@ class FormSemestre(db.Model):
|
|||||||
self.modalite = FormationModalite.DEFAULT_MODALITE
|
self.modalite = FormationModalite.DEFAULT_MODALITE
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
|
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
||||||
|
|
||||||
|
def sort_key(self) -> tuple:
|
||||||
|
"""clé pour tris par ordre alphabétique
|
||||||
|
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
||||||
|
return (self.date_debut, self.semestre_id)
|
||||||
|
|
||||||
def to_dict(self, convert_objects=False) -> dict:
|
def to_dict(self, convert_objects=False) -> dict:
|
||||||
"""dict (compatible ScoDoc7).
|
"""dict (compatible ScoDoc7).
|
||||||
@ -173,7 +185,7 @@ class FormSemestre(db.Model):
|
|||||||
d["responsables"] = [u.id for u in self.responsables]
|
d["responsables"] = [u.id for u in self.responsables]
|
||||||
d["titre_formation"] = self.titre_formation()
|
d["titre_formation"] = self.titre_formation()
|
||||||
if convert_objects:
|
if convert_objects:
|
||||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()]
|
||||||
d["departement"] = self.departement.to_dict()
|
d["departement"] = self.departement.to_dict()
|
||||||
d["formation"] = self.formation.to_dict()
|
d["formation"] = self.formation.to_dict()
|
||||||
d["etape_apo"] = self.etapes_apo_str()
|
d["etape_apo"] = self.etapes_apo_str()
|
||||||
@ -200,9 +212,10 @@ class FormSemestre(db.Model):
|
|||||||
d["etape_apo"] = self.etapes_apo_str()
|
d["etape_apo"] = self.etapes_apo_str()
|
||||||
d["formsemestre_id"] = self.id
|
d["formsemestre_id"] = self.id
|
||||||
d["formation"] = self.formation.to_dict()
|
d["formation"] = self.formation.to_dict()
|
||||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()]
|
||||||
d["responsables"] = [u.id for u in self.responsables]
|
d["responsables"] = [u.id for u in self.responsables]
|
||||||
d["titre_court"] = self.formation.acronyme
|
d["titre_court"] = self.formation.acronyme
|
||||||
|
d["titre_formation"] = self.titre_formation()
|
||||||
d["titre_num"] = self.titre_num()
|
d["titre_num"] = self.titre_num()
|
||||||
d["session_id"] = self.session_id()
|
d["session_id"] = self.session_id()
|
||||||
return d
|
return d
|
||||||
@ -222,7 +235,8 @@ class FormSemestre(db.Model):
|
|||||||
d["mois_debut_ord"] = self.date_debut.month
|
d["mois_debut_ord"] = self.date_debut.month
|
||||||
d["mois_fin_ord"] = self.date_fin.month
|
d["mois_fin_ord"] = self.date_fin.month
|
||||||
# La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre
|
# La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre
|
||||||
# devrait sans doute pouvoir etre changé...
|
# devrait sans doute pouvoir etre changé... XXX PIVOT
|
||||||
|
d["periode"] = self.periode()
|
||||||
if self.date_debut.month >= 8 and self.date_debut.month <= 10:
|
if self.date_debut.month >= 8 and self.date_debut.month <= 10:
|
||||||
d["periode"] = 1 # typiquement, début en septembre: S1, S3...
|
d["periode"] = 1 # typiquement, début en septembre: S1, S3...
|
||||||
else:
|
else:
|
||||||
@ -241,17 +255,36 @@ class FormSemestre(db.Model):
|
|||||||
d["etapes_apo_str"] = self.etapes_apo_str()
|
d["etapes_apo_str"] = self.etapes_apo_str()
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def get_parcours_apc(self) -> list[ApcParcours]:
|
||||||
|
"""Liste des parcours proposés par ce semestre.
|
||||||
|
Si aucun n'est coché et qu'il y a un référentiel, tous ceux du référentiel.
|
||||||
|
"""
|
||||||
|
r = self.parcours or (
|
||||||
|
self.formation.referentiel_competence
|
||||||
|
and self.formation.referentiel_competence.parcours
|
||||||
|
)
|
||||||
|
return r or []
|
||||||
|
|
||||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||||
"""UE des modules de ce semestre, triées par numéro.
|
"""UE des modules de ce semestre, triées par numéro.
|
||||||
- Formations classiques: les UEs auxquelles appartiennent
|
- Formations classiques: les UEs auxquelles appartiennent
|
||||||
les modules mis en place dans ce semestre.
|
les modules mis en place dans ce semestre.
|
||||||
- Formations APC / BUT: les UEs de la formation qui ont
|
- Formations APC / BUT: les UEs de la formation qui
|
||||||
le même numéro de semestre que ce formsemestre.
|
- ont le même numéro de semestre que ce formsemestre
|
||||||
|
- sont associées à l'un des parcours de ce formsemestre (ou à aucun)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.formation.get_parcours().APC_SAE:
|
if self.formation.get_parcours().APC_SAE:
|
||||||
sem_ues = UniteEns.query.filter_by(
|
sem_ues = UniteEns.query.filter_by(
|
||||||
formation=self.formation, semestre_idx=self.semestre_id
|
formation=self.formation, semestre_idx=self.semestre_id
|
||||||
)
|
)
|
||||||
|
if self.parcours:
|
||||||
|
# Prend toutes les UE de l'un des parcours du sem., ou déclarées sans parcours
|
||||||
|
sem_ues = sem_ues.filter(
|
||||||
|
(UniteEns.parcour == None)
|
||||||
|
| (UniteEns.parcour_id.in_([p.id for p in self.parcours]))
|
||||||
|
)
|
||||||
|
# si le sem. ne coche aucun parcours, prend toutes les UE
|
||||||
else:
|
else:
|
||||||
sem_ues = db.session.query(UniteEns).filter(
|
sem_ues = db.session.query(UniteEns).filter(
|
||||||
ModuleImpl.formsemestre_id == self.id,
|
ModuleImpl.formsemestre_id == self.id,
|
||||||
@ -263,8 +296,11 @@ class FormSemestre(db.Model):
|
|||||||
return sem_ues.order_by(UniteEns.numero)
|
return sem_ues.order_by(UniteEns.numero)
|
||||||
|
|
||||||
def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery:
|
def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery:
|
||||||
"""UE que suit l'étudiant dans ce semestre BUT
|
"""XXX inutilisé à part pour un test unitaire => supprimer ?
|
||||||
|
UEs que suit l'étudiant dans ce semestre BUT
|
||||||
en fonction du parcours dans lequel il est inscrit.
|
en fonction du parcours dans lequel il est inscrit.
|
||||||
|
Si l'étudiant n'est inscrit à aucun parcours,
|
||||||
|
renvoie uniquement les UEs de tronc commun (sans parcours).
|
||||||
|
|
||||||
Si voulez les UE d'un parcours, il est plus efficace de passer par
|
Si voulez les UE d'un parcours, il est plus efficace de passer par
|
||||||
`formation.query_ues_parcour(parcour)`.
|
`formation.query_ues_parcour(parcour)`.
|
||||||
@ -275,7 +311,13 @@ class FormSemestre(db.Model):
|
|||||||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||||
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
|
or_(
|
||||||
|
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
|
||||||
|
and_(
|
||||||
|
FormSemestreInscription.parcour_id.is_(None),
|
||||||
|
UniteEns.parcour_id.is_(None),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@ -288,7 +330,7 @@ class FormSemestre(db.Model):
|
|||||||
if self.formation.is_apc():
|
if self.formation.is_apc():
|
||||||
modimpls.sort(
|
modimpls.sort(
|
||||||
key=lambda m: (
|
key=lambda m: (
|
||||||
m.module.module_type or 0,
|
m.module.module_type or 0, # ressources (2) avant SAEs (3)
|
||||||
m.module.numero or 0,
|
m.module.numero or 0,
|
||||||
m.module.code or 0,
|
m.module.code or 0,
|
||||||
)
|
)
|
||||||
@ -327,7 +369,7 @@ class FormSemestre(db.Model):
|
|||||||
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
|
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""Vrai si user peut modifier ce semestre"""
|
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
|
||||||
if not user.has_permission(Permission.ScoImplement): # pas chef
|
if not user.has_permission(Permission.ScoImplement): # pas chef
|
||||||
if not self.resp_can_edit or user.id not in [
|
if not self.resp_can_edit or user.id not in [
|
||||||
resp.id for resp in self.responsables
|
resp.id for resp in self.responsables
|
||||||
@ -341,7 +383,7 @@ class FormSemestre(db.Model):
|
|||||||
(les dates de début et fin sont incluses)
|
(les dates de début et fin sont incluses)
|
||||||
"""
|
"""
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
return (self.date_debut <= today) and (today <= self.date_fin)
|
return self.date_debut <= today <= self.date_fin
|
||||||
|
|
||||||
def contient_periode(self, date_debut, date_fin) -> bool:
|
def contient_periode(self, date_debut, date_fin) -> bool:
|
||||||
"""Vrai si l'intervalle [date_debut, date_fin] est
|
"""Vrai si l'intervalle [date_debut, date_fin] est
|
||||||
@ -354,29 +396,99 @@ class FormSemestre(db.Model):
|
|||||||
"""Test si sem est entièrement sur la même année scolaire.
|
"""Test si sem est entièrement sur la même année scolaire.
|
||||||
(ce n'est pas obligatoire mais si ce n'est pas le
|
(ce n'est pas obligatoire mais si ce n'est pas le
|
||||||
cas les exports Apogée risquent de mal fonctionner)
|
cas les exports Apogée risquent de mal fonctionner)
|
||||||
Pivot au 1er août.
|
Pivot au 1er août par défaut.
|
||||||
"""
|
"""
|
||||||
if self.date_debut > self.date_fin:
|
if self.date_debut > self.date_fin:
|
||||||
|
flash(f"Dates début/fin inversées pour le semestre {self.titre_annee()}")
|
||||||
log(f"Warning: semestre {self.id} begins after ending !")
|
log(f"Warning: semestre {self.id} begins after ending !")
|
||||||
annee_debut = self.date_debut.year
|
annee_debut = self.date_debut.year
|
||||||
if self.date_debut.month < 8: # août
|
month_debut_annee = ScoDocSiteConfig.get_month_debut_annee_scolaire()
|
||||||
# considere que debut sur l'anne scolaire precedente
|
if self.date_debut.month < month_debut_annee:
|
||||||
|
# début sur l'année scolaire précédente (juillet inclus par défaut)
|
||||||
annee_debut -= 1
|
annee_debut -= 1
|
||||||
annee_fin = self.date_fin.year
|
annee_fin = self.date_fin.year
|
||||||
if self.date_fin.month < 9:
|
if self.date_fin.month < (month_debut_annee + 1):
|
||||||
# 9 (sept) pour autoriser un début en sept et une fin en aout
|
# 9 (sept) pour autoriser un début en sept et une fin en août
|
||||||
annee_fin -= 1
|
annee_fin -= 1
|
||||||
return annee_debut == annee_fin
|
return annee_debut == annee_fin
|
||||||
|
|
||||||
def est_decale(self):
|
def est_decale(self):
|
||||||
"""Vrai si semestre "décalé"
|
"""Vrai si semestre "décalé"
|
||||||
c'est à dire semestres impairs commençant entre janvier et juin
|
c'est à dire semestres impairs commençant (par défaut)
|
||||||
et les pairs entre juillet et decembre
|
entre janvier et juin et les pairs entre juillet et décembre.
|
||||||
"""
|
"""
|
||||||
if self.semestre_id <= 0:
|
if self.semestre_id <= 0:
|
||||||
return False # formations sans semestres
|
return False # formations sans semestres
|
||||||
return (self.semestre_id % 2 and self.date_debut.month <= 6) or (
|
return (
|
||||||
not self.semestre_id % 2 and self.date_debut.month > 6
|
# impair
|
||||||
|
(
|
||||||
|
self.semestre_id % 2
|
||||||
|
and self.date_debut.month < scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||||
|
)
|
||||||
|
or
|
||||||
|
# pair
|
||||||
|
(
|
||||||
|
(not self.semestre_id % 2)
|
||||||
|
and self.date_debut.month >= scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def comp_periode(
|
||||||
|
cls,
|
||||||
|
date_debut: datetime,
|
||||||
|
mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
|
||||||
|
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||||
|
jour_pivot_annee=1,
|
||||||
|
jour_pivot_periode=1,
|
||||||
|
):
|
||||||
|
"""Calcule la session associée à un formsemestre commençant en date_debut
|
||||||
|
sous la forme (année, période)
|
||||||
|
année: première année de l'année scolaire
|
||||||
|
période = 1 (première période de l'année scolaire, souvent automne)
|
||||||
|
ou 2 (deuxième période de l'année scolaire, souvent printemps)
|
||||||
|
Les quatre derniers paramètres forment les dates pivots pour l'année
|
||||||
|
(1er août par défaut) et pour la période (1er décembre par défaut).
|
||||||
|
|
||||||
|
Les calculs se font à partir de la date de début indiquée.
|
||||||
|
Exemples dans tests/unit/test_periode
|
||||||
|
|
||||||
|
Implémentation:
|
||||||
|
Cas à considérer pour le calcul de la période
|
||||||
|
|
||||||
|
pa < pp -----------------|-------------------|---------------->
|
||||||
|
(A-1, P:2) pa (A, P:1) pp (A, P:2)
|
||||||
|
pp < pa -----------------|-------------------|---------------->
|
||||||
|
(A-1, P:1) pp (A-1, P:2) pa (A, P:1)
|
||||||
|
"""
|
||||||
|
pivot_annee = 100 * mois_pivot_annee + jour_pivot_annee
|
||||||
|
pivot_periode = 100 * mois_pivot_periode + jour_pivot_periode
|
||||||
|
pivot_sem = 100 * date_debut.month + date_debut.day
|
||||||
|
if pivot_sem < pivot_annee:
|
||||||
|
annee = date_debut.year - 1
|
||||||
|
else:
|
||||||
|
annee = date_debut.year
|
||||||
|
if pivot_annee < pivot_periode:
|
||||||
|
if pivot_sem < pivot_annee or pivot_sem >= pivot_periode:
|
||||||
|
periode = 2
|
||||||
|
else:
|
||||||
|
periode = 1
|
||||||
|
else:
|
||||||
|
if pivot_sem < pivot_periode or pivot_sem >= pivot_annee:
|
||||||
|
periode = 1
|
||||||
|
else:
|
||||||
|
periode = 2
|
||||||
|
return annee, periode
|
||||||
|
|
||||||
|
def periode(self) -> int:
|
||||||
|
"""La période:
|
||||||
|
* 1 : première période: automne à Paris
|
||||||
|
* 2 : deuxième période, printemps à Paris
|
||||||
|
"""
|
||||||
|
return FormSemestre.comp_periode(
|
||||||
|
self.date_debut,
|
||||||
|
mois_pivot_annee=ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||||
|
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
||||||
@ -429,7 +541,7 @@ class FormSemestre(db.Model):
|
|||||||
|
|
||||||
def annee_scolaire(self) -> int:
|
def annee_scolaire(self) -> int:
|
||||||
"""L'année de début de l'année scolaire.
|
"""L'année de début de l'année scolaire.
|
||||||
Par exemple, 2022 si le semestre va de septebre 2022 à février 2023."""
|
Par exemple, 2022 si le semestre va de septembre 2022 à février 2023."""
|
||||||
return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
||||||
|
|
||||||
def annee_scolaire_str(self):
|
def annee_scolaire_str(self):
|
||||||
@ -479,7 +591,9 @@ class FormSemestre(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def titre_annee(self) -> str:
|
def titre_annee(self) -> str:
|
||||||
""" """
|
"""Le titre avec l'année
|
||||||
|
'DUT Réseaux et Télécommunications semestre 3 FAP 2020-2021'
|
||||||
|
"""
|
||||||
titre_annee = (
|
titre_annee = (
|
||||||
f"{self.titre_num()} {self.modalite or ''} {self.date_debut.year}"
|
f"{self.titre_num()} {self.modalite or ''} {self.date_debut.year}"
|
||||||
)
|
)
|
||||||
@ -585,14 +699,43 @@ class FormSemestre(db.Model):
|
|||||||
db.session.add(partition)
|
db.session.add(partition)
|
||||||
db.session.flush() # pour avoir un id
|
db.session.flush() # pour avoir un id
|
||||||
flash("Partition Parcours créée.")
|
flash("Partition Parcours créée.")
|
||||||
|
elif partition.groups_editable:
|
||||||
|
# Il ne faut jamais laisser éditer cette partition de parcours
|
||||||
|
partition.groups_editable = False
|
||||||
|
db.session.add(partition)
|
||||||
|
|
||||||
for parcour in self.parcours:
|
for parcour in self.get_parcours_apc():
|
||||||
if parcour.code:
|
if parcour.code:
|
||||||
group = GroupDescr.query.filter_by(
|
group = GroupDescr.query.filter_by(
|
||||||
partition_id=partition.id, group_name=parcour.code
|
partition_id=partition.id, group_name=parcour.code
|
||||||
).first()
|
).first()
|
||||||
if not group:
|
if not group:
|
||||||
partition.groups.append(GroupDescr(group_name=parcour.code))
|
partition.groups.append(GroupDescr(group_name=parcour.code))
|
||||||
|
db.session.flush()
|
||||||
|
# S'il reste des groupes de parcours qui ne sont plus dans le semestre
|
||||||
|
# - s'ils n'ont pas d'inscrits, supprime-les.
|
||||||
|
# - s'ils ont des inscrits: avertissement
|
||||||
|
for group in GroupDescr.query.filter_by(partition_id=partition.id):
|
||||||
|
if group.group_name not in (p.code for p in self.get_parcours_apc()):
|
||||||
|
if (
|
||||||
|
len(
|
||||||
|
[
|
||||||
|
inscr
|
||||||
|
for inscr in self.inscriptions
|
||||||
|
if (inscr.parcour is not None)
|
||||||
|
and inscr.parcour.code == group.group_name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
):
|
||||||
|
flash(f"Suppression du groupe de parcours vide {group.group_name}")
|
||||||
|
db.session.delete(group)
|
||||||
|
else:
|
||||||
|
flash(
|
||||||
|
f"""Attention: groupe de parcours {group.group_name} non vide:
|
||||||
|
réaffectez ses étudiants dans des parcours du semestre"""
|
||||||
|
)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def update_inscriptions_parcours_from_groups(self) -> None:
|
def update_inscriptions_parcours_from_groups(self) -> None:
|
||||||
@ -653,7 +796,7 @@ class FormSemestre(db.Model):
|
|||||||
|
|
||||||
def etud_validations_description_html(self, etudid: int) -> str:
|
def etud_validations_description_html(self, etudid: int) -> str:
|
||||||
"""Description textuelle des validations de jury de cet étudiant dans ce semestre"""
|
"""Description textuelle des validations de jury de cet étudiant dans ce semestre"""
|
||||||
from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
|
|
||||||
vals_sem = ScolarFormSemestreValidation.query.filter_by(
|
vals_sem = ScolarFormSemestreValidation.query.filter_by(
|
||||||
etudid=etudid, formsemestre_id=self.id, ue_id=None
|
etudid=etudid, formsemestre_id=self.id, ue_id=None
|
||||||
@ -914,8 +1057,8 @@ class NotesSemSet(db.Model):
|
|||||||
|
|
||||||
title = db.Column(db.Text)
|
title = db.Column(db.Text)
|
||||||
annee_scolaire = db.Column(db.Integer, nullable=True, default=None)
|
annee_scolaire = db.Column(db.Integer, nullable=True, default=None)
|
||||||
# periode: 0 (année), 1 (Simpair), 2 (Spair)
|
sem_id = db.Column(db.Integer, nullable=False, default=0)
|
||||||
sem_id = db.Column(db.Integer, nullable=True, default=None)
|
"période: 0 (année), 1 (Simpair), 2 (Spair)"
|
||||||
|
|
||||||
|
|
||||||
# Association: many to many
|
# Association: many to many
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
@ -87,6 +87,7 @@ class Partition(db.Model):
|
|||||||
def to_dict(self, with_groups=False) -> dict:
|
def to_dict(self, with_groups=False) -> dict:
|
||||||
"""as a dict, with or without groups"""
|
"""as a dict, with or without groups"""
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
|
d["partition_id"] = self.id
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
d.pop("formsemestre", None)
|
d.pop("formsemestre", None)
|
||||||
|
|
||||||
|
@ -20,14 +20,12 @@ class ModuleImpl(db.Model):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
moduleimpl_id = db.synonym("id")
|
moduleimpl_id = db.synonym("id")
|
||||||
module_id = db.Column(
|
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
|
||||||
db.Integer,
|
|
||||||
db.ForeignKey("notes_modules.id"),
|
|
||||||
)
|
|
||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("notes_formsemestre.id"),
|
db.ForeignKey("notes_formsemestre.id"),
|
||||||
index=True,
|
index=True,
|
||||||
|
nullable=False,
|
||||||
)
|
)
|
||||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||||
# formule de calcul moyenne:
|
# formule de calcul moyenne:
|
||||||
@ -62,7 +60,7 @@ class ModuleImpl(db.Model):
|
|||||||
"""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) -> bool:
|
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
||||||
"""true si les poids des évaluations du module permettent de satisfaire
|
"""true si les poids des évaluations du module permettent de satisfaire
|
||||||
les coefficients du PN.
|
les coefficients du PN.
|
||||||
"""
|
"""
|
||||||
@ -76,7 +74,7 @@ class ModuleImpl(db.Model):
|
|||||||
return moy_mod.moduleimpl_is_conforme(
|
return moy_mod.moduleimpl_is_conforme(
|
||||||
self,
|
self,
|
||||||
self.get_evaluations_poids(),
|
self.get_evaluations_poids(),
|
||||||
self.module.formation.get_module_coefs(self.module.semestre_id),
|
res.modimpl_coefs_df,
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self, convert_objects=False, with_module=True):
|
def to_dict(self, convert_objects=False, with_module=True):
|
||||||
@ -101,6 +99,22 @@ class ModuleImpl(db.Model):
|
|||||||
d.pop("module", None)
|
d.pop("module", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def est_inscrit(self, etud: Identite) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||||
|
|
||||||
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_module: int = (
|
||||||
|
ModuleImplInscription.query.filter_by(
|
||||||
|
etudid=etud.id, moduleimpl_id=self.id
|
||||||
|
).count()
|
||||||
|
> 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return is_module
|
||||||
|
|
||||||
|
|
||||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||||
notes_modules_enseignants = db.Table(
|
notes_modules_enseignants = db.Table(
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models.but_refcomp import app_critiques_modules, parcours_modules
|
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
@ -37,7 +37,9 @@ class Module(db.Model):
|
|||||||
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
||||||
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
|
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
|
||||||
# Relations:
|
# Relations:
|
||||||
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
modimpls = db.relationship(
|
||||||
|
"ModuleImpl", backref="module", lazy="dynamic", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
||||||
tags = db.relationship(
|
tags = db.relationship(
|
||||||
"NotesTag",
|
"NotesTag",
|
||||||
@ -66,7 +68,39 @@ class Module(db.Model):
|
|||||||
super(Module, self).__init__(**kwargs)
|
super(Module, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"
|
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
|
||||||
|
} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
"""Create a new copy of this module."""
|
||||||
|
mod = Module(
|
||||||
|
titre=self.titre,
|
||||||
|
abbrev=self.abbrev,
|
||||||
|
code=self.code + "-copie",
|
||||||
|
heures_cours=self.heures_cours,
|
||||||
|
heures_td=self.heures_td,
|
||||||
|
heures_tp=self.heures_tp,
|
||||||
|
coefficient=self.coefficient,
|
||||||
|
ects=self.ects,
|
||||||
|
ue_id=self.ue_id,
|
||||||
|
matiere_id=self.matiere_id,
|
||||||
|
formation_id=self.formation_id,
|
||||||
|
semestre_id=self.semestre_id,
|
||||||
|
numero=self.numero, # il est conseillé de renuméroter
|
||||||
|
code_apogee="", # volontairement vide pour éviter les erreurs
|
||||||
|
module_type=self.module_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Les tags:
|
||||||
|
for tag in self.tags:
|
||||||
|
mod.tags.append(tag)
|
||||||
|
# Les parcours
|
||||||
|
for parcour in self.parcours:
|
||||||
|
mod.parcours.append(parcour)
|
||||||
|
# Les AC
|
||||||
|
for app_critique in self.app_critiques:
|
||||||
|
mod.app_critiques.append(app_critique)
|
||||||
|
return mod
|
||||||
|
|
||||||
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
|
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
|
||||||
"""If convert_objects, convert all attributes to native types
|
"""If convert_objects, convert all attributes to native types
|
||||||
@ -188,25 +222,31 @@ class Module(db.Model):
|
|||||||
# à redéfinir les relationships...
|
# à redéfinir les relationships...
|
||||||
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
||||||
|
|
||||||
def ue_coefs_list(self, include_zeros=True):
|
def ue_coefs_list(
|
||||||
|
self, include_zeros=True, ues: list["UniteEns"] = None
|
||||||
|
) -> list[tuple["UniteEns", float]]:
|
||||||
"""Liste des coefs vers les UE (pour les modules APC).
|
"""Liste des coefs vers les UE (pour les modules APC).
|
||||||
Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
|
Si ues est spécifié, restreint aux UE indiquées.
|
||||||
|
Sinon si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
|
||||||
sauf UE bonus sport.
|
sauf UE bonus sport.
|
||||||
Result: List of tuples [ (ue, coef) ]
|
Result: List of tuples [ (ue, coef) ]
|
||||||
"""
|
"""
|
||||||
if not self.is_apc():
|
if not self.is_apc():
|
||||||
return []
|
return []
|
||||||
if include_zeros:
|
if include_zeros and ues is None:
|
||||||
# Toutes les UE du même semestre:
|
# Toutes les UE du même semestre:
|
||||||
ues_semestre = (
|
ues = (
|
||||||
self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx)
|
self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx)
|
||||||
.filter(UniteEns.type != UE_SPORT)
|
.filter(UniteEns.type != UE_SPORT)
|
||||||
.order_by(UniteEns.numero)
|
.order_by(UniteEns.numero)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
if not ues:
|
||||||
|
return []
|
||||||
|
if ues:
|
||||||
coefs_dict = self.get_ue_coef_dict()
|
coefs_dict = self.get_ue_coef_dict()
|
||||||
coefs_list = []
|
coefs_list = []
|
||||||
for ue in ues_semestre:
|
for ue in ues:
|
||||||
coefs_list.append((ue, coefs_dict.get(ue.id, 0.0)))
|
coefs_list.append((ue, coefs_dict.get(ue.id, 0.0)))
|
||||||
return coefs_list
|
return coefs_list
|
||||||
# Liste seulement les coefs définis:
|
# Liste seulement les coefs définis:
|
||||||
@ -218,6 +258,19 @@ class Module(db.Model):
|
|||||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
def get_parcours(self) -> list[ApcParcours]:
|
||||||
|
"""Les parcours utilisant ce module.
|
||||||
|
Si tous les parcours, liste vide (!).
|
||||||
|
"""
|
||||||
|
ref_comp = self.formation.referentiel_competence
|
||||||
|
if not ref_comp:
|
||||||
|
return []
|
||||||
|
tous_parcours_ids = {p.id for p in ref_comp.parcours}
|
||||||
|
parcours_ids = {p.id for p in self.parcours}
|
||||||
|
if tous_parcours_ids == parcours_ids:
|
||||||
|
return []
|
||||||
|
return self.parcours
|
||||||
|
|
||||||
|
|
||||||
class ModuleUECoef(db.Model):
|
class ModuleUECoef(db.Model):
|
||||||
"""Coefficients des modules vers les UE (APC, BUT)
|
"""Coefficients des modules vers les UE (APC, BUT)
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app import db
|
import pandas as pd
|
||||||
|
|
||||||
|
from app import db, log
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
|
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
||||||
|
from app.models.modules import Module
|
||||||
|
from app.scodoc.sco_exceptions import ScoFormationConflict
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -49,9 +54,19 @@ class UniteEns(db.Model):
|
|||||||
niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"))
|
niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"))
|
||||||
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
|
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
|
||||||
|
|
||||||
|
# Une ue appartient soit à tous les parcours (tronc commun), soit à un seul:
|
||||||
|
parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True)
|
||||||
|
parcour = db.relationship("ApcParcours", back_populates="ues")
|
||||||
|
|
||||||
# relations
|
# relations
|
||||||
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
||||||
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
||||||
|
dispense_ues = db.relationship(
|
||||||
|
"DispenseUE",
|
||||||
|
back_populates="ue",
|
||||||
|
cascade="all, delete",
|
||||||
|
passive_deletes=True,
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
|
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
|
||||||
@ -59,6 +74,28 @@ class UniteEns(db.Model):
|
|||||||
self.semestre_idx} {
|
self.semestre_idx} {
|
||||||
'EXTERNE' if self.is_external else ''})>"""
|
'EXTERNE' if self.is_external else ''})>"""
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
"""Create a new copy of this ue.
|
||||||
|
Ne copie pas le code, ni le code Apogée, ni les liens au réf. de comp.
|
||||||
|
(parcours et niveau).
|
||||||
|
"""
|
||||||
|
ue = UniteEns(
|
||||||
|
formation_id=self.formation_id,
|
||||||
|
acronyme=self.acronyme + "-copie",
|
||||||
|
numero=self.numero,
|
||||||
|
titre=self.titre,
|
||||||
|
semestre_idx=self.semestre_idx,
|
||||||
|
type=self.type,
|
||||||
|
ue_code="", # ne duplique pas le code
|
||||||
|
ects=self.ects,
|
||||||
|
is_external=self.is_external,
|
||||||
|
code_apogee="", # ne copie pas les codes Apo
|
||||||
|
coefficient=self.coefficient,
|
||||||
|
coef_rcue=self.coef_rcue,
|
||||||
|
color=self.color,
|
||||||
|
)
|
||||||
|
return ue
|
||||||
|
|
||||||
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
||||||
"""as a dict, with the same conversions as in ScoDoc7
|
"""as a dict, with the same conversions as in ScoDoc7
|
||||||
(except ECTS: keep None)
|
(except ECTS: keep None)
|
||||||
@ -74,6 +111,7 @@ class UniteEns(db.Model):
|
|||||||
e["ects"] = e["ects"]
|
e["ects"] = e["ects"]
|
||||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||||
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
||||||
|
e["parcour"] = self.parcour.to_dict() if self.parcour else None
|
||||||
if with_module_ue_coefs:
|
if with_module_ue_coefs:
|
||||||
if convert_objects:
|
if convert_objects:
|
||||||
e["module_ue_coefs"] = [
|
e["module_ue_coefs"] = [
|
||||||
@ -83,6 +121,12 @@ class UniteEns(db.Model):
|
|||||||
e.pop("module_ue_coefs", None)
|
e.pop("module_ue_coefs", None)
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
def annee(self) -> int:
|
||||||
|
"""L'année dans la formation (commence à 1).
|
||||||
|
En APC seulement, en classic renvoie toujours 1.
|
||||||
|
"""
|
||||||
|
return 1 if self.semestre_idx is None else (self.semestre_idx - 1) // 2 + 1
|
||||||
|
|
||||||
def is_locked(self):
|
def is_locked(self):
|
||||||
"""True if UE should not be modified
|
"""True if UE should not be modified
|
||||||
(contains modules used in a locked formsemestre)
|
(contains modules used in a locked formsemestre)
|
||||||
@ -135,3 +179,137 @@ class UniteEns(db.Model):
|
|||||||
if self.code_apogee:
|
if self.code_apogee:
|
||||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
def _check_apc_conflict(self, new_niveau_id: int, new_parcour_id: int):
|
||||||
|
"raises ScoFormationConflict si (niveau, parcours) pas unique dans ce semestre"
|
||||||
|
# Les UE du même semestre que nous:
|
||||||
|
ues_sem = self.formation.ues.filter_by(semestre_idx=self.semestre_idx)
|
||||||
|
if (new_niveau_id, new_parcour_id) in (
|
||||||
|
(oue.niveau_competence_id, oue.parcour_id)
|
||||||
|
for oue in ues_sem
|
||||||
|
if oue.id != self.id
|
||||||
|
):
|
||||||
|
log(
|
||||||
|
f"set_ue_niveau_competence: {self}: ({new_niveau_id}, {new_parcour_id}) déjà associé"
|
||||||
|
)
|
||||||
|
raise ScoFormationConflict()
|
||||||
|
|
||||||
|
def set_niveau_competence(self, niveau: ApcNiveau):
|
||||||
|
"""Associe cette UE au niveau de compétence indiqué.
|
||||||
|
Le niveau doit être dans le parcours de l'UE, s'il y en a un.
|
||||||
|
Assure que ce soit la seule dans son parcours.
|
||||||
|
Sinon, raises ScoFormationConflict.
|
||||||
|
|
||||||
|
Si niveau est None, désassocie.
|
||||||
|
"""
|
||||||
|
if niveau is not None:
|
||||||
|
self._check_apc_conflict(niveau.id, self.parcour_id)
|
||||||
|
# Le niveau est-il dans le parcours ? Sinon, erreur
|
||||||
|
if self.parcour and niveau.id not in (
|
||||||
|
n.id
|
||||||
|
for n in niveau.niveaux_annee_de_parcours(
|
||||||
|
self.parcour, self.annee(), self.formation.referentiel_competence
|
||||||
|
)
|
||||||
|
):
|
||||||
|
log(
|
||||||
|
f"set_niveau_competence: niveau {niveau} hors parcours {self.parcour}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.niveau_competence = niveau
|
||||||
|
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
# Invalidation du cache
|
||||||
|
self.formation.invalidate_cached_sems()
|
||||||
|
log(f"ue.set_niveau_competence( {self}, {niveau} )")
|
||||||
|
|
||||||
|
def set_parcour(self, parcour: ApcParcours):
|
||||||
|
"""Associe cette UE au parcours indiqué.
|
||||||
|
Assure que ce soit la seule dans son parcours.
|
||||||
|
Sinon, raises ScoFormationConflict.
|
||||||
|
|
||||||
|
Si niveau est None, désassocie.
|
||||||
|
"""
|
||||||
|
if (parcour is not None) and self.niveau_competence is not None:
|
||||||
|
self._check_apc_conflict(self.niveau_competence.id, parcour.id)
|
||||||
|
self.parcour = parcour
|
||||||
|
# Le niveau est-il dans ce parcours ? Sinon, l'enlève
|
||||||
|
if (
|
||||||
|
parcour
|
||||||
|
and self.niveau_competence
|
||||||
|
and self.niveau_competence.id
|
||||||
|
not in (
|
||||||
|
n.id
|
||||||
|
for n in self.niveau_competence.niveaux_annee_de_parcours(
|
||||||
|
parcour, self.annee(), self.formation.referentiel_competence
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self.niveau_competence = None
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
# Invalidation du cache
|
||||||
|
self.formation.invalidate_cached_sems()
|
||||||
|
log(f"ue.set_parcour( {self}, {parcour} )")
|
||||||
|
|
||||||
|
|
||||||
|
class DispenseUE(db.Model):
|
||||||
|
"""Dispense d'UE
|
||||||
|
Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
|
||||||
|
qu'ils ne refont pas.
|
||||||
|
La dispense d'UE n'est PAS une validation:
|
||||||
|
- elle n'est pas affectée par les décisions de jury (pas effacée)
|
||||||
|
- elle est associée à un formsemestre
|
||||||
|
- elle ne permet pas la délivrance d'ECTS ou du diplôme.
|
||||||
|
|
||||||
|
On utilise cette dispense et non une "inscription" par souci d'efficacité:
|
||||||
|
en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours,
|
||||||
|
la dispense étant une exception.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),)
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
formsemestre_id = formsemestre_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||||
|
)
|
||||||
|
ue_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
ue = db.relationship("UniteEns", back_populates="dispense_ues")
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
etud = db.relationship("Identite", back_populates="dispense_ues")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"""<{self.__class__.__name__} {self.id} etud={
|
||||||
|
repr(self.etud)} ue={repr(self.ue)}>"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_formsemestre_dispense_ues_set(
|
||||||
|
cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns]
|
||||||
|
) -> set[tuple[int, int]]:
|
||||||
|
"""Construit l'ensemble des
|
||||||
|
etudids = modimpl_inscr_df.index, # les etudids
|
||||||
|
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
|
||||||
|
|
||||||
|
Résultat: set de (etudid, ue_id).
|
||||||
|
"""
|
||||||
|
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
|
||||||
|
# puis filtre sur inscrits et ues
|
||||||
|
ue_ids = {ue.id for ue in ues}
|
||||||
|
dispense_ues = {
|
||||||
|
(dispense_ue.etudid, dispense_ue.ue_id)
|
||||||
|
for dispense_ue in DispenseUE.query.filter_by(
|
||||||
|
formsemestre_id=formsemestre.id
|
||||||
|
)
|
||||||
|
if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids
|
||||||
|
}
|
||||||
|
return dispense_ues
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app import log
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
from app.models.events import Scolog
|
from app.models.events import Scolog
|
||||||
@ -58,7 +59,7 @@ class ScolarFormSemestreValidation(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.ue_id:
|
if self.ue_id:
|
||||||
@ -93,6 +94,10 @@ class ScolarAutorisationInscription(db.Model):
|
|||||||
db.ForeignKey("notes_formsemestre.id"),
|
db.ForeignKey("notes_formsemestre.id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
||||||
|
self.etudid}, semestre_id={self.semestre_id})"""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"as a dict"
|
"as a dict"
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
@ -116,7 +121,10 @@ class ScolarAutorisationInscription(db.Model):
|
|||||||
semestre_id=semestre_id,
|
semestre_id=semestre_id,
|
||||||
)
|
)
|
||||||
db.session.add(autorisation)
|
db.session.add(autorisation)
|
||||||
Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}")
|
Scolog.logdb(
|
||||||
|
"autorise_etud", etudid=etudid, msg=f"Passage vers S{semestre_id}: autorisé"
|
||||||
|
)
|
||||||
|
log(f"ScolarAutorisationInscription: recording {autorisation}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete_autorisation_etud(
|
def delete_autorisation_etud(
|
||||||
@ -130,10 +138,11 @@ class ScolarAutorisationInscription(db.Model):
|
|||||||
)
|
)
|
||||||
for autorisation in autorisations:
|
for autorisation in autorisations:
|
||||||
db.session.delete(autorisation)
|
db.session.delete(autorisation)
|
||||||
|
log(f"ScolarAutorisationInscription: deleting {autorisation}")
|
||||||
Scolog.logdb(
|
Scolog.logdb(
|
||||||
"autorise_etud",
|
"autorise_etud",
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
msg=f"annule passage vers S{autorisation.semestre_id}",
|
msg=f"Passage vers S{autorisation.semestre_id}: effacé",
|
||||||
)
|
)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -459,8 +459,7 @@ class JuryPE(object):
|
|||||||
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
||||||
(_, parcours) = sco_report.get_codeparcoursetud(etud)
|
(_, parcours) = sco_report.get_codeparcoursetud(etud)
|
||||||
if (
|
if (
|
||||||
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values()))
|
len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0
|
||||||
> 0
|
|
||||||
): # Eliminé car NAR apparait dans le parcours
|
): # Eliminé car NAR apparait dans le parcours
|
||||||
reponse = True
|
reponse = True
|
||||||
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
|
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
|
||||||
@ -563,9 +562,8 @@ class JuryPE(object):
|
|||||||
dec = nt.get_etud_decision_sem(
|
dec = nt.get_etud_decision_sem(
|
||||||
etudid
|
etudid
|
||||||
) # quelle est la décision du jury ?
|
) # quelle est la décision du jury ?
|
||||||
if dec and dec["code"] in list(
|
if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
|
||||||
sco_codes_parcours.CODES_SEM_VALIDES.keys()
|
# isinstance( sesMoyennes[i+1], float) and
|
||||||
): # isinstance( sesMoyennes[i+1], float) and
|
|
||||||
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
|
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
|
||||||
leFid = sem["formsemestre_id"]
|
leFid = sem["formsemestre_id"]
|
||||||
else:
|
else:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
43
app/profiler.py
Normal file
43
app/profiler.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from time import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Profiler:
|
||||||
|
OUTPUT: str = "/tmp/scodoc.profiler.csv"
|
||||||
|
|
||||||
|
def __init__(self, tag: str) -> None:
|
||||||
|
self.tag: str = tag
|
||||||
|
self.start_time: time = None
|
||||||
|
self.stop_time: time = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.start_time = time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.stop_time = time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def elapsed(self) -> float:
|
||||||
|
return self.stop_time - self.start_time
|
||||||
|
|
||||||
|
def dates(self) -> tuple[datetime, datetime]:
|
||||||
|
return datetime.fromtimestamp(self.start_time), datetime.fromtimestamp(
|
||||||
|
self.stop_time
|
||||||
|
)
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
with open(Profiler.OUTPUT, "a") as file:
|
||||||
|
dates: tuple = self.dates()
|
||||||
|
date_str = (dates[0].isoformat(), dates[1].isoformat())
|
||||||
|
file.write(f"\n{self.tag},{self.elapsed() : .2}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write_in(cls, msg: str):
|
||||||
|
with open(cls.OUTPUT, "a") as file:
|
||||||
|
file.write(f"\n# {msg}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear(cls):
|
||||||
|
with open(cls.OUTPUT, "w") as file:
|
||||||
|
file.write("")
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -274,7 +274,7 @@ def sco_header(
|
|||||||
H.append("""<div id="gtrcontent">""")
|
H.append("""<div id="gtrcontent">""")
|
||||||
# En attendant le replacement complet de cette fonction,
|
# En attendant le replacement complet de cette fonction,
|
||||||
# inclusion ici des messages flask
|
# inclusion ici des messages flask
|
||||||
H.append(render_template("flashed_messages.html"))
|
H.append(render_template("flashed_messages.j2"))
|
||||||
#
|
#
|
||||||
# Barre menu semestre:
|
# Barre menu semestre:
|
||||||
H.append(formsemestre_page_title(formsemestre_id))
|
H.append(formsemestre_page_title(formsemestre_id))
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -101,7 +101,6 @@ def sidebar(etudid: int = None):
|
|||||||
etudid = request.form.get("etudid", None)
|
etudid = request.form.get("etudid", None)
|
||||||
|
|
||||||
if etudid is not None:
|
if etudid is not None:
|
||||||
etudi = int(etudid)
|
|
||||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||||
params.update(etud)
|
params.update(etud)
|
||||||
params["fiche_url"] = url_for(
|
params["fiche_url"] = url_for(
|
||||||
@ -167,6 +166,6 @@ def sidebar(etudid: int = None):
|
|||||||
def sidebar_dept():
|
def sidebar_dept():
|
||||||
"""Partie supérieure de la marge de gauche"""
|
"""Partie supérieure de la marge de gauche"""
|
||||||
return render_template(
|
return render_template(
|
||||||
"sidebar_dept.html",
|
"sidebar_dept.j2",
|
||||||
prefs=sco_preferences.SemPreferences(),
|
prefs=sco_preferences.SemPreferences(),
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -83,7 +83,7 @@ def histogram_notes(notes):
|
|||||||
return "\n".join(D)
|
return "\n".join(D)
|
||||||
|
|
||||||
|
|
||||||
def make_menu(title, items, css_class="", alone=False):
|
def make_menu(title, items, css_class="", alone=False) -> str:
|
||||||
"""HTML snippet to render a simple drop down menu.
|
"""HTML snippet to render a simple drop down menu.
|
||||||
items is a list of dicts:
|
items is a list of dicts:
|
||||||
{ 'title' :
|
{ 'title' :
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -42,6 +42,8 @@ from app.scodoc import sco_cache
|
|||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
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.models import Assiduite
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
# --- Misc tools.... ------------------
|
# --- Misc tools.... ------------------
|
||||||
@ -1052,6 +1054,36 @@ def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||||
|
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||||
|
tuple (nb abs, nb abs justifiées)
|
||||||
|
Utilise un cache.
|
||||||
|
"""
|
||||||
|
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso
|
||||||
|
r = sco_cache.AbsSemEtudCache.get(key)
|
||||||
|
if not r:
|
||||||
|
|
||||||
|
date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||||
|
date_fin: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||||
|
|
||||||
|
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||||
|
|
||||||
|
assiduites = scass.filter_assiduites_by_date(assiduites, date_debut, sup=True)
|
||||||
|
assiduites = scass.filter_assiduites_by_date(assiduites, date_fin, sup=False)
|
||||||
|
|
||||||
|
nb_abs = scass.get_count(assiduites)["demi"]
|
||||||
|
nb_abs_just = count_abs_just(
|
||||||
|
etudid=etudid,
|
||||||
|
debut=date_debut_iso,
|
||||||
|
fin=date_fin_iso,
|
||||||
|
)
|
||||||
|
r = (nb_abs, nb_abs_just)
|
||||||
|
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||||
|
if not ans:
|
||||||
|
log("warning: get_abs_count failed to cache")
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def invalidate_abs_count(etudid, sem):
|
def invalidate_abs_count(etudid, sem):
|
||||||
"""Invalidate (clear) cached counts"""
|
"""Invalidate (clear) cached counts"""
|
||||||
date_debut = sem["date_debut_iso"]
|
date_debut = sem["date_debut_iso"]
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -685,7 +685,7 @@ def EtatAbsences():
|
|||||||
|
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
</form>"""
|
</form>"""
|
||||||
% (scu.AnneeScolaire(), datetime.datetime.now().strftime("%d/%m/%Y")),
|
% (scu.annee_scolaire(), datetime.datetime.now().strftime("%d/%m/%Y")),
|
||||||
html_sco_header.sco_footer(),
|
html_sco_header.sco_footer(),
|
||||||
]
|
]
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
@ -719,15 +719,27 @@ def formChoixSemestreGroupe(all=False):
|
|||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_sco_year(year) -> int:
|
||||||
|
try:
|
||||||
|
year = int(year)
|
||||||
|
if year > 1900 and year < 2999:
|
||||||
|
return year
|
||||||
|
except:
|
||||||
|
raise ScoValueError("année scolaire invalide")
|
||||||
|
|
||||||
|
|
||||||
def CalAbs(etudid, sco_year=None):
|
def CalAbs(etudid, sco_year=None):
|
||||||
"""Calendrier des absences d'un etudiant"""
|
"""Calendrier des absences d'un etudiant"""
|
||||||
# crude portage from 1999 DTML
|
# crude portage from 1999 DTML
|
||||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||||
etudid = etud["etudid"]
|
etudid = etud["etudid"]
|
||||||
anneescolaire = int(scu.AnneeScolaire(sco_year))
|
if sco_year:
|
||||||
datedebut = str(anneescolaire) + "-08-01"
|
annee_scolaire = _convert_sco_year(sco_year)
|
||||||
datefin = str(anneescolaire + 1) + "-07-31"
|
else:
|
||||||
annee_courante = scu.AnneeScolaire()
|
annee_scolaire = scu.annee_scolaire()
|
||||||
|
datedebut = str(annee_scolaire) + "-08-01"
|
||||||
|
datefin = str(annee_scolaire + 1) + "-07-31"
|
||||||
|
annee_courante = scu.annee_scolaire()
|
||||||
nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin)
|
nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin)
|
||||||
nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin)
|
nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin)
|
||||||
events = []
|
events = []
|
||||||
@ -746,7 +758,7 @@ def CalAbs(etudid, sco_year=None):
|
|||||||
events.append(
|
events.append(
|
||||||
(str(a["jour"]), "X", "#8EA2C6", "", a["matin"], a["description"])
|
(str(a["jour"]), "X", "#8EA2C6", "", a["matin"], a["description"])
|
||||||
)
|
)
|
||||||
CalHTML = sco_abs.YearTable(anneescolaire, events=events, halfday=1)
|
CalHTML = sco_abs.YearTable(annee_scolaire, events=events, halfday=1)
|
||||||
|
|
||||||
#
|
#
|
||||||
H = [
|
H = [
|
||||||
@ -777,12 +789,12 @@ def CalAbs(etudid, sco_year=None):
|
|||||||
CalHTML,
|
CalHTML,
|
||||||
"""<form method="GET" action="CalAbs" name="f">""",
|
"""<form method="GET" action="CalAbs" name="f">""",
|
||||||
"""<input type="hidden" name="etudid" value="%s"/>""" % etudid,
|
"""<input type="hidden" name="etudid" value="%s"/>""" % etudid,
|
||||||
"""Année scolaire %s-%s""" % (anneescolaire, anneescolaire + 1),
|
"""Année scolaire %s-%s""" % (annee_scolaire, annee_scolaire + 1),
|
||||||
""" Changer année: <select name="sco_year" onchange="document.f.submit()">""",
|
""" Changer année: <select name="sco_year" onchange="document.f.submit()">""",
|
||||||
]
|
]
|
||||||
for y in range(annee_courante, min(annee_courante - 6, anneescolaire - 6), -1):
|
for y in range(annee_courante, min(annee_courante - 6, annee_scolaire - 6), -1):
|
||||||
H.append("""<option value="%s" """ % y)
|
H.append("""<option value="%s" """ % y)
|
||||||
if y == anneescolaire:
|
if y == annee_scolaire:
|
||||||
H.append("selected")
|
H.append("selected")
|
||||||
H.append(""">%s</option>""" % y)
|
H.append(""">%s</option>""" % y)
|
||||||
H.append("""</select></form>""")
|
H.append("""</select></form>""")
|
||||||
@ -811,7 +823,11 @@ def ListeAbsEtud(
|
|||||||
"""
|
"""
|
||||||
# si absjust_only, table absjust seule (export xls ou pdf)
|
# si absjust_only, table absjust seule (export xls ou pdf)
|
||||||
absjust_only = scu.to_bool(absjust_only)
|
absjust_only = scu.to_bool(absjust_only)
|
||||||
datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
|
if sco_year:
|
||||||
|
annee_scolaire = _convert_sco_year(sco_year)
|
||||||
|
else:
|
||||||
|
annee_scolaire = scu.annee_scolaire()
|
||||||
|
datedebut = f"{annee_scolaire}-{scu.MONTH_DEBUT_ANNEE_SCOLAIRE+1}-01"
|
||||||
etudid = etudid or False
|
etudid = etudid or False
|
||||||
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
|
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
|
||||||
if not etuds:
|
if not etuds:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -43,6 +43,7 @@ Pour chaque étudiant commun:
|
|||||||
comparer les résultats
|
comparer les résultats
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from flask import g, url_for
|
||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc import sco_apogee_csv
|
from app.scodoc import sco_apogee_csv
|
||||||
@ -72,11 +73,11 @@ def apo_compare_csv_form():
|
|||||||
"""
|
"""
|
||||||
<div class="apo_compare_csv_form_but">
|
<div class="apo_compare_csv_form_but">
|
||||||
Fichier Apogée A:
|
Fichier Apogée A:
|
||||||
<input type="file" size="30" name="A_file"/>
|
<input type="file" size="30" name="file_a"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="apo_compare_csv_form_but">
|
<div class="apo_compare_csv_form_but">
|
||||||
Fichier Apogée B:
|
Fichier Apogée B:
|
||||||
<input type="file" size="30" name="B_file"/>
|
<input type="file" size="30" name="file_b"/>
|
||||||
</div>
|
</div>
|
||||||
<input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
|
<input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
|
||||||
<div class="apo_compare_csv_form_submit">
|
<div class="apo_compare_csv_form_submit">
|
||||||
@ -88,17 +89,36 @@ def apo_compare_csv_form():
|
|||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
def apo_compare_csv(A_file, B_file, autodetect=True):
|
def apo_compare_csv(file_a, file_b, autodetect=True):
|
||||||
"""Page comparing 2 Apogee CSV files"""
|
"""Page comparing 2 Apogee CSV files"""
|
||||||
A = _load_apo_data(A_file, autodetect=autodetect)
|
try:
|
||||||
B = _load_apo_data(B_file, autodetect=autodetect)
|
apo_data_a = _load_apo_data(file_a, autodetect=autodetect)
|
||||||
|
apo_data_b = _load_apo_data(file_b, autodetect=autodetect)
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError) as exc:
|
||||||
|
dest_url = url_for("notes.semset_page", scodoc_dept=g.scodoc_dept)
|
||||||
|
if autodetect:
|
||||||
|
raise ScoValueError(
|
||||||
|
"""
|
||||||
|
Erreur: l'encodage de l'un des fichiers est mal détecté.
|
||||||
|
Essayez sans auto-détection, ou vérifiez le codage et le contenu
|
||||||
|
des fichiers.
|
||||||
|
""",
|
||||||
|
dest_url=dest_url,
|
||||||
|
) from exc
|
||||||
|
else:
|
||||||
|
raise ScoValueError(
|
||||||
|
f"""
|
||||||
|
Erreur: l'encodage de l'un des fichiers est incorrect.
|
||||||
|
Vérifiez qu'il est bien en {sco_apogee_csv.APO_INPUT_ENCODING}
|
||||||
|
""",
|
||||||
|
dest_url=dest_url,
|
||||||
|
) from exc
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
|
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
|
||||||
"<h2>Comparaison de fichiers Apogée</h2>",
|
"<h2>Comparaison de fichiers Apogée</h2>",
|
||||||
_help_txt,
|
_help_txt,
|
||||||
'<div class="apo_compare_csv">',
|
'<div class="apo_compare_csv">',
|
||||||
_apo_compare_csv(A, B),
|
_apo_compare_csv(apo_data_a, apo_data_b),
|
||||||
"</div>",
|
"</div>",
|
||||||
"""<p><a href="apo_compare_csv_form" class="stdlink">Autre comparaison</a></p>""",
|
"""<p><a href="apo_compare_csv_form" class="stdlink">Autre comparaison</a></p>""",
|
||||||
html_sco_header.sco_footer(),
|
html_sco_header.sco_footer(),
|
||||||
@ -112,9 +132,9 @@ def _load_apo_data(csvfile, autodetect=True):
|
|||||||
if autodetect:
|
if autodetect:
|
||||||
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
|
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
|
||||||
if message:
|
if message:
|
||||||
log("apo_compare_csv: %s" % message)
|
log(f"apo_compare_csv: {message}")
|
||||||
if not data_b:
|
if not data_b:
|
||||||
raise ScoValueError("apo_compare_csv: no data")
|
raise ScoValueError("fichier vide ? (apo_compare_csv: no data)")
|
||||||
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
|
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
|
||||||
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
|
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
|
||||||
return apo_data
|
return apo_data
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -155,28 +155,25 @@ def fix_data_encoding(
|
|||||||
text: bytes,
|
text: bytes,
|
||||||
default_source_encoding=APO_INPUT_ENCODING,
|
default_source_encoding=APO_INPUT_ENCODING,
|
||||||
dest_encoding=APO_INPUT_ENCODING,
|
dest_encoding=APO_INPUT_ENCODING,
|
||||||
) -> bytes:
|
) -> tuple[bytes, str]:
|
||||||
"""Try to ensure that text is using dest_encoding
|
"""Try to ensure that text is using dest_encoding
|
||||||
returns converted text, and a message describing the conversion.
|
returns converted text, and a message describing the conversion.
|
||||||
|
|
||||||
|
Raises UnicodeEncodeError en cas de problème, en général liée à
|
||||||
|
une auto-détection errornée.
|
||||||
"""
|
"""
|
||||||
message = ""
|
message = ""
|
||||||
detected_encoding = guess_data_encoding(text)
|
detected_encoding = guess_data_encoding(text)
|
||||||
if not detected_encoding:
|
if not detected_encoding:
|
||||||
if default_source_encoding != dest_encoding:
|
if default_source_encoding != dest_encoding:
|
||||||
message = "converting from %s to %s" % (
|
message = f"converting from {default_source_encoding} to {dest_encoding}"
|
||||||
default_source_encoding,
|
text = text.decode(default_source_encoding).encode(dest_encoding)
|
||||||
dest_encoding,
|
|
||||||
)
|
|
||||||
text = text.decode(default_source_encoding).encode(
|
|
||||||
dest_encoding
|
|
||||||
) # XXX #py3 #sco8 à tester
|
|
||||||
else:
|
else:
|
||||||
if detected_encoding != dest_encoding:
|
if detected_encoding != dest_encoding:
|
||||||
message = "converting from detected %s to %s" % (
|
message = (
|
||||||
detected_encoding,
|
f"converting from detected {default_source_encoding} to {dest_encoding}"
|
||||||
dest_encoding,
|
|
||||||
)
|
)
|
||||||
text = text.decode(detected_encoding).encode(dest_encoding) # XXX
|
text = text.decode(detected_encoding).encode(dest_encoding)
|
||||||
return text, message
|
return text, message
|
||||||
|
|
||||||
|
|
||||||
@ -511,7 +508,7 @@ class ApoEtud(dict):
|
|||||||
# print 'comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id'])
|
# print 'comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id'])
|
||||||
if not cur_sem:
|
if not cur_sem:
|
||||||
# l'étudiant n'a pas de semestre courant ?!
|
# l'étudiant n'a pas de semestre courant ?!
|
||||||
log("comp_elt_annuel: etudid %s has no cur_sem" % etudid)
|
log(f"comp_elt_annuel: etudid {etudid} has no cur_sem")
|
||||||
return VOID_APO_RES
|
return VOID_APO_RES
|
||||||
cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"])
|
cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"])
|
||||||
cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre)
|
cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre)
|
||||||
@ -586,7 +583,11 @@ class ApoEtud(dict):
|
|||||||
(sem["semestre_id"] == apo_data.cur_semestre_id)
|
(sem["semestre_id"] == apo_data.cur_semestre_id)
|
||||||
and (apo_data.etape in sem["etapes"])
|
and (apo_data.etape in sem["etapes"])
|
||||||
and (
|
and (
|
||||||
sco_formsemestre.sem_in_annee_scolaire(sem, apo_data.annee_scolaire)
|
sco_formsemestre.sem_in_semestre_scolaire(
|
||||||
|
sem,
|
||||||
|
apo_data.annee_scolaire,
|
||||||
|
0, # annee complete
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -28,7 +28,7 @@
|
|||||||
"""ScoDoc : gestion des archives des PV et bulletins, et des dossiers etudiants (admission)
|
"""ScoDoc : gestion des archives des PV et bulletins, et des dossiers etudiants (admission)
|
||||||
|
|
||||||
|
|
||||||
Archives are plain files, stored in
|
Archives are plain files, stored in
|
||||||
<SCODOC_VAR_DIR>/archives/<dept_id>
|
<SCODOC_VAR_DIR>/archives/<dept_id>
|
||||||
(where <SCODOC_VAR_DIR> is usually /opt/scodoc-data, and <dept_id> a departement id (int))
|
(where <SCODOC_VAR_DIR> is usually /opt/scodoc-data, and <dept_id> a departement id (int))
|
||||||
|
|
||||||
@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
Les maquettes Apogée pour l'export des notes sont dans
|
Les maquettes Apogée pour l'export des notes sont dans
|
||||||
<archivedir>/apo_csv/<dept_id>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
|
<archivedir>/apo_csv/<dept_id>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
|
||||||
|
|
||||||
Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt
|
Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt
|
||||||
qui est une description (humaine, format libre) de l'archive.
|
qui est une description (humaine, format libre) de l'archive.
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ from app import log
|
|||||||
from app.but import jury_but_pv
|
from app.but import jury_but_pv
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import Departement, FormSemestre
|
from app.models import FormSemestre
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
@ -89,6 +89,11 @@ class BaseArchiver(object):
|
|||||||
self.archive_type = archive_type
|
self.archive_type = archive_type
|
||||||
self.initialized = False
|
self.initialized = False
|
||||||
self.root = None
|
self.root = None
|
||||||
|
self.dept_id = None
|
||||||
|
|
||||||
|
def set_dept_id(self, dept_id: int):
|
||||||
|
"set dept"
|
||||||
|
self.dept_id = dept_id
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
if self.initialized:
|
if self.initialized:
|
||||||
@ -105,20 +110,21 @@ class BaseArchiver(object):
|
|||||||
try:
|
try:
|
||||||
scu.GSL.acquire()
|
scu.GSL.acquire()
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
log("creating directory %s" % path)
|
log(f"creating directory {path}")
|
||||||
os.mkdir(path)
|
os.mkdir(path)
|
||||||
finally:
|
finally:
|
||||||
scu.GSL.release()
|
scu.GSL.release()
|
||||||
self.initialized = True
|
self.initialized = True
|
||||||
|
if self.dept_id is None:
|
||||||
|
self.dept_id = getattr(g, "scodoc_dept_id")
|
||||||
|
|
||||||
def get_obj_dir(self, oid):
|
def get_obj_dir(self, oid: int):
|
||||||
"""
|
"""
|
||||||
:return: path to directory of archives for this object (eg formsemestre_id or etudid).
|
:return: path to directory of archives for this object (eg formsemestre_id or etudid).
|
||||||
If directory does not yet exist, create it.
|
If directory does not yet exist, create it.
|
||||||
"""
|
"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
dept_dir = os.path.join(self.root, str(self.dept_id))
|
||||||
dept_dir = os.path.join(self.root, str(dept.id))
|
|
||||||
try:
|
try:
|
||||||
scu.GSL.acquire()
|
scu.GSL.acquire()
|
||||||
if not os.path.isdir(dept_dir):
|
if not os.path.isdir(dept_dir):
|
||||||
@ -137,12 +143,11 @@ class BaseArchiver(object):
|
|||||||
:return: list of archive oids
|
:return: list of archive oids
|
||||||
"""
|
"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
base = os.path.join(self.root, str(self.dept_id)) + os.path.sep
|
||||||
base = os.path.join(self.root, str(dept.id)) + os.path.sep
|
|
||||||
dirs = glob.glob(base + "*")
|
dirs = glob.glob(base + "*")
|
||||||
return [os.path.split(x)[1] for x in dirs]
|
return [os.path.split(x)[1] for x in dirs]
|
||||||
|
|
||||||
def list_obj_archives(self, oid):
|
def list_obj_archives(self, oid: int):
|
||||||
"""Returns
|
"""Returns
|
||||||
:return: list of archive identifiers for this object (paths to non empty dirs)
|
:return: list of archive identifiers for this object (paths to non empty dirs)
|
||||||
"""
|
"""
|
||||||
@ -157,7 +162,7 @@ class BaseArchiver(object):
|
|||||||
dirs.sort()
|
dirs.sort()
|
||||||
return dirs
|
return dirs
|
||||||
|
|
||||||
def delete_archive(self, archive_id):
|
def delete_archive(self, archive_id: str):
|
||||||
"""Delete (forever) this archive"""
|
"""Delete (forever) this archive"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
try:
|
try:
|
||||||
@ -166,7 +171,7 @@ class BaseArchiver(object):
|
|||||||
finally:
|
finally:
|
||||||
scu.GSL.release()
|
scu.GSL.release()
|
||||||
|
|
||||||
def get_archive_date(self, archive_id):
|
def get_archive_date(self, archive_id: str):
|
||||||
"""Returns date (as a DateTime object) of an archive"""
|
"""Returns date (as a DateTime object) of an archive"""
|
||||||
return datetime.datetime(
|
return datetime.datetime(
|
||||||
*[int(x) for x in os.path.split(archive_id)[1].split("-")]
|
*[int(x) for x in os.path.split(archive_id)[1].split("-")]
|
||||||
@ -183,17 +188,17 @@ class BaseArchiver(object):
|
|||||||
files.sort()
|
files.sort()
|
||||||
return [f for f in files if f and f[0] != "_"]
|
return [f for f in files if f and f[0] != "_"]
|
||||||
|
|
||||||
def get_archive_name(self, archive_id):
|
def get_archive_name(self, archive_id: str):
|
||||||
"""name identifying archive, to be used in web URLs"""
|
"""name identifying archive, to be used in web URLs"""
|
||||||
return os.path.split(archive_id)[1]
|
return os.path.split(archive_id)[1]
|
||||||
|
|
||||||
def is_valid_archive_name(self, archive_name):
|
def is_valid_archive_name(self, archive_name: str):
|
||||||
"""check if name is valid."""
|
"""check if name is valid."""
|
||||||
return re.match(
|
return re.match(
|
||||||
"^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name
|
"^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_id_from_name(self, oid, archive_name):
|
def get_id_from_name(self, oid, archive_name: str):
|
||||||
"""returns archive id (check that name is valid)"""
|
"""returns archive id (check that name is valid)"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
if not self.is_valid_archive_name(archive_name):
|
if not self.is_valid_archive_name(archive_name):
|
||||||
@ -206,7 +211,7 @@ class BaseArchiver(object):
|
|||||||
raise ScoValueError(f"Archive {archive_name} introuvable")
|
raise ScoValueError(f"Archive {archive_name} introuvable")
|
||||||
return archive_id
|
return archive_id
|
||||||
|
|
||||||
def get_archive_description(self, archive_id):
|
def get_archive_description(self, archive_id: str) -> str:
|
||||||
"""Return description of archive"""
|
"""Return description of archive"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
filename = os.path.join(archive_id, "_description.txt")
|
filename = os.path.join(archive_id, "_description.txt")
|
||||||
@ -247,7 +252,7 @@ class BaseArchiver(object):
|
|||||||
data = data.encode(scu.SCO_ENCODING)
|
data = data.encode(scu.SCO_ENCODING)
|
||||||
self.initialize()
|
self.initialize()
|
||||||
filename = scu.sanitize_filename(filename)
|
filename = scu.sanitize_filename(filename)
|
||||||
log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id))
|
log(f"storing {filename} ({len(data)} bytes) in {archive_id}")
|
||||||
try:
|
try:
|
||||||
scu.GSL.acquire()
|
scu.GSL.acquire()
|
||||||
fname = os.path.join(archive_id, filename)
|
fname = os.path.join(archive_id, filename)
|
||||||
@ -261,16 +266,18 @@ class BaseArchiver(object):
|
|||||||
"""Retreive data"""
|
"""Retreive data"""
|
||||||
self.initialize()
|
self.initialize()
|
||||||
if not scu.is_valid_filename(filename):
|
if not scu.is_valid_filename(filename):
|
||||||
log('Archiver.get: invalid filename "%s"' % filename)
|
log(f"""Archiver.get: invalid filename '{filename}'""")
|
||||||
raise ScoValueError("archive introuvable (déjà supprimée ?)")
|
raise ScoValueError("archive introuvable (déjà supprimée ?)")
|
||||||
fname = os.path.join(archive_id, filename)
|
fname = os.path.join(archive_id, filename)
|
||||||
log("reading archive file %s" % fname)
|
log(f"reading archive file {fname}")
|
||||||
with open(fname, "rb") as f:
|
with open(fname, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_archived_file(self, oid, archive_name, filename):
|
def get_archived_file(self, oid, archive_name, filename):
|
||||||
"""Recupere donnees du fichier indiqué et envoie au client"""
|
"""Recupère les donnees du fichier indiqué et envoie au client.
|
||||||
|
Returns: Response
|
||||||
|
"""
|
||||||
archive_id = self.get_id_from_name(oid, archive_name)
|
archive_id = self.get_id_from_name(oid, archive_name)
|
||||||
data = self.get(archive_id, filename)
|
data = self.get(archive_id, filename)
|
||||||
mime = mimetypes.guess_type(filename)[0]
|
mime = mimetypes.guess_type(filename)[0]
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -373,7 +373,7 @@ def etudarchive_import_files(
|
|||||||
filename_title="fichier_a_charger",
|
filename_title="fichier_a_charger",
|
||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"scolar/photos_import_files.html",
|
"scolar/photos_import_files.j2",
|
||||||
page_title="Téléchargement de fichiers associés aux étudiants",
|
page_title="Téléchargement de fichiers associés aux étudiants",
|
||||||
ignored_zipfiles=ignored_zipfiles,
|
ignored_zipfiles=ignored_zipfiles,
|
||||||
unmatched_files=unmatched_files,
|
unmatched_files=unmatched_files,
|
||||||
|
108
app/scodoc/sco_archives_justificatifs.py
Normal file
108
app/scodoc/sco_archives_justificatifs.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
from app.scodoc.sco_archives import BaseArchiver
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.models import Identite, Departement
|
||||||
|
from flask import g
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class JustificatifArchiver(BaseArchiver):
|
||||||
|
"""
|
||||||
|
|
||||||
|
TOTALK:
|
||||||
|
- oid -> etudid
|
||||||
|
- archive_id -> date de création de l'archive (une archive par dépot de document)
|
||||||
|
|
||||||
|
justificatif
|
||||||
|
└── <dept_id>
|
||||||
|
└── <etudid/oid>
|
||||||
|
└── <archive_id>
|
||||||
|
├── [_description.txt]
|
||||||
|
└── [<filename.ext>]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
BaseArchiver.__init__(self, archive_type="justificatifs")
|
||||||
|
|
||||||
|
def save_justificatif(
|
||||||
|
self,
|
||||||
|
etudid: int,
|
||||||
|
filename: str,
|
||||||
|
data: bytes or str,
|
||||||
|
archive_name: str = None,
|
||||||
|
description: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Ajoute un fichier dans une archive "justificatif" pour l'etudid donné
|
||||||
|
Retourne l'archive_name utilisé
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
if archive_name is None:
|
||||||
|
archive_id: str = self.create_obj_archive(
|
||||||
|
oid=etudid, description=description
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
archive_id: str = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
fname: str = self.store(archive_id, filename, data)
|
||||||
|
|
||||||
|
return self.get_archive_name(archive_id), fname
|
||||||
|
|
||||||
|
def delete_justificatif(self, etudid: int, archive_name: str, filename: str = None):
|
||||||
|
"""
|
||||||
|
Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
if str(etudid) not in self.list_oids():
|
||||||
|
raise ValueError(f"Aucune archive pour etudid[{etudid}]")
|
||||||
|
|
||||||
|
archive_id = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
if filename is not None:
|
||||||
|
if filename not in self.list_archive(archive_id):
|
||||||
|
raise ValueError(
|
||||||
|
f"filename {filename} inconnu dans l'archive archive_id[{archive_id}] -> etudid[{etudid}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename)
|
||||||
|
|
||||||
|
if os.path.isfile(path):
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.delete_archive(
|
||||||
|
os.path.join(
|
||||||
|
self.get_obj_dir(etudid),
|
||||||
|
archive_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_justificatifs(self, archive_name: str, etudid: int) -> list[str]:
|
||||||
|
"""
|
||||||
|
Retourne la liste des noms de fichiers dans l'archive donnée
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
filenames: list[str] = []
|
||||||
|
archive_id = self.get_id_from_name(etudid, archive_name)
|
||||||
|
|
||||||
|
filenames = self.list_archive(archive_id)
|
||||||
|
return filenames
|
||||||
|
|
||||||
|
def get_justificatif_file(self, archive_name: str, etudid: int, filename: str):
|
||||||
|
"""
|
||||||
|
Retourne une réponse de téléchargement de fichier si le fichier existe
|
||||||
|
"""
|
||||||
|
self._set_dept(etudid)
|
||||||
|
archive_id: str = self.get_id_from_name(etudid, archive_name)
|
||||||
|
if filename in self.list_archive(archive_id):
|
||||||
|
return self.get_archived_file(etudid, archive_name, filename)
|
||||||
|
raise ScoValueError(
|
||||||
|
f"Fichier {filename} introuvable dans l'archive {archive_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_dept(self, etudid: int):
|
||||||
|
"""
|
||||||
|
Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant
|
||||||
|
"""
|
||||||
|
etud: Identite = Identite.query.filter_by(id=etudid).first()
|
||||||
|
self.set_dept_id(etud.dept_id)
|
401
app/scodoc/sco_assiduites.py
Normal file
401
app/scodoc/sco_assiduites.py
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
from datetime import date, datetime, time, timedelta
|
||||||
|
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.models.assiduites import Assiduite, Justificatif
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
|
|
||||||
|
|
||||||
|
class CountCalculator:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
morning: time = time(8, 0),
|
||||||
|
noon: time = time(12, 0),
|
||||||
|
after_noon: time = time(14, 00),
|
||||||
|
evening: time = time(18, 0),
|
||||||
|
skip_saturday: bool = True,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
self.morning: time = morning
|
||||||
|
self.noon: time = noon
|
||||||
|
self.after_noon: time = after_noon
|
||||||
|
self.evening: time = evening
|
||||||
|
self.skip_saturday: bool = skip_saturday
|
||||||
|
|
||||||
|
delta_total: timedelta = datetime.combine(date.min, evening) - datetime.combine(
|
||||||
|
date.min, morning
|
||||||
|
)
|
||||||
|
delta_lunch: timedelta = datetime.combine(
|
||||||
|
date.min, after_noon
|
||||||
|
) - datetime.combine(date.min, noon)
|
||||||
|
|
||||||
|
self.hour_per_day: float = (delta_total - delta_lunch).total_seconds() / 3600
|
||||||
|
|
||||||
|
self.days: list[date] = []
|
||||||
|
self.half_days: list[tuple[date, bool]] = [] # tuple -> (date, morning:bool)
|
||||||
|
self.hours: float = 0.0
|
||||||
|
|
||||||
|
self.count: int = 0
|
||||||
|
|
||||||
|
def add_half_day(self, day: date, is_morning: bool = True):
|
||||||
|
key: tuple[date, bool] = (day, is_morning)
|
||||||
|
if key not in self.half_days:
|
||||||
|
self.half_days.append(key)
|
||||||
|
|
||||||
|
def add_day(self, day: date):
|
||||||
|
if day not in self.days:
|
||||||
|
self.days.append(day)
|
||||||
|
|
||||||
|
def check_in_morning(self, period: tuple[datetime, datetime]) -> bool:
|
||||||
|
|
||||||
|
interval_morning: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.morning)),
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.noon)),
|
||||||
|
)
|
||||||
|
|
||||||
|
in_morning: bool = scu.is_period_overlapping(period, interval_morning)
|
||||||
|
return in_morning
|
||||||
|
|
||||||
|
def check_in_evening(self, period: tuple[datetime, datetime]) -> bool:
|
||||||
|
|
||||||
|
interval_evening: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.after_noon)),
|
||||||
|
scu.localize_datetime(datetime.combine(period[0].date(), self.evening)),
|
||||||
|
)
|
||||||
|
|
||||||
|
in_evening: bool = scu.is_period_overlapping(period, interval_evening)
|
||||||
|
|
||||||
|
return in_evening
|
||||||
|
|
||||||
|
def compute_long_assiduite(self, assi: Assiduite):
|
||||||
|
|
||||||
|
pointer_date: date = assi.date_debut.date() + timedelta(days=1)
|
||||||
|
start_hours: timedelta = assi.date_debut - scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_debut, self.morning)
|
||||||
|
)
|
||||||
|
finish_hours: timedelta = assi.date_fin - scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_fin, self.morning)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_day(assi.date_debut.date())
|
||||||
|
self.add_day(assi.date_fin.date())
|
||||||
|
|
||||||
|
start_period: tuple[datetime, datetime] = (
|
||||||
|
assi.date_debut,
|
||||||
|
scu.localize_datetime(
|
||||||
|
datetime.combine(assi.date_debut.date(), self.evening)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
finish_period: tuple[datetime, datetime] = (
|
||||||
|
scu.localize_datetime(datetime.combine(assi.date_fin.date(), self.morning)),
|
||||||
|
assi.date_fin,
|
||||||
|
)
|
||||||
|
hours = 0.0
|
||||||
|
for period in (start_period, finish_period):
|
||||||
|
if self.check_in_evening(period):
|
||||||
|
self.add_half_day(period[0].date(), False)
|
||||||
|
if self.check_in_morning(period):
|
||||||
|
self.add_half_day(period[0].date())
|
||||||
|
|
||||||
|
while pointer_date < assi.date_fin.date():
|
||||||
|
if pointer_date.weekday() < (6 - self.skip_saturday):
|
||||||
|
self.add_day(pointer_date)
|
||||||
|
self.add_half_day(pointer_date)
|
||||||
|
self.add_half_day(pointer_date, False)
|
||||||
|
self.hours += self.hour_per_day
|
||||||
|
hours += self.hour_per_day
|
||||||
|
|
||||||
|
pointer_date += timedelta(days=1)
|
||||||
|
|
||||||
|
self.hours += finish_hours.total_seconds() / 3600
|
||||||
|
self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600)
|
||||||
|
|
||||||
|
def compute_assiduites(self, assiduites: Assiduite):
|
||||||
|
assi: Assiduite
|
||||||
|
for assi in assiduites.all():
|
||||||
|
self.count += 1
|
||||||
|
delta: timedelta = assi.date_fin - assi.date_debut
|
||||||
|
|
||||||
|
if delta.days > 0:
|
||||||
|
# raise Exception(self.hours)
|
||||||
|
self.compute_long_assiduite(assi)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
period: tuple[datetime, datetime] = (assi.date_debut, assi.date_fin)
|
||||||
|
deb_date: date = assi.date_debut.date()
|
||||||
|
if self.check_in_morning(period):
|
||||||
|
self.add_half_day(deb_date)
|
||||||
|
if self.check_in_evening(period):
|
||||||
|
self.add_half_day(deb_date, False)
|
||||||
|
|
||||||
|
self.add_day(deb_date)
|
||||||
|
|
||||||
|
self.hours += delta.total_seconds() / 3600
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"compte": self.count,
|
||||||
|
"journee": len(self.days),
|
||||||
|
"demi": len(self.half_days),
|
||||||
|
"heure": round(self.hours, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduites_stats(
|
||||||
|
assiduites: Assiduite, metric: str = "all", filtered: dict[str, object] = None
|
||||||
|
) -> Assiduite:
|
||||||
|
|
||||||
|
if filtered is not None:
|
||||||
|
deb, fin = None, None
|
||||||
|
for key in filtered:
|
||||||
|
if key == "etat":
|
||||||
|
assiduites = filter_assiduites_by_etat(assiduites, filtered[key])
|
||||||
|
elif key == "date_fin":
|
||||||
|
fin = filtered[key]
|
||||||
|
elif key == "date_debut":
|
||||||
|
deb = filtered[key]
|
||||||
|
elif key == "moduleimpl_id":
|
||||||
|
assiduites = filter_by_module_impl(assiduites, filtered[key])
|
||||||
|
elif key == "formsemestre":
|
||||||
|
assiduites = filter_by_formsemestre(assiduites, filtered[key])
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
assiduites = filter_by_date(assiduites, Assiduite, deb, fin)
|
||||||
|
|
||||||
|
calculator: CountCalculator = CountCalculator()
|
||||||
|
calculator.compute_assiduites(assiduites)
|
||||||
|
count: dict = calculator.to_dict()
|
||||||
|
|
||||||
|
metrics: list[str] = metric.split(",")
|
||||||
|
|
||||||
|
output: dict = {}
|
||||||
|
|
||||||
|
for key, val in count.items():
|
||||||
|
if key in metrics:
|
||||||
|
output[key] = val
|
||||||
|
return output if output else count
|
||||||
|
|
||||||
|
|
||||||
|
# def big_counter(
|
||||||
|
# interval: tuple[datetime],
|
||||||
|
# pref_time: time = time(12, 0),
|
||||||
|
# ):
|
||||||
|
# curr_date: datetime
|
||||||
|
|
||||||
|
# if interval[0].time() >= pref_time:
|
||||||
|
# curr_date = scu.localize_datetime(
|
||||||
|
# datetime.combine(interval[0].date(), pref_time)
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# curr_date = scu.localize_datetime(
|
||||||
|
# datetime.combine(interval[0].date(), time(0, 0))
|
||||||
|
# )
|
||||||
|
|
||||||
|
# def next_(curr: datetime, journee):
|
||||||
|
# if curr.time() != pref_time:
|
||||||
|
# next_time = scu.localize_datetime(datetime.combine(curr.date(), pref_time))
|
||||||
|
# else:
|
||||||
|
# next_time = scu.localize_datetime(
|
||||||
|
# datetime.combine(curr.date() + timedelta(days=1), time(0, 0))
|
||||||
|
# )
|
||||||
|
# journee += 1
|
||||||
|
# return next_time, journee
|
||||||
|
|
||||||
|
# demi: int = 0
|
||||||
|
# j: int = 0
|
||||||
|
# while curr_date <= interval[1]:
|
||||||
|
# next_time: datetime
|
||||||
|
# next_time, j = next_(curr_date, j)
|
||||||
|
# if scu.is_period_overlapping((curr_date, next_time), interval, True):
|
||||||
|
# demi += 1
|
||||||
|
# curr_date = next_time
|
||||||
|
|
||||||
|
# delta: timedelta = interval[1] - interval[0]
|
||||||
|
# heures: float = delta.total_seconds() / 3600
|
||||||
|
|
||||||
|
# if delta.days >= 1:
|
||||||
|
# heures -= delta.days * 16
|
||||||
|
|
||||||
|
# return (demi, j, heures)
|
||||||
|
|
||||||
|
|
||||||
|
# def get_count(
|
||||||
|
# assiduites: Assiduite, noon: time = time(hour=12)
|
||||||
|
# ) -> dict[str, int or float]:
|
||||||
|
# """Fonction permettant de compter les assiduites
|
||||||
|
# -> seul "compte" est correcte lorsque les assiduites viennent de plusieurs étudiants
|
||||||
|
# """
|
||||||
|
# # TODO: Comptage demi journée / journée d'assiduité longue
|
||||||
|
# output: dict[str, int or float] = {}
|
||||||
|
# compte: int = assiduites.count()
|
||||||
|
# heure: float = 0.0
|
||||||
|
# journee: int = 0
|
||||||
|
# demi: int = 0
|
||||||
|
|
||||||
|
# all_assiduites: list[Assiduite] = assiduites.order_by(Assiduite.date_debut).all()
|
||||||
|
|
||||||
|
# current_day: date = None
|
||||||
|
# current_time: str = None
|
||||||
|
|
||||||
|
# midnight: time = time(hour=0)
|
||||||
|
|
||||||
|
# def time_check(dtime):
|
||||||
|
# return midnight <= dtime.time() <= noon
|
||||||
|
|
||||||
|
# for ass in all_assiduites:
|
||||||
|
# delta: timedelta = ass.date_fin - ass.date_debut
|
||||||
|
|
||||||
|
# if delta.days > 0:
|
||||||
|
|
||||||
|
# computed_values: tuple[int, int, float] = big_counter(
|
||||||
|
# (ass.date_debut, ass.date_fin), noon
|
||||||
|
# )
|
||||||
|
|
||||||
|
# demi += computed_values[0] - 1
|
||||||
|
# journee += computed_values[1] - 1
|
||||||
|
# heure += computed_values[2]
|
||||||
|
|
||||||
|
# current_day = ass.date_fin.date()
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# heure += delta.total_seconds() / 3600
|
||||||
|
|
||||||
|
# ass_time: str = time_check(ass.date_debut)
|
||||||
|
|
||||||
|
# if current_day != ass.date_debut.date():
|
||||||
|
# current_day = ass.date_debut.date()
|
||||||
|
# current_time = ass_time
|
||||||
|
# demi += 1
|
||||||
|
# journee += 1
|
||||||
|
|
||||||
|
# if current_time != ass_time:
|
||||||
|
# current_time = ass_time
|
||||||
|
# demi += 1
|
||||||
|
|
||||||
|
# heure = round(heure, 2)
|
||||||
|
# return {"compte": compte, "journee": journee, "heure": heure, "demi": demi}
|
||||||
|
|
||||||
|
|
||||||
|
def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de leur état
|
||||||
|
"""
|
||||||
|
etats: list[str] = list(etat.split(","))
|
||||||
|
etats = [scu.EtatAssiduite.get(e, -1) for e in etats]
|
||||||
|
return assiduites.filter(Assiduite.etat.in_(etats))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_date(
|
||||||
|
collection: Assiduite or Justificatif,
|
||||||
|
collection_cls: Assiduite or Justificatif,
|
||||||
|
date_deb: datetime = None,
|
||||||
|
date_fin: datetime = None,
|
||||||
|
strict: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'une date
|
||||||
|
"""
|
||||||
|
if date_deb is None:
|
||||||
|
date_deb = datetime.min
|
||||||
|
if date_fin is None:
|
||||||
|
date_fin = datetime.max
|
||||||
|
|
||||||
|
date_deb = scu.localize_datetime(date_deb)
|
||||||
|
date_fin = scu.localize_datetime(date_fin)
|
||||||
|
if not strict:
|
||||||
|
return collection.filter(
|
||||||
|
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
||||||
|
)
|
||||||
|
return collection.filter(
|
||||||
|
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_deb
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_justificatifs_by_etat(
|
||||||
|
justificatifs: Justificatif, etat: str
|
||||||
|
) -> Justificatif:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection de justificatifs en fonction de leur état
|
||||||
|
"""
|
||||||
|
etats: list[str] = list(etat.split(","))
|
||||||
|
etats = [scu.EtatJustificatif.get(e, -1) for e in etats]
|
||||||
|
return justificatifs.filter(Justificatif.etat.in_(etats))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_justificatifs_by_date(
|
||||||
|
justificatifs: Justificatif, date_: datetime, sup: bool = True
|
||||||
|
) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'une date
|
||||||
|
|
||||||
|
Sup == True -> les assiduites doivent débuter après 'date'\n
|
||||||
|
Sup == False -> les assiduites doivent finir avant 'date'
|
||||||
|
"""
|
||||||
|
|
||||||
|
if date_.tzinfo is None:
|
||||||
|
first_justificatif: Justificatif = justificatifs.first()
|
||||||
|
if first_justificatif is not None:
|
||||||
|
date_: datetime = date_.replace(tzinfo=first_justificatif.date_debut.tzinfo)
|
||||||
|
|
||||||
|
if sup:
|
||||||
|
return justificatifs.filter(Justificatif.date_debut >= date_)
|
||||||
|
|
||||||
|
return justificatifs.filter(Justificatif.date_fin <= date_)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_module_impl(
|
||||||
|
assiduites: Assiduite, module_impl_id: int or None
|
||||||
|
) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl
|
||||||
|
"""
|
||||||
|
return assiduites.filter(Assiduite.moduleimpl_id == module_impl_id)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemestre):
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'un formsemestre
|
||||||
|
"""
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return assiduites_query.filter(False)
|
||||||
|
|
||||||
|
assiduites_query = (
|
||||||
|
assiduites_query.join(Identite, Assiduite.etudid == Identite.id)
|
||||||
|
.join(
|
||||||
|
FormSemestreInscription,
|
||||||
|
Identite.id == FormSemestreInscription.etudid,
|
||||||
|
)
|
||||||
|
.filter(FormSemestreInscription.formsemestre_id == formsemestre.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
assiduites_query = assiduites_query.filter(
|
||||||
|
Assiduite.date_debut >= formsemestre.date_debut
|
||||||
|
)
|
||||||
|
return assiduites_query.filter(Assiduite.date_fin <= formsemestre.date_fin)
|
||||||
|
|
||||||
|
|
||||||
|
def justifies(justi: Justificatif) -> list[int]:
|
||||||
|
"""
|
||||||
|
Retourne la liste des assiduite_id qui sont justifié par la justification
|
||||||
|
Une assiduité est justifiée si elle est STRICTEMENT comprise dans la plage du justificatif
|
||||||
|
et que l'état du justificatif est "validé"
|
||||||
|
"""
|
||||||
|
|
||||||
|
justified: list[int] = []
|
||||||
|
|
||||||
|
if justi.etat != scu.EtatJustificatif.VALIDE:
|
||||||
|
return justified
|
||||||
|
|
||||||
|
assiduites_query: Assiduite = Assiduite.query.join(
|
||||||
|
Justificatif, Assiduite.etudid == Justificatif.etudid
|
||||||
|
).filter(Assiduite.etat != scu.EtatAssiduite.PRESENT)
|
||||||
|
|
||||||
|
assiduites_query = filter_by_date(
|
||||||
|
assiduites_query, Assiduite, justi.date_debut, justi.date_fin
|
||||||
|
)
|
||||||
|
|
||||||
|
justified = [assi.id for assi in assiduites_query.all()]
|
||||||
|
|
||||||
|
return justified
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Gestion scolarite IUT
|
# Gestion scolarite IUT
|
||||||
#
|
#
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -926,7 +926,7 @@ def formsemestre_bulletinetud(
|
|||||||
_formsemestre_bulletinetud_header_html(etud, formsemestre, format, version),
|
_formsemestre_bulletinetud_header_html(etud, formsemestre, format, version),
|
||||||
bulletin,
|
bulletin,
|
||||||
render_template(
|
render_template(
|
||||||
"bul_foot.html",
|
"bul_foot.j2",
|
||||||
appreciations=None, # déjà affichées
|
appreciations=None, # déjà affichées
|
||||||
css_class="bul_classic_foot",
|
css_class="bul_classic_foot",
|
||||||
etud=etud,
|
etud=etud,
|
||||||
@ -990,6 +990,8 @@ def do_formsemestre_bulletinetud(
|
|||||||
version=version,
|
version=version,
|
||||||
)
|
)
|
||||||
return bul, ""
|
return bul, ""
|
||||||
|
if version.endswith("_mat"):
|
||||||
|
version = version[:-4] # enlève le "_mat"
|
||||||
|
|
||||||
if formsemestre.formation.is_apc():
|
if formsemestre.formation.is_apc():
|
||||||
etudiant = Identite.query.get(etudid)
|
etudiant = Identite.query.get(etudid)
|
||||||
@ -1082,7 +1084,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
|||||||
recipients = [recipient_addr]
|
recipients = [recipient_addr]
|
||||||
sender = sco_preferences.get_preference("email_from_addr", formsemestre_id)
|
sender = sco_preferences.get_preference("email_from_addr", formsemestre_id)
|
||||||
if copy_addr:
|
if copy_addr:
|
||||||
bcc = copy_addr.strip()
|
bcc = copy_addr.strip().split(",")
|
||||||
else:
|
else:
|
||||||
bcc = ""
|
bcc = ""
|
||||||
|
|
||||||
@ -1092,7 +1094,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
|||||||
subject,
|
subject,
|
||||||
sender,
|
sender,
|
||||||
recipients,
|
recipients,
|
||||||
bcc=[bcc],
|
bcc=bcc,
|
||||||
text_body=hea,
|
text_body=hea,
|
||||||
attachments=[
|
attachments=[
|
||||||
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}
|
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}
|
||||||
@ -1215,7 +1217,8 @@ def make_menu_autres_operations(
|
|||||||
"formsemestre_id": formsemestre.id,
|
"formsemestre_id": formsemestre.id,
|
||||||
"etudid": etud.id,
|
"etudid": etud.id,
|
||||||
},
|
},
|
||||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
|
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id)
|
||||||
|
and not formsemestre.formation.is_apc(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Entrer décisions jury",
|
"title": "Entrer décisions jury",
|
||||||
@ -1256,7 +1259,7 @@ def _formsemestre_bulletinetud_header_html(
|
|||||||
cssstyles=["css/radar_bulletin.css"],
|
cssstyles=["css/radar_bulletin.css"],
|
||||||
),
|
),
|
||||||
render_template(
|
render_template(
|
||||||
"bul_head.html",
|
"bul_head.j2",
|
||||||
etud=etud,
|
etud=etud,
|
||||||
format=format,
|
format=format,
|
||||||
formsemestre=formsemestre,
|
formsemestre=formsemestre,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user